8000 ci: hold releases on the release-approval environment instead of a runner by colinhacks · Pull Request #79 · colinhacks/zshy · GitHub
[go: up one dir, main page]

Skip to content

ci: hold releases on the release-approval environment instead of a runner - #79

Merged
colinhacks merged 2 commits into
mainfrom
release-ci
Oct 21, 2026
Merged

colinhacks merged 2 commits into
mainfrom
release-ci

Conversation

@colinhacks
Copy link
Copy Markdown
Owner

Ports the shape of nubjs/nub's release.yml to zshy. Today the release run stages zshy, then a runner polls npm for up to 350 minutes waiting for the 2FA approval, so a late approval needs a re-run. Now GitHub holds the run on the release-approval environment with no runner, for up to 30 days, and nothing is tagged or released until npm actually serves the version this run built.

The file keeps its name, publish-latest.yml, because npm's trusted publisher for zshy is bound to it (zshy@0.8.0's provenance names it). Its display name is now "Release". This supersedes the unmerged ci/release-approval-environment branch (970203a).

gh workflow run publish-latest.yml -R colinhacks/zshy                  # dry run (default)
gh workflow run publish-latest.yml -R colinhacks/zshy -f publish=true  # release

Jobs

  1. verify (read-only) refuses, before anything is built: a ref other than main; VERSION.txt and package.json disagreeing; the tag already existing at another commit; npm already serving the version and the release existing; npm serving the version from another commit (by its provenance); a version not above npm's latest; and a release-approval environment that is missing, has no required reviewer, or admits branches other than main. GitHub creates an unknown environment with no protection on first use, so the gate is read back rather than assumed. On a dry run these are warnings. Lookups fail closed. It also writes the release notes.
  2. build (read-only, no dependency cache) tests, builds, checks the tree is clean, packs with versace.ts pack, and installs the tarball into an empty project to build a library with it. A dry run ends here.
  3. stage (id-token: write, no checkout) stages that exact tarball through the stage-only trusted publisher. It runs none of the repo's code or dependencies. Already live or already staged is not staged again.
  4. await-approval runs on release-approval. Once approved it waits for npm to serve the version, checks npm serves this run's tarball (or that the provenance names this commit and workflow), and pushes the v<version> tag with a deploy key only that environment holds.
  5. npm-served installs the release from npm and builds with it.
  6. github-release (contents: write, no checkout) creates the release on the existing tag with --verify-tag.

Also

  • package.json carries the released version (0.8.0) instead of 0.0.0, because stager finds the held run by the version in package.json at the run's commit. A release commit bumps both files; verify checks they match.
  • publish-canary.yml is deleted: it no longer reached npm, and test.yml already tests and builds every push to main.
  • The shared setup action pins its actions to commits and can turn its cache off.
  • .github/scripts/smoke-install.sh loads every entry point zshy writes, through import and require, so it works for today's dual output and the ESM-only output of feat!: build ES modules only #78.

Settings outside the repo

The release-approval environment exists (reviewer colinhacks). Until it is restricted to main, verify refuses a publishing run. The tag deploy key, the RELEASE_TAG_KEY secret and a tag-creation ruleset are also needed. npm needs no change if its trusted publisher is GitHub Actions / colinhacks/zshy / publish-latest.yml with no environment.

Verified locally: actionlint and shellcheck pass; zizmor reports nothing high or medium; verify's shell ran against the live API; check-provenance.sh passes on 0.8.0 at its commit and fails elsewhere; the smoke install passes on both builds.

…nner

The release run staged zshy and then polled npm inside the job for up to
350 minutes, so an approval that came later needed a re-run. It now
follows nub's release.yml at zshy's scale, in one dispatch-only workflow
with a publish input (default off, a dry run that stages nothing):

- verify reads the version and refuses, before anything is built, a ref
  other than main, a VERSION.txt that package.json disagrees with, a tag
  at another commit, a version npm serves from another commit, a version
  not above npm's latest, and a release-approval environment that has no
  required reviewer or admits branches other than main. GitHub creates an
  unknown environment with no protection on first use, so the gate is
  read back rather than assumed. A dry run reports these as warnings.
- build tests, builds, packs with `versace.ts pack`, and installs the
  tarball into a scratch project to build a library with it. It holds no
  npm credential and uses no dependency cache.
- stage gets only the tarball, and stages it through the stage-only
  trusted publisher. It is the only job with id-token: write, and it runs
  none of the repo's code or dependencies.
- await-approval is a job on release-approval, so GitHub holds the run
  with no runner for up to 30 days. Once approved, it waits for npm to
  serve the version, checks that npm serves this run's tarball (or, for a
  version this run did not stage, that its provenance names this commit
  and workflow), and pushes the tag with a deploy key held only by that
  environment.
- npm-served installs the release from npm and builds with it, and
  github-release creates the release on the existing tag with
  `--verify-tag`, so no job token writes a ref.

stager finds the held run to resume by the version in package.json at
the run's commit, so package.json now carries the released version
(0.8.0) and versace.ts puts the original back instead of 0.0.0.

The file keeps its name because npm's trusted publisher is bound to it.
publish-canary.yml is gone: it no longer reached npm, and test.yml already
tests and builds every push to main. The shared setup action is pinned
and can turn its cache off.
Copilot AI balanced review requested due to automatic review settings October 21, 2026 18:31
@pullfrog
pullfrog Bot commented Oct 21, 2026 •
Copy link
Copy Markdown
Contributor

No configured credential can serve this run. Your Claude Pro/Max subscription was rejected. Sign in again by running pullfrog auth claude in a terminal.

The providers' responses
CLAUDE_CODE_OAUTH_TOKEN: 401: OAuth access token has been revoked.

Pick another model →

Pullfrog  | Rerun failed job ➔ | View workflow run | via Pullfrog | Using claude-opus-5-5 (free via Pullfrog for OSS)

Copilot AI 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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

The first dry run on GitHub failed at the pack step: npm pack
--pack-destination does not create the directory it is given.
@colinhacks
colinhacks merged commit 4930aee into main Oct 21, 2026
8 checks passed
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

0