Document Weatherreporter release procedure
This commit is contained in:
269
docs/release.md
Normal file
269
docs/release.md
Normal file
@@ -0,0 +1,269 @@
|
||||
# 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/<tag>.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, workspace 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-<os>-<arch>` 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.
|
||||
Reference in New Issue
Block a user