Skip to content

Publish /snapit snapshots through release.yml with npm OIDC - #4057

Open
fredericoo wants to merge 1 commit into
mainfrom
fb-snapit-oidc-dispatch
Open

fredericoo wants to merge 1 commit into
mainfrom
fb-snapit-oidc-dispatch

Conversation

@fredericoo

Copy link
Copy Markdown
Contributor

WHY are these changes introduced?

Supersedes #4018. /snapit has been broken since 2025-12-09. That day npm revoked classic tokens, and the NPM_TOKEN it used stopped working. Every /snapit since then has failed. Run 28592264687 shows E404 PUT @shopify/cli-hydrogen with the token set.

The approaches discussed on #4018 would not work either:

  • Register snapit.yml as a second trusted publisher. npm does allow several trusted publishers per package now (up to 10, docs), so the one-workflow limit no longer applies. That alone isn't enough, though: since about 2026-09-17, npm rejects OIDC token exchanges for issue_comment runs. Shopify/cli runs /snapit inside its trusted release.yml. Its diagnostics (Diagnose npm OIDC failures without publishing cli#8582) show identical sub/repository/workflow_ref claims for both events, but issue_comment gets 404 while workflow_dispatch gets 201. Add manual snapshot releases and remove temporary diagnostics cli#8583 and Shopify/app-bridge#3432 both moved to workflow_dispatch.
  • Move snapit into release.yml under issue_comment. It fails for the same reason.
  • Shopify/snapit@v0.1.0. It documents trigger_comment but reads comment_command, so with the documented inputs it does nothing and reports success.

release.yml is already the trusted publisher for all 7 packages (I checked the provenance on npm), so this PR needs no changes to npm settings.

WHAT is this pull request doing?

  • snapit.yml is now a thin bridge. It never checks out or runs PR code. On /snapit it:

    • checks that the commenter has write access (and refuses if the check itself errors);
    • refuses closed PRs, forks, and branches whose release.yml doesn't have the snapshot jobs yet (e.g. the preview line);
    • dispatches release.yml on the PR branch with expected_sha, so the run fails if the branch moved after the comment.
  • release.yml gets three workflow_dispatch jobs:

    • snapshot-build builds and packs the branch with contents: read only and no publish credential.
    • snapshot-publish runs no repo code, only id-token: write. It validates each tarball, then publishes over OIDC under the snapshot tag. The checks are:
      • every file sits under package/, there's exactly one manifest, and there are no links;
      • the package name is on the allowlist;
      • the version matches 0.0.0-snapshot-<timestamp>;
      • publishConfig exactly matches the expected value;
      • npm's own --dry-run reports the same name and version.
    • snapshot-report comments the versions on the PR, or a failure link.

    Splitting build from publish means a compromised dependency or build step on an unreviewed branch (say, a Dependabot PR) can never get a publish token. The tarball checks close the remaining gap: a poisoned build could produce a real-looking 2026.x version, which ^ ranges would install even under a non-latest dist-tag.

  • Also in release.yml:

    • Snapshot runs get their own concurrency group, so a dispatch can't cancel a production release and a second /snapit can't cancel one mid-publish.
    • Top-level permissions: {}, with compile now declaring its contents: write. No other job changes behaviour.
  • Updates the release-process skill.

You can also start a snapshot without commenting: Actions → Release → Run workflow → pick the branch.

HOW to test your changes?

This can't be tried end to end before merge. The issue_comment trigger always runs from main, and dispatching this branch would publish real snapshot versions. What I checked locally:

  • actionlint: nothing new (the 10 existing shellcheck infos are on untouched jobs).
  • Build steps: ran them in a clean clone. That produced 3 tarballs with the snapshot version in LIB_VERSION and oclif.manifest.json, and no workspace:/catalog: left in the published manifests.
  • Validator: extracted it from the workflow and ran it against the real tarballs plus 15 crafted bad ones, under both GNU tar and bsdtar. All 15 were rejected: wrong version, name or publishConfig; extra root directory; duplicate manifest; ./, package/./ and .. paths; symlink; hardlink; duplicate name; extra files; 0 or 8 tarballs.
  • Bridge and report scripts: ran against mocks for the success, fork, deleted-fork, closed, read-only, stale-branch and 422 cases.

After merge: comment /snapit on a PR that's up to date with main. Expect 👀 then 🚀, then a comment listing @shopify/hydrogen and @shopify/cli-hydrogen snapshot versions. One thing is still unproven: whether npm accepts a dispatch whose actor is github-actions[bot] (dispatches by a person are proven by Shopify/cli). If it doesn't, the manual Run workflow path still works.

Post-merge steps

  • Delete the NPM_TOKEN repo secret once the first snapshot publishes.
  • Follow-up (not changed here): npm trusted publishing doesn't check which branch a run comes from. 0.0.0-next-1842c33 shipped from refs/heads/2025-05, so anyone who can push a branch can publish any version through an edited release.yml. Worth binding the trusted publisher to a GitHub Environment limited to release branches.

Found along the way, not fixed here

  • next-release has silently stopped publishing. pnpm run changeset -- version --snapshot … logs "Too many arguments passed to changesets", the job still succeeds, and the next dist-tag hasn't moved since 2026-03-19 (run 34604190295).
  • The release job's major-bypass step calls gh pr close with a GITHUB_TOKEN that has no pull-requests: write.

Checklist

  • I've read the Contributing Guidelines
  • I've considered possible cross-platform impacts (Mac, Linux, Windows)
  • I've added a changeset if this PR contains user-facing or functional changes. Test changes or internal-only config changes do not require a changeset.
  • I've added tests to cover my changes
  • I've added or updated the documentation

npm now rejects Trusted Publishing token exchanges for issue_comment
runs, and the NPM_TOKEN snapit relied on has been dead since npm revoked
classic tokens. snapit.yml now only validates the /snapit request and
dispatches release.yml (already the trusted publisher for every package)
on the PR branch. There, the branch is built and packed without a
publish credential, and a job that runs no repository code validates the
tarballs and publishes them over OIDC.
@shopify

shopify Bot commented Sep 23, 2026

Copy link
Copy Markdown
Contributor

Oxygen deployed a preview of your fb-snapit-oidc-dispatch branch. Details:

Storefront Status Preview link Deployment details Last update (UTC)
Skeleton (skeleton.hydrogen.shop) ✅ Successful (Logs) Preview deployment Inspect deployment September 23, 202612:07 PM

Learn more about Hydrogen's GitHub integration.

@fredericoo
fredericoo marked this pull request as ready for review September 23, 2026 12:50
@fredericoo
fredericoo requested a review from a team as a code owner September 23, 2026 12:50

This branch has not been deployed

No deployments
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.

1 participant