After this lesson, you will be able to: Write a complete CI workflow: lint + type-check + test + build + Docker image build and push, all in one file.
Time to glue it all together. This is the workflow you'll copy into every new project.
Drop this in .github/workflows/ci.yml. Customize the image name + registry.
name: CIon:pull_request:push:branches: [main]concurrency:group: ci-${{ github.ref }}cancel-in-progress: truepermissions:contents: readpackages: write # to push to GHCRid-token: write # for OIDC (cloud auth without long-lived keys)jobs:test:runs-on: ubuntu-lateststeps:- uses: actions/checkout@v4- uses: actions/setup-node@v4with: { node-version: 20, cache: npm }- run: npm ci- run: npm run lint- run: npx tsc --noEmit- run: npm test -- --rundocker:needs: testif: github.ref == 'refs/heads/main'runs-on: ubuntu-lateststeps:- uses: actions/checkout@v4- uses: docker/setup-qemu-action@v3- uses: docker/setup-buildx-action@v3- uses: docker/login-action@v3with:registry: ghcr.iousername: ${{ github.actor }}password: ${{ secrets.GITHUB_TOKEN }}- id: metauses: docker/metadata-action@v5with:images: ghcr.io/${{ github.repository }}tags: |type=sha,prefix=type=ref,event=branchtype=raw,value=latest,enable={{is_default_branch}}- uses: docker/build-push-action@v5with:context: .push: truetags: ${{ steps.meta.outputs.tags }}cache-from: type=ghacache-to: type=gha,mode=max
`concurrency` + `cancel-in-progress`: don't run the 5th workflow for an old commit if a newer commit is in flight. `permissions`: scoped tokens. Default is read; we explicitly grant what we need. `needs: test`: don't waste minutes building a broken image. `if: github.ref == 'refs/heads/main'`: don't push images from PR branches. `docker/metadata-action`: generates good tags (commit SHA + branch + latest on main). `cache-from: type=gha`: Docker layer cache between builds. Massively speeds up image builds.
On PRs, you still want to know if the Docker build succeeds.
# Replace the 'docker' job push: true with conditional push:- uses: docker/build-push-action@v5with:context: .push: ${{ github.event_name != 'pull_request' }}tags: ${{ steps.meta.outputs.tags }}cache-from: type=ghacache-to: type=gha,mode=max# Now: PRs build (catches Dockerfile breakages) but don't push (security).# Pushes to main both build and push.
Building images on every PR and pushing them all to the registry. Storage costs balloon; private registry quota fills up. Caching that uses input-volatile keys (e.g. timestamp). Cache never hits. Skipping image scanning in the workflow. Add a Trivy step; fail on critical CVEs. Using `:latest` as the only tag. Rollback becomes ambiguous. Always tag with SHA. Trusting `GITHUB_TOKEN` for everything. For deploys to AWS / GCP, use OIDC federation (no long-lived keys).
Pick the cleanest answer.
Sign in and purchase access to unlock this lesson.