270 lines
8.4 KiB
Markdown
270 lines
8.4 KiB
Markdown
# 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.
|