Lesson 04 GitHub Actions Pipeline
Theme:

Lab 04 — Wire Up Your First GitHub Actions Pipeline

Lesson 04 · Cloud Infrastructure & DevOps

Goal: end the lab with ci.yml committed on a feature branch, the pipeline running on push, and a green check on a pull request.

Prerequisites

Step 1 — Create the workflow file

The complete ci.yml is already committed in the course repository at cloud-devops/sample-app/.github/workflows/ci.yml. Walk through each section to understand it before running it.

name: CI

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

# Cancel superseded runs on the same branch (saves minutes).
concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: true

jobs:
  lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with:
          python-version: "3.11"
          cache: pip
          cache-dependency-path: |
            cloud-devops/sample-app/api/requirements.txt
            cloud-devops/sample-app/worker/requirements.txt
      - name: Install ruff
        run: pip install ruff==0.6.9
      - name: Lint
        run: ruff check cloud-devops/sample-app/api cloud-devops/sample-app/worker
      - name: Format check
        run: ruff format --check cloud-devops/sample-app/api cloud-devops/sample-app/worker

  test:
    runs-on: ubuntu-latest
    strategy:
      fail-fast: false
      matrix:
        python-version: ["3.11", "3.12"]
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with:
          python-version: ${{ matrix.python-version }}
          cache: pip
          cache-dependency-path: |
            cloud-devops/sample-app/api/requirements.txt
            cloud-devops/sample-app/worker/requirements.txt
      - name: Install API deps
        run: pip install -r cloud-devops/sample-app/api/requirements.txt
      - name: Install worker deps
        run: pip install -r cloud-devops/sample-app/worker/requirements.txt
      - name: Run API tests
        run: pytest cloud-devops/sample-app/api/tests -v
      - name: Run worker tests
        run: pytest cloud-devops/sample-app/worker/tests -v

  build:
    runs-on: ubuntu-latest
    needs: [lint, test]
    strategy:
      matrix:
        service: [api, worker]
    steps:
      - uses: actions/checkout@v4
      - uses: docker/setup-buildx-action@v3
      - name: Build ${{ matrix.service }} image
        uses: docker/build-push-action@v5
        with:
          context: ./cloud-devops/sample-app/${{ matrix.service }}
          push: false
          tags: ${{ matrix.service }}:ci-${{ github.sha }}
          cache-from: type=gha,scope=${{ matrix.service }}
          cache-to: type=gha,mode=max,scope=${{ matrix.service }}

What each job does:

Step 2 — Test the workflow locally with act (optional)

nektos/act is a tool that runs GitHub Actions workflows locally against a Docker daemon — useful for iterating without pushing commits. Install on macOS:

brew install act

Run just the lint job:

act -j lint

One paragraph of caution: act emulates the runner environment but is not identical to GitHub’s hosted runners. Actions that rely on runner-specific metadata (e.g., github.sha resolution, OIDC token issuance) may behave differently. Use act for fast local iteration, but treat the real GitHub run as the source of truth.

Step 3 — Commit and push to a feature branch

git checkout -b ci/initial-pipeline
git add cloud-devops/sample-app/.github/workflows/ci.yml
git commit -m "ci: add initial pipeline"
git push -u origin ci/initial-pipeline

The push triggers the pull_request event against main once you open the PR in Step 5, but the push trigger only fires on commits directly to main. Right now you are on a feature branch, so only the PR trigger will fire when you open the PR.

Step 4 — Watch the run

Navigate to your repository on GitHub and click the Actions tab. The CI workflow appears. Click through to see the five jobs in parallel:

Expected timings: cold cache run takes approximately 3–5 minutes total. After the cache warms (second run), expect 1–2 minutes. You can see the cache hit/miss in the “Set up Python” step log.

Step 5 — Open a PR and observe the check

Open a pull request from ci/initial-pipeline → main on GitHub. The CI check appears at the bottom of the PR under “Checks.” At this point the check is informational — it does not block the merge button. That changes in Lesson 05, where branch protection makes these checks required. For now, confirm you can see the green checkmarks from all five jobs.

Step 6 — Intentionally break a test

Edit cloud-devops/sample-app/api/tests/test_app.py. Change one assertion to assert an incorrect value (e.g., change assert response.status_code == 200 to assert response.status_code == 999). Commit and push:

git add cloud-devops/sample-app/api/tests/test_app.py
git commit -m "test: intentional break"
git push

Watch the test matrix on GitHub — both test (3.11) and test (3.12) should turn red. The build jobs will be skipped because test failed (needs: is respected). Then revert the change:

git revert HEAD
git push

Step 7 — Verify the cache is working

On the second or third run after the first push, expand the Set up Python step in one of the test jobs. You should see a line like:

Cache hit: pip-Linux-x64-3.11-...

On the build (api) step, expand the Build api image log. In the BuildKit output, most layers should show CACHED rather than executing. The only layers that rebuild are those after the last changed layer (typically the COPY . . step if you edited source code).

What you should leave with

Troubleshooting

Common problems

“Lint fails on ruff format --check” → run ruff format cloud-devops/sample-app/api cloud-devops/sample-app/worker locally, commit the reformatted files, and push again. The CI format check enforces that files are already auto-formatted.

“Pip install fails with a cache key error” → the cache-dependency-path glob doesn’t match any file. Check that the paths in the YAML exactly match the locations of your requirements.txt files (they are relative to the repository root).

“Docker build times out” → the first run is 3–5 minutes and that is normal. If a run consistently exceeds 10 minutes, the Dockerfile is probably re-installing deps after every code change — review the layer order (copy requirements.txt and pip install before copying the rest of the source; see Lesson 02).

“Actions tab says no workflow runs” → confirm the YAML file is in .github/workflows/ci.yml (not workflows/ci.yml or any other path). GitHub only scans .github/workflows/.