Lab: build a protected YAML pipeline

Outcome

Build a GitHub Actions workflow that runs tests on pull requests, publishes a small artifact, and requires approval before a production deployment job. You will deliberately break a test and the environment approval path, then diagnose the exact guard that stopped release.

Before you begin

Use a personal training repository. Do not use a production repository, production environment, or real credentials. You need permission to create a workflow, branch-protection rule, and a GitHub environment.

Learn

  • YAML defines pipeline behavior in version control.
  • Pull-request triggers validate proposed changes before merge; an environment protects a deployment target after build.
  • Tests, required checks, approvals, secrets, and retention are separate controls. Passing one does not bypass another.

Do

  1. Create test.sh with #!/usr/bin/env bash, set -euo pipefail, and echo "tests passed". Make it executable and commit it.
  2. Create .github/workflows/quality-and-release.yml:
name: quality-and-release
 
on:
  pull_request:
  push:
    branches: [main]
 
permissions:
  contents: read
 
jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: ./test.sh
      - uses: actions/upload-artifact@v4
        with:
          name: test-evidence
          path: test.sh
          retention-days: 14
 
  deploy-training:
    if: github.ref == 'refs/heads/main'
    needs: test
    runs-on: ubuntu-latest
    environment: training-production
    steps:
      - run: echo 'Safe training deployment after approval'
  1. In repository settings, create the training-production environment. Add yourself as a required reviewer. Do not add a real deployment secret.
  2. Protect main: require a pull request and require the quality-and-release / test status check before merging.
  3. Open a pull request. Confirm that test runs and that deploy-training does not run for the pull request. Merge it, then approve the environment deployment.

Verify

Record all of the following:

  1. The pull request’s passing test check.
  2. The artifact named test-evidence and its 14-day retention setting.
  3. The merged run paused at the training-production environment before approval.
  4. The deployment job’s successful log after approval.

Explain why a passing test cannot automatically satisfy the production environment approval.

Add test results and coverage

Replace the shell-only test with a small JavaScript test that produces machine-readable evidence. In the training repository, add package.json and src/math.js:

{
  "private": true,
  "devDependencies": { "c8": "^10.1.3", "mocha": "^11.0.1", "mocha-junit-reporter": "^2.2.1" },
  "scripts": {
    "test:ci": "c8 --reporter=cobertura --reporter=text mocha --reporter mocha-junit-reporter --reporter-options mochaFile=test-results.xml test/**/*.test.js"
  }
}
// src/math.js
export function fee(amount) { return amount >= 100 ? 0 : 5 }
// test/math.test.js
import assert from 'node:assert/strict'
import { fee } from '../src/math.js'
describe('fee', () => it('waives the fee at 100', () => assert.equal(fee(100), 0)))

Add "type": "module" to package.json. Replace the test job steps after checkout with:

      - uses: actions/setup-node@v4
        with: { node-version: 22, cache: npm }
      - run: npm ci
      - run: npm run test:ci
      - name: Require at least 80% line coverage
        run: node -e "const c=require('fs').readFileSync('coverage/cobertura-coverage.xml','utf8'); const m=c.match(/line-rate=\"([\d.]+)\"/); if (!m || Number(m[1]) < .8) process.exit(1)"
      - if: always()
        uses: actions/upload-artifact@v4
        with:
          name: test-and-coverage-evidence
          path: |
            test-results.xml
            coverage/
          retention-days: 14

Keep the original artifact only if it teaches a separate purpose; the test-results XML and Cobertura report are the quality evidence. GitHub can also display coverage directly on pull requests through its Code Quality upload path, which requires code-quality: write; do not grant that permission to untrusted fork pull requests without following GitHub’s documented security guidance.

Run the workflow once with the single test. Then add assert.equal(fee(10), 5) and observe the coverage increase. Finally add an uncovered branch to fee, or set the threshold to 1, and confirm the threshold step blocks deployment while the if: always() artifact still uploads. Restore the justified 80% threshold and the intended code.

Scenario practice

  1. Configure: Choose a test layer, coverage threshold, artifact retention period, and environment approval for a release with an audit requirement.
  2. Diagnose: A workflow uploads test evidence but does not deploy. Which test, coverage, condition, environment, and artifact observations explain the stop?
  3. Distinguish: Contrast an Actions required check, a coverage threshold, an environment approval, and an artifact-retention policy.

Break and fix

  1. On a branch, change test.sh to exit 1. The test job should fail and prevent deployment. Restore exit 0/successful behavior, then rerun.
  2. Remove the environment name from the deployment job on a training branch. Observe that no environment approval is requested. Restore it and explain why this is a governance regression even though the YAML still runs.
  3. Change retention-days to a value that violates your imagined 30-day audit requirement. Restore a justified value and write the requirement next to the pipeline policy, not only in a developer’s memory.

Extend

Replace the echo deployment with an Azure deployment only after you can use OIDC/workload identity federation and a least-privilege scope. Do not start with a long-lived Azure credential stored as a GitHub secret.

Clean up

Delete the training repository or the training-production environment and any artifacts when the lab is complete. Remove branch protection only if the repository is being discarded.

Sources and re-audit

Re-audit when GitHub Actions permissions, artifact behavior, or Code Quality coverage upload changes.

CI MOC · deployment-environment · pipeline-retention · secretless-pipeline-authentication · All labs