# Releasing Narratio This is the maintainer runbook for the [Release Policy](policy/release.md). The source release completes when the intended tag is successfully pushed to `origin`; binary delivery is a separate asynchronous status. ## Prepare the candidate Use `main` and the authoritative Git remote `origin`. Install Git, a POSIX shell, standard Unix build tools, and Go 1.26. Have the selected Go modules available before offline validation. Git access must permit fetch and tag push. Application-provider credentials are not required; Gitea delivery credentials belong to CI. Choose an unused stable `vMAJOR.MINOR.PATCH` under the [compatibility rules](policy/release.md#versioning-and-compatibility). Narratio is past v1.0.0, so intentional CLI or configuration breaks require a major release. Update affected canonical documentation with the change. Write the matching `docs/releases/vMAJOR.MINOR.PATCH.md` with a short summary and any relevant compatibility impact or upgrade action; choose sections to suit the release. End that checked-in note with these three links, replacing the version in each URL with the intended tag. The tag need not exist remotely yet. Check that `COPYRIGHT`, `LICENSE`, and `docs/oss/attribution.md` are present in the candidate. The tag CI reuses the checked-in note as the hosted release body. ```markdown [COPYRIGHT](https://gitea.maximumdirect.net/eric/narratio/src/tag/vMAJOR.MINOR.PATCH/COPYRIGHT) | [LICENSE](https://gitea.maximumdirect.net/eric/narratio/src/tag/vMAJOR.MINOR.PATCH/LICENSE) | [ATTRIBUTION](https://gitea.maximumdirect.net/eric/narratio/src/tag/vMAJOR.MINOR.PATCH/docs/oss/attribution.md) ``` Retire completed roadmaps included in the release while preserving planned or deferred work and repairing affected links. Review the change through the ordinary development workflow for unintended files, credentials, private data, and documentation accuracy. Commit and push the candidate to `origin/main` before tagging; record its exact commit if needed: ```sh git rev-parse HEAD ``` ## Publish From a clean `main` checkout at the pushed candidate, run one command: ```sh scripts/release.sh vMAJOR.MINOR.PATCH ``` It fetches `origin/main` and tags; requires local `HEAD` to equal `origin/main`; checks the committed note and unused version; runs the candidate checker **once**; then repeats cheap mutable-state guards before creating an unsigned lightweight tag at the recorded commit. It verifies the tag target and pushes only `refs/tags/vMAJOR.MINOR.PATCH:refs/tags/vMAJOR.MINOR.PATCH`. It does not commit, push `main`, force updates, or publish all tags. The checker runs `go mod tidy -diff`, tracked Go formatting and diff whitespace checks, one `go test -mod=readonly -race -count=1 ./...` full suite, and `go vet -mod=readonly ./...`. Its asset builder cross-builds Linux amd64, Linux arm64, and best-effort macOS arm64, then checks the embedded version on a host binary. The macOS cross-build proves compilation, not native runtime behavior. Owned Go commands run with `GOWORK=off`; tests, vet, and builds use readonly module mode. The deterministic suite includes documentation and maintained-example checks. The command does not contact application services or Gitea and does not wait for CI. A successful tag push is the source-release completion point. The command prints the tag and commit and exits without a separate network read-back. For optional diagnosis before publication, run `scripts/check-release-candidate.sh vMAJOR.MINOR.PATCH`; publication will validate the candidate again, so this is not a required preparation step. ## Artifact delivery Tag-triggered `.woodpecker/release.yml` independently validates the tagged source, builds the same three targets, and publishes these Gitea downloads: - `narratio-vMAJOR.MINOR.PATCH-linux-amd64` - `narratio-vMAJOR.MINOR.PATCH-linux-arm64` - `narratio-vMAJOR.MINOR.PATCH-darwin-arm64` (best effort) - `SHA256SUMS` The builder embeds the tag version and checks host `version` output. CI generates checksums and uses non-overwriting publication settings. It does not select versions, change the source, or move tags. No image or package registry output is declared. Tag publication is not proof that these asynchronous downloads are available. ## Failure and recovery If validation fails before tagging, correct and commit the candidate, push it to `main`, and rerun the publication command. If the tag push reports failure, its outcome may be ambiguous: keep the local tag and inspect the exact `refs/tags/vMAJOR.MINOR.PATCH` at `origin` before declaring success or retrying. A reachable matching remote tag at the recorded commit establishes publication; a confirmed absent tag permits investigation of the transport failure. If `origin` cannot be inspected, publication status is unknown. Do not move, overwrite, or automatically retry the tag. Remove a local tag only after confirming it was never published or used by a release channel. A transient CI or delivery failure can be retried from the same immutable tag. Missing assets may be added later if they match the tagged source and any published checksum manifest; do not overwrite existing binaries, checksums, or release-note contents. A source/content defect, or delivery that cannot be corrected without replacement, requires a new suitable version and matching note. Leave the original release intact. Optional consumer inspection may compare the tag's remote commit with the recorded candidate, check the Woodpecker delivery job and hosted Gitea note, download the assets into an empty directory, run `sha256sum --check SHA256SUMS` (or `shasum -a 256 --check SHA256SUMS` on macOS), and run the matching host binary's `version` command. This is separate delivery observation, never a source-release gate.