# Release Procedure ## Release Model Weatherreporter publishes executable binaries through tagged commits on `main`. Releases use stable semantic-version tags in the form `vMAJOR.MINOR.PATCH`. The current pipeline does not publish prereleases. Every release has one nonempty, version-matched note at `docs/releases/.md`. After the tag is pushed, the Woodpecker release pipeline validates the tagged source, builds six binaries, creates SHA-256 checksums, and creates the corresponding Gitea release. The pipeline uses the checked-in release note as the Gitea release body and does not overwrite an existing release. Before `v1.0.0`, a minor release may deliberately change user-facing interfaces when its release note explains the compatibility impact and required operator action. Patch releases must not intentionally break the documented CLI, configuration, durable artifact, or integration contracts in their minor line. Published tags and their generated releases are immutable. Never move, reuse, or delete a published tag, and never manually overwrite the release produced from it. ## Select The Version And Write The Release Note Choose an unpublished version and export it as `RELEASE_VERSION`. Run the commands in this procedure from the Weatherreporter repository root in one POSIX shell: ```sh export RELEASE_VERSION=vMAJOR.MINOR.PATCH ``` Create `docs/releases/$RELEASE_VERSION.md` with this structure: ```markdown # Weatherreporter vMAJOR.MINOR.PATCH This release ... ## Summary Summarize the release's purpose and most important outcomes. ## Compatibility State compatibility with the preceding release and identify any changed CLI, configuration, durable artifact, integration, or operating contract. ## Upgrade State the operator actions required to upgrade, or state that no special action is required. ## Changes Describe the material user-visible, operational, and maintainer-visible changes. Link to canonical documentation for exact current contracts. ``` The note is a concise changelog and adoption aid, not a replacement for current documentation. Update every affected canonical document in the same candidate commit. Do not include credentials, private infrastructure details, or claims that are not true of the candidate. Require the version, path, heading, and minimum sections before continuing: ```sh set -eu : "${RELEASE_VERSION:?export an unpublished vMAJOR.MINOR.PATCH version}" if ! printf '%s\n' "$RELEASE_VERSION" | grep -Eq '^v(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$' then printf '%s\n' "invalid release version: $RELEASE_VERSION" >&2 exit 1 fi RELEASE_NOTE="docs/releases/$RELEASE_VERSION.md" export RELEASE_NOTE test -s "$RELEASE_NOTE" grep -Fx "# Weatherreporter $RELEASE_VERSION" "$RELEASE_NOTE" grep -Fx '## Summary' "$RELEASE_NOTE" grep -Fx '## Compatibility' "$RELEASE_NOTE" grep -Fx '## Upgrade' "$RELEASE_NOTE" grep -Fx '## Changes' "$RELEASE_NOTE" ``` ## Validate The Candidate Run the same substantive checks enforced by the tag pipeline before committing the release note: ```sh test -z "$(git ls-files go.work go.work.sum)" test ! -e vendor if grep -Eq '^[[:space:]]*replace([[:space:]]|\()' go.mod then printf '%s\n' 'go.mod contains a replacement' >&2 exit 1 fi GOWORK=off go test -count=1 ./... GOWORK=off go test -race -count=1 ./... GOWORK=off go vet ./... GOWORK=off go build ./... GOWORK=off go mod tidy -diff unformatted=$( git ls-files '*.go' | while IFS= read -r go_file do gofmt -l "$go_file" done ) test -z "$unformatted" git diff --check git diff --cached --check ``` Follow every added or changed Markdown link and confirm that its local target exists. Review the candidate for generated binaries, test output, credentials, temporary files, replacements, vendored dependencies, and other files that do not belong in source control. ## Publish The Candidate Commit Commit the release note and any final current-state documentation updates, then push `main` through the ordinary repository workflow: ```sh git add "$RELEASE_NOTE" git commit -m "Document Weatherreporter $RELEASE_VERSION" git push origin main ``` Do not tag an uncommitted or unpushed candidate. Record and export the exact candidate commit after the push: ```sh RELEASE_COMMIT=$(git rev-parse --verify 'HEAD^{commit}') export RELEASE_COMMIT ``` ## Guard And Tag The Candidate Run this guard immediately before creating the tag. It requires a clean checkout on synchronized `main`, valid module hygiene, the version-matched release note, and an unpublished local and remote tag: ```sh check_release_candidate() { test "$(git branch --show-current)" = main test -z "$(git status --porcelain)" gowork_value=$(go env GOWORK) case "$gowork_value" in ''|off) ;; *) printf '%s\n' "active Go workspace: $gowork_value" >&2 return 1 ;; esac test -z "$(git ls-files go.work go.work.sum)" test ! -e vendor if grep -Eq '^[[:space:]]*replace([[:space:]]|\()' go.mod then printf '%s\n' 'go.mod contains a replacement' >&2 return 1 fi test -s "$RELEASE_NOTE" grep -Fx "# Weatherreporter $RELEASE_VERSION" "$RELEASE_NOTE" git fetch origin main --tags test "$RELEASE_COMMIT" = \ "$(git rev-parse --verify 'refs/remotes/origin/main^{commit}')" if git show-ref --verify --quiet "refs/tags/$RELEASE_VERSION" then printf '%s\n' "local tag already exists: $RELEASE_VERSION" >&2 return 1 fi if test -n "$( git ls-remote --tags origin \ "refs/tags/$RELEASE_VERSION" \ "refs/tags/$RELEASE_VERSION^{}" )" then printf '%s\n' "remote tag already exists: $RELEASE_VERSION" >&2 return 1 fi } check_release_candidate ``` Create a lightweight tag, matching Weatherreporter's existing release tags, and bind it explicitly to the guarded commit: ```sh git tag "$RELEASE_VERSION" "$RELEASE_COMMIT" test "$(git cat-file -t "refs/tags/$RELEASE_VERSION")" = commit test "$(git rev-parse --verify "refs/tags/$RELEASE_VERSION^{commit}")" = \ "$RELEASE_COMMIT" git show --no-patch --decorate "refs/tags/$RELEASE_VERSION" ``` If inspection finds an error, delete the unpublished local tag, correct the candidate, and repeat the procedure. Once the tag is pushed, it is immutable. ## Publish And Verify The Release Push only the selected tag ref. Do not use `git push --tags`: ```sh git push origin \ "refs/tags/$RELEASE_VERSION:refs/tags/$RELEASE_VERSION" ``` The tag event starts the release pipeline. Its validation step rejects a non-stable semantic tag, a missing release note, module or repository hygiene violations, and any failing test, race test, vet, build, module-tidiness, formatting, or whitespace check. Its build step also verifies that the host binary reports `weatherreporter $RELEASE_VERSION`. Wait for the pipeline to succeed, then confirm that the Gitea release: - targets `RELEASE_COMMIT` through `RELEASE_VERSION`; - is titled `Weatherreporter $RELEASE_VERSION`; - uses `RELEASE_NOTE` from the tagged commit as its body; - contains `SHA256SUMS`; and - contains Linux, macOS, and Windows binaries for both `amd64` and `arm64`, named `weatherreporter-$RELEASE_VERSION--` with `.exe` on Windows. Compare the remote tag with the guarded commit: ```sh remote_commit=$( git ls-remote --tags origin "refs/tags/$RELEASE_VERSION" | awk 'NR == 1 { print $1 }' ) test "$remote_commit" = "$RELEASE_COMMIT" ``` Download `SHA256SUMS` and every release binary into a new temporary directory, run `sha256sum --check SHA256SUMS`, and execute the binary for the maintainer's host platform with `--version`. It must print exactly: ```text weatherreporter vMAJOR.MINOR.PATCH ``` ## Failed Publication And Corrections If the tag pipeline fails after publication, preserve the tag and diagnose the failure from the pipeline logs. Fix the cause on `main`, select a new patch version, prepare a new release note, and repeat the complete procedure. Do not move or recreate the failed published tag. Do not manually edit an automatically generated Gitea release or republish its assets. A wording-only correction may be committed to the historical document on `main`, with an explicit correction note, but it does not alter the file at the tag or the generated release. Publish a new patch release when the error is material to installation, compatibility, security, or operation.