█
LastWrite
  • > Curriculum
  • > Pricing
  • > For Educators
  • > About
  • > Contact
Log InGet Started

Questions, concerns, bug reports, or suggestions? We read every message, write to us at [email protected].

More ways to reach us →
LastWrite

Structured computer science lessons for aspiring developers and security professionals.

[email protected]

(201) 785-7951

Mon–Fri, 9 AM–5 PM EST

Learn

  • Curriculum
  • Pricing

Company

  • About
  • For Educators & Schools
  • Contact Us

Legal

  • Terms of Service
  • Privacy Policy
© 2026 LastWrite. All rights reserved.
Curriculum/DevOps and Infrastructure/CI/CD Pipelines/A Complete CI Pipeline
55 minIntermediate

A Complete CI Pipeline

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.

Prerequisites:GitHub Actions in Depth

The full CI workflow (with Docker push)

Drop this in .github/workflows/ci.yml. Customize the image name + registry.

tsx
name: CI
on:
pull_request:
push:
branches: [main]
concurrency:
group: ci-${{ github.ref }}
cancel-in-progress: true
permissions:
contents: read
packages: write # to push to GHCR
id-token: write # for OIDC (cloud auth without long-lived keys)
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: 20, cache: npm }
- run: npm ci
- run: npm run lint
- run: npx tsc --noEmit
- run: npm test -- --run
docker:
needs: test
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: docker/setup-qemu-action@v3
- uses: docker/setup-buildx-action@v3
- uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- id: meta
uses: docker/metadata-action@v5
with:
images: ghcr.io/${{ github.repository }}
tags: |
type=sha,prefix=
type=ref,event=branch
type=raw,value=latest,enable={{is_default_branch}}
- uses: docker/build-push-action@v5
with:
context: .
push: true
tags: ${{ steps.meta.outputs.tags }}
cache-from: type=gha
cache-to: type=gha,mode=max

Why each piece matters

`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.

PR-aware version: build but don't push

On PRs, you still want to know if the Docker build succeeds.

tsx
# Replace the 'docker' job push: true with conditional push:
- uses: docker/build-push-action@v5
with:
context: .
push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.meta.outputs.tags }}
cache-from: type=gha
cache-to: type=gha,mode=max
# Now: PRs build (catches Dockerfile breakages) but don't push (security).
# Pushes to main both build and push.

💡 Pin actions by major version + Dependabot

Use `actions/checkout@v4`, not `@v4.0.1` or `@main`. Major-version tag is the sweet spot for stability + getting bug fixes. Enable Dependabot updates for `.github/workflows/`. It opens PRs when actions release new majors. Review + merge. Without this, actions go stale and break randomly when upstream changes.

Common mistakes only experienced engineers avoid

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).

Quick Check

Why use docker/metadata-action instead of hardcoded tags?

Pick the cleanest answer.

Sign in and purchase access to unlock this lesson.

Sign in to purchase
←GitHub Actions in Depth
Back to CI/CD Pipelines
A Complete CD Pipeline→