Chore(ci): pin every GitHub Actions uses: to a full commit SHA - #505
Open
AmaadMartin wants to merge 2 commits into
Open
Chore(ci): pin every GitHub Actions uses: to a full commit SHA#505AmaadMartin wants to merge 2 commits into
AmaadMartin wants to merge 2 commits into
Conversation
added 2 commits
August 2, 2026 00:27
…tion majors csat.yml was stranded on actions/checkout@v3 and actions/github-script@v6, and auto-assignment.yml on actions/checkout@v4, while every other workflow already uses checkout@v6 and github-script@v7. Pinning those stale majors to a commit SHA would freeze the staleness permanently, so bump them first as a separate, independently reviewable step. Neither bumped checkout call site passes any `with:` inputs, and no github-script input contract changed in v7; the CSAT script uses only require(), context.payload.issue, context.repo and github.rest.issues.createComment, all of which v7 still provides.
Every `uses:` reference in .github/workflows/ resolved a mutable Git tag. A tag is a movable pointer: whoever controls (or compromises) an upstream action repository can force-move `v6` to arbitrary code, which then executes with whatever token scopes the workflow holds. release-please.yml is the sharp edge - it runs on every push to main with `contents: write` and a dedicated release token. Pin all twelve references to immutable 40-character commit SHAs, following the convention GitHub documents and adk-python already uses: `<sha> # v<major>`. Each SHA is what its tag resolves to right now, so the ten already-current references are behavioural no-ops. googleapis/release-please-action@v4 is an *annotated* tag: refs/tags/v4 points at a tag object (8b8fd2cc...), which is not a commit and would not resolve as a `uses:` ref. The dereferenced commit 5c625bfb... is pinned instead, confirmed identical to refs/tags/v4.4.1. Nothing outside a `uses:` line changes.
This was referenced Aug 4, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Please ensure you have read the contribution guide before creating a pull request.
Link to Issue or Description of Change
No existing issue — description below.
Problem: Every
uses:reference in.github/workflows/resolved a mutable Git tag (@v3,@v4,@v5,@v6,@v7). A tag is a movable pointer: whoever controls an upstream action repository — or anyone who compromises it — can force-movev6to an arbitrary commit, and every adk-js workflow run after that moment executes the attacker's code with whatever token scopes the workflow holds.Three of the twelve references sit in workflows holding write-scoped credentials, which is where the exposure stops being theoretical:
csat.ymlcontents: read,issues: write,pull-requests: writeauto-assignment.ymlcontents: read,issues: write,pull-requests: writerelease-please.ymlcontents: write,issues: write,pull-requests: writesecrets.MY_RELEASE_PLEASE_TOKENrelease-please.ymlis the worst case: it runs on every push tomainwithcontents: writeand a dedicated release token, so a movedv4tag would be executing inside a job that can push commits, create tags, and cut releases.That these tags really do move is directly demonstrable: the sibling repo
adk-pythonpinsactions/checkout# v6atdf4cb1c0…, but resolving thev6tag today givesd23441a4…(release v6.1.0). The same@v6reference points at two different commits depending on when you look.Solution: Pin all twelve references to immutable 40-character commit SHAs, using the form GitHub documents for third-party action hardening and that
adk-pythonalready uses —<sha> # v<major>. This is repo-to-repo consistency, not a new policy.The change is two commits so the version decision is reviewable separately from the mechanical pinning:
chore(ci): converge csat and auto-assignment on the repo's current action majors— bumps the stale majors before any SHA is written, so pinning does not freeze the staleness permanently:csat.yml:actions/checkout@v3→@v6,actions/github-script@v6→@v7auto-assignment.yml:actions/checkout@v4→@v6chore(ci): pin every GitHub Actions uses: to a full commit SHA— the twelve pins.Resulting pins (every SHA re-resolved from the upstream repository at authoring time, not copied from
adk-python, whose pins are older and internally inconsistent):actions/checkoutv6d23441a48e516b6c34aea4fa41551a30e30af803actions/github-scriptv7f28e40c7f34bde8b3046d885e986cb6290c5673bactions/setup-nodev6249970729cb0ef3589644e2896645e5dc5ba9c38actions/setup-gov540f1582b2485089dde7abd97c1529aa768e1baffactions/setup-pythonv5a26af69be951a213d495a4c3e4e4022e16d87065googleapis/release-please-actionv45c625bfb5d1ff62eadeeb3772007f7f66fdcf071Nine of the twelve pins are behavioural no-ops — each SHA is exactly what its tag resolves to right now. Only the three major bumps in commit 1 change what code runs.
googleapis/release-please-action@v4is an annotated tag — the trap in this change.refs/tags/v4points at a tag object whose own SHA is8b8fd2cc23b2e18957157a9d923d75aa0c6f6ad5. That is not a commit, and GitHub Actions resolvesuses:against commits, so writing it would have brokenrelease-please.ymlon the next push tomain— in the one workflow holdingcontents: writeand the release token. Confirmed empirically:The dereferenced commit
5c625bfb…(refs/tags/v4^{}) is pinned instead, and is identical to the commit behindrefs/tags/v4.4.1. The other five tags are lightweight and point directly at commits.Why the three major bumps are safe.
actions/checkoutv3→v6 (csat.yml), v4→v6 (auto-assignment.yml). Across v4/v5/v6 the changes are the Node runtime (16→20→24), a minimum runner requirement of v2.327.1 for v5+, and credentials persisting to a separate file in v6. No input contract changed, and neither bumped call site passes anywith:inputs — both are bareuses: actions/checkout@vN. Runner compatibility is already proven in this repo:validation.yamlrunscheckout@v6on ubuntu/windows/macOS today, and both bumped workflows run onubuntu-latest.actions/github-scriptv6→v7 (csat.yml). v7.0.0 adds a Node 20 runtime, an optionalbase-urlinput, and exported JSDoc types. Nothing was removed..github/scripts/csat.cjsuses onlyrequire()(still injected by v7),context.payload.issue,context.issue.number,context.repoandgithub.rest.issues.createComment— verified by reading the script, all unchanged in v7. Already proven in-repo:auto-assignment.ymlrunsgithub-script@v7against the samegithub.rest.*shape.Scope notes for the reviewer.
auto-assignment.ymlis trivially separable. The task text spells out onlycsat.ymlas stale, butauto-assignment.ymlwas the last remainingcheckout@v4holdout; leaving it would freeze a third distinctcheckoutmajor into the repo. If you would rather it moved separately, that is a one-line revert.actions/checkouthas since published av7, andactions/github-scriptav8andv9. This PR converges on the majors already in use elsewhere in this repo (checkout@v6,github-script@v7) because the goal here is one version per action plus immutability; adopting a brand-new major is a separate version-upgrade decision with its own compatibility review, and folding it into a pinning PR would hide it. Once Dependabot is configured (the queued follow-up) it will raise those bumps as reviewable PRs, which is the right venue.github-script@v7targets Node 20 and the runner now warns it isbeing forced to run on Node.js 24. This is pre-existing onmain—auto-assignment.ymlwas already on@v7— and is not introduced here; extendingv7tocsat.ymlis still a strict improvement overv6(Node 16). It does add weight to doing the major-upgrade follow-up reasonably soon.scripts/check_license.shonly scans*.js/*.ts(check_license.sh:7-8), none of the six files has one today, and adding them is out of scope.gh pr list --repo AmaadMartin/adk-js --state open --limit 1000returned no PR that pins actions to SHAs — every open PR touching.github/workflows/was checked withgh pr diff <n>and none adds a line matchinguses: .*@[0-9a-f]{40}. The nearest neighbour is Chore(ci): add job-level timeout-minutes to the remaining five workflows #403 (feat/workflow-job-timeout-minutes), which touches five of the same six files but adds onlytimeout-minutes:lines at job level and nouses:line, so it does not conflict. Branched frommainrather than stacking.Testing Plan
Please describe the tests that you ran to verify your changes. This is required for all PRs that are not small documentation or typo fixes.
Unit Tests:
[ ] I have added or updated unit tests for my change. — not applicable; this change adds zero lines of executable code (see below)
[ ] All unit tests pass locally. — no test file is affected by this change
No unit tests were added, deliberately. This change contains zero lines of executable application code — six YAML workflow files, twelve changed lines, nothing for
vitestto import and nothing for@vitest/coverage-v8to instrument. The coverage bar is satisfied vacuously because the denominator is zero. Please read the absence of tests as a scope decision, not an oversight:.github/workflows/*.ymland asserts a pinning regex would test the YAML fixture, not the product, and would need updating on every legitimate pin bump.scripts/check_pinning.shplus a CI job is a new enforcement mechanism, not this fix, and the sustainable answer to pin freshness is the separately queued Dependabot work.Verification is therefore static analysis plus real workflow runs.
Static assertions (all run from the repo root, all passing):
grep -rn "uses:" .github/workflows/ | grep -v '@[0-9a-f]\{40\} # v'^\s*(- )?uses: [\w.-]+/[\w.-]+@[0-9a-f]{40} # v\d+$— 12/12.gh api repos/<owner>/<repo>/commits/<sha>calls, each echoing back the same SHA.checkout×5,github-script×2,setup-node×2,setup-go×1,setup-python×1,release-please-action×1 = 12, each action carrying an identical SHA at every occurrence.yaml.safe_load), with job and step counts unchanged.npx secretlint "**/*"(the exact commandvalidation.yamlruns) exits 0 — a 40-hex SHA trips neither the recommended preset nor this repo's customAIza…pattern.git diff main --stat= 6 files, 12 insertions, 12 deletions; and a-U0diff filtered to non-uses:lines prints nothing, i.e. nowith:,permissions:, step name or trigger was touched..editorconfigcompliance: no CRLF, no trailing whitespace, final newline intact in all six files. Indentation preserved exactly — notecsat.ymlandrelease-please.ymllegitimately sit at a different indent from the other four because their steps are unnamed.actionlintwas not run — not installed, and the plan explicitly rules out installing a new toolchain for it.Proof the assertions can actually fail. Coverage/pass-rate is not evidence, so each assertion was run against a deliberately mutated tree. The two interesting results show the assertion set is non-redundant rather than three spellings of the same grep:
release-pleaseto@v4release-pleaseto the tag object8b8fd2cc…No commit found for SHA: 8b8fd2cc… (HTTP 422)checkoutoccurrence a different real SHAactions/checkoutMutations C and D are exactly the two mistakes this change could plausibly ship, and both sail past the regex checks — C is the annotated-tag trap and D is a copy-paste slip. The tree was restored clean (
git status --porcelain= 0) after each.Manual End-to-End (E2E) Tests:
Please provide instructions on how to manually test your changes, including any necessary setup or configuration.
Automatic on this PR — ran and passed, 9 of the 12 pins exercised for real. All CI checks are green on this PR:
run-tests (ubuntu-latest)validation.yamlrun-tests (windows-latest)validation.yamlrun-tests (macos-latest)validation.yamlrun-testscross-language-integration.ymlcheck-licenselicense-check.ymlauto-assignauto-assignment.ymlauto-assignment.ymlalso fires onpull_request: types: [opened], so it ran here too — that is two more pins exercised than expected, includinggithub-script.The runner logs confirm the pins resolved as SHAs rather than tags, which is the property under test:
That is five of the six distinct actions verified at runtime across Linux, Windows and macOS. The three unexercised lines are
csat.yml's two (manual, below) andrelease-please.yml's one (inspection-only, below).Manual, for the two
on: issuesworkflows. Gotcha: GitHub only runsissues-triggered workflows from the repository's default branch, so opening an issue while this sits on a feature branch runs the oldmainversion and proves nothing. To exercise them, first land the change on (or temporarily repoint) the default branch, then:auto-assignment.ymlshould run and the job should be green. ItsaddAssigneescall may legitimately fail where the hard-coded assignee is not a collaborator; the script catches that and logsFailed to assign, so a green job is still the correct signal — the point is thatcheckout@<sha>andgithub-script@<sha>resolved and executed.bug) to that issue and close it →csat.ymlshould run green and post the survey comment. This is the single most valuable manual check, becausecsat.ymlis the only file where the code being run actually changes (checkout v3→v6, github-script v6→v7).release-please.ymlis inspection-verified only, by design. It ison: push: branches: [main]and consumessecrets.MY_RELEASE_PLEASE_TOKEN; firing a real release run is not an acceptable test. Verified instead by: (a) the file parses as YAML; (b)5c625bfb…confirmed viagh api repos/googleapis/release-please-action/commits/…to be a commit in the correct repository; (c) confirmed to be the dereference ofrefs/tags/v4and identical torefs/tags/v4.4.1, so the pin is behaviourally a no-op. Flagging this explicitly so it is clear one line is inspection-verified rather than run-verified.Checklist
[x] I have read the CONTRIBUTING.md document.
[x] I have performed a self-review of my own code.
[x] I have commented my code, particularly in hard-to-understand areas. (the
# v<major>trailing comments)[ ] I have added tests that prove my fix is effective or that my feature works. — replaced by the static assertions and the mutation table above
[x] New and existing unit tests pass locally with my changes. — verified in CI on this PR (
validation.yamlrunsnpm run test:coverageon ubuntu/windows/macOS, all three green); not re-run locally, per the targeted-tests-only policy.