core/docs/RELEASE.md
Release
Two human steps. Everything else is automation.
| 1 | 1. git tag && git push → CI creates a GitHub draft |
| 2 | 2. Edit Notes, click Publish |
| 3 | stable → ployz.sh/stable, /v<major>/stable, both betas + Homebrew |
| 4 | beta → ployz.sh/beta, /v<major>/beta |
No next branch. Features and fixes both land on main. Beta is a tag.
Cut a release
- From
core/, set[workspace.package] versioninCargo.tomlto the version you will tag (0.2.0or0.3.0-beta.1).check-release-tag.shrejects a tag if the Cargo version is missing or differs. - Merge that commit to
main. - Tag and push:
| 1 | git tag v0.2.0 |
| 2 | git push origin v0.2.0 |
Beta: v0.3.0-beta.1 with Cargo version 0.3.0-beta.1. -rc and every other suffix are rejected.
- Wait for the Release workflow. The tag run validates the tag and commit, builds the six CLI and daemon archives using the shared kache/R2 cache, builds and pushes
ghcr.io/getployz/ployz-cloud:<tag>, and opens a draft GitHub release (--prereleaseon beta tags) withployz-cloud-compose.yml(pinned to the tag) andployz-cloud.env.exampleattached for self-hosting. - Fill
## Notesas Release notes describes. Nick approves the notes before Publish. Click Publish. That click is the review gate. Drafts are not public downloads.
The ghcr.io/getployz/ployz-cloud:<tag> image is pushed during the draft run and is pullable by exact tag before Publish; only Publish announces it and moves the channel pointers.
Automatic releases run the workflow version stored in the tagged commit. For recovery using the current workflow, dispatch release.yml from main with tag and its expected commit sha.
main pushes populate the shared R2 compiler cache through the checks selected for that change. Release archive checks run when packaging inputs, Cargo manifests, Cargo.lock, or core/.cargo change. Cloud reuses a native SDK and config WASM artifact only when their source-input hash matches exactly, and builds from source on a miss. These checks do not publish releases. Only main pushes receive R2 write credentials, and a main push without them fails rather than silently skipping cache writes. PR, tag, release, scheduled, and manual runs use separate R2 read-only credentials. GitHub Actions variables KACHE_S3_BUCKET, KACHE_S3_ENDPOINT, and KACHE_S3_REGION select the bucket; secrets KACHE_S3_ACCESS_KEY_ID and KACHE_S3_SECRET_ACCESS_KEY provide write access. Create a separate R2 token with Object Read only permission scoped to the cache bucket, and store its credentials as KACHE_S3_READ_ACCESS_KEY_ID and KACHE_S3_READ_SECRET_ACCESS_KEY. Same-repository PRs reuse the shared cache with these read-only keys. Both credential pairs must be configured. GitHub does not expose these secrets to fork PRs.
Confidence before Publish
ci.yml selects checks for PR and main changes and reports one CI ready result. Require that check in branch protection after the workflow lands. Rust lint and tests run independently; Cloud static checks, SDK preparation, frontend builds, and tests have separate results. No workflow runs the ignored cluster suite. Run it by hand with core/scripts/run-layer3-tests.sh, which builds a testkit image of the checkout and writes per-suite logs, with five-minute suite limits and no whole-batch retries. It is informing: a red run does not block a tag or Publish.
The live agent eval runs only on a developer's machine, never in GitHub Actions, and no release waits on it. From dashboard/, pnpm eval:live runs every pinned model on every task, 8 trials each, and writes evals/results/gate.json, which lists every blocker against the floors in evals/agent/metrics.ts. Point PLOYZ_EVAL_LAST at an earlier run's evals/results/summary.json to also flag pass^1 drops. It needs Docker and signed-in claude and codex CLIs.
No-model agent replays are also local only. Nothing under dashboard/evals runs in GitHub Actions, and rust-tests skips the ployz mcp transcript replays. Run both before a release. From dashboard/, pnpm sdk:eval, then pnpm eval:replay replays the golden trajectories through the sidebar. It needs Docker and cargo build -p ployz-testkit --bin ployz-fake-fleet. pnpm eval runs everything under dashboard/evals, with live trials still skipped. From core/, cargo nextest run -p ployz --run-ignored only -E 'test(/transcript_replays/)' replays every agent tool transcript through ployz mcp.
Before Publish, use the beta on real Machines through Cloud: enrol, deploy, attach a domain and certificate, and upgrade. There is no scripted qualification; when the informing cluster suite and those Machines disagree, the real Machines are the authority. Testkit bugs do not block a release.
Release notes
Release notes build trust, and readers often hand them to an AI to judge whether Ployz is safe to use. Write them for that reader.
- Lead with what the release lets people do: benefits and new features first.
- Keep fixes generic, in one line such as "Reliability and safety improvements across Volumes, enrollment, upgrades and Server removal." Don't describe bugs one by one, and don't link bug issues.
- Have no Known issues section. Open bugs live in GitHub issues.
- Keep only what a user must act on to upgrade safely: the upgrade path (for example, first Server over SSH, then the rest through it), real breaking changes, and self-hosted Cloud's Postgres backup and
db:migrate. State them calmly and briefly. - Avoid alarming words such as critical, data loss, brick or freeze.
- Under the Stable promise, name any new wire surface, such as an ALPN, as additive.
- End with the install lines and the Full Changelog link for the new tag.
What Publish does
scripts/promote-release.sh runs on release: published.
- Writes one-line pointer files (
v0.2.0) on thechannelsbranch: the tag's line pointer (v0/stableorv0/beta) and the unscoped one (stableorbeta). A stable tag also writes bothbetapointers. - A pointer only moves to a higher semver tag. Publishing an older-line fix moves that line's pointers only; an older tag moves nothing.
- Only when the unscoped
stablepointer names the published tag: regeneratesFormula/ployz.rbfromchecksums.txtand pushesgetployz/homebrew-ployz.
Promotion runs one at a time. GitHub keeps only one waiting run, so publishing three releases in quick succession cancels the middle one's promotion: re-run any cancelled Promote published release run. Re-running is safe; pointers only move forward.
Needs repo secret HOMEBREW_TAP_TOKEN (write access to the tap). Channel updates use GITHUB_TOKEN. Publish then dispatches ployz.sh, which deploys install.sh plus the channels branch files to Cloudflare Pages.
Install
| 1 | curl -fsSL https://ployz.sh | sh # stable |
| 2 | curl -fsSL https://ployz.sh | sh -s beta # highest published release, beta or stable |
| 3 | curl -fsSL https://ployz.sh | sh -s 0.2.0 # pin |
| 4 | brew install getployz/ployz/ployz # stable |
stable and beta are the only channels.
The installer reads the unscoped https://ployz.sh/stable (one line, vX.Y.Z) or /beta (vX.Y.Z or vX.Y.Z-beta.N). Missing or invalid channel files fail the install, including a prerelease on stable. Pins skip the channel fetch.
Artifacts stay on GitHub Releases. ployz.sh is the pointer plus CLI installer.
Cloudflare
The repository's .github/workflows/ployz-sh.yml Direct-Uploads the staged site to the existing ployz-sh Pages project. Installer changes on main deploy immediately. Pointer updates deploy because Publish dispatches this workflow (channels has no workflow file, so a push there cannot). Production always uses --branch=main.
| URL | Body |
|---|---|
https://ployz.sh | install.sh from this repo |
https://ployz.sh/stable | channels branch file stable |
https://ployz.sh/beta | channels branch file beta |
https://ployz.sh/v<major>/stable | channels branch file v<major>/stable |
https://ployz.sh/v<major>/beta | channels branch file v<major>/beta |
Needs repo secrets CLOUDFLARE_ACCOUNT_ID and CLOUDFLARE_API_TOKEN.
Apex and channel URLs must serve these bodies.
Homebrew
Goreleaser does not touch the tap: the config has no Homebrew step, and the release workflow also passes --skip=homebrew. scripts/promote-release.sh writes the formula. Bottles 404 if the formula is pushed while the GitHub release is still a draft. The tap updates only when a stable Publish leaves the unscoped stable pointer on that tag.
Machine daemon
ployzd install on Linux installs or replaces a Machine daemon. It accepts --version stable, --version beta, or an exact version. A channel reads the daemon's own line pointer, ployz.sh/v<major>/<channel>, and keeps an installed daemon that is newer than the pointer; only an exact version moves a Machine backwards or across lines. Use --software-only for ordinary replacement after the Machine has already been prepared. Setup downloads and verifies the CLI release's daemon as a temporary bootstrap, then that daemon installs the selected Machine release through this interface.
Management transport
Each of the six archives contains a single binary, covered by checksums.txt;
Homebrew, the CLI installer, and Machine installation copy one file. The iroh
management transport is in-process in the CLI, the daemon, and every native SDK
binding, so no helper is packaged and no extra systemd unit is installed.
Clients reach Machines through the Ployz-hosted Ployz Relay at relay.ployz.dev
(relay/). It is deployed separately from these releases;
the hostname is a compiled constant in ployz-core.