Skip to content

Release workflow: gated dry-run, candidate and publish with checked artifacts - #524

Draft
raiseCatError wants to merge 5 commits into
devfrom
feature/release-workflow-20261029
Draft

raiseCatError wants to merge 5 commits into
devfrom
feature/release-workflow-20261029

Conversation

@raiseCatError

Copy link
Copy Markdown
Owner

A release workflow for NMSh's existing source-release model: an annotated tag on the master merge commit, a GitHub Release, and a Homebrew source formula updated by a tap PR. Merging this publishes nothing. Publishing needs a manual dispatch from master with mode: publish, the confirmation publish vX.Y.Z, and approval in the nmsh-production environment.

What's added

  • scripts/release/:
    • prepare.mjs: clean tree, semver, package/lockfile/changelog consistency, ref and approval checks, a physical-QA gate (docs/releases/vX.Y.Z-qa.json), build/typecheck/test:fast/npm audit, provenance (commit, tree, Node/npm, lockfile digest, dependencies) and release notes. Prints a blocker list.
    • package.mjs: a git archive of the exact commit, plus SHA256SUMS.
    • verify-artifact.mjs: checks the checksums, then installs the archive the way the formula does, under a private HOME (npm ci, build, prune, --version, node-pty spawn).
    • check-ci.mjs: exact-commit Verify (22.x) and Verify (26.x) check runs plus a successful dispatched CI run.
    • publish.mjs: annotated tag at the commit → draft release → upload → re-download and verify → publish. A rerun resumes a tag at the same commit with only a draft. It refuses a published release, a tag at another commit, provenance that records blockers, an archive digest that doesn't match the prepare job's output, and a draft with unexpected assets.
    • homebrew.mjs: changes the formula's url and sha256 only, with the digest taken from GitHub's tag archive. It opens the tap PR nmsh X.Y.Z only if a HOMEBREW_TAP_TOKEN secret exists. Otherwise it uploads the formula as an artifact. The tap's own brew test-bot and brew pr-pull stay in charge of testing and bottles.
  • .github/workflows/release.yml:
    • Triggers: workflow_dispatch, and pull requests that touch release files. Pull requests are always dry runs.
    • Permissions: {} at the top, least privilege per job, and only publish can write.
    • Runs under bash -eo pipefail.
    • Every action is pinned to the same SHAs as the website deploy.
    • Inputs reach the shell only through env.
    • Separate concurrency groups for pull-request runs and release runs.
  • docs/development/release-workflow.md: how to run the workflow, plus recovery for failed CI, packaging, an unverifiable asset, a partial GitHub Release, a Homebrew PR failure, an existing release, and a failed local install.
  • tests/releaseWorkflow.test.ts: version mismatches, invalid refs, missing approvals and QA, missing artifacts, checksum failures, duplicate and resumable releases, and static checks of the workflow's triggers, pinning, gating, permissions and pipefail.

Website deployment is unchanged and stays a separate PR plus the manual Publish NMSh website dispatch.

Verification

  • Focused tests/releaseWorkflow.test.ts and tests/buildInfo.test.ts: 19/19 pass. tsc clean, git diff --check clean.
  • Local dry runs:
    • --version 0.23.0 reports release-exists and qa-missing.
    • --version 0.24.0 reports version-mismatch, changelog-missing and qa-missing.
    • Packaging and isolated archive installation pass.
    • npm audit --omit=dev: 0 vulnerabilities.
  • An independent safety review found no path from a PR or fork to write access or secrets. Its findings are fixed in a354113.

Not verified

  • publish.mjs and the tap-PR path have never run against GitHub, because no dispatch or publish was executed. The first real release should run mode: dry-run from master first.
  • Tarball builds still report build unknown.
  • nmsh-production allows self-approval (prevent_self_review is off).

GitHub's default bash runs without pipefail, so 'prepare.mjs | tee' would
report success even when prepare failed. The workflow now declares
'shell: bash' (-eo pipefail), and prepare records its blockers in
provenance.json, which publish refuses unless the list is empty.
- An untagged draft release is not resumable: create the annotated tag
  first and pin target_commitish, so GitHub never makes a lightweight tag.
- The archive digest reaches publish as a prepare-job output, outside the
  artifact; release-notes.md is covered by SHA256SUMS and uploaded.
- A resumed draft must carry exactly the verified assets.
- Pull-request dry runs use their own concurrency group.
- HOMEBREW_TAP_TOKEN is scoped to the one step that opens the tap PR.
- The dispatched version is validated before it reaches step outputs.
- The CI gate counts only GitHub Actions' own check runs.

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