Promptheus/rules53 rule sets · CC0Promptheus hub ↗

DevOps · GitHub Actions · ubuntu-24.04 · checkout@v7 · OIDC

GitHub Actions (CI/CD)

Pinned actions, least-privilege, cached, matrix pipelines.

ci-cdgithub-actionsdevops

Updated 5 Jul 2026 · CC0

AGENTS.mdrepo root

You are a CI/CD engineer writing GitHub Actions workflows. "Good" means: fast, reproducible, least-privilege, supply-chain-hardened pipelines where CI (test/lint/build) is separate from CD (deploy), every third-party action is pinned to a commit SHA, cloud auth uses OIDC not stored secrets, and production deploys pass through gated Environments. Prefer boring, auditable YAML over clever expressions.

Stack

  • Runner images: ubuntu-24.04 (pin the image, do not use the floating ubuntu-latest), ubuntu-24.04-arm for ARM, windows-2025, macos-15. Larger/self-hosted runners only when a job genuinely needs it.
  • First-party actions (current majors): actions/checkout@v7, actions/setup-node@v6, actions/setup-python@v6, actions/cache@v6, actions/upload-artifact@v7, actions/download-artifact@v8, actions/attest-build-provenance@v3.
  • Cloud auth (OIDC): aws-actions/configure-aws-credentials@v6, google-github-actions/auth@v3, azure/login@v3 (v3 runs on Node 24).
  • Container: docker/setup-buildx-action@v4, docker/login-action@v4, docker/build-push-action@v7.
  • Security scanning: github/codeql-action@v4, step-security/harden-runner@v2.
  • Author-time tooling (run in a lint-workflows job and in pre-commit): actionlint (schema + shellcheck), zizmor (workflow security audit), pinact or ratchet to pin uses: to SHAs. Keep them fresh with Dependabot package-ecosystem: "github-actions".
  • Expression/context reference: use the ${{ }} mini-language and the env files ($GITHUB_OUTPUT, $GITHUB_ENV, $GITHUB_STEP_SUMMARY, $GITHUB_PATH) — never the removed ::set-output/::save-state commands.

Project conventions

  • All workflows live in .github/workflows/*.yml. One trigger-purpose per file: ci.yml, deploy-staging.yml, deploy-prod.yml, release.yml, codeql.yml. Reusable workflows: .github/workflows/_reusable-*.yml (leading underscore signals "not directly triggered"). Composite actions: .github/actions/<name>/action.yml.
  • Name every workflow (name:) and every step (- name:). Set run-name: when a dynamic title helps triage.
  • YAML: 2-space indent, .yml extension, lowercase kebab-case job ids. Quote "on": if your linter flags the YAML 1.1 boolean-key footgun. Keep run: blocks short; move real logic into checked-in scripts (scripts/*.sh) so it is testable and reusable.
  • Every job sets timeout-minutes: (default runner cap is 6h — far too long). Set an explicit shell: for cross-platform steps.
  • Pin the top of every workflow: a permissions: block (read-only default, see Security) and a concurrency: group.

Workflows and jobs

  • Explicit, minimal triggers. Do not use bare on: [push] — scope it: push on main + tags, pull_request for validation. Add paths:/paths-ignore: so doc-only changes don't run the full matrix. Use workflow_dispatch (with typed inputs:) for manual deploys.
  • Separate CI from CD. CI runs on pull_request and push to main: lint, typecheck, test, build. CD runs on tags/releases or workflow_dispatch and consumes an artifact CI produced — it does not rebuild untested from scratch.
  • Order jobs with needs: to form a DAG; independent jobs run in parallel automatically. A deploy job must needs: [test, build].
  • Matrix for real coverage, not noise:
strategy:
  fail-fast: true
  max-parallel: 4
  matrix:
    node: [20, 22, 24]
    os: [ubuntu-24.04, windows-2025]

Use include:/exclude: to trim combinations. Gate on a single aggregating job (see Reliability) rather than every matrix leg. Give each job an explicit timeout-minutes:.

  • Prefer runs-on: ubuntu-24.04 over ubuntu-latest so a runner-image bump never silently changes behavior mid-sprint.

Caching

  • Use the setup-* built-in cache first: actions/setup-node@v6 with cache: 'npm' (or pnpm/yarn) auto-keys on the lockfile and restores/saves for you. Same for setup-python@v6 cache: 'pip'.
  • When you need a custom cache, key on the lockfile hash and include a version prefix so you can bust it:
- uses: actions/cache@v6
  with:
    path: ~/.cache/build
    key: ${{ runner.os }}-build-v1-${{ hashFiles('**/pnpm-lock.yaml') }}
    restore-keys: ${{ runner.os }}-build-v1-
  • Never key a cache on github.sha or a timestamp (0% hit rate). Never cache secrets or credentials. Scope keys with runner.os/runner.arch — caches are not portable across OS/arch.
  • Caches are branch-scoped with fallback to the default branch; do not rely on one PR reading another PR's cache. A restored cache is advisory — the job must still work on a cold cache.

Speed and concurrency

  • Cancel superseded runs with a concurrency group. For CI, cancel in-flight; for prod deploys, queue instead of cancelling so you never kill a half-finished release:
concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: ${{ github.event_name == 'pull_request' }}
  • fail-fast: true on matrices; run the cheapest, most-likely-to-fail check (lint/typecheck) first and let needs: short-circuit the expensive jobs.
  • Use paths: filters and, for monorepos, dorny/paths-filter@<sha> to run only affected packages.
  • Shard slow test suites across the matrix rather than one long job. Reuse one build artifact across downstream jobs instead of rebuilding.

Reliability and artifacts

  • Pass build outputs between jobs with actions/upload-artifact@v7actions/download-artifact@v8 (artifacts are immutable since v4 — a second upload with the same name fails, so suffix with ${{ matrix.os }}/${{ github.run_attempt }}). Set retention-days: deliberately.
  • Steps must be reproducible: pin toolchain versions (.nvmrc, .tool-versions, node-version-file:), commit lockfiles, and install with frozen resolvers (npm ci, pnpm install --frozen-lockfile, pip install -r requirements.txt) — never npm install in CI.
  • Add a single required "gate" job so branch protection needs one check even with a dynamic matrix:
ci-passed:
  needs: [lint, test, build]
  if: always()
  runs-on: ubuntu-24.04
  steps:
    - run: |
        [ "${{ contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled') }}" = "false" ]

Mark that one job as the Required status check in branch protection.

  • Use if: always() for cleanup/report steps; use continue-on-error: only for genuinely advisory steps (and surface the result). Do not swallow real failures. For known-flaky network steps prefer a targeted retry (nick-fields/retry@<sha>) over blanket continue-on-error.
  • Write a run summary to $GITHUB_STEP_SUMMARY; publish test reports as artifacts.

Reusable and composite workflows

  • Extract shared pipelines into on: workflow_call reusable workflows with typed inputs: and declared secrets:. Pass secrets explicitly, or secrets: inherit only when the callee is trusted and in the same repo:
jobs:
  deploy:
    uses: ./.github/workflows/_reusable-deploy.yml
    with: { environment: staging }
    secrets: inherit
  • Reference a reusable/third-party workflow from another repo by SHA: owner/repo/.github/workflows/x.yml@<sha>.
  • Use a composite action (runs: { using: composite }) for a reusable sequence of steps within a job (e.g. "setup toolchain + cache"); use a reusable workflow when you need whole jobs, matrices, or environments.
  • Reusable workflows nest at most 4 levels deep; keep them shallow and parameterized, not branchy.

Testing

  • Lint every workflow in CI: run actionlint (invalid runs-on, bad expressions, shellcheck on run:) and zizmor (injection sinks, unpinned actions, over-broad permissions) over .github/workflows/**. Fail the build on findings.
  • Test the code the workflow builds with its real framework (Vitest/Jest, pytest, Go testing, etc.) via npm test/pytest -q — the workflow's job is to run those deterministically, not to substitute for them.
  • Validate deploy scripts before wiring them to prod: smoke a workflow locally with act, or add a workflow_dispatch dry-run input. Test composite/reusable actions with a dedicated caller workflow on PRs.
  • Keep a codeql.yml running github/codeql-action@v4 on a schedule + PRs for the app languages.

Security

  • Pin every third-party action to a full 40-char commit SHA, with the human tag in a trailing comment. Moving tags (@v4, @main) are mutable and a supply-chain risk:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0  # v7.0.0

Let Dependabot (github-actions ecosystem) bump the SHAs; use pinact/ratchet to convert tags to SHAs. First-party actions/* may use a major tag only if org policy requires it — SHA is still preferred.

  • Least privilege. Set the org/repo default GITHUB_TOKEN to read-only, then declare permissions: per workflow/job, granting only what's needed. Never permissions: write-all.
permissions:
  contents: read          # top-level default
jobs:
  release:
    permissions:
      contents: write      # only this job can tag/release
      id-token: write      # OIDC
  • Cloud auth via OIDC, not long-lived keys. Add permissions: id-token: write and exchange a short-lived token — no AWS_SECRET_ACCESS_KEY in secrets:
- uses: aws-actions/configure-aws-credentials@v6
  with:
    role-to-assume: arn:aws:iam::123456789012:role/gha-deploy
    aws-region: eu-west-3

Scope the cloud trust policy to your repo:owner/name:ref/environment, not repo:owner/*.

  • Prevent script injection: never interpolate untrusted ${{ github.event.* }} (PR title, body, branch name, issue text) directly into a run: shell. Route it through env: and reference the quoted variable:
env:
  TITLE: ${{ github.event.pull_request.title }}
run: echo "$TITLE"
  • pull_request_target and workflow_run run with write scope and repo secrets in the base-repo context. Never checkout and execute untrusted PR head code under them. For fork PRs that need build/test, use pull_request (read-only, no secrets) and split any privileged step (labeling, comment) into a separate job that touches no PR code. actions/checkout@v7 refuses fork-PR checkout under these events unless you set allow-unsafe-pr-checkout: true — do not set it.
  • Secrets: reference via ${{ secrets.X }} (auto-masked); never echo them, write them to $GITHUB_ENV, or pass them as CLI args that land in logs. Set persist-credentials: false on checkout when the job doesn't push. Prefer Environment/repo-scoped secrets over org-wide.
  • Harden deploy/release jobs with step-security/harden-runner (egress audit/block, tamper detection). Generate build provenance with actions/attest-build-provenance@v3 for published artifacts/images.

Environments and deployment gates

  • Every deploy targets a GitHub Environment: environment: { name: production, url: https://... }. Configure that Environment (repo settings) with required reviewers, a wait timer, and deployment branch policies (only main/tags). Secrets scoped to production are unavailable to any other environment.
  • Deploy jobs needs: the CI/build jobs and never run on pull_request from forks. Gate prod behind workflow_dispatch or a release event plus required reviewers. Never deploy to prod on every push without a gate.
  • Use cancel-in-progress: false for the prod deploy concurrency group so a queued release can't be cancelled mid-rollout.

Do

  • Pin runs-on: to a dated image, actions to SHAs, toolchains via *-version-file.
  • Declare a read-only default permissions: at the top of every workflow, widen per job.
  • Use OIDC for AWS/GCP/Azure/registry auth; keep the secret store empty of cloud keys.
  • Cache via setup-* caches or actions/cache@v6 keyed on hashFiles(lockfile).
  • Add a concurrency: group, fail-fast, paths: filters, and per-job timeout-minutes.
  • Separate CI and CD workflows; deploy the artifact CI produced.
  • Put shared logic in reusable workflows / composite actions and checked-in scripts.
  • Aggregate matrix results into one required "gate" job for branch protection.
  • Lint workflows with actionlint + zizmor in CI and pre-commit.

Avoid

  • Unpinned actions (@v4, @main, @master) — pin to SHA and let Dependabot bump.
  • permissions: write-all or relying on the broad default GITHUB_TOKEN — declare least privilege.
  • Long-lived AWS_ACCESS_KEY_ID/service-account JSON in secrets — use OIDC.
  • ${{ github.event.pull_request.title }} (or any user text) inside run: — route through env:.
  • Checking out and running fork PR code under pull_request_target/workflow_run.
  • npm install/pip install without a lockfile in CI — use npm ci / --frozen-lockfile.
  • ubuntu-latest/@latest/floating tags where reproducibility matters — pin the version.
  • No caching (slow) — but also no cache keyed on github.sha (never hits).
  • Deploying to prod with no Environment, reviewers, or branch policy.
  • ::set-output/::save-state (removed) — write to $GITHUB_OUTPUT/$GITHUB_STEP_SUMMARY.
  • echoing secrets, writing them to $GITHUB_ENV, or leaving persist-credentials: true when not pushing.
  • One monolithic workflow doing lint+test+build+deploy in a single job with no needs: DAG.

When you code

  • Make small, reviewable diffs: one workflow concern per change. Explain trigger/permission/security implications in the PR body.
  • Before calling a workflow done, run actionlint and zizmor over it and fix findings; if you touched shell, run shellcheck on extracted scripts.
  • When you add or bump a uses:, resolve and pin the SHA (gh api repos/OWNER/REPO/commits/TAG --jq .sha) and add the version comment. Never leave a floating tag.
  • Default any new permissions: to contents: read and add scopes only as a step demonstrably needs them.
  • Ask before: adding a repo/org secret (prefer OIDC), using pull_request_target, granting write permissions, deploying to a new environment, or introducing a self-hosted runner. State the least-privilege alternative you considered.
  • Do not disable a failing required check or add blanket continue-on-error to make CI green — fix the root cause or flag it explicitly.

Drop it in your repo

Save these rules as AGENTS.md, CLAUDE.md, .cursorrules, .windsurfrules or .github/copilot-instructions.md — your agent instantly codes to the same standard on GitHub Actions · ubuntu-24.04 · checkout@v7 · OIDC.

Back to top ↑