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
- Create
test.shwith#!/usr/bin/env bash,set -euo pipefail, andecho "tests passed". Make it executable and commit it. - 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'- In repository settings, create the
training-productionenvironment. Add yourself as a required reviewer. Do not add a real deployment secret. - Protect
main: require a pull request and require thequality-and-release / teststatus check before merging. - Open a pull request. Confirm that
testruns and thatdeploy-trainingdoes not run for the pull request. Merge it, then approve the environment deployment.
Verify
Record all of the following:
- The pull request’s passing test check.
- The artifact named
test-evidenceand its 14-day retention setting. - The merged run paused at the
training-productionenvironment before approval. - 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: 14Keep 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
- Configure: Choose a test layer, coverage threshold, artifact retention period, and environment approval for a release with an audit requirement.
- Diagnose: A workflow uploads test evidence but does not deploy. Which test, coverage, condition, environment, and artifact observations explain the stop?
- Distinguish: Contrast an Actions required check, a coverage threshold, an environment approval, and an artifact-retention policy.
Break and fix
- On a branch, change
test.shtoexit 1. The test job should fail and prevent deployment. Restoreexit 0/successful behavior, then rerun. - 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.
- Change
retention-daysto 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
- Set up code coverage for a GitHub repository, checked 2026-08-22.
- Store workflow artifacts, checked 2026-08-22.
Re-audit when GitHub Actions permissions, artifact behavior, or Code Quality coverage upload changes.
Related
CI MOC · deployment-environment · pipeline-retention · secretless-pipeline-authentication · All labs