8.4 KiB
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:
export RELEASE_VERSION=vMAJOR.MINOR.PATCH
Create docs/releases/$RELEASE_VERSION.md with this structure:
# 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:
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:
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:
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:
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:
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:
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:
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_COMMITthroughRELEASE_VERSION; - is titled
Weatherreporter $RELEASE_VERSION; - uses
RELEASE_NOTEfrom the tagged commit as its body; - contains
SHA256SUMS; and - contains Linux, macOS, and Windows binaries for both
amd64andarm64, namedweatherreporter-$RELEASE_VERSION-<os>-<arch>with.exeon Windows.
Compare the remote tag with the guarded commit:
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:
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.