Pin and modernize GitHub Actions references - #194
Merged
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
An earlier commit pinned this at v2407 because the notify-slack it should point
at is the one this branch produces, which has no commit on master until this
merges. $/ removes that problem instead of working around it: it resolves to
this repository at the running commit, so notify-release always invokes the
notify-slack from its own commit. There is no ref to pin, no pre-pin copy
executing after merge, and no re-pin to remember.
Each claim was measured rather than inferred, since none of these files get PR
CI:
A composite action really cannot reach a sibling by relative path. On a
cross-repository call, ./actions/sibling resolved against the caller's
workspace and failed with "Can't find 'action.yml' ... Did you forget to run
actions/checkout before running your local action?".
$/ works inside a composite action's steps. The runner resolved the step
against the action's own repository, logging it as __selfRepository, with no
actions/checkout in the consuming workflow and no second action download.
$/ needs no Actions allowlist entry. Measured with allowed_actions: selected,
github_owned_allowed: false, and a single unrelated pattern: the step still
resolved and the workflow did not fail at startup. The repositories that
consume this action need no allowlist change.
zizmor 1.29.0 reports no finding for $/ and does not treat it as malformed.
$/ is documented as unavailable on GitHub Enterprise Server, which does not
affect any repository that consumes these actions.
Reported by zizmor as `unpinned-uses`.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Committed-By-Agent: claude
The consequences of resolving at the running commit are visible from the reference itself. What a reader cannot guess is the syntax, so the comment keeps only that and the link. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Committed-By-Agent: claude
A run that ends in startup_failure cannot be re-run from the API, so a new head commit is the only way to get a fresh attempt now that the allowlist admits zizmor-action. Committed-By-Agent: claude
Describes the whole-repository scope by what it covers rather than ranking it, and states the severity gate directly. Committed-By-Agent: claude
xavdid
approved these changes
Aug 25, 2026
jar-stripe
marked this pull request as ready for review
August 25, 2026 16:00
3 tasks
jar-stripe
pushed a commit
that referenced
this pull request
Aug 26, 2026
#194 migrated actions/approve to actions/create-github-app-token and scoped the token to permission-pull-requests: write only. The Auto-approve step resolves the PR by branch name, so `gh pr view <branch>` reads repository.defaultBranchRef, which requires contents read. Without it the GraphQL query fails with "Resource not accessible by integration (repository.defaultBranchRef)", PR_STATUS comes back empty, and the step fails under `set -e -o pipefail`. Add permission-contents: read to restore the ref read while keeping the tighter scoping #194 introduced. Committed-By-Agent: claude Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
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.
Why?
Remediates 21 high-severity zizmor findings across
publish.yml,release.yml, andsync-openapi-artifacts.yml.This also upgrades the end-of-life actions the pins would otherwise freeze in place, and replaces the archived
tibdex/github-app-token.What?
tibdex/github-app-tokentoactions/create-github-app-token@v3softprops/action-gh-releasewithgh releasein a script step.release.ymlnow refuses to run from a non-tag ref.peter-evans/create-pull-requestv6 → v8 andactions/checkout→ v7.actions/enable-auto-mergeis now a typed GraphQL variable, not text pasted into the query.notify-releasereaches its siblingnotify-slackthrough$/.$/resolves to this repository at the running commit.Also fixed:
artipacked(persisted checkout credentials, except where absolutely needed e.g.sync-openapi-artifacts.yml),archived-uses,excessive-permissionsonrelease.yml, and thegithub-appfindings introduced by the token swap.Verification.
Every pin was checked against the tag its comment claims, and each resolves to a commit rather than a tag object.
After merging
tibdex/github-app-token@v1/@v1.5.2andsoftprops/action-gh-release@v1become unused once this merges and can be dropped from the allowed actions.ruby/setup-ruby@v1is already stale; nothing in the repo uses it.See Also
unpinned-usesandtemplate-injection; this also clearsarchived-uses,github-app,artipacked,excessive-permissions, andsuperfluous-actions: https://docs.zizmor.sh/audits/