# Release Guide

This document records the release model `soldr` uses today.

It is written for two audiences:

- the repository owner, who configures the GitHub-side and PyPI-side controls
- a future agent, who needs to understand what the release workflow enforces

## Goal

The release model satisfies these properties:

- a release is triggered by bumping `workspace.package.version` in `Cargo.toml` on `main`
- CI must pass on the bumping commit before any release artifacts are published
- the matching `vX.Y.Z` tag and GitHub Release are minted by the workflow itself
- wheels are published to PyPI through OIDC Trusted Publishing
- no human approval and no GitHub App credentials are required at release time
- published release assets are immutable, attestable, and verifiable

## Current State

These controls are in place:

- `main` is **not currently branch-protected**: there are no required
  checks, no rulesets, and merges are not mechanically gated (verified
  2026-08-11; restoring protection is tracked in soldr#2469)
- immutable GitHub Releases are enabled
- GitHub Actions requires full-SHA pinning for third-party actions
- `.github/workflows/release-auto.yml` is the only release workflow
- PyPI publication uses OIDC Trusted Publishing bound to `release-auto.yml`

crates.io publication is not part of the current release direction. `soldr` is being released as a hardened binary tool, not as a promised Rust library API surface.

## Current Release Model

The release flow is unattended and PyPI-centric:

1. A reviewed PR bumps the workspace version in `Cargo.toml`.
2. That PR is merged to `main` (not currently branch-protected — see
   Current State above).
3. `.github/workflows/release-auto.yml` triggers from that push (it is filtered to `paths: Cargo.toml`).
4. The `prepare` job derives `vX.Y.Z` from `Cargo.toml`, looks up the latest version on PyPI, and sets `should_release=true` only when the Cargo version is strictly greater. It also records `tag_exists` for the catch-up case.
5. The release workflow does **not** re-run lint/test/e2e gates on the
   merged commit today; the only validation before publication is what
   ran on the PR itself. Restoring release-time gates is soldr#2469's
   target state.
6. The `build` and `build-pypi` jobs produce the platform archives and hardened wheel set.
7. The `publish` job — gated on `tag_exists == 'false'` — uses the workflow's built-in `GITHUB_TOKEN` to create the `vX.Y.Z` GitHub Release with `SHA256SUMS.txt` and a build provenance attestation.
8. The `publish-pypi` job uploads the wheel set through `pypa/gh-action-pypi-publish` using OIDC.

The intentional authorization step is the reviewed version-bump merge. PyPI is the source of truth for "have we already shipped this," which means a Cargo bump that is ahead of PyPI but behind the latest GitHub tag still publishes to PyPI without re-creating the GitHub Release.

## Owner Setup

These are the one-time controls that make the unattended flow work.

### 1. Keep `main` Protected

Desired branch policy:

- no direct human pushes
- pull-request-only updates
- linear history enabled
- force pushes disabled
- deletions disabled
- required checks enabled

The release workflow assumes that any version bump reaching `main` has already passed the required validation gate.

### 2. Register PyPI Trusted Publisher

Register a GitHub publisher on https://pypi.org/manage/project/soldr/settings/publishing/ as a maintainer of the `soldr` project:

- Owner: `zackees`
- Repository: `soldr`
- Workflow filename: `release-auto.yml`
- Environment: leave blank

There is no environment approval boundary in the unattended model.

### 3. Tag Protection

If `refs/tags/v*.*.*` is protected by a repository ruleset, the built-in `github-actions` integration must be an allowed bypass actor — otherwise the workflow cannot mint the tag with `GITHUB_TOKEN`. If tag protection is not required for this model, the ruleset can be disabled.

The previous GitHub App tag-creation path is not used. Any `RELEASE_APP_ID` variable, `RELEASE_APP_PRIVATE_KEY` secret, and the standalone release App can be removed.

## Future Agent Instructions

Before changing the release workflow:

1. Read this file.
2. Audit the live GitHub-side controls before trusting the checked-in docs.
3. Confirm the PyPI Trusted Publisher for `release-auto.yml` is still registered.
4. Confirm `main` still requires pull requests and the expected validation checks.
5. Confirm tag protection (if any) still permits `github-actions` to mint `v*.*.*` tags.
6. Confirm that `Cargo.toml` workspace version is still the trigger surface — do not reintroduce `workflow_dispatch` or environment approval boundaries without explicit owner instruction.

If the live GitHub-side or PyPI-side controls drift from the documented flow, stop and report the drift instead of assuming the release posture is intact.

When preparing a normal release:

1. Bump `[workspace.package].version` in `Cargo.toml`.
2. Bump `package.json` to the exact same version.
3. Confirm the candidate is not already published on PyPI as `soldr` and not already published on npm as `@zackees/soldr`.
4. Confirm `git ls-remote --tags origin vX.Y.Z` does not find the candidate tag.
5. Open and merge the version-bump PR to `main`; the release workflow creates the `vX.Y.Z` tag and GitHub Release from that merge commit.

If `Autonomous Release` is run manually without an unpublished package version, the prepare job will set `should_release=false` and all build/publish jobs will be skipped. That is expected behavior, not a release failure. For a new release, prepare a new version-bump PR instead of manually rerunning the workflow.

## Verification Checklist

After an autonomous release:

- the `Autonomous Release` run on `main` reports `should_release=true` and the expected `version=vX.Y.Z`
- a non-draft GitHub Release exists at `vX.Y.Z` with the platform archives and `SHA256SUMS.txt`
- `https://pypi.org/project/soldr/X.Y.Z/` shows `Uploaded using Trusted Publishing? Yes`
- `gh attestation verify dist/soldr-vX.Y.Z-*-SHA256SUMS.txt --repo zackees/soldr` succeeds against a downloaded archive

## Related Documents

- [README.md](./README.md)
- [SECURITY.md](./SECURITY.md)
- [docs/PYPI_TRUSTED_PUBLISHING.md](./docs/PYPI_TRUSTED_PUBLISHING.md)
- [docs/RELEASE_VERIFICATION.md](./docs/RELEASE_VERIFICATION.md)
- [docs/TRUST_BOUNDARIES.md](./docs/TRUST_BOUNDARIES.md)
