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.
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.
# 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 pushStep 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.
# 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-apiStep 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.
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.
# 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-botWarning: 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 ``. 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.