Skip to content

Latest commit

 

History

History
223 lines (186 loc) · 10.9 KB

File metadata and controls

223 lines (186 loc) · 10.9 KB

Releasing the runx CLI

Maintainer doc. Most contributors do not need it.

Identity

The CLI ships from github.com/runxhq/runx. Release tags are cli-vX.Y.Z (prefixed so they do not collide with the repo's other release trains). In the workspace, release/status.json is the operator source of truth for package release status, the CLI package allowlist, and the cloud pin. The git tag is the immutable OSS release event that the public workflow builds.

Release policy lives at the workspace root (AGENTS.md plus release/status.json). This doc is the OSS maintainer runbook. If a release doc, package manifest, cloud pin, or channel table disagrees with release/status.json, fix the drift there and run the root checks; do not invent a second release flow.

The same product version is used on every active channel. The governed release is verified only after independent readback from its required synchronous channels:

  • GitHub Release: cli-vX.Y.Z (the hub; serves the raw per-target archives)
  • npm: @runxhq/cli@X.Y.Z (+ @runxhq/cli-<platform>@X.Y.Z)
  • Docker (GHCR): ghcr.io/runxhq/runx:X.Y.Z, anonymously pullable
  • Homebrew and Scoop: X.Y.Z

Winget is an asynchronous submission recorded separately after its pull request exists. AUR is not an active Runx-owned channel while runx-bin belongs to another maintainer; the generated manifest is only a handoff artifact. Neither channel can silently weaken or block verification of the synchronous release set.

The crates.io package is not a CLI release channel. runx-cli depends on the internal Rust crate graph, whose versions move under a separate, explicit library release. A CLI tag must not publish those libraries implicitly.

runx --version reports CARGO_PKG_VERSION, so the crate and npm versions are stamped from the tag at build time and the number is truthful regardless of how the binary was installed.

Versioning model

The source tree carries the reviewed CLI candidate version. The release profile requires that committed version to match the requested release before approval; the tag workflow then re-stamps and verifies the same value in each ephemeral checkout. cli-vX.Y.Z is the CLI distribution version, not a workspace-wide library-crate release. One command updates only the CLI package surfaces: npm package.json + its optionalDependencies, runx-cli, the execution-runtime release identity, and the runx-cli lockfile entry.

pnpm exec tsx scripts/set-release-version.ts X.Y.Z          # write
pnpm exec tsx scripts/set-release-version.ts --check X.Y.Z  # CI drift guard

It accepts a raw cli-vX.Y.Z / vX.Y.Z tag and strips the prefix.

CLI releases do not publish Cargo packages. runx-cli is stamped so the native binary reports the same truthful version as the npm distribution, but its internal Rust dependencies may be published only through a separately approved library-crate release. Never cut a new patch just to repair a package-manager manifest; repair the existing release asset, channel manifest, or workflow in place.

Pipeline

.github/workflows/release.yml fires on cli-v* tags. workflow_dispatch (with a version input) runs a build + render dry-run with no publishing.

Stages (the order is intentional — the GitHub Release must exist before any channel that downloads its archives):

  1. prepare — resolve the version, stamp + --check manifests, verify:fast.
  2. build (5-platform matrix) — resolve the matrix from packages/cli/native/supported-platforms.json, run the shared hostile-module contract on each native runner, then build runx and runx-js-worker together with the pinned toolchain. Per platform, package both executables into the signed npm artifact, raw archive, and Linux .deb, and record digest-bound worker evidence.
  3. smoke (5-platform matrix) — downloads each built archive and runs the extracted runx beside its matching worker on the real OS. In addition to version and archive-shape checks, the packaged candidate must execute a nested signed-registry skill, pass declared workspace environment into frozen JavaScript context, deliver complete digest-bound SKILL.md context beyond one megabyte, preserve opaque scopes, close one human approval without a duplicate gate, and terminate active JavaScript work on interruption. Windows process-tree interruption remains covered by the dedicated Windows host-job lifecycle gate. A broken, incomplete, wrong-arch, or semantically incomplete archive fails before anything is published. The same smoke runs in dry-runs.
  4. github-release — assemble checksums.txt, generate a CycloneDX SBOM, emit build-provenance attestations for the binaries, stage the install scripts, and publish the Release with all archives. This is the hub.
  5. publish-npm — verify + publish the selector and native packages with npm provenance (skip-existing).
  6. package-managers — build the channel input from the published checksums (build-channel-input.mjs), render Homebrew / Scoop / winget / AUR manifests (gen-channel-manifests.ts), verify them against the actual release archive contents (check-channel-manifests.mjs), and attach them to the Release.
  7. publish-{homebrew,scoop} — update the required package-manager channels. publish-winget submits the validated channels/winget/ manifest set asynchronously; it must not use a generator that guesses archive nesting. The AUR manifest remains a handoff artifact until Runx owns runx-bin.
  8. publish-docker — multi-arch GHCR image (pulls the musl archive from the Release; no Rust toolchain in the image build).

GitHub Actions is deliberately disabled on the winget-pkgs fork. The fork only hosts the PR head branch; the checks that gate a winget submission run on microsoft/winget-pkgs. Upstream's workflows are written for that repo's pull_request_target context, where spell check reads only the files a PR touches. A push to a fork branch has no PR context, so the same workflow scans the whole repository and fails on upstream's own vocabulary (HKLM, UAC, dsc) no matter what the manifest contains. Leave Actions off; a red run there is not a signal about the release.

Installing (end users)

These work the moment a cli-v* tag ships, with no package-manager setup:

# macOS / Linux
curl -fsSL runx.ai/install | sh
# Windows
irm runx.ai/install.ps1 | iex

runx.ai/install and runx.ai/install.ps1 are clean public paths that proxy to the scripts in this repo (scripts/install and scripts/install.ps1 on main); the script bodies are not duplicated on the site. Both detect OS/arch, download the matching archive from the GitHub Release, verify its sha256, and install to a user bin dir. Overrides: RUNX_VERSION, RUNX_INSTALL_DIR, RUNX_BASE_URL (private mirror).

The archive is one release unit: installers place runx-js-worker beside runx, and deterministic-module execution fails closed if that version-compatible worker is absent.

Site proxy: point runx.ai/install → the raw scripts/install and runx.ai/install.ps1 → raw scripts/install.ps1 (302 or pass-through). Keep the path extensionless for the shell installer.

Channel credentials

Missing credentials for the required synchronous channels fail publication or independent verification. Optional asynchronous channels never masquerade as verified release evidence.

Secret Channel Role
NPM_TOKEN npm required selector + native package publication
HOMEBREW_TAP_TOKEN Homebrew required push to runxhq/homebrew-tap
SCOOP_BUCKET_TOKEN Scoop required push to runxhq/scoop-bucket
WINGET_TOKEN winget optional asynchronous PR to microsoft/winget-pkgs
GITHUB_TOKEN GitHub Release, GHCR provided automatically

Runx owns runxhq/homebrew-tap and runxhq/scoop-bucket. The winget package is tracked through its upstream PR. Do not configure an AUR key for a package owned by an unrelated maintainer.

Cutting a release

# 1. Write release/notes/X.Y.Z.md with every required section and the exact
#    previous-tag comparison link, then validate it.
node scripts/check-runx-cli-release-notes.mjs --version X.Y.Z

# 2. On a clean OSS main commit, run the project-owned profile through the
#    canonical governed release lane. This prepares, obtains approval for, and
#    pushes the exact tag, then independently verifies every required channel.
runx skill skills/release release \
  -i project_root=. \
  -i profile_ref=release/runx-cli.json \
  -i channel=runx-cli \
  -i version=X.Y.Z \
  --json

# 3. After the release runner returns verified, add the reviewed release entry
#    to the Cloud changelog. From the workspace root, adopt the independently
#    verified release across status, Cloud's exact npm pin, lockfile, and notary.
pnpm release:adopt -- --version X.Y.Z
pnpm release:check
pnpm release:check:live

The release profile refuses a dirty checkout, a commit that differs from origin/main, an exact candidate commit without successful checks and gitleaks results, an existing tag bound to another commit, or a GHCR package that is not anonymously pullable. It also requires a complete versioned release-note file; the tag workflow publishes that exact reviewed body instead of reconstructing a partial changelog from pull requests. Use workflow_dispatch with a version only when a release-pipeline change needs a no-publish matrix rehearsal. Do not duplicate the complete platform build for an ordinary release: the governed tag publish starts the workflow that builds and smokes every target.

Never move a published semver tag. Never bump a new patch just to repair channel drift; fix the existing channel artifact or workflow unless the binary itself is wrong.

Layout

crates/rust-toolchain.toml    # pinned Rust version for reproducible builds
scripts/
  check-runx-cli-release-notes.mjs # validate complete versioned release notes
  set-release-version.ts      # stamp / --check the version across manifests
  release-platform-matrix.mjs # canonical topology -> GitHub matrix
  build-release-archives.ts   # CLI + worker archive + .sha256 per target
  build-channel-input.mjs     # checksums -> channel manifest input
  gen-channel-manifests.ts    # render Homebrew / Scoop / winget / AUR
  check-channel-manifests.mjs # verify channel manifests against real archives
  publish-winget-manifest.mjs # submit the validated winget manifest set
  make-signature-manifest.ts  # CLI + worker signature manifest
  package-rust-cli.ts         # selector + paired native package staging
  check-rust-cli-release-artifacts.ts  # npm release contract validator
  install / install.ps1       # end-user one-liner installers (proxied via runx.ai/install)
packaging/
  docker/Dockerfile           # GHCR image (fetches the musl archive)
release/
  notes/X.Y.Z.md               # exact reviewed GitHub Release body