Pin and modernize GitHub Actions references - #194
Draft
jar-stripe wants to merge 14 commits into
Draft
Conversation
Every uses: in publish.yml, release.yml and sync-openapi-artifacts.yml was pinned to a mutable tag, so upstream could change what runs in this repo's CI with no review on our side. Generated with `zizmor --fix=all --min-severity=high`, which resolves the commit behind the ref already written rather than upgrading it, so this commit changes no versions. Upgrades follow separately. Reported by zizmor as `unpinned-uses`. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Committed-By-Agent: claude
The three files were running checkout v2 (three sites), v3 and v4. Pinning those in place would have frozen a 2022 action here permanently. The sites pass only token, repository and path, all of which v7 still accepts. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Committed-By-Agent: claude
This step opens the OpenAPI Update PR on stripe-mock, so v7's and v8's release
notes were checked rather than assumed to be runtime bumps:
* v7 renamed the git-token input to branch-token. We pass token, so no site
changes.
* v7 removed the deprecated PULL_REQUEST_NUMBER environment output. We read
steps.cpr.outputs.pull-request-url, which is unchanged, and both downstream
consumers still resolve.
* v7 made pull-request-operation return 'none' when nothing happened. Unused.
* v8 is a Node 24 runtime bump; ubuntu-latest supplies it.
Every key in the with: block is still declared in v8.1.1's action.yml, so
nothing is silently dropped.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Committed-By-Agent: claude
The v1 tag resolves to v0.1.15, released 2022-11-21. There is no 1.x line, so the pin from the previous commit would have frozen a three-and-a-half-year-old action into this repo's release path. v2.0.0 is a Node 20 runtime bump and v3.0.0 a Node 24 one. Reading the releases in between turns up nothing that touches this step: only body_path and token are passed, both still declared in v3.0.2's action.yml, and the inputs that did gain behavior (files, fail_on_unmatched_files, overwrite_files, make_latest) are unused here. zizmor also raises superfluous-actions on this step, arguing for `gh release` instead of a third-party action. That is the better end state and moots this pin entirely, but it rewrites a file with no PR CI, so it lands as its own later commit that can be dropped independently. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Committed-By-Agent: claude
…pp-token
tibdex/github-app-token is archived -- last release v2.1.0 in 2023. Four sites
here, plus a fifth inside actions/approve that a later commit handles.
The input rename is mechanical (app_id -> app-id, private_key -> private-key)
and the token output keeps its name, so no downstream step moves.
The scope is not mechanical, and it is the reason this is its own commit.
tibdex defaulted to every repository the installation could reach;
create-github-app-token defaults to the current repository only. Each of the
four tokens is scoped to what its consumers actually do:
* publish.yml, publish-latest-release: dispatches sdk-codegen's codegen
workflow, so sdk-codegen with actions: write.
* publish.yml, publish-latest-master: checks out stripe-mock, pushes the
update-openapi branch, opens the PR and enables auto-merge, so stripe-mock
with contents: write and pull-requests: write.
* release.yml: creates a release here, so the default repository scope with
contents: write.
* sync-openapi-artifacts.yml: checks out and pushes here, same scope.
Scoping at the same time as the swap is deliberate rather than tidy: requesting
the installation's full permission set is what zizmor's github-app audit flags.
It reports none of them here.
Reported by zizmor as `unpinned-uses` and `archived-uses`.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Committed-By-Agent: claude
Ten high-severity template-injection findings, all of them in this repo. Nine are in sync-openapi-artifacts.yml, where the workflow_dispatch input 'version' is pasted straight into seven curl URLs and a git commit -m. The job holds contents: write and an App token, so a dispatcher supplying a version like `$(...)` gets shell execution with a credential in scope. Hoisting the value into env: and expanding it as a shell variable means the shell never parses it as code. The tenth is release.yml's tag-message extraction. The tag trigger is constrained to v[0-9]+, but workflow_dispatch fires the same workflow with github.ref_name set to a caller-chosen branch name. zizmor rewrote it to the runner's own GITHUB_REF_NAME rather than adding an env: key, which is the better fix; the added quoting is mine, so a name containing whitespace does not word-split into extra arguments to git tag -l. Generated with `zizmor --fix=all --min-severity=high` and reviewed hunk by hunk. .github/workflows/ now reports zero findings at high severity. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Committed-By-Agent: claude
checkout leaves the job's token in .git/config by default, where any later step
-- or anything that uploads the workspace as an artifact -- can read it. Four of
the five checkouts here have no reason to keep it:
* publish.yml's two openapi checkouts read the spec files and nothing else.
* publish.yml's stripe-mock checkout is followed by create-pull-request, which
installs its own credential from its token input. Verified in v8.1.1's
git-config-helper.ts: savePersistedAuth() deliberately unsets any persisted
extraheader before configureToken() sets its own, so dropping it is what
upstream expects rather than something it tolerates.
* release.yml's checkout runs `git fetch --tags`, which needs no credential
because this repository is public.
The fifth, in sync-openapi-artifacts.yml, keeps its credential and gets a
comment saying why: its final step is a plain `git push` back to this
repository. The audit's concern is the credential escaping through an uploaded
artifact, and this workflow uploads none -- the push is the last step, so there
is no window after it. Restructuring the push to carry the token on the git
command line instead would move the credential from .git/config into process
arguments, which is not an improvement.
That leaves one artipacked finding standing, deliberately. It is low severity,
so the high-severity CI gate added later in this branch does not flag it and no
zizmor.yml ignore entry is needed.
Reported by zizmor as `artipacked`.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Committed-By-Agent: claude
…ssions Two one-line structural fixes, unrelated except in shape. publish.yml interpolated the App token directly into a curl argument. The single quotes around it were not protection: template expansion happens before the shell ever sees the line, so the token was pasted in as literal text and whatever it expanded to was parsed as shell. Reading it from env instead means the value is never part of the script. release.yml declared no permissions: block at either level, so its job ran with the default broad token. It authenticates every privileged call with the App token instead, so the workflow token needs only contents: read for checkout. Reported by zizmor as `template-injection` and `excessive-permissions`. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Committed-By-Agent: claude
zizmor's superfluous-actions audit argues, at high confidence, that this step does something the runner already ships a tool for. Acting on it removes the last third-party action from this repo's release path, which also means the pin two commits back stops being something anyone has to maintain. The rewrite preserves the behaviour that matters. action-gh-release overwrites an existing release rather than failing, which is what makes re-running the workflow on a tag safe, and `gh release create` alone would not -- hence the view/edit/create branch instead of a bare create. The token moves into env, so it is no longer an action input, and gh reads GH_TOKEN and GH_REPO directly. release.yml has no PR CI, so this is deliberately last and separable from everything before it: dropping this one commit leaves the pinned v3.0.2 action in place and every finding still fixed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Committed-By-Agent: claude
The scan that produced this work covered .github/workflows/ only, so actions/* went unexamined, but all seven SDK repos consume stripe/openapi/actions/stripe-mock and stripe/openapi/actions/notify-release at @master, and none of its four references were pinned. Three are mechanical: two slackapi/slack-github-action and one tibdex, pinned in place with no version change (the following commit replaces the tibdex one). The fourth needs explaining. notify-release invokes notify-slack, which lives beside it in this repository, but a composite action cannot reach a sibling by relative path -- ./notify-slack resolves against the caller's workspace, not this repo -- so it has to name stripe/openapi explicitly. That creates a bootstrap: the commit it should point at is the one this branch produces, which will not exist on master until this merges. It is pinned to master's current tip (v2407) instead, which is immutable and strictly better than @master, but it means the notify-slack that actually runs is the pre-pin copy. Re-pinning after merge is a follow-up, called out in the PR body. Reported by zizmor as `unpinned-uses`. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Committed-By-Agent: claude
Fifth and last tibdex site in the repo, and the only one outside .github/workflows/. It mints a token with approval rights and the upstream repo has been archived since 2023. Same mechanical swap as the workflow sites (app_id -> app-id, private_key -> private-key, token output name unchanged), plus a scope. The new action requests the installation's full permission set by default; this token only needs pull-requests: write on the one repository the caller named. The resolve step exists because create-github-app-token wants an owner and a bare repository name where callers already pass owner/name for `gh -R`. Only caller today is publish.yml, with repo: stripe/stripe-mock. Reported by zizmor as `unpinned-uses`, `archived-uses` and `github-app`. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Committed-By-Agent: claude
21 template-injection findings across three composites -- approve (4),
enable-auto-merge (4), stripe-mock (13). They sit outside .github/workflows/, so
the scan that produced this work never reached them, but actions/stripe-mock
runs in all seven SDK repos' CI on every push.
Mostly zizmor --fix=all, which hoists each expression into env: and references
the shell variable. Three hand corrections on top of it:
* enable-auto-merge's merge mode now travels as a typed GraphQL variable
instead of an env reference. The fixer's version was broken: the mutation is
a single-quoted shell string, so ${INPUTS_MODE} would have reached the
server literally, and continue-on-error: true would have hidden it. As a
variable the server validates it against the PullRequestMergeMethod enum,
which is a stronger guarantee than quoting.
* Quoted the expansions the fixer left bare. Same behavior for well-formed
values; the unquoted form was only ever safe by accident.
* Left `contains(inputs.base, 'b')` interpolated in stripe-mock. It is not a
finding and cannot be one: the function yields a boolean literal, never
caller-supplied text.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Committed-By-Agent: claude
Nothing else in this branch stops someone adding an unpinned uses: next month, so the check ships alongside the fixes. Gated at --min-severity=high, matching the threshold these findings were reported at. At zizmor's default threshold, a future release adding a low or medium audit could turn the repo red with no workflow having changed. Unlike the other repos in this sweep, the default whole-repository scope matters here: it covers the composite actions under actions/, which all seven SDK repos consume and which the original scan never reached. No .github/zizmor.yml: every finding in the repo is fixed except one deliberate artipacked exception in sync-openapi-artifacts.yml, and that one is low severity, below this gate. An ignore entry would be inert, and an empty config file is worse than none. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Committed-By-Agent: claude
Restores a guard that came free with the action this workflow no longer uses. release.yml's push trigger only fires on v[0-9]+ tags, but it also has workflow_dispatch, where GITHUB_REF_NAME is whatever ref you dispatched from. action-gh-release@v1 threw "GitHub Releases requires a tag" in that case and created nothing. `gh release create` has no such scruples: given a branch name it creates the release and cuts a new tag to match. release.yml has no PR CI, so that would have stayed invisible until someone dispatched it. Dispatching from an actual tag still works, which is the case worth keeping. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Committed-By-Agent: claude
jar-stripe
force-pushed
the
jar/sat-28956-pin-workflows
branch
from
August 15, 2026 00:08
f2fcbe9 to
559905e
Compare
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.
Remediates 21 high-severity zizmor findings across
publish.yml,release.yml, andsync-openapi-artifacts.yml.Rather than only hash-pinning, this also upgrades the end-of-life actions the pins would otherwise freeze in place, and replaces the archived
tibdex/github-app-token. Commits are ordered so the reported findings come first and everything beyond them is separable — any of the later commits can be dropped without touching the remediation.Behavior changes, called out individually
These are the parts that are not mechanical. None of the three files has PR CI, so review carries the weight here.
tibdex/github-app-token→actions/create-github-app-token@v3, 5 sites. Upstream is archived (last release 2023).app_id/private_keybecomeapp-id/private-key; thetokenoutput name is unchanged, so nothing downstream moves, and no new secret or variable is needed. Each of the 5 tokens is scoped to the repositories and permissions its own consumers need — the new action otherwise requests the installation's full permission set, which is itself a high-severity finding.softprops/action-gh-releaseis gone, replaced bygh releasein a script step. zizmor flags the action as superfluous, and removing it drops a third-party action from the release path entirely. The script mirrors the action's create-or-update behavior, so re-running on an existing tag still overwrites the notes instead of failing.release.ymlnow refuses to run from a non-tag ref. This restores a guard that came free with the removed action: it threwGitHub Releases requires a tagwhen dispatched from a branch, whereasgh release createwould create the release and cut a tag named after the branch.peter-evans/create-pull-requestv6 → v8 andactions/checkout→ v7. v7 renamedgit-tokentobranch-token; we pass neither, so nothing changes here.actions/enable-auto-mergeis now a typed GraphQL variable, not text pasted into the query. The server validates it against thePullRequestMergeMethodenum, so it cannot carry query syntax at all.Beyond the reported 21
The scan covered
.github/workflows/only, so it never reached the composite actions underactions/, which all 7 SDK repos invoke on every CI run. They held 25 more high-severity findings (21template-injection, 4unpinned-uses), fixed here.Also fixed:
artipacked(persisted checkout credentials),archived-uses,excessive-permissionsonrelease.yml, and thegithub-appfindings introduced by the token swap.One deliberate exception
sync-openapi-artifacts.yml's checkout keeps its persisted credential, because the workflow's last step is a plaingit pushthat needs it. Theartipackedfinding is about that credential escaping through an uploaded artifact; this workflow uploads none, and the push is the final step. It is low severity, below the CI gate, so nozizmor.ymlignore entry is needed.Verification
Every pin was checked against the tag its comment claims, and each resolves to a commit rather than a tag object — openapi's tags are annotated, so
refs/tags/v2407is a tag object and would not work as a pin.The composite actions were also validated from a consumer, since nothing here exercises them: a scratch branch on
stripe-rubypointedactions/stripe-mockat this branch and passed on all three input paths any real caller uses — no inputs (what all 7 SDK repos do),beta, andspec_path/fixtures. Theopenapi_versionpath fails, but it fails identically atmaster: it builds a host path and hands it to a container that only mounts the workspace, so it has been broken for any Linux runner independently of this PR.The last commit adds zizmor to CI at
--min-severity=high, matching the threshold these findings were reported at. Here the action's default whole-repository scope is a feature: it coversactions/, which the original scan never reached.Before merging
The repo's Actions allowlist has to be widened first, or this breaks at startup.
patterns_allowedentries whose ref is version-shaped (@v6) cannot match a hex SHA, so a pinned action the allowlist only admits as@v6makes the workflow fail to start. Needed:peter-evans/create-pull-request@v4,@v6peter-evans/create-pull-request@*slackapi/slack-github-action@v1.23.0@*zizmorcore/zizmor-action@*— new; without it the CI check here cannot starttibdex/github-app-token@v1/@v1.5.2andsoftprops/action-gh-release@v1become unused once this merges and can be dropped afterward — not before, since master still references them. (ruby/setup-ruby@v1is already stale; nothing in the repo uses it.)stripe/openapineeds no entry: the only full-path self-reference isnotify-release→notify-slack, andnotify-releaseis invoked from the SDK repos and stripe-mock, whose allowlists already carrystripe/openapi@*.Follow-up, not in this PR
actions/notify-releasepinsnotify-slackat master's current tip (325f3b15,v2407), which is the pre-pin copy. Once this merges it should be re-pinned to a commit that contains these fixes.