TL;DR

GitHub Actions pipelines live in .github/workflows/*.yaml. Separate build, test, and deploy jobs. Use OIDC (not long-lived secrets) for AWS auth. For GitOps clusters, CI should update the manifest repo — not run kubectl apply directly.

Full CI Pipeline

This template covers lint, test, Docker build + push to ECR, and manifest update for GitOps — all in one workflow triggered on PR and push to main.

yaml.github/workflows/ci.yaml
name: CI

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

permissions:
  contents: read
  id-token: write    # required for OIDC

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v4
    - uses: actions/setup-python@v5
      with: {python-version: "3.12"}
    - run: pip install -r requirements.txt
    - run: pytest --cov=app tests/

  build-push:
    needs: test
    if: github.ref == 'refs/heads/main'
    runs-on: ubuntu-latest
    outputs:
      image: ${{ steps.meta.outputs.tags }}
      digest: ${{ steps.push.outputs.digest }}
    steps:
    - uses: actions/checkout@v4

    # Authenticate to AWS using OIDC — no long-lived secrets
    - uses: aws-actions/configure-aws-credentials@v4
      with:
        role-to-assume: arn:aws:iam::123456789:role/github-actions-ecr
        aws-region: us-east-1

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

    - name: Docker metadata
      id: meta
      uses: docker/metadata-action@v5
      with:
        images: 123456789.dkr.ecr.us-east-1.amazonaws.com/myapp
        tags: |
          type=sha,prefix=,format=short
          type=raw,value=latest,enable={{is_default_branch}}

    - uses: docker/build-push-action@v5
      id: push
      with:
        context: .
        push: true
        tags: ${{ steps.meta.outputs.tags }}
        cache-from: type=gha
        cache-to: type=gha,mode=max

    # Sign image with cosign (keyless)
    - uses: sigstore/cosign-installer@v3
    - run: cosign sign --yes 123456789.dkr.ecr.us-east-1.amazonaws.com/myapp@${{ steps.push.outputs.digest }}

  update-manifests:
    needs: build-push
    if: github.ref == 'refs/heads/main'
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v4
      with:
        repository: myorg/k8s-manifests
        token: ${{ secrets.MANIFEST_REPO_PAT }}
    - name: Update image tag in manifest
      run: |
        cd production/myapp
        sed -i "s|image: .*myapp.*|image: 123456789.dkr.ecr.us-east-1.amazonaws.com/myapp@${{ needs.build-push.outputs.digest }}|" deployment.yaml
        git config user.email "ci@myorg.com"
        git config user.name "GitHub Actions"
        git add -A
        git commit -m "chore: update myapp to ${{ github.sha }}" || echo "No changes"
        git push

Reusable Workflows

Extract common CI logic (Docker build, Trivy scan, Helm lint) into reusable workflows in a central repo; call them from application repos to avoid duplicating pipeline code across projects.

yaml.github/workflows/caller.yaml
jobs:
  build:
    uses: myorg/platform-workflows/.github/workflows/docker-build.yaml@main
    with:
      image-name: myapp
      ecr-repo: 123456789.dkr.ecr.us-east-1.amazonaws.com/myapp
    secrets: inherit   # pass all secrets from calling workflow

Useful Tips

  • Use actions/checkout@v4 with fetch-depth: 0 when you need the full history (e.g., for git log, semantic versioning, or git diff).
  • Cache dependencies with actions/cache or the setup-* action's built-in cache to keep builds under 2 minutes.
  • Use matrix builds to test across Python/Node versions or OS in parallel without duplicating jobs.
  • !Don't store secrets in workflow files — use GitHub repository/environment secrets and OIDC for cloud auth.
  • !Pin action versions to commit SHAs (e.g., actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683) to prevent supply-chain attacks from tag mutation.