100% Free Forever
AI-Powered Learning
Industry Expert Content
Certificates & Badges
Learn At Your Own Pace
CI/CD, GitOps, DevSecOps & Observability
55 minintermediate

GitHub Actions pipeline — SAST, SCA, image scan, sign and push

What You'll Build

In this lesson you will assemble the complete capstone CI pipeline as a single GitHub Actions workflow file that integrates all security gates from Modules 1 and 3 into a four-stage job dependency chain. The pipeline runs SAST with Semgrep and SCA with Snyk in parallel, then—only if both pass—builds the Docker image, scans it with Trivy, pushes to ECR, and signs the image digest with Cosign. A final job updates the GitOps repository with the new signed image digest, triggering ArgoCD to deploy.

This pipeline is the full supply chain: from a code commit, every change passes automated security analysis of the source code, dependency vulnerability scanning, container image vulnerability scanning, and cryptographic signing before it reaches the GitOps repository that controls what runs in production. Any failure at any gate blocks the subsequent gates without requiring manual intervention.

Analogy🏏Cricket
Think of it like cricket: Picture setting up a remote ground management system for a cricket ground in a new city. First, the infrastructure must be installed: the pitch sensors, the scoreboard network, and the broadcast uplink—equivalent to installing ArgoCD on the EKS cluster. Then, the venue configuration must be committed to the central venue management database: the pitch dimensions, the lighting schedule, the boundary positions—equivalent to committing the Kubernetes manifests to the GitOps repository. Then, the venue must be registered with the central management system, which then automatically enforces the declared configuration at the ground—equivalent to creating the ArgoCD Application that connects the repository to the cluster. When a ground manager moves a boundary rope by hand, the sensors detect the drift and alert the management system to restore the declared position—equivalent to ArgoCD detecting and reverting the manual replica scale. This reveals why the lab sequence matters: you cannot verify GitOps until all three components—operator, repository, and Application—are connected and working together.

Prerequisites

  • The india-squad-helm project from M2 Exercise 13 with a working GitHub repository and the Dockerfile from M3 Exercise 20.
  • GitHub repository secrets configured: ECR_ROLE_ARN for OIDC ECR push, SNYK_TOKEN for SCA, and GITOPS_PAT for updating the GitOps repository.
  • The india-squad-gitops repository from M2 Lab 14 with the production overlay Kustomization structure ready to receive image tag updates.
  • The Kyverno ClusterPolicy from M3 Lab 21 deployed in the cluster, so the signed image from this pipeline will be admitted.
  • The kube-prometheus-stack from M5 Lab 35 deployed, since the pipeline's verification step in the update-gitops job assumes Prometheus is available to validate the subsequent rollout.

Pipeline Architecture

The pipeline has four jobs in a dependency chain. The `sast` and `sca` jobs run in parallel immediately on push or PR—they are the cheapest gates and should complete within 90 seconds for most codebases. The `build-scan-sign` job runs only after both `sast` and `sca` pass, performing the expensive operations: Docker build, Trivy scan, ECR push, and Cosign signing. The `update-gitops` job runs only on pushes to main—not on PRs—after the image is signed, committing the new image digest to the GitOps repository.

Analogy🏏Cricket
Think of it like cricket: The pipeline's job dependency structure mirrors the ICC's multi-stage player certification process. Fitness and equipment checks run simultaneously—there is no reason to wait for one before starting the other. Only certified players with passing scores in both enter the field trial—the expensive evaluation reserved for qualified candidates. Only players who complete the field trial successfully receive their official ICC credentials and are added to the squad list. The squad list is updated only after the credential ceremony—the GitOps commit happens only after the signed image confirms the complete supply chain. Just as running the field trial before the fitness check would waste the coaching staff's time on unfit candidates, running the container build before SAST and SCA would waste ECR storage and runner minutes on code with security vulnerabilities.
yaml
# Complete capstone CI pipeline: .github/workflows/capstone-ci.yml

name: india-squad-capstone-ci

on:
  push:         { branches: [main] }
  pull_request: { branches: [main] }

permissions:
  contents:        read
  id-token:        write   # OIDC for ECR + Cosign
  security-events: write   # SARIF uploads

env:
  AWS_REGION:  ap-south-1
  ECR_REPO:    india-squad-api
  REGISTRY:    123456789.dkr.ecr.ap-south-1.amazonaws.com

jobs:
  # ── Gate 1: SAST ──────────────────────────────────────────────────────
  sast:
    name: SAST  Semgrep
    runs-on: ubuntu-latest
    container: { image: semgrep/semgrep }
    steps:
      - uses: actions/checkout@v4
      - run: |
          semgrep ci \
            --config p/python \
            --config .semgrep/ \
            --sarif --output semgrep.sarif \
            --error
      - if: always()
        uses: github/codeql-action/upload-sarif@v3
        with: { sarif_file: semgrep.sarif, category: semgrep }

  # ── Gate 2: SCA ───────────────────────────────────────────────────────
  sca:
    name: SCA  Snyk
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with: { python-version: '3.12' }
      - uses: snyk/actions/python@master
        env: { SNYK_TOKEN: '${{ secrets.SNYK_TOKEN }}' }
        with:
          args: --severity-threshold=high --fail-on=upgradeable --sarif-file-output=snyk.sarif
      - if: always()
        uses: github/codeql-action/upload-sarif@v3
        with: { sarif_file: snyk.sarif, category: snyk }

  # ── Gate 3: Build → Trivy Scan → Cosign Sign → Push ──────────────────
  build-scan-sign:
    name: Build · Scan · Sign
    needs: [sast, sca]
    runs-on: ubuntu-latest
    outputs:
      image:  ${{ steps.meta.outputs.image }}
      digest: ${{ steps.push.outputs.digest }}
    steps:
      - uses: actions/checkout@v4

      - name: Configure AWS OIDC
        uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: ${{ secrets.ECR_ROLE_ARN }}
          aws-region: ${{ env.AWS_REGION }}

      - id: ecr-login
        uses: aws-actions/amazon-ecr-login@v2

      - uses: docker/setup-buildx-action@v3

      - name: Build (no push yet  scan first)
        uses: docker/build-push-action@v5
        with:
          push: false
          load: true
          tags: ${{ env.REGISTRY }}/${{ env.ECR_REPO }}:${{ github.sha }}
          cache-from: type=gha
          cache-to:   type=gha,mode=max

      - name: Trivy scan  fail on HIGH/CRITICAL
        uses: aquasecurity/trivy-action@master
        with:
          image-ref: ${{ env.REGISTRY }}/${{ env.ECR_REPO }}:${{ github.sha }}
          severity: HIGH,CRITICAL
          exit-code: '1'
          ignore-unfixed: true
          format: sarif
          output: trivy.sarif

      - if: always()
        uses: github/codeql-action/upload-sarif@v3
        with: { sarif_file: trivy.sarif, category: trivy }

      - name: Push image to ECR
        id: push
        uses: docker/build-push-action@v5
        with:
          push: true
          tags: ${{ env.REGISTRY }}/${{ env.ECR_REPO }}:${{ github.sha }}
          cache-from: type=gha

      - name: Install Cosign
        uses: sigstore/cosign-installer@v3

      - name: Sign image by digest
        run: |
          cosign sign --yes \
            ${{ env.REGISTRY }}/${{ env.ECR_REPO }}@${{ steps.push.outputs.digest }}

      - name: Verify signature
        run: |
          cosign verify \
            --certificate-identity-regexp='https://github.com/${{ github.repository }}' \
            --certificate-oidc-issuer='https://token.actions.githubusercontent.com' \
            ${{ env.REGISTRY }}/${{ env.ECR_REPO }}@${{ steps.push.outputs.digest }}

      - id: meta
        run: echo 'image=${{ env.REGISTRY }}/${{ env.ECR_REPO }}' >> $GITHUB_OUTPUT

  # ── Gate 4: Update GitOps repo with new image digest ─────────────────
  update-gitops:
    name: Update GitOps
    needs: build-scan-sign
    runs-on: ubuntu-latest
    if: github.ref == 'refs/heads/main'    # only on main branch merges
    steps:
      - name: Checkout GitOps repo
        uses: actions/checkout@v4
        with:
          repository: MY_ORG/india-squad-gitops
          token: ${{ secrets.GITOPS_PAT }}

      - name: Update image digest in overlay
        run: |
          cd overlays/production
          # Replace the image tag in kustomization.yaml
          kustomize edit set image \
            ${{ needs.build-scan-sign.outputs.image }}@${{ needs.build-scan-sign.outputs.digest }}

      - name: Commit and push
        run: |
          git config user.name  'india-squad-ci-bot'
          git config user.email '[email protected]'
          git add -A
          git commit -m 'chore(deploy): update india-squad-api to ${{ github.sha }}'
          git push

Step 1 — Semgrep and Snyk Secrets Setup

Before the pipeline runs successfully, configure the three required secrets in the GitHub repository. The ECR_ROLE_ARN is the OIDC-federation IAM role ARN from M1 Lab 7. The SNYK_TOKEN is obtained from your Snyk account settings under API tokens. The GITOPS_PAT is a GitHub Personal Access Token with `repo` scope for the GitOps repository—use a dedicated machine user account rather than a personal account so the CI bot's commits are distinguishable from developer commits in the GitOps history.

Analogy🏏Cricket
🏏 Think of it like cricket: Setting up the pipeline's secrets is like issuing the correct credentials to a support-staff member before they can enter restricted areas of the stadium. Just as a physio needs a specific accreditation pass for the medical room, a different one for the dressing room, and another for the boundary, your pipeline needs three distinct secrets: the ECR_ROLE_ARN that grants federated access to push images, the SNYK_TOKEN that authorises the vulnerability scan, and the GITOPS_PAT that lets the pipeline commit to the delivery repository. Just as a careful club issues a dedicated staff pass tied to a role rather than lending out a star player's personal all-areas pass — which would be over-privileged and impossible to revoke cleanly — you use a dedicated machine-user account for the GITOPS_PAT rather than a personal token. The payoff: scoped, role-specific credentials mean each stage of the pipeline holds exactly the access it needs and no more, so a leak stays contained and cleanly revocable.
bash
# Verify the pipeline secrets are configured in GitHub:
# Repository → Settings → Secrets and variables → Actions
# Required secrets:
# ┌─────────────────┬────────────────────────────────────────────────┐
# │ Secret name     │ Value source                                   │
# ├─────────────────┼────────────────────────────────────────────────┤
# │ ECR_ROLE_ARN    │ arn:aws:iam::123456789:role/india-squad-ecr    │
# │ SNYK_TOKEN      │ Snyk account → Settings → API tokens          │
# │ GITOPS_PAT      │ GitHub → Settings → Developer settings → PATs │
# └─────────────────┴────────────────────────────────────────────────┘

# Test OIDC federation before the full pipeline run:
# Create a minimal test workflow that only runs configure-aws-credentials
# and tries to list ECR repositories. If it succeeds, ECR_ROLE_ARN is correct.
aws ecr describe-repositories --region ap-south-1
# Expected: your ECR repositories listed, including india-squad-api

Step 2 — Add the Custom Semgrep Rules

Create the `.semgrep/` directory with the custom india-squad security rules from M3 Exercise 20. These rules detect hardcoded credentials and Flask debug mode—patterns specific to the project that the OWASP pack does not cover. The directory must contain at least one `.yaml` rule file; an empty `.semgrep/` directory causes Semgrep to silently skip custom rules without failing the pipeline.

Analogy🏏Cricket
🏏 Think of it like cricket: The standard OWASP rule pack is like the ICC's general code of conduct — it catches the universal offences every match watches for. But your custom Semgrep rules are like a specific ground's supplementary regulations that address hazards unique to that venue: a particular boundary rope that must never be crossed, a local pitch condition to flag. Just as a home ground writes down its own extra rules to catch dangers the general code does not mention, you author custom rules in the `.semgrep/` directory to detect this project's specific patterns — hardcoded credentials and Flask debug mode left enabled — that the OWASP pack does not cover. And just as an empty supplementary rulebook means the officials silently enforce nothing extra, an empty `.semgrep/` directory makes Semgrep quietly skip custom rules without failing the pipeline — so the folder must contain at least one `.yaml` rule file. The payoff: project-specific rules catch the project-specific mistakes that a generic, one-size-fits-all scanner will always miss.
bash
mkdir -p .semgrep

cat > .semgrep/india-squad.yaml << 'EOF'
rules:
  - id: hardcoded-india-squad-credential
    message: 'Hardcoded credential detected. Use environment variables or secrets manager.'
    severity: ERROR
    languages: [python]
    patterns:
      - pattern: $VAR = '...'
      - metavariable-regex:
          metavariable: $VAR
          regex: '(?i)(key|secret|password|token|credential)'
  - id: flask-debug-enabled
    message: 'Flask debug mode must not be enabled in production code.'
    severity: ERROR
    languages: [python]
    pattern: app.run(debug=True)
EOF

git add .semgrep/
git commit -m 'ci: add custom Semgrep rules'

Step 3 — Test the Full Pipeline

Push the complete workflow file and observe all four jobs run in the correct sequence. The SAST and SCA jobs should complete in parallel within 90 seconds. The build-scan-sign job should complete within 3-5 minutes. The update-gitops job should complete within 30 seconds and create a new commit in the GitOps repository. Verify the Cosign signature on the pushed image.

Analogy🏏Cricket
🏏 Think of it like cricket: Testing the full pipeline is like running a complete pre-season practice match to confirm every phase of your side's routine fires in the right order and within its allotted time. Just as the warm-up drills run in parallel before the toss, the SAST and SCA jobs run side by side and finish within about 90 seconds. Just as the main innings takes the bulk of the session, the build-scan-sign job runs for three to five minutes, ending by stamping the ball — the Cosign signature — onto the image. And just as the scorer records the result into the official ledger the moment play ends, the update-gitops job finishes in about 30 seconds by writing a fresh commit into the GitOps repository. Just as a captain watches the whole match to confirm the plan holds together, you observe all four jobs run in sequence and verify the signature on the pushed image. The payoff: exercising the entire pipeline end to end proves the stages hand off correctly before real commits ever depend on it.
bash
# Push the workflow file
git add .github/workflows/capstone-ci.yml
git commit -m 'ci: add complete capstone CI pipeline'
git push origin main

# Monitor the pipeline run:
# https://github.com/MY_ORG/india-squad-helm/actions

# After the pipeline completes, verify the signature:
IMAGE=$(aws ecr describe-images \
  --repository-name india-squad-api \
  --region ap-south-1 \
  --query 'sort_by(imageDetails, &imagePushedAt)[-1].imageTags[0]' \
  --output text)

cosign verify \
  --certificate-identity-regexp='https://github.com/MY_ORG/india-squad-helm' \
  --certificate-oidc-issuer='https://token.actions.githubusercontent.com' \
  123456789.dkr.ecr.ap-south-1.amazonaws.com/india-squad-api:${IMAGE}
# Expected: Verification for ...: true

# Verify the GitOps repo was updated:
cd ../india-squad-gitops
git pull
git log --oneline -3
# Expected: latest commit from india-squad-ci-bot

Warning: The update-gitops job uses a GitHub PAT to push to the GitOps repository. This PAT should have the minimum required scope—`repo` for the GitOps repository only—and should be stored as a repository secret with access restricted to the specific workflow. Rotate the PAT every 90 days or configure a GitHub App with fine-grained permissions for the GitOps repository update. Never use a personal access token from an active developer account for CI automation; use a dedicated machine user or GitHub App to ensure the token's revocation does not disrupt anyone's personal workflow.

Capstone Tip: Add a pipeline status badge to the india-squad-helm README by adding `![CI](https://github.com/MY_ORG/india-squad-helm/actions/workflows/capstone-ci.yml/badge.svg)`. The badge provides at-a-glance pipeline health and is conventionally the first indicator a reviewer checks when evaluating a repository's CI/CD maturity.

Lesson 30 of 33
0% complete