# Releasing
Releases are automated. You do **not** hand-edit versions, hand-tag, or run a publish script.
## The pipeline
```
Conventional-commit PRs land on main
│
▼
release-please.yml ── maintains a standing "release PR" that:
│ • bumps the single workspace version
│ ([workspace.package].version — all 43 crates inherit it
│ via version.workspace = true)
│ • rewrites CHANGELOG.md
▼
You merge the release PR
│
▼
release-please ── creates the GitHub Release + pushes tag vX.Y.Z
│ then dispatches release.yml (a tag pushed with GITHUB_TOKEN
│ does NOT trigger `on: push: tags` — GitHub's recursion guard —
│ so the publish is invoked explicitly)
▼
release.yml ── publishes all 43 geiserx_* crates to crates.io via OIDC
trusted publishing (no stored token), driving
scripts/publish-crates.sh (leaf-first order, resumes past
already-published crates, self-heals through the 429 rate limit).
release-binaries.yml separately attaches the C-library artifacts.
```
So the entire release action is: **merge the release PR.** Everything else is automatic.
## Version policy (pre-1.0)
`release-please` derives the bump from Conventional Commit types (`release-please-config.json`,
`bump-minor-pre-major: true`):
| `fix:` | patch (`0.x.PATCH`) |
| `feat:` | minor (`0.MINOR.0`) |
| `feat!:`/`BREAKING CHANGE:` | minor while < 1.0 (not a major) |
The version lives in exactly one place — `[workspace.package].version` in the root `Cargo.toml` —
and `release-please` bumps it via a generic-TOML `extra-files` updater on
`$.workspace.package.version`. (It uses `release-type: simple`, **not** `rust`: the `rust` updater
rewrites `[package].version` with a literal, which would break the facade crate's
`version.workspace = true` inheritance and split the workspace version across the 43 crates.)
## One-time setup: crates.io trusted publishing
`release.yml` authenticates to crates.io with **OIDC trusted publishing** — no long-lived token is
stored in the repo. This requires registering this repository as a *trusted publisher* on crates.io,
**once per crate** (all 43 crates already exist on crates.io, so this is pure configuration — no
token bootstrap is needed).
For **each** `geiserx_*` crate, on its crates.io *Settings → Trusted Publishing* page, add a GitHub
publisher with:
| Repository owner | `GeiserX` |
| Repository name | `tailscale-rs` |
| Workflow filename | `release.yml` |
| Environment | *(leave empty)* |
Until a crate has its trusted publisher configured, `release.yml`'s publish step fails *for that
crate only*; the version-bump + tag + GitHub-Release half (release-please.yml) works regardless.
> **Bulk option (recommended over 43 web forms):** `scripts/setup-trusted-publishing.sh` registers
> the trusted publisher on every publishable crate in one pass via the crates.io API. It is
> idempotent (skips crates already configured) and derives the crate list from `cargo metadata`, so
> it never drifts from what `publish-crates.sh` ships:
>
> ```sh
> export CARGO_REGISTRY_TOKEN=<a crates.io API token> # publish-scoped is fine
> ./scripts/setup-trusted-publishing.sh --dry-run # preview (no token needed, no writes)
> ./scripts/setup-trusted-publishing.sh # register all 43
> ```
>
> The manual web form (above) remains the supported baseline for a single crate.
## Manual fallback
If trusted publishing is unavailable, `scripts/publish-crates.sh` still works locally with a token:
```sh
export CARGO_REGISTRY_TOKEN=<crates.io token>
TS_RS_EXPERIMENT=this_is_unstable_software SKIP_PUBLISHED=1 ./scripts/publish-crates.sh
```
This is the same script `release.yml` runs in CI; the only difference is where `CARGO_REGISTRY_TOKEN`
comes from (a local token vs. the OIDC-minted one).