CI Dependency Pinning Policy¶
Every external dependency the CI pipeline consumes must be pinned by 40-hex commit SHA. The policy applies to:
- Third-party actions —
uses: owner/action@<sha>(already enforced via Scorecard'sPinned-Dependenciescheck at score ≥ 9). - Reusable workflows in this repo —
uses: sebastienrousseau/dotfiles/.github/workflows/reusable-X.yml@<sha>(added with #855; enforced bytools/ci/lint-reusable-pins.sh). - Container base images —
FROM image:tag@sha256:<digest>(closed by #886). - Release binaries downloaded at build time —
curl … && 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¶
- #855 — original tracking issue.
tools/ci/lint-reusable-pins.sh— the enforcement script.tests/unit/ci/test_reusable_pin_lint.sh— the negative test..github/workflows/bump-reusable-pins.yml— the auto-bump bot.- GitHub: pinning actions to a full-length commit SHA.