Harden release publication plumbing

This commit is contained in:
2026-07-31 19:13:45 +00:00
parent 8d8cdbf3c5
commit cf82633ab7
6 changed files with 104 additions and 2 deletions

View File

@@ -2,8 +2,50 @@ when:
- event: tag - event: tag
steps: steps:
- name: validate-release
image: golang:1.26.5
commands:
- |
set -eu
version="$CI_COMMIT_TAG"
release_note="docs/releases/$version.md"
if ! printf '%s\n' "$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 tag: $version" >&2
exit 1
fi
test -s "$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
- name: build-release-assets - name: build-release-assets
image: golang:1.25 image: golang:1.26.5
depends_on:
- validate-release
commands: commands:
- | - |
set -eu set -eu
@@ -33,8 +75,11 @@ steps:
build_binary windows amd64 ".exe" build_binary windows amd64 ".exe"
build_binary windows arm64 ".exe" build_binary windows arm64 ".exe"
host_binary="$dist/weatherreporter-$version-$(go env GOOS)-$(go env GOARCH)"
test "$("$host_binary" --version)" = "weatherreporter $version"
- name: publish-release - name: publish-release
image: woodpeckerci/plugin-release image: woodpeckerci/plugin-release:0.3.1
depends_on: depends_on:
- build-release-assets - build-release-assets
settings: settings:
@@ -42,6 +87,8 @@ steps:
from_secret: GITEA_RELEASE_TOKEN from_secret: GITEA_RELEASE_TOKEN
files: files:
- dist/weatherreporter-* - dist/weatherreporter-*
title: Weatherreporter ${CI_COMMIT_TAG}
note: docs/releases/${CI_COMMIT_TAG}.md
checksum: sha256 checksum: sha256
checksum-file: SHA256SUMS checksum-file: SHA256SUMS
checksum-flatten: true checksum-flatten: true

View File

@@ -17,6 +17,7 @@ required Weather API endpoint.
```text ```text
weatherreporter --help weatherreporter --help
weatherreporter --version
weatherreporter generate daily --date YYYY-MM-DD [--config PATH] [--units VALUE] [--tz NAME] [--out PATH] [--llm-debug-dir PATH] [--quiet] weatherreporter generate daily --date YYYY-MM-DD [--config PATH] [--units VALUE] [--tz NAME] [--out PATH] [--llm-debug-dir PATH] [--quiet]
weatherreporter generate today [--config PATH] [--units VALUE] [--tz NAME] [--out PATH] [--date YYYY-MM-DD] [--llm-debug-dir PATH] [--quiet] weatherreporter generate today [--config PATH] [--units VALUE] [--tz NAME] [--out PATH] [--date YYYY-MM-DD] [--llm-debug-dir PATH] [--quiet]
weatherreporter generate tomorrow [--config PATH] [--units VALUE] [--tz NAME] [--out PATH] [--llm-debug-dir PATH] [--quiet] weatherreporter generate tomorrow [--config PATH] [--units VALUE] [--tz NAME] [--out PATH] [--llm-debug-dir PATH] [--quiet]
@@ -31,6 +32,10 @@ weatherreporter inspect prior [--config PATH] RUN_ID
weatherreporter inspect sources [--config PATH] RUN_ID weatherreporter inspect sources [--config PATH] RUN_ID
``` ```
`weatherreporter --version` prints the version embedded in the executable.
Tagged release binaries report their semantic version tag; ordinary local
builds report `development`.
| Command | Contract | | Command | Contract |
| --- | --- | | --- | --- |
| `generate daily` | Requires `--date YYYY-MM-DD`; the date is interpreted in the effective report timezone. | | `generate daily` | Requires `--date YYYY-MM-DD`; the date is interpreted in the effective report timezone. |

View File

@@ -4,6 +4,10 @@
requests, and translates app results to bounded JSON summaries. The user requests, and translates app results to bounded JSON summaries. The user
contract belongs in the [CLI reference](../cli.md). contract belongs in the [CLI reference](../cli.md).
The root `--version` flag reports the build version supplied by
`internal/buildinfo`. Tagged release builds replace its development default at
link time.
For each `generate` or `run` action, `Runner` constructs one project-owned For each `generate` or `run` action, `Runner` constructs one project-owned
Promptkit executor after configuration loads. It passes the executor and any Promptkit executor after configuration loads. It passes the executor and any
`--llm-debug-dir` request into the app. `run` accepts the debug flag as well `--llm-debug-dir` request into the app. `run` accepts the debug flag as well

View File

@@ -0,0 +1,6 @@
// Package buildinfo exposes release metadata injected by the build pipeline.
package buildinfo
// Version identifies this Weatherreporter build. Release builds replace the
// development value with their semantic version tag through the Go linker.
var Version = "development"

View File

@@ -7,6 +7,7 @@ import (
"io" "io"
"gitea.maximumdirect.net/eric/weatherreporter/internal/app" "gitea.maximumdirect.net/eric/weatherreporter/internal/app"
"gitea.maximumdirect.net/eric/weatherreporter/internal/buildinfo"
"gitea.maximumdirect.net/eric/weatherreporter/internal/config" "gitea.maximumdirect.net/eric/weatherreporter/internal/config"
"gitea.maximumdirect.net/eric/weatherreporter/internal/report" "gitea.maximumdirect.net/eric/weatherreporter/internal/report"
"gitea.maximumdirect.net/eric/weatherreporter/internal/timeutil" "gitea.maximumdirect.net/eric/weatherreporter/internal/timeutil"
@@ -16,6 +17,7 @@ const helpText = `weatherreporter prepares weather reports from normalized forec
Usage: Usage:
weatherreporter --help weatherreporter --help
weatherreporter --version
weatherreporter generate daily --date YYYY-MM-DD [--config PATH] [--units VALUE] [--tz NAME] [--out PATH] [--llm-debug-dir PATH] [--quiet] weatherreporter generate daily --date YYYY-MM-DD [--config PATH] [--units VALUE] [--tz NAME] [--out PATH] [--llm-debug-dir PATH] [--quiet]
weatherreporter generate today [--config PATH] [--units VALUE] [--tz NAME] [--out PATH] [--date YYYY-MM-DD] [--llm-debug-dir PATH] [--quiet] weatherreporter generate today [--config PATH] [--units VALUE] [--tz NAME] [--out PATH] [--date YYYY-MM-DD] [--llm-debug-dir PATH] [--quiet]
weatherreporter generate tomorrow [--config PATH] [--units VALUE] [--tz NAME] [--out PATH] [--llm-debug-dir PATH] [--quiet] weatherreporter generate tomorrow [--config PATH] [--units VALUE] [--tz NAME] [--out PATH] [--llm-debug-dir PATH] [--quiet]
@@ -31,6 +33,7 @@ Usage:
Options: Options:
-h, --help Show this help message. -h, --help Show this help message.
--version Show the Weatherreporter version.
--config PATH Load configuration from PATH instead of /usr/local/etc/weatherreporter/config.yml. --config PATH Load configuration from PATH instead of /usr/local/etc/weatherreporter/config.yml.
--units VALUE Override weather API units. --units VALUE Override weather API units.
--tz NAME Override weather API timezone. --tz NAME Override weather API timezone.
@@ -43,6 +46,7 @@ Options:
type Runner struct { type Runner struct {
Clock timeutil.Clock Clock timeutil.Clock
ExecutorFactory ExecutorFactory ExecutorFactory ExecutorFactory
Version string
} }
func Run(ctx context.Context, args []string, stdout io.Writer, stderr io.Writer) error { func Run(ctx context.Context, args []string, stdout io.Writer, stderr io.Writer) error {
@@ -57,6 +61,17 @@ func (r Runner) Run(ctx context.Context, args []string, stdout io.Writer, stderr
_, err := fmt.Fprint(stdout, helpText) _, err := fmt.Fprint(stdout, helpText)
return err return err
} }
if args[0] == "--version" {
if len(args) != 1 {
return fmt.Errorf("--version does not accept arguments")
}
version := r.Version
if version == "" {
version = buildinfo.Version
}
_, err := fmt.Fprintf(stdout, "weatherreporter %s\n", version)
return err
}
switch args[0] { switch args[0] {
case "generate": case "generate":

View File

@@ -88,6 +88,7 @@ func TestRunnerHelpListsOnlySupportedCommands(t *testing.T) {
t.Fatalf("Run(--help) error = %v", err) t.Fatalf("Run(--help) error = %v", err)
} }
for _, command := range []string{ for _, command := range []string{
"--version",
"generate daily", "generate today", "generate tomorrow", "generate hourly", "run morning", "run evening", "generate daily", "generate today", "generate tomorrow", "generate hourly", "run morning", "run evening",
"inspect reports", "inspect metadata", "inspect modules", "inspect data-package", "inspect prior", "inspect sources", "inspect reports", "inspect metadata", "inspect modules", "inspect data-package", "inspect prior", "inspect sources",
} { } {
@@ -102,6 +103,30 @@ func TestRunnerHelpListsOnlySupportedCommands(t *testing.T) {
} }
} }
func TestRunnerVersion(t *testing.T) {
for _, test := range []struct {
name string
runner Runner
version string
}{
{name: "development default", runner: Runner{}, version: "development"},
{name: "injected release", runner: Runner{Version: "v0.9.0-test"}, version: "v0.9.0-test"},
} {
t.Run(test.name, func(t *testing.T) {
output, err := runCLICommand(test.runner, "--version")
if err != nil {
t.Fatalf("Run(--version) error = %v", err)
}
if output.stdout != "weatherreporter "+test.version+"\n" || output.stderr != "" {
t.Fatalf("Run(--version) output = stdout %q stderr %q", output.stdout, output.stderr)
}
})
}
if _, err := runCLICommand(Runner{Version: "v0.9.0-test"}, "--version", "extra"); err == nil {
t.Fatal("Run(--version extra) error = nil")
}
}
func TestResolveSupportedCommandsAndFlags(t *testing.T) { func TestResolveSupportedCommandsAndFlags(t *testing.T) {
configPath := writeCLIConfig(t, t.TempDir(), "") configPath := writeCLIConfig(t, t.TempDir(), "")
runner, constructions := countingRunner(cliExecutor{}) runner, constructions := countingRunner(cliExecutor{})