Skip to content

ci: pin GitHub Actions to commit SHAs - #7

Merged
rivernate merged 9 commits into
mainfrom
pin-actions-to-sha
Aug 24, 2026
Merged

ci: pin GitHub Actions to commit SHAs#7
rivernate merged 9 commits into
mainfrom
pin-actions-to-sha

Conversation

@rivernate

@rivernate rivernate commented Aug 21, 2026

Copy link
Copy Markdown
Member

Pins every third-party uses: action reference in this repo's workflows to its resolved commit SHA, keeping the original version tag as a trailing comment (e.g. actions/checkout@d23441a... # v6).

Why: a mutable version tag can be retargeted by the action's owner after the fact, with no PR or diff ever appearing in this repo — "Dependabot bumped a trusted action to a newer version" and "a tag got silently retargeted to something malicious" are indistinguishable from CI's point of view. A SHA pin is immutable, so that retargeting can't affect an already-pinned workflow. This doesn't guard against a legitimately-tagged new release that's malicious at the source — required checks still only prove the pipeline runs, not that new code is trustworthy — but it closes the silent-retarget gap.

Dependabot understands this pattern natively: it recognizes the trailing version comment and keeps both the SHA and the comment in sync on future bumps, so this doesn't change how version updates flow through this repo.

Context: derivita/infrastructure#1856

Mechanically generated: a script scanned every file under .github/workflows/ (3 total), resolved each uses: line's tag to a commit SHA via the GitHub API (gh api repos/OWNER/REPO/commits/REF --jq .sha), and rewrote it in place. 2 file(s) needed changes (shown in this diff); any others were already SHA-pinned.

Also adds .github/dependabot.yml with a github-actions ecosystem entry — this repo had none, so without it the SHA pins above would have no update path (the "Dependabot keeps this in sync" claim above wasn't true until this file existed). Covers github-actions only; the repo's existing npm updates were running as Dependabot security updates, which don't need a config and aren't affected either way.

Same script and pattern already validated end-to-end on derivita/agent-plugins#141 (full /pre-pr gate: fleet review, correctness pass, prose-vs-diff, actionlint/zizmor — all clean). Skipping a repeat full gate here per that validation; see derivita/infrastructure#1856 for context.

Mitigates tag-retargeting risk discussed in derivita/infrastructure#1856.
@rivernate
rivernate requested a review from wbhumphrey August 21, 2026 16:40
Without this, nothing bumps the SHA pins this PR adds -- the floating
tags they replace at least absorbed upstream patches automatically;
pinned with no update path, they'd be frozen forever instead. This repo
had no dependabot.yml at all.

@wbhumphrey wbhumphrey left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The SHA pins themselves check out — I verified all seven against upstream tags (actions/checkout v4.4.0, setup-node v4.4.0, upload-artifact v4.6.2, download-artifact v4.3.0, setup-bazel 0.8.5, codeql-action v3.37.8, upload-release-action v2). Every SHA is the commit the tag points at.

One blocking problem: the rewrite dropped the /init and /analyze subpaths from the two CodeQL steps, so the CodeQL workflow will fail to resolve an action.

Comment thread .github/workflows/codeql.yml Outdated
Comment thread .github/workflows/codeql.yml Outdated
Comment thread .github/workflows/build.yml
The pinning script dropped everything after owner/repo (e.g.
github/codeql-action/init -> github/codeql-action), breaking action
resolution. Fixed the script and regenerated this diff from the
original content.
@rivernate
rivernate requested a review from wbhumphrey August 21, 2026 17:37

@wbhumphrey wbhumphrey left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Both blocking findings are fixed correctly, and the fixes are minimal — the two CodeQL steps regained their /init and /analyze subpaths with the SHA and comment untouched. I re-checked every uses: line across all three workflow files: no other subpath action lost its path, and the seven SHAs I verified against upstream tags on the first pass are unchanged. Approving.

Adding dependabot.yml is a good catch and closes a real gap in the PR's own reasoning: without a config, the "Dependabot understands this pattern natively, so this doesn't change how version updates flow" claim wasn't true for the github-actions ecosystem, since version updates there require a config entry. It's true now.

One stale-prose note, not worth a force-push on its own but worth fixing if you touch the description again: it still says "2 file(s) needed changes ... No other content changed," and the PR now carries three files including a hand-written config that isn't a SHA rewrite.

Left the # v4 comment-precision thread open — that's a house-convention call for Brad, not something I want to resolve on his behalf.

Comment thread .github/dependabot.yml
…racing them

Both bazel build commands ran as separate async exec() calls sharing
one callback, so cb() fired as soon as EITHER finished, not both.
gulp's series() then advanced to genproto_wellknowntypes (which
invokes bazel-bin/generator/protoc-gen-js) before that build was
necessarily done -- intermittently 'program not found or is not
executable' depending on which bazel invocation happened to finish
first. Joined into one shell command via && so cb() only fires once,
after both builds have actually completed. Pre-existing bug, unrelated
to this PR's SHA-pinning change -- fixing it separately since it's
what's actually blocking build/CodeQL from going green.
@rivernate
rivernate requested a review from wbhumphrey August 21, 2026 19:14
…closure-compiler runs

Every source file's own import (e.g. binary/utils.js's
'../../closure-library/closure/goog/crypt/base64.js') hardcodes a
sibling checkout one directory above the repo root -- not where npm
actually installs google-closure-library
(node_modules/google-closure-library, per package.json). Rewriting
every import across every source file to match a different path was
the wrong fix (tried and reverted); making the path they already
expect actually resolve is simpler and doesn't touch source files.
Verified locally: this closes the JSC_JS_MODULE_LOAD_WARNING errors
and gets the compiler invocation from 'files not found' to actually
analyzing the files.

This does NOT make the closure-compiler build fully pass -- it now
fails on JSC_CANNOT_PATH_IMPORT_CLOSURE_FILE (this exact
google-closure-compiler@20240317.0.0, the version package.json pins,
rejects Closure files imported by raw path; the affected source files
predate that requirement and need 'goog:namespace'-style imports
instead). That's a real source-code migration across 4 files with
runtime-behavior risk if the namespace mapping is wrong, not a config
fix -- leaving it for a follow-up with closer review rather than
guessing here. This commit is still real, verified progress: it fixes
an actual bug and gets the build past the point this rollout's SHA
pins can be blamed for.
These 6 imports across 4 files pointed at closure-library files by
relative path (e.g. '../closure-library/closure/goog/crypt/base64.js'),
which google-closure-compiler@20240317.0.0 (the version package.json
pins) rejects outright: 'Cannot import Closure files by path. Use
either import goog:namespace or goog.require(namespace)'. Converted
each to the goog:<namespace> form the compiler asks for --
goog.array, goog.object, goog.string, goog.crypt, goog.crypt.base64 --
using default-import syntax (compiler error JSC_NAMESPACE_IMPORT_CANNOT_USE_STAR
rejects 'import * as' for goog: specifiers, and says so explicitly).
Also dropped one redundant bare side-effect import in debug.js that
duplicated the next line's named import of the same file.

Verified locally: ran the exact google-closure-compiler invocation
gen_google_protobuf_js uses, with the previous version of these files
it failed (JSC_JS_MODULE_LOAD_WARNING, then after the symlink fix
JSC_CANNOT_PATH_IMPORT_CLOSURE_FILE); with this commit it exits 0 and
produces a real compiled google-protobuf.js (691 lines). Every renamed
import's usage in these files (goog.array.map/forEach, goog.object.getKeys,
goog.string.quote, goog.crypt.byteArrayToString, goog.crypt.base64.*) matches
Closure Library's actual, stable public API for that namespace.
Same class of bug as the previous commit, in the second (and only
other) file that gets fed through google-closure-compiler
(commonjs_testdeps task). Converted all 6 imports to goog:<namespace>
form: goog.crypt (named import of byteArrayToString/byteArrayToHex --
compiler accepts named imports, only rejected 'import * as'),
goog.crypt.base64, goog.testing.PropertyReplacer, goog.userAgent,
goog.array, goog.object.

Verified locally: ran the exact compiler invocation
commonjs_testdeps uses: previously failed with
JSC_CANNOT_PATH_IMPORT_CLOSURE_FILE, now exits 0 and produces a real
compiled testdeps_commonjs.js (699 lines).

Confirmed these are the only two call sites of getClosureCompilerCommand
in this gulpfile, so this closes out every place npm test's build path
actually invokes the compiler. Other files still importing
closure-library by raw path (the various *_test.js files,
experimental/runtime/*) run via test_closure/test_commonjs's Jasmine
execution instead, which never goes through the compiler's strict
goog: import requirement -- confirmed by tracing gulpfile.js's own
series() task graph, not left as an assumption.

@wbhumphrey wbhumphrey left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The four new commits move this PR from a CI-pinning change into a build-system repair, and the repair isn't finished yet — build is still failing at 5c95bf4. So this is a COMMENT rather than an approval; the SHA pinning I approved last round is unaffected and still looks right.

For the record on the CI signal: build has been red at every commit on this branch, including d71c349, so none of these commits broke it. But a871e50, 22d41b4, 96ab974 and 5c95bf4 are all attempts to make it green, and it isn't green, so I don't have a working build to check any of this against. If you can paste the current failure, I can look at whether it's the finding below or something further down the pipeline.

One blocking item, in commonjs/export_testdeps.js: PropertyReplacer needs a default import rather than a named one. Details inline — the short version is that the namespace is the class there, unlike goog.crypt on the line above where the named form is correct. Since export_testdeps.js is the entry point for commonjs_testdeps, which npm test runs, that one is on the path to the failure you're chasing.

The direction of 96ab974 is right, and worth saying explicitly: package.json pins google-closure-library@~20200315.0.0, which predates the ES-module build of the library entirely. Those files are goog.provide/goog.module, so the old import * as x from '../../closure-library/.../base64.js' was never going to bind no matter where the path pointed — which is why the symlink alone (22d41b4) didn't fix it. goog: specifiers are the actual mechanism, and switching the import * as forms to defaults along the way is exactly right; import * as from a goog: specifier is a hard compiler error.

Two things that follow from that, neither blocking:

The conversion is partial. Fourteen files still import closure-library by path — every *_test.js and all of experimental/runtime/. They aren't in the --js= list so they don't affect the artifact, but commonjs_out rewrites the test files and test_closure loads them, and the tree now carries two idioms for the same import. Worth either finishing the sweep or leaving a note about where the boundary is and why.

a871e50 is a real bug fix and the commit message describes it accurately — two exec() calls sharing one callback meant cb() fired on whichever bazel build finished first. Nothing to add.

Last, the framing: title and description still describe this as pinning actions to SHAs, and the body still says "2 file(s) needed changes ... No other content changed" against a nine-file diff that now includes a build-system fix and an import migration across six JS files. I'd rather see the build fixes as their own PR — the pinning change is reviewable in one pass and the closure migration is not — but if they stay together the description needs to say what's actually in here. Flagging, not editing.

Comment thread commonjs/export_testdeps.js Outdated
Comment thread gulpfile.js Outdated
Comment thread gulpfile.js Outdated
PropertyReplacer is a default export, not a named one -- the named
import destructured an undefined property, breaking the closure
compile of commonjs_testdeps and cascading into every test in CI
(ERR_MODULE_NOT_FOUND for commonjs_out/debug.js and friends).

Also switch the symlink existence check to lstatSync so a dangling
link (post rm -rf node_modules) doesn't throw EEXIST, trim the
getClosureCompilerCommand comment to what's still true after the
goog:namespace import rewrites, and remove the symlink in
remove_gen_files/gulp clean.
@rivernate
rivernate requested a review from wbhumphrey August 24, 2026 15:41

@wbhumphrey wbhumphrey left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Both of my substantive findings are fixed correctly. PropertyReplacer is a default import now, and the symlink guard uses lstatSync so a dangling link no longer throws EEXIST. The trimmed comment is accurate about what the symlink is still for.

One new blocking item, introduced by the cleanup half of that second fix: remove_gen_files now runs rm -rf against ../closure-library unconditionally. That path predates this branch — it's the sibling-checkout convention already in gulpfile.js on main — so on a machine that has a real closure-library clone there, npm run clean deletes it recursively. rm -rf doesn't follow symlinks, so the case you were targeting is fine; it's the case you weren't targeting that's destructive. A [ -L ] guard, or an lstatSync().isSymbolicLink() check next to the code that creates the link, closes it. Suggestion inline. That one's on me for asking for the cleanup without saying what it should be guarded on.

On the CI story: the root-cause explanation in the export_testdeps.js thread doesn't survive contact with this commit's results. build and Analyze (javascript) are both still failing at 907e2a3 with the default import in place, so the ERR_MODULE_NOT_FOUND on commonjs_out/debug.js has another cause — or at least another cause behind it. Worth getting the current log before concluding that layer is done; the import fix was necessary, but it doesn't look sufficient.

Everything else from the last pass stands unchanged: the conversion is still partial (the *_test.js files and experimental/runtime/ remain on path imports), and the title and description still describe this as an actions-pinning PR rather than the build repair it has become.

Comment thread gulpfile.js Outdated
Comment thread gulpfile.js
gulp clean ran `rm -rf ../closure-library` unconditionally, which would
recursively delete a real sibling closure-library checkout -- the layout
convention gulpfile.js has assumed since before this branch. Only unlink
it when it is a symlink.

Creating the link now also distinguishes a dangling link (lstat succeeds,
exists fails) from a live one and replaces it, instead of skipping and
leaving the build to fail on unmatched --js= globs.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DZX1q5jSbLDx2N8VX26aLt
@rivernate
rivernate requested a review from wbhumphrey August 24, 2026 16:24
@rivernate
rivernate merged commit c5e080c into main Aug 24, 2026
2 of 5 checks passed
@rivernate
rivernate deleted the pin-actions-to-sha branch August 24, 2026 16:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants