---
type: Process Specification
title: Release Specification
description: Defines the release contract for publishing Yolop and its supporting crates.
---
# Release Specification
## Abstract
This spec defines how yolop is cut, published, and verified. Releases are
agent-prepared, human-merged, and CI-published to two registries: crates.io
and the `everruns/homebrew-tap` Homebrew tap.
The canonical agent workflow lives in
[`.agents/skills/release/SKILL.md`](../../.agents/skills/release/SKILL.md). That
skill is user-invocable as `/release`.
## Versioning
Yolop follows [Semantic Versioning](https://semver.org/):
- **MAJOR** (X.0.0): incompatible CLI flags, removed providers, breaking config
- **MINOR** (0.X.0): new features, new tools, new providers
- **PATCH** (0.0.X): bug fixes, documentation, dependency bumps
Pre-1.0 (current): minor bumps may carry breaking changes if they are flagged
in the changelog.
## Release Targets
Every yolop release ships to:
| GitHub Release | tag `vX.Y.Z`, source archive, binaries | `gh release download vX.Y.Z` |
| crates.io | `yolop` binary + three library crates | `cargo install yolop --locked` |
| Homebrew tap | formula at `everruns/homebrew-tap` | `brew install everruns/tap/yolop` |
The three library crates (`yolop-yep`, `tuika`, and `tuika-codeformatters`)
are versioned independently of `yolop` and are published as a side effect of a
`yolop` release only when their in-tree version isn't already on crates.io (see
§ `publish.yml`). They can also be consumed on their own (`cargo add tuika`).
Prebuilt CLI binaries are produced for:
| macOS | `aarch64-apple-darwin` | `macos-latest` |
| macOS | `x86_64-apple-darwin` | `macos-latest` |
| Linux | `x86_64-unknown-linux-gnu` | `ubuntu-latest` |
## Release Flow
```
┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐
│ Human │ │ Agent │ │ Agent │ │ Human │ │ CI │ │ Agent │
│ asks │──>│ prepares │──>│ verifies │──>│ merges │──>│ tags + │──>│ monitors │
│ release │ │ PR │ │ publish │ │ PR │ │ publishes│ │ registries│
└──────────┘ └──────────┘ └──────────┘ └──────────┘ └──────────┘ └──────────┘
```
Skipping `verify-can-publish` risks tagging a release that fails to publish.
Skipping `monitor-published` risks declaring "shipped" while one of the two
registries silently failed.
### Human Steps
1. **Ask the agent** to create a release:
- "Cut release v0.2.0"
- "Prepare a patch release"
2. **Review the PR** the agent opens, including its publish-readiness report.
3. **Squash and merge** — CI handles the GitHub Release, crates.io publish,
binary builds, and Homebrew formula update.
4. **Ask the agent to monitor** (or let it auto-monitor if subscribed to PR
activity) until both registries report the new version.
### Agent Steps
When asked to release, the agent:
0. **Ensure full git history.** Cloud sandboxes are often shallow-cloned,
which silently hides commits and yields a wrong commit count or changelog.
Run `git fetch --unshallow origin main 2>/dev/null || git fetch origin main`
before counting or listing commits.
1. **Determine the version.** Use the version specified by the human, or
propose the next version based on the unreleased commits (patch / minor /
major) and confirm before proceeding.
2. **Update `CHANGELOG.md`.** Add a `## [X.Y.Z] - YYYY-MM-DD` section, list
PRs in descending order with GitHub-style links and contributor handles,
end with `**Full Changelog**: URL`. For minor/major bumps, add an explicit
`### Breaking Changes` block with before/after migration snippets.
3. **Bump the version** in `Cargo.toml` and regenerate `Cargo.lock`
(`cargo update -p yolop`). The library crates under `crates/` — the
`yolop-yep` SDK, the `tuika` TUI toolkit, and `tuika-codeformatters` — are
**versioned separately**: bump one when its API or published dependency range
changes (for `yolop-yep`, also the wire protocol), and update every workspace
path dependency requirement that references it. A Tuika bump usually requires
a `tuika-codeformatters` bump because the published formatter pins a compatible
Tuika range.
4. **Run local verification:**
- `cargo fmt --check`
- `cargo clippy --all-targets --all-features -- -D warnings`
- `cargo test`
5. **Verify publish-readiness** (catches what local tests don't — the
`cargo publish` packaging step, missing files, version drift):
- `cargo publish --dry-run -p yolop-yep`, `-p tuika`, and
`-p tuika-codeformatters` must succeed.
- Confirm `Cargo.toml` and `Cargo.lock` agree on `X.Y.Z`.
- Confirm `X.Y.Z` is greater than the latest published version on
crates.io (`cargo search yolop --limit 1`).
- If any check fails, fix the root cause and re-run before opening the
PR. **Do not** merge a release PR with a known-broken publish path.
**Four crates, ordered publish.** `yolop` depends on `yolop-yep`, `tuika`,
and `tuika-codeformatters` by version, so crates.io requires all three live
first. `publish.yml` derives their dependency-first order from Cargo metadata
(currently `yolop-yep`, `tuika`, `tuika-codeformatters`, then `yolop`) and
skips versions already live. Because
of that ordering, `cargo publish --dry-run -p yolop` can fail locally until
all in-tree library versions are on crates.io. That is expected, not a broken
release; dry-run all three library crates and rely on CI to validate `yolop`
after they go live.
6. **Commit and push** the changes on a feature branch with message
`chore(release): prepare vX.Y.Z`.
7. **Open a PR** titled `chore(release): prepare vX.Y.Z`. Include the
changelog excerpt and a **publish-readiness report** (which dry-runs ran,
what the registry currently shows).
8. **Monitor and verify post-merge publishing.** After the human
squash-merges the PR:
- Watch `release.yml` complete and confirm tag `vX.Y.Z` + the GitHub
Release were created.
- Watch `publish.yml` and `cli-binaries.yml` to completion. Surface any
failure immediately.
- Run post-release verification (see below) yourself and report which
targets show the new version. Workflow success is not enough: the agent
must independently check crates.io and the Homebrew tap after merge.
- Only declare the release **shipped** when crates.io reports `X.Y.Z` and
the Homebrew tap formula points at `vX.Y.Z`. If one fails, open a hotfix
PR rather than leaving the release half-published.
## CI Automation
### `release.yml`
- **Trigger**: push to `main` whose commit message starts with
`chore(release): prepare v`, or manual `workflow_dispatch`.
- **Actions**: extracts the version from the commit subject, verifies it
matches `Cargo.toml`, extracts the matching `CHANGELOG.md` section as
release notes, creates the GitHub Release with tag `vX.Y.Z`, then
explicitly dispatches `publish.yml` and `cli-binaries.yml` against the
new tag.
- **Why explicit dispatch**: a GitHub Release created with `GITHUB_TOKEN`
does not fire `release: published` events (anti-recursion), so the
downstream workflows must be kicked manually.
### `publish.yml`
- **Trigger**: `release: published`, or `workflow_dispatch --ref vX.Y.Z` from
`release.yml`.
- **Actions**: installs the pinned Rust toolchain, verifies the tag matches
`Cargo.toml`, publishes `yolop-yep`, `tuika`, `tuika-codeformatters`, and
`yolop` in dependency order (skipping versions already live), then runs
`scripts/verify_crates_publish.py` to confirm crates.io serves the new version.
- **Secret**: `CARGO_REGISTRY_TOKEN`.
### `cli-binaries.yml`
- **Trigger**: `workflow_dispatch --ref vX.Y.Z` with the `tag` input, from
`release.yml`.
- **Actions**: builds release binaries for the three CLI targets, packages
them as `yolop-<target>.tar.gz`, uploads tarballs and `.sha256` files to
the GitHub Release, then regenerates the Homebrew formula and pushes it
to `everruns/homebrew-tap`.
- **Secret**: `DOPPLER_TOKEN`. The Doppler config holds
`HOMEBREW_TAP_GITHUB_TOKEN`, a fine-grained PAT scoped to
`everruns/homebrew-tap` only.
## Pre-Release Checklist
The agent verifies before opening the release PR:
- [ ] All CI checks pass on `main`.
- [ ] `cargo fmt`, `cargo clippy`, `cargo test` clean.
- [ ] `CHANGELOG.md` has an entry for every commit since the last release.
- [ ] `Cargo.toml` and `Cargo.lock` both read `X.Y.Z`.
- [ ] `cargo publish --dry-run -p yolop` succeeds.
- [ ] `X.Y.Z` is greater than the latest crates.io version.
- [ ] Manual terminal matrix walked (see below) if the TUI renderer changed.
## Manual Terminal Matrix
The automated tests cover the *protocol* the terminal renderer emits — the
`tests/tuika_pty.rs` PTY smoke drives the real binary and asserts alternate-screen
enter/exit, OSC 9;4 progress, OSC 8 hyperlinks, 24-bit truecolor SGR, and Braille
glyphs. What they cannot verify is how a specific emulator actually *paints* those
bytes. This is a manual checklist to walk before a release when the TUI renderer
changed, not a record of verified results — tick a box only after confirming it
yourself.
Run `cargo run -- tuika-gallery` in each terminal and check alt-screen
enter/exit, Braille/wide glyphs, truecolor, mouse-wheel scroll, and — with
`YOLOP_HYPERLINKS=1` — that the footer URL is a clickable OSC 8 link:
- [ ] Ghostty
- [ ] iTerm2
- [ ] WezTerm
- [ ] Kitty
- [ ] Windows Terminal
- [ ] Konsole
- [ ] tmux (truecolor needs `Tc`/`RGB` in `terminal-overrides`)
**Native OSC 9;4 progress** support is a fixed property of each terminal (not
something to re-verify per release). Terminals that render it: **Ghostty** (bar
at the top of the window), **Windows Terminal** and **ConEmu** (taskbar),
**WezTerm**, **Konsole**, **mintty**. Others (e.g. **iTerm2**, **Kitty**)
silently ignore the unknown OSC, so emitting it is safe everywhere — the
in-terminal UI is unaffected.
**OSC 8 hyperlinks** (`HyperlinkBackend` in `crates/tuika/src/hyperlink.rs`)
wrap `http(s)` URL runs so a supporting terminal makes them clickable:
**Ghostty**, **iTerm2**, **WezTerm**, **Kitty**, **Konsole**, recent **GNOME
Terminal / VTE**. Others ignore the escape and render the URL as plain (usually
still auto-linkified) text, so emitting it is safe everywhere. Unlike OSC 9;4,
this one *is* worth re-checking, because it writes styled spans straight to the
terminal: confirm the link is clickable **and** that surrounding text, colors,
and wrapping are undamaged. In yolop it is opt-in (`YOLOP_HYPERLINKS=1`),
default-off until this matrix is walked — that is what the checkbox above
verifies.
### Nightly cross-terminal job
`.github/workflows/nightly-terminals.yml` runs `yolop tuika-gallery` inside real
terminal emulators on a nightly schedule (and on `workflow_dispatch`), narrowing
how much of the matrix a human has to walk. In-repo tests already prove yolop
emits the right bytes (`tests/tuika_pty.rs` asserts the protocol and the parsed
vt100 grid); the nightly checks how emulators *interpret* those bytes. Legs
differ in maturity:
| tmux | Linux | `capture-pane` text | **Asserted** — `crates/tuika/scripts/nightly-assert-gallery.sh` gates the job on the box chrome, a real Braille glyph, and the footer URL. |
| kitty | Linux (Xvfb, software GL) | remote-control text + screenshot | Best-effort — captured as an artifact; assertion is a warning, not a failure. |
| iTerm2 | macOS | AppleScript session text + `screencapture` | Best-effort — artifact for inspection. |
| Windows Terminal | Windows | screenshot | Best-effort — artifact for inspection. |
A green **tmux** leg means the "alt-screen / Braille / layout / footer" rows are
already verified in a real emulator, so the manual walk reduces to the
per-emulator painting the best-effort legs only screenshot. Promote a best-effort
leg to asserting once its capture is proven stable on the runner. The best-effort
legs are `continue-on-error`, so a flaky GUI runner never reports the nightly red
on its own.
## Post-Release Verification
Run after both publish workflows finish. This is a required post-merge gate;
the release is not complete until crates.io serves `X.Y.Z` and the Homebrew
tap formula points at `vX.Y.Z`.
```bash
# crates.io
cargo search yolop --limit 1 # shows X.Y.Z
# GitHub Release
gh release view vX.Y.Z --repo everruns/yolop # tarballs + checksums present
# Homebrew tap — the formula has no explicit `version`; Homebrew scans it
# from the release tag in the download URL, so verify that instead.
curl -sSfL https://raw.githubusercontent.com/everruns/homebrew-tap/main/Formula/yolop.rb \
| grep -oE 'download/v[0-9][^/]*' | sed 's|download/||' # shows vX.Y.Z
# End-to-end install (optional, on macOS / Linux)
brew untap everruns/tap 2>/dev/null; brew install everruns/tap/yolop
yolop --version
```
If any registry is missing the new version, inspect the corresponding
workflow run (`gh run view <run-id> --log-failed`) and either re-run
(transient) or open a hotfix PR (packaging bug).
## Changelog Format
Follow the everruns convention:
```markdown
## [X.Y.Z] - YYYY-MM-DD
### Highlights
- 2–5 bullet points summarizing the most impactful changes.
### Breaking Changes
- **Short description**: what changed, why, migration.
- Before: `old_flag`
- After: `new_flag`
### What's Changed
* feat(scope): description ([#42](https://github.com/everruns/yolop/pull/42)) by @contributor
* fix(scope): description ([#41](https://github.com/everruns/yolop/pull/41)) by @contributor
**Full Changelog**: https://github.com/everruns/yolop/compare/vA.B.C...vX.Y.Z
```
Rules:
- PRs listed newest-first by number.
- `### Breaking Changes` only when present; required for MINOR or MAJOR.
- `### Highlights` is the human summary; `### What's Changed` is the
mechanical PR list.
## Hotfix Releases
For urgent fixes:
1. Ask agent: "Cut patch release vX.Y.Z+1 for the <fix>".
2. Agent branches from the latest tag, cherry-picks the fix, runs the same
pre-release checklist, and opens the PR.
3. Human reviews and merges.
## Rollback
If a published version is broken, yank it:
```bash
cargo yank --version X.Y.Z yolop
```
Yanked versions remain usable by existing `Cargo.lock` files but are not
selected for new resolves. For Homebrew, push a follow-up commit to
`everruns/homebrew-tap` that reverts `Formula/yolop.rb` to the previous
release.
## Authentication
**Repo secrets** (Settings → Secrets and variables → Actions):
| `CARGO_REGISTRY_TOKEN` | `publish.yml` | https://crates.io/settings/tokens — publish scope |
| `DOPPLER_TOKEN` | `cli-binaries.yml` | Doppler service token for the `release` config |
**Doppler secrets** (loaded by `cli-binaries.yml` via `doppler secrets get`):
| `HOMEBREW_TAP_GITHUB_TOKEN` | Fine-grained PAT scoped to `everruns/homebrew-tap` only. |
Scoping the tap PAT to the tap repo means a leak cannot touch the main
`yolop` repo.
## Related
- [`.agents/skills/release/SKILL.md`](../../.agents/skills/release/SKILL.md)
- [`knowledge/specs/shipping.md`](./shipping.md)
- [`knowledge/specs/maintenance.md`](./maintenance.md)