Skip to content

CI Dependency Pinning Policy

Every external dependency the CI pipeline consumes must be pinned by 40-hex commit SHA. The policy applies to:

  1. Third-party actionsuses: owner/action@<sha> (already enforced via Scorecard's Pinned-Dependencies check at score ≥ 9).
  2. Reusable workflows in this repouses: sebastienrousseau/dotfiles/.github/workflows/reusable-X.yml@<sha> (added with #855; enforced by tools/ci/lint-reusable-pins.sh).
  3. Container base imagesFROM image:tag@sha256:<digest> (closed by #886).
  4. Release binaries downloaded at build timecurl … && echo "<sha256> ..." | sha256sum -c (closed by #888).

The sole exception is the SLSA generic reusable workflow. Its bootstrap validates that the caller reference has the form refs/tags/vX.Y.Z and fails when invoked through a bare commit SHA. Therefore generator_generic_slsa3.yml is pinned to the exact release tag v2.1.0; the corresponding commit SHA is recorded beside the call site and must be verified before any tag bump. OpenSSF Scorecard explicitly exempts the SLSA generator from its SHA-pinning check for this constraint.

Why SHA-pin reusable workflows

When ci.yml calls a reusable via ./.github/workflows/reusable-X.yml, GitHub resolves the reusable from the same ref as the calling workflow at run time. For an in-repo PR that's the PR's branch — fine. The risk is the inverse: a malicious push to main (or any ref the calling workflow might resolve from) can swap reusable content under a CI run, with no audit trail in the PR diff.

Pinning to a 40-hex SHA freezes the reusable's content at the pinned commit. To swap the reusable, you have to bump every call site — visible in the PR diff, reviewable, revertible.

Acceptable forms

# Acceptable — full SHA pin.
uses: sebastienrousseau/dotfiles/.github/workflows/reusable-shell-lint.yml@b0615f8fb5c0f3826f58904a5567eff11b6c500e # main

The trailing comment is a human-readable hint at what the SHA represented when it was pinned (typically main, sometimes a tag like v0.2.501). The hint is documentation only — the SHA is what GitHub uses.

Rejected forms

# Rejected — relative path is a mutable ref.
uses: ./.github/workflows/reusable-shell-lint.yml

# Rejected — branch ref is mutable.
uses: sebastienrousseau/dotfiles/.github/workflows/reusable-shell-lint.yml@main

# Rejected — tag ref is mutable (tags can be moved).
uses: sebastienrousseau/dotfiles/.github/workflows/[email protected]

The lint-reusable-pins job in ci.yml runs tools/ci/lint-reusable-pins.sh on every workflow change. The lint fails the build on any of the rejected forms above.

Refreshing pinned SHAs

bump-reusable-pins.yml handles this automatically. On every push to main that touches .github/workflows/reusable-*.yml, the bot scans caller workflows for stale pins and opens a PR bumping them to the new SHA. Signed with ACTIONS_BOT_SIGNING_KEY so the resulting commit passes Verify Commit Signatures.

The manual recipe below stays here as a fallback — for example, if you need to bump pins before a merge to main, or if the bot's run failed and you want to short-circuit waiting for the next push trigger.

# 1. Land the change to the reusable on main via a PR.
# 2. After merge, capture the new main SHA:
git fetch origin main
PIN=$(git rev-parse origin/main)
echo "$PIN"

# 3. Bump every call site:
find .github/workflows -name '*.yml' -exec sed -i.bak -E \
  "s|(/reusable-[a-z0-9-]+\.yml@)[0-9a-f]{40}|\\1${PIN}|g" {} +
rm -f .github/workflows/*.bak

# 4. Verify the lint still passes:
bash tools/ci/lint-reusable-pins.sh

# 5. Land the bump on a follow-up PR with a single-purpose commit:
git commit -am "chore(ci): bump reusable-workflow pins to ${PIN:0:10}"

Whether bumped by the bot or by hand, the resulting PR runs the full CI suite — a reviewer still confirms the new reusable content is intentional before merge.

Dependabot

Dependabot's github-actions ecosystem does not support same-repo reusable workflow SHA bumps as of May 2026 — it only updates references to external actions. Same-repo reusables are tracked manually via the recipe above. The Dependabot config in .github/dependabot.yml covers the external dimension; this document covers the in-repo one.

If GitHub ships native Dependabot support for reusable workflows, delete this section and switch to package-ecosystem: github-actions with directory: /.github/workflows. Track on github/feedback#10539.

Negative test

tests/unit/ci/test_reusable_pin_lint.sh deliberately drops an unpinned reusable reference into a sandboxed workflow tree and asserts that lint-reusable-pins.sh exits non-zero with the expected error message. The test runs as part of the standard test suite — a regression in the lint catches at PR time, not at merge time.

See also