Files
weatherreporter/docs/release.md

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_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:

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.