DRY GitHub Actions with reusable workflows
Extract shared build-and-scan steps into a reusable workflow and call it from every repo with inputs and secrets.
Fifty repos with fifty nearly identical .github/workflows/ci.yml files is how security policy drifts: one team pins actions/checkout@v4, another stays on v2, and nobody owns the Trivy flags. Reusable workflows in GitHub Actions define workflow_call once — build, test, scan, sign — and caller repos invoke it with inputs and secrets. Fix OIDC trust or a scanner threshold in one repo; bump a tag to roll it everywhere.
You will author a callable workflow in a templates repo, invoke it from an application repo with secrets: inherit, and pin the ref you call. GitLab has includes and components with the same goal; the pattern generalizes across CI systems covered in Secure CI/CD with GitLab.
The reusable workflow runs in the caller repo context — it sees the caller checkout and secrets you pass or inherit. Treat the template repo like a dependency with semver.
Define the reusable workflow once
Put org-wide steps here: checkout, setup language, build image, Trivy or Grype gate, Cosign sign, SBOM upload. Expose inputs for image name, Dockerfile path, and severity threshold; declare which secrets callers must provide (registry token, Cosign key optional if using OIDC).
name: Reusable build and scanon:workflow_call:inputs:image:required: truetype: stringseverity:required: falsetype: stringdefault: HIGH,CRITICALsecrets:registry_token:required: falsejobs:build:runs-on: ubuntu-latestpermissions:contents: readpackages: writeid-token: writesteps:- uses: actions/checkout@v4- run: docker build -t ${{ inputs.image }} .- uses: aquasecurity/[email protected]with:image-ref: ${{ inputs.image }}severity: ${{ inputs.severity }}exit-code: 1
Call it from every application repo
The caller workflow stays thin — triggers on push, delegates to the template. secrets: inherit passes repository and organization secrets the reusable workflow declared; alternatively map explicitly so unrelated secrets never leak into the job graph. Document required secrets in the template README so new repos know what to configure before their first green build.
name: CIon:push:branches: [main]pull_request:jobs:pipeline:uses: acme/ci-templates/.github/workflows/[email protected]with:image: ghcr.io/acme/api:${{ github.sha }}severity: HIGH,CRITICALsecrets: inherit
grep "uses: acme/ci-templates" .github/workflows/ci.ymluses: acme/ci-templates/.github/workflows/[email protected]bump v1.4.0 → v1.5.0 when the template adds a scanner gateuses: .../build-scan.yml@main — silent org-wide behavior changeGovernance: who can change the template
Restrict write access on the templates repo. Tag releases with changelog notes when scanner flags or action SHAs change. Caller repos should depend on tags or full SHAs — not floating branches. Code review on the template is code review on fifty pipelines at once; that is the point and the risk.
Composite actions solve smaller reuse (a single job step bundle); reusable workflows own entire jobs with their own runner matrix and permissions block. Reach for workflows when you need a standard job — scan gate, deploy to staging, sign and push — not just a shared checkout snippet.
Where this goes next
Reuse gets you consistent gates; supply-chain maturity adds SBOM archival, provenance attestation, and admission policies that verify signatures. Wire Cosign and SLSA-style provenance into the same template so security wins ship with semver. Software supply chain security covers scanning, signing, and policy end to end.
Go deeper in a courseSoftware supply chain securityPipeline reuse, pinning, SBOMs, and org-wide secure defaults.View course