CI/CD & Software Supply Chain›Modules · Cheat sheet & self-check
Learning Hub / Delivery & Infrastructure as Code / CI/CD & Software Supply Chain

Modules · wrap-up

Cheat sheet & self-check

Every command from this section on one page.

01 · The execution model

Structure

.github/workflows/*.ymlWhere workflows live
on: [push, pull_request, workflow_dispatch, schedule]Events that trigger a workflow
jobs.<id>.runs-on: ubuntu-latestWhich runner (label) runs the job
jobs.<id>.needs: [build]Run after another job (otherwise jobs run in parallel)
permissions: { contents: read }Least-privilege GITHUB_TOKEN

Data & speed

${{ github.sha }} ${{ secrets.X }} ${{ vars.Y }} ${{ needs.build.outputs.digest }}Contexts in expressions
echo "name=value" >> "$GITHUB_OUTPUT"Set a step output
actions/cache / setup-* with cache:Reuse dependencies between runs
actions/upload-artifact / download-artifactPass files between jobs or keep build outputs
concurrency: { group: deploy-prod, cancel-in-progress: false }One deployment at a time

02 · Real pipelines

Reuse

on: workflow_call: { inputs: …, secrets: … }Make a workflow callable
jobs.x.uses: org/ci/.github/workflows/build.yml@v1Call it (job level)
secrets: inheritPass the caller's secrets (same org/enterprise)
action.yml → runs: { using: composite, steps: … }A composite action (reusable steps)

Build & deploy

strategy: { matrix: { python: ["3.11","3.12"] }, fail-fast: false }Test several versions
docker/build-push-action → outputs.digestBuild, push, and capture the image digest
environment: prodJob gated by the prod environment's protection rules
on.push.paths: [ "services/api/**" ]Run only when relevant files change

03 · Actions Runner Controller

Install (Helm, OCI charts)

helm install arc -n arc-systems --create-namespace oci://ghcr.io/actions/actions-runner-controller-charts/gha-runner-scale-set-controllerThe controller
helm install arc-runner-set -n arc-runners --create-namespace -f values.yaml oci://ghcr.io/actions/actions-runner-controller-charts/gha-runner-scale-setA runner scale set
runs-on: arc-runner-setWorkflows target the scale set by its installation name

Inspect

kubectl get pods -n arc-systemsController and listener pods
kubectl get pods -n arc-runners -wRunner pods appearing per job
kubectl get autoscalingrunnersets,ephemeralrunners -AARC's custom resources

04 · OIDC federation & secret hygiene

OIDC in a workflow

permissions: { id-token: write, contents: read }Allow the job to request an OIDC token
aws-actions/configure-aws-credentials (role-to-assume)AWS: assume an IAM role
google-github-actions/auth (workload_identity_provider)GCP: Workload Identity Federation
azure/login (client-id, tenant-id, subscription-id)Azure: federated credential
hashicorp/vault-action (method: jwt)Vault: JWT auth role

Subject claims to trust

repo:acme/infra:ref:refs/heads/mainMain branch of one repo
repo:acme/infra:environment:prodJobs using the prod environment
repo:acme/infra:pull_requestPull request runs (read-only roles only)

05 · Supply chain: sign, attest, scan, verify

In the pipeline

syft ghcr.io/acme/api@$DIGEST -o spdx-json > sbom.jsonGenerate an SBOM
trivy image --exit-code 1 --severity CRITICAL ghcr.io/acme/api@$DIGESTFail on critical vulnerabilities
cosign sign --yes ghcr.io/acme/api@$DIGESTKeyless signing (OIDC identity of the workflow)
cosign attest --yes --type spdxjson --predicate sbom.json ghcr.io/acme/api@$DIGESTAttach the SBOM as a signed attestation
actions/attest-build-provenance (subject-name, subject-digest, push-to-registry)GitHub artifact attestation (SLSA provenance)

Verifying

cosign verify --certificate-identity-regexp '^https://github.com/acme/' --certificate-oidc-issuer https://token.actions.githubusercontent.com IMAGE@DIGESTVerify a keyless signature
gh attestation verify oci://ghcr.io/acme/api@$DIGEST --owner acmeVerify a GitHub attestation

06 · Hardening the runner

Isolation

Ephemeral runners (one job per runner)No state or malware survives between jobs
Separate runner pools/scale sets per trust levelUntrusted PRs never share runners with deploy jobs
Runner groups → selected repositoriesLimit which repos can use which runners
Dedicated, tainted nodes for runnersKeep CI pods away from production workloads

Workflow hygiene

uses: owner/action@<full commit SHA>Pin third-party actions
permissions: {} at top, grant per jobLeast-privilege GITHUB_TOKEN
Avoid pull_request_target + checkout of PR codeClassic secret-exfiltration path
NetworkPolicy egress allow-list for runner podsLimit where a compromised job can send data

07 · The air-gapped port

Server & runner

app.ini: [actions] ENABLED = trueEnable Actions (on by default in recent Gitea versions)
act_runner register --instance https://git.internal --token <token>Register a Gitea runner (token from the admin/org/repo settings)
act_runner daemonStart the runner
forgejo-runner register / forgejo-runner daemonThe Forgejo equivalent
.gitea/workflows/ or .forgejo/workflows/Workflow folders (.github/workflows is also read by Gitea)

Offline dependencies

Mirror actions repos into the internal forgeuses: resolves to your mirrors (DEFAULT_ACTIONS_URL setting)
Internal registry (e.g. Harbor) + pull-through/replicationBase images and build output
Package mirrors (apt/rpm, PyPI, npm, Go, Maven)Builds never reach the internet
Trivy offline DB, cosign with keysScanning and signing without public services

08 · Jenkins in the real world

Jenkinsfile

pipeline { agent … stages { stage('x') { steps { … } } } post { … } }Declarative skeleton
options { timeout(time: 30, unit: 'MINUTES'); buildDiscarder(logRotator(numToKeepStr: '30')) }Timeouts and build retention
withCredentials([string(credentialsId: 'api-token', variable: 'TOKEN')]) { sh 'use-token' }Credentials binding (masked in logs)
@Library('platform-lib@v2') _Load a versioned shared library
agent { kubernetes { yaml '''…pod spec…''' } }Ephemeral pod agent (Kubernetes plugin)

Operating

Built-in node executors: 0Never build on the controller
Configuration as Code (JCasC) YAMLController config in Git
plugins.txt + jenkins-plugin-cliPinned plugin set, baked into the image
Back up JENKINS_HOME (jobs, credentials, config)Restore path for the controller