From 6debe7e6417407136ea973b5f6ee8a66215ad8ff Mon Sep 17 00:00:00 2001 From: Eric Rakestraw Date: Fri, 9 Oct 2026 04:09:26 +0000 Subject: [PATCH] Upgrade ReleaseKit and simplify release tooling --- .woodpecker/release.yml | 2 +- README.md | 2 +- docs/development.md | 4 +- docs/oss/attribution.md | 46 ++++---- docs/release.md | 32 ++--- docs/roadmap/releasekit.md | 39 +++++-- go.mod | 8 +- go.sum | 8 +- scripts/release.sh | 21 +--- scripts/release_test.go | 232 ++++++++++--------------------------- 10 files changed, 147 insertions(+), 247 deletions(-) diff --git a/.woodpecker/release.yml b/.woodpecker/release.yml index f15a50f..ecc7d4c 100644 --- a/.woodpecker/release.yml +++ b/.woodpecker/release.yml @@ -1,6 +1,6 @@ matrix: include: - - GO_IMAGE: golang:1.25.5 + - GO_IMAGE: golang:1.26.8-bookworm when: - event: tag diff --git a/README.md b/README.md index f9a75de..70aad63 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ reports for review and subprocess integration. ## Quickstart -From a checkout, build with Go 1.25.5 or newer: +From a checkout, build with Go 1.26.0 or newer: ```sh go build -o ./bin/audita ./cmd/audita diff --git a/docs/development.md b/docs/development.md index 3c29d64..4225ced 100644 --- a/docs/development.md +++ b/docs/development.md @@ -41,7 +41,9 @@ ownership. Commit discipline and agent Git restrictions are in the ## Baseline Validation -Use Go 1.25.5 or newer. From the repository root: +Use Go 1.26.0 or newer for source builds. The selected release toolchain is +documented in the [release procedure](release.md#prerequisites). From the +repository root: ```sh go build -o ./bin/audita ./cmd/audita diff --git a/docs/oss/attribution.md b/docs/oss/attribution.md index caffd7e..c49c06a 100644 --- a/docs/oss/attribution.md +++ b/docs/oss/attribution.md @@ -38,7 +38,7 @@ License and notices: [LGPL v3](licenses/LGPL-3.0.txt) | ## ReleaseKit -Selected module: `gitea.maximumdirect.net/eric/ReleaseKit` at `v0.1.0`. +Selected module: `gitea.maximumdirect.net/eric/ReleaseKit` at `v0.3.0`. Copyright (C) 2026 Eric Rakestraw @@ -46,79 +46,79 @@ SPDX-License-Identifier: LGPL-3.0-or-later License and notices: [LGPL v3](licenses/LGPL-3.0.txt) | [GPL v3](licenses/GPL-3.0.txt) | -[COPYRIGHT](https://gitea.maximumdirect.net/eric/ReleaseKit/src/commit/5e60fe3113c69f95c42fbbe32a8ca4b51f5b83c1/COPYRIGHT). +[COPYRIGHT](https://gitea.maximumdirect.net/eric/ReleaseKit/src/commit/e43df4416e0ed987349b2ea04a2c68c25e552772/COPYRIGHT). -[Source](https://gitea.maximumdirect.net/eric/ReleaseKit/src/commit/5e60fe3113c69f95c42fbbe32a8ca4b51f5b83c1). +[Source](https://gitea.maximumdirect.net/eric/ReleaseKit/src/commit/e43df4416e0ed987349b2ea04a2c68c25e552772). ## Go runtime and standard library -Selected toolchain: Go 1.25.5 for the distributed Linux binaries. +Selected toolchain: Go 1.26.8 for the distributed Linux binaries. Copyright 2009 The Go Authors. SPDX-License-Identifier: BSD-3-Clause License and notices: [BSD 3-Clause](licenses/BSD-3-Clause-Go.txt) | -[LICENSE](https://github.com/golang/go/blob/fefb02adf45c4bcc879bd406a8d61f2a292c26a9/LICENSE) | -[PATENTS](https://github.com/golang/go/blob/fefb02adf45c4bcc879bd406a8d61f2a292c26a9/PATENTS). +[LICENSE](https://github.com/golang/go/blob/c293dd49cbe25e1fe8d97d94a5cb618e7b6d831e/LICENSE) | +[PATENTS](https://github.com/golang/go/blob/c293dd49cbe25e1fe8d97d94a5cb618e7b6d831e/PATENTS). -[Source](https://github.com/golang/go/commit/fefb02adf45c4bcc879bd406a8d61f2a292c26a9). +[Source](https://github.com/golang/go/commit/c293dd49cbe25e1fe8d97d94a5cb618e7b6d831e). ## Go vendored golang.org/x/crypto -Material: `golang.org/x/crypto` at `v0.39.0` in the Go 1.25.5 standard library. +Material: `golang.org/x/crypto` at `v0.46.1-0.20251210140736-7dacc380ba00` in the Go 1.26.8 standard library. Copyright 2009 The Go Authors. SPDX-License-Identifier: BSD-3-Clause License and notices: [BSD 3-Clause](licenses/BSD-3-Clause-Go.txt) | -[LICENSE](https://github.com/golang/go/blob/fefb02adf45c4bcc879bd406a8d61f2a292c26a9/src/vendor/golang.org/x/crypto/LICENSE) | -[PATENTS](https://github.com/golang/go/blob/fefb02adf45c4bcc879bd406a8d61f2a292c26a9/src/vendor/golang.org/x/crypto/PATENTS). +[LICENSE](https://github.com/golang/go/blob/c293dd49cbe25e1fe8d97d94a5cb618e7b6d831e/src/vendor/golang.org/x/crypto/LICENSE) | +[PATENTS](https://github.com/golang/go/blob/c293dd49cbe25e1fe8d97d94a5cb618e7b6d831e/src/vendor/golang.org/x/crypto/PATENTS). -[Source](https://github.com/golang/go/commit/fefb02adf45c4bcc879bd406a8d61f2a292c26a9). +[Source](https://github.com/golang/go/commit/c293dd49cbe25e1fe8d97d94a5cb618e7b6d831e). ## Go vendored golang.org/x/net -Material: `golang.org/x/net` at `v0.41.0` in the Go 1.25.5 standard library. +Material: `golang.org/x/net` at `v0.47.1-0.20260731170545-cb1d721f7fb3` in the Go 1.26.8 standard library. Copyright 2009 The Go Authors. SPDX-License-Identifier: BSD-3-Clause License and notices: [BSD 3-Clause](licenses/BSD-3-Clause-Go.txt) | -[LICENSE](https://github.com/golang/go/blob/fefb02adf45c4bcc879bd406a8d61f2a292c26a9/src/vendor/golang.org/x/net/LICENSE) | -[PATENTS](https://github.com/golang/go/blob/fefb02adf45c4bcc879bd406a8d61f2a292c26a9/src/vendor/golang.org/x/net/PATENTS). +[LICENSE](https://github.com/golang/go/blob/c293dd49cbe25e1fe8d97d94a5cb618e7b6d831e/src/vendor/golang.org/x/net/LICENSE) | +[PATENTS](https://github.com/golang/go/blob/c293dd49cbe25e1fe8d97d94a5cb618e7b6d831e/src/vendor/golang.org/x/net/PATENTS). -[Source](https://github.com/golang/go/commit/fefb02adf45c4bcc879bd406a8d61f2a292c26a9). +[Source](https://github.com/golang/go/commit/c293dd49cbe25e1fe8d97d94a5cb618e7b6d831e). ## Go vendored golang.org/x/sys -Material: `golang.org/x/sys` at `v0.33.0` in the Go 1.25.5 standard library. +Material: `golang.org/x/sys` at `v0.39.0` in the Go 1.26.8 standard library. Copyright 2009 The Go Authors. SPDX-License-Identifier: BSD-3-Clause License and notices: [BSD 3-Clause](licenses/BSD-3-Clause-Go.txt) | -[LICENSE](https://github.com/golang/go/blob/fefb02adf45c4bcc879bd406a8d61f2a292c26a9/src/vendor/golang.org/x/sys/LICENSE) | -[PATENTS](https://github.com/golang/go/blob/fefb02adf45c4bcc879bd406a8d61f2a292c26a9/src/vendor/golang.org/x/sys/PATENTS). +[LICENSE](https://github.com/golang/go/blob/c293dd49cbe25e1fe8d97d94a5cb618e7b6d831e/src/vendor/golang.org/x/sys/LICENSE) | +[PATENTS](https://github.com/golang/go/blob/c293dd49cbe25e1fe8d97d94a5cb618e7b6d831e/src/vendor/golang.org/x/sys/PATENTS). -[Source](https://github.com/golang/go/commit/fefb02adf45c4bcc879bd406a8d61f2a292c26a9). +[Source](https://github.com/golang/go/commit/c293dd49cbe25e1fe8d97d94a5cb618e7b6d831e). ## Go vendored golang.org/x/text -Material: `golang.org/x/text` at `v0.26.0` in the Go 1.25.5 standard library. +Material: `golang.org/x/text` at `v0.32.0` in the Go 1.26.8 standard library. Copyright 2009 The Go Authors. SPDX-License-Identifier: BSD-3-Clause License and notices: [BSD 3-Clause](licenses/BSD-3-Clause-Go.txt) | -[LICENSE](https://github.com/golang/go/blob/fefb02adf45c4bcc879bd406a8d61f2a292c26a9/src/vendor/golang.org/x/text/LICENSE) | -[PATENTS](https://github.com/golang/go/blob/fefb02adf45c4bcc879bd406a8d61f2a292c26a9/src/vendor/golang.org/x/text/PATENTS). +[LICENSE](https://github.com/golang/go/blob/c293dd49cbe25e1fe8d97d94a5cb618e7b6d831e/src/vendor/golang.org/x/text/LICENSE) | +[PATENTS](https://github.com/golang/go/blob/c293dd49cbe25e1fe8d97d94a5cb618e7b6d831e/src/vendor/golang.org/x/text/PATENTS). -[Source](https://github.com/golang/go/commit/fefb02adf45c4bcc879bd406a8d61f2a292c26a9). +[Source](https://github.com/golang/go/commit/c293dd49cbe25e1fe8d97d94a5cb618e7b6d831e). ## go-yaml v4 diff --git a/docs/release.md b/docs/release.md index 28f4424..af79974 100644 --- a/docs/release.md +++ b/docs/release.md @@ -2,8 +2,8 @@ The [release policy](policy/release.md) owns release requirements. This runbook uses the native commands in [scripts/release.sh](../scripts/release.sh), which -builds and invokes ReleaseKit. A confirmed source tag completes the release; -Woodpecker reports binary delivery separately. +invokes the module-selected ReleaseKit tool. A confirmed source tag completes +the release; Woodpecker reports binary delivery separately. ## Prerequisites @@ -12,16 +12,16 @@ which must point to `https://gitea.maximumdirect.net/eric/audita.git`; the primary branch is `main`. Source publication needs no delivery credential. Woodpecker uses the existing `GITEA_RELEASE_TOKEN` secret for delivery. -The source-build minimum is Go 1.25.5, declared in [go.mod](../go.mod). -This patch minimum is required by the selected PromptKit dependency. +The source-build minimum is Go 1.26.0, declared in [go.mod](../go.mod). +This minimum is required by the selected ReleaseKit dependency. Distributed executables do not require a Go installation. The selected release -toolchain is Go 1.25.5, matching the current Woodpecker image and distributed +toolchain is Go 1.26.8, matching the current Woodpecker image and distributed runtime notices. Use that local installation for tool bootstrap, candidate validation, and release builds. Replace the installation path below with its actual directory: ```sh -export PATH="/path/to/go1.25.5/bin:$PATH" +export PATH="/path/to/go1.26.8/bin:$PATH" export GOTOOLCHAIN=local go version ``` @@ -40,10 +40,12 @@ GOWORK=off GOTOOLCHAIN=local go mod download ``` If dependency preparation changes `go.mod` or `go.sum`, include those changes in -the committed candidate. The release wrapper separately builds the ReleaseKit -version selected by `go.mod` into ignored `bin/releasekit`. It uses temporary -copies of `go.mod` and `go.sum` for tool dependency preparation; that build is -tool preparation, not candidate validation. +the committed candidate. ReleaseKit's CLI is registered as a Go tool dependency; +`go mod tidy` retains its dependencies and checksums. The release wrapper changes +to the repository root and invokes `go tool releasekit` with workspace discovery +and automatic toolchain downloads disabled. Go builds or reuses the selected +tool from its normal build cache. Tool preparation precedes offline candidate +validation. ## Prepare and publish the candidate @@ -106,7 +108,7 @@ out detached, and confirms the source tree is clean. It copies the tagged `go.mod` and `go.sum` into temporary module metadata outside the checkout. Go confirms that this configuration selects ReleaseKit, then builds its CLI using an alternate `-modfile`. The bootstrap executable and temporary files remain -outside the tagged source. The workflow runs with the existing Go 1.25.5 image, +outside the tagged source. The workflow runs with the selected Go 1.26.8 image, `GOWORK=off`, and `GOTOOLCHAIN=local`. The resulting executable receives: @@ -125,8 +127,8 @@ after source publication; its status and logs report delivery progress or failure. ReleaseKit handles content-aware retries and retained-artifact recovery. See -the pinned [publication recovery guide](https://gitea.maximumdirect.net/eric/ReleaseKit/src/tag/v0.1.0/docs/consumers/publish.md) -and [delivery guide](https://gitea.maximumdirect.net/eric/ReleaseKit/src/tag/v0.1.0/docs/consumers/delivery.md) +the pinned [publication recovery guide](https://gitea.maximumdirect.net/eric/ReleaseKit/src/tag/v0.3.0/docs/consumers/publish.md) +and [delivery guide](https://gitea.maximumdirect.net/eric/ReleaseKit/src/tag/v0.3.0/docs/consumers/delivery.md) for those mechanics. A delivery failure does not undo a confirmed source release. Retry the same immutable tag after resolving the reported state; do not replace published files or a checksum manifest. @@ -139,11 +141,11 @@ captured authoritative endpoint and publication state. Resolve the result against that endpoint before retrying. An exact remote tag-object match, with its source available on remote `main`, can confirm publication. If the remote state cannot be read, the result remains unknown; do not remove or replace the -tag while uncertain. Follow the pinned [publication recovery guide](https://gitea.maximumdirect.net/eric/ReleaseKit/src/tag/v0.1.0/docs/consumers/publish.md) +tag while uncertain. Follow the pinned [publication recovery guide](https://gitea.maximumdirect.net/eric/ReleaseKit/src/tag/v0.3.0/docs/consumers/publish.md) for object-level recovery details. A source-release success and artifact-delivery status are separate. For a partial or uncertain delivery, inspect the job output and follow the pinned -[delivery recovery guidance](https://gitea.maximumdirect.net/eric/ReleaseKit/src/tag/v0.1.0/docs/consumers/delivery.md). +[delivery recovery guidance](https://gitea.maximumdirect.net/eric/ReleaseKit/src/tag/v0.3.0/docs/consumers/delivery.md). A source or published-content correction requires a new appropriate version; leave the original tag and its contents intact. diff --git a/docs/roadmap/releasekit.md b/docs/roadmap/releasekit.md index 3dff33c..a5bbba7 100644 --- a/docs/roadmap/releasekit.md +++ b/docs/roadmap/releasekit.md @@ -1,7 +1,8 @@ # ReleaseKit v0.3.0 Upgrade Roadmap **Last updated:** 2026-10-08 -**Status:** Release policy adopted; library and toolchain upgrade pending. +**Status:** Implementation complete; first upgraded release and live delivery +observation pending. ## Purpose @@ -11,9 +12,9 @@ ReleaseKit; Audita supplies product facts, compatibility decisions, release notes, and thin integration wiring. Standardization and less locally owned release code remain the reasons for adoption. -This roadmap replaces the completed initial-adoption roadmap. It defines the -upgrade's target state and required work; a later implementation plan can own -ordered execution steps. +This roadmap replaces the completed initial-adoption roadmap. It records the +upgrade's target state and completed scope. The upgrade was implemented in one +pass without a separate staged plan. ## Current Baseline @@ -41,10 +42,11 @@ At Audita commit `17421bc5f352f3fdeb67561b2b60b49979822228`: The initial adoption's [review findings](remediation.md) are resolved. They are historical context, not additional work for this upgrade. -Since that baseline, Audita has adopted release policy 1.6 and updated its -provenance, policy attribution, and release runbook. The current Go minimum and -selected release toolchain remain 1.25.5. The dependency, tool registration, -bootstrap simplification, and Go 1.26 upgrade described below remain pending. +Since that baseline, Audita has adopted release policy 1.6, registered and +upgraded the ReleaseKit tool to v0.3.0, simplified the local wrapper, and +updated CI and notices. The current Go minimum is 1.26.0; the selected release +toolchain is 1.26.8. The scope below records the requirements fulfilled by this +implementation. ## Selected Upstream Revisions @@ -94,7 +96,7 @@ increases in minor releases, with the increase and required source-consumer action explained in the release note. Patch releases preserve the consumer minimum. Other compatibility changes remain governed by the adopted policy. -## Work Required +## Implemented Scope ### Module Selection and Local Tooling @@ -208,3 +210,22 @@ asynchronously when the first upgraded release is published and address any deployment failures through the shared recovery procedure. No significant feature decisions remain open; implementation details and final release preparation can be resolved within the agreed scope. + +## Implementation Completion + +Completed with Go 1.26.8. The readonly offline full suite passed. The real local +wrapper selected ReleaseKit v0.3.0; disposable Go-tool fixtures verified native +argument boundaries, statuses 0/2/3, and unchanged module files. The CI workflow +matches the v0.3.0 maintained template with Audita's existing secret mapping. +The real ReleaseKit builder produced both required Linux artifacts using the +test identifier v0.0.0, passed its native version smoke check, and emitted +matching artifact digests in its build receipt. Temporary output was removed. +The new release-tool dependencies are absent from Audita's runtime dependency +list. ReleaseKit and Go runtime/vendor attribution identify their selected +source revisions, with existing applicable license texts reused. + +This verification built modified local source and did not claim a clean, +published release candidate. Release preparation will validate its final +committed candidate through the ordinary publication command. No tags, source +pushes, hosted uploads, or LLM calls were made during implementation; live +artifact delivery remains a separate observation for the next release. diff --git a/go.mod b/go.mod index e81a71e..dc1ef12 100644 --- a/go.mod +++ b/go.mod @@ -1,11 +1,12 @@ module gitea.maximumdirect.net/eric/audita -go 1.25.5 +go 1.26.0 require ( gitea.maximumdirect.net/eric/ConfigKit v0.5.0 - gitea.maximumdirect.net/eric/ReleaseKit v0.1.0 + gitea.maximumdirect.net/eric/ReleaseKit v0.3.0 gitea.maximumdirect.net/eric/promptkit v0.12.0 + golang.org/x/mod v0.40.0 gopkg.in/yaml.v3 v3.0.1 ) @@ -15,7 +16,10 @@ require ( github.com/kr/pretty v0.3.1 // indirect github.com/rogpeppe/go-internal v1.14.1 // indirect github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 // indirect + github.com/yuin/goldmark/v2 v2.1.6 // indirect go.yaml.in/yaml/v4 v4.0.0-rc.6 // indirect golang.org/x/text v0.14.0 // indirect gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect ) + +tool gitea.maximumdirect.net/eric/ReleaseKit/cmd/releasekit diff --git a/go.sum b/go.sum index 2e596fd..a7e5b78 100644 --- a/go.sum +++ b/go.sum @@ -1,7 +1,7 @@ gitea.maximumdirect.net/eric/ConfigKit v0.5.0 h1:2KwG10+kApWM3avOoYA+p768xcbHUp7tQ2rqizBNpyg= gitea.maximumdirect.net/eric/ConfigKit v0.5.0/go.mod h1:tYWpsrbLyu05r8+1MapWp5fN6ZrX9I8hU2xEPedYmYk= -gitea.maximumdirect.net/eric/ReleaseKit v0.1.0 h1:Ejk85+9Of+A8M7Mod91iB3KxrrQYgnJcz9hPA6fL1a4= -gitea.maximumdirect.net/eric/ReleaseKit v0.1.0/go.mod h1:KmH607Vc/RK/zKrCB96cWh0QZYvkbxBFvtqV+BkKaxs= +gitea.maximumdirect.net/eric/ReleaseKit v0.3.0 h1:aTMn+50MEpHpHI2RoCCmJYsy7Ohrpa+RSva63Kq0/tU= +gitea.maximumdirect.net/eric/ReleaseKit v0.3.0/go.mod h1:LVodqNQ3OIKv9NBq+fFeyMByd1lHY25YOmmpah/NaNI= gitea.maximumdirect.net/eric/promptkit v0.12.0 h1:oIFWEg6GPxtmX3Plqv0euP+FnK1LCk/t4xDy6oKCpP4= gitea.maximumdirect.net/eric/promptkit v0.12.0/go.mod h1:pFOOy5v1zH17stEi9rmHYmfbuLjnfVMcUhfmk+4VtJY= gitea.maximumdirect.net/eric/promptkit-backend-openrouter/v3 v3.20260930.0 h1:lzfCDbzJQRGzIlRPRprnAV9hVVL5zUQy80VMQ0kbYa0= @@ -24,8 +24,12 @@ github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0t github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 h1:KRzFb2m7YtdldCEkzs6KqmJw4nqEVZGK7IN2kJkjTuQ= github.com/santhosh-tekuri/jsonschema/v6 v6.0.2/go.mod h1:JXeL+ps8p7/KNMjDQk3TCwPpBy0wYklyWTfbkIzdIFU= +github.com/yuin/goldmark/v2 v2.1.6 h1:igy0q9JcqovoFXFx/XjtU8sHVhcUHavPj2sA3EoKJUE= +github.com/yuin/goldmark/v2 v2.1.6/go.mod h1:G6M4/qOFtfn01/o14BU1UR2Lo5N3S9Qo7xCuz/sHjGQ= go.yaml.in/yaml/v4 v4.0.0-rc.6 h1:1h7H1ohdUh93/FyE4YaDa1Zh64K6VVbjF4K6WUxMtH4= go.yaml.in/yaml/v4 v4.0.0-rc.6/go.mod h1:aZqd9kCMsGL7AuUv/m/PvWLdg5sjJsZ4oHDEnfPPfY0= +golang.org/x/mod v0.40.0 h1:hUv+3cXcdRHz08UmSiOob7sadHig73uo5bkXxQ/tvUs= +golang.org/x/mod v0.40.0/go.mod h1:0/weTWkPWGBikyTWAX3dkjVztMmBA5hM0DH6BElSupE= golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/scripts/release.sh b/scripts/release.sh index 511fb8e..86c4913 100755 --- a/scripts/release.sh +++ b/scripts/release.sh @@ -3,23 +3,4 @@ set -euo pipefail script_dir=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd) cd "$script_dir/.." - -releasekit_module=gitea.maximumdirect.net/eric/ReleaseKit -bootstrap_dir=$(mktemp -d "${TMPDIR:-/tmp}/releasekit-bootstrap.XXXXXX") -trap 'rm -rf "$bootstrap_dir"' EXIT -cp go.mod "$bootstrap_dir/go.mod" -if [[ -f go.sum ]]; then - cp go.sum "$bootstrap_dir/go.sum" -else - : > "$bootstrap_dir/go.sum" -fi -cp "$bootstrap_dir/go.mod" "$bootstrap_dir/releasekit.mod" -cp "$bootstrap_dir/go.sum" "$bootstrap_dir/releasekit.sum" - -mkdir -p bin -GOWORK=off GOTOOLCHAIN=local go list -mod=mod -modfile="$bootstrap_dir/releasekit.mod" -m "$releasekit_module" >/dev/null -GOWORK=off GOTOOLCHAIN=local go build -mod=mod -modfile="$bootstrap_dir/releasekit.mod" \ - -o ./bin/releasekit "$releasekit_module/cmd/releasekit" -rm -rf "$bootstrap_dir" -trap - EXIT -exec ./bin/releasekit "$@" +GOWORK=off GOTOOLCHAIN=local exec go tool releasekit "$@" diff --git a/scripts/release_test.go b/scripts/release_test.go index d48d1b6..435679f 100644 --- a/scripts/release_test.go +++ b/scripts/release_test.go @@ -2,64 +2,30 @@ package scripts import ( "encoding/json" + "go/version" "maps" "os" "os/exec" "path/filepath" "reflect" - "slices" "strconv" "strings" "testing" + "golang.org/x/mod/modfile" "gopkg.in/yaml.v3" ) const goStub = `#!/usr/bin/env bash set -euo pipefail -case ${1:?missing Go command} in -list) - log=$RELEASEKIT_LIST_LOG - printf '%s\0' "$PWD" "GOWORK=$GOWORK" "GOTOOLCHAIN=$GOTOOLCHAIN" "$@" > "$log" - modfile= - for argument in "$@"; do - case $argument in -modfile=*) modfile=${argument#-modfile=} ;; esac - done - [[ -n $modfile && -f $modfile && -f ${modfile%.mod}.sum ]] - cp "$modfile" "$RELEASEKIT_MODULE_LOG" - cp "${modfile%.mod}.sum" "$RELEASEKIT_SUM_LOG" - if [[ ${RELEASEKIT_BOOTSTRAP_STATUS:-0} != 0 ]]; then - exit "$RELEASEKIT_BOOTSTRAP_STATUS" - fi - printf '%s\n' v0.1.0 - ;; -build) - printf '%s\0' "$PWD" "GOWORK=$GOWORK" "GOTOOLCHAIN=$GOTOOLCHAIN" "$@" > "$RELEASEKIT_BUILD_LOG" - if [[ ${RELEASEKIT_BOOTSTRAP_STATUS:-0} != 0 ]]; then - exit "$RELEASEKIT_BOOTSTRAP_STATUS" - fi - output= - while (($#)); do - case $1 in - -o) - shift - output=${1:?missing output path} - ;; - -o=*) output=${1#-o=} ;; - esac - shift - done - [[ -n $output ]] - cat > "$output" <<'RELEASEKIT_TOOL' -#!/usr/bin/env bash -set -euo pipefail +printf '%s\0' "$PWD" "GOWORK=$GOWORK" "GOTOOLCHAIN=$GOTOOLCHAIN" "$@" > "$RELEASEKIT_GO_LOG" +if [[ ${RELEASEKIT_BOOTSTRAP_STATUS:-0} != 0 ]]; then + exit "$RELEASEKIT_BOOTSTRAP_STATUS" +fi +[[ $1 == tool && $2 == releasekit ]] +shift 2 printf '%s\0' "$PWD" "$@" > "$RELEASEKIT_ARGS_LOG" exit "${RELEASEKIT_TOOL_STATUS:-0}" -RELEASEKIT_TOOL - chmod +x "$output" - ;; -*) exit 64 ;; -esac ` func TestReleaseWrapperForwardsNativeArgumentsAndStatus(t *testing.T) { @@ -71,10 +37,7 @@ func TestReleaseWrapperForwardsNativeArgumentsAndStatus(t *testing.T) { cmd.Dir = layout.caller cmd.Env = layout.environment(map[string]string{ "RELEASEKIT_ARGS_LOG": layout.argsLog, - "RELEASEKIT_BUILD_LOG": layout.buildLog, - "RELEASEKIT_LIST_LOG": layout.listLog, - "RELEASEKIT_MODULE_LOG": layout.moduleLog, - "RELEASEKIT_SUM_LOG": layout.sumLog, + "RELEASEKIT_GO_LOG": layout.goLog, "RELEASEKIT_TOOL_STATUS": strconv.Itoa(toolStatus), }) output, err := cmd.CombinedOutput() @@ -84,58 +47,13 @@ func TestReleaseWrapperForwardsNativeArgumentsAndStatus(t *testing.T) { if len(output) != 0 { t.Fatalf("unexpected output: %q", output) } - - listInvocation := readNULArguments(t, layout.listLog) - buildInvocation := readNULArguments(t, layout.buildLog) - for name, invocation := range map[string][]string{"list": listInvocation, "build": buildInvocation} { - if len(invocation) < 4 || invocation[0] != layout.repo { - t.Fatalf("%s invocation=%q, want repository %q", name, invocation, layout.repo) - } - if invocation[1] != "GOWORK=off" || invocation[2] != "GOTOOLCHAIN=local" { - t.Fatalf("%s environment=%q", name, invocation[1:3]) - } + wantGo := append([]string{layout.repo, "GOWORK=off", "GOTOOLCHAIN=local", "tool", "releasekit"}, args...) + if got := readNULArguments(t, layout.goLog); !reflect.DeepEqual(got, wantGo) { + t.Fatalf("Go invocation=%q, want %q", got, wantGo) } - listArgs := listInvocation[3:] - buildArgs := buildInvocation[3:] - modfile := argumentValue(listArgs, "-modfile=") - if listArgs[0] != "list" || !slices.Contains(listArgs, "-mod=mod") || !slices.Contains(listArgs, "-m") || !slices.Contains(listArgs, "gitea.maximumdirect.net/eric/ReleaseKit") { - t.Fatalf("unexpected module-selection arguments: %q", listArgs) - } - if modfile == "" || argumentValue(buildArgs, "-modfile=") != modfile || !slices.Contains(buildArgs, "-mod=mod") { - t.Fatalf("list/build did not share alternate module metadata: list=%q build=%q", listArgs, buildArgs) - } - if buildArgs[0] != "build" || !slices.Contains(buildArgs, "gitea.maximumdirect.net/eric/ReleaseKit/cmd/releasekit") { - t.Fatalf("unexpected build arguments: %q", buildArgs) - } - if relative, err := filepath.Rel(layout.repo, modfile); err != nil || relative == "." || (!strings.HasPrefix(relative, ".."+string(filepath.Separator)) && relative != "..") { - t.Fatalf("alternate module file %q is not outside repository %q: relative=%q error=%v", modfile, layout.repo, relative, err) - } - if module := readText(t, layout.moduleLog); !strings.Contains(module, "gitea.maximumdirect.net/eric/ReleaseKit v0.1.0") { - t.Fatalf("temporary module file does not retain selected ReleaseKit version: %q", module) - } - if sum := readText(t, layout.sumLog); !strings.Contains(sum, "fixture checksum") { - t.Fatalf("temporary sum file did not copy source checksums: %q", sum) - } - if sourceModule := readText(t, filepath.Join(layout.repo, "go.mod")); !strings.Contains(sourceModule, "gitea.maximumdirect.net/eric/ReleaseKit v0.1.0") { - t.Fatalf("source go.mod changed: %q", sourceModule) - } - if sourceSum := readText(t, filepath.Join(layout.repo, "go.sum")); sourceSum != "fixture checksum\n" { - t.Fatalf("source go.sum changed: %q", sourceSum) - } - if _, err := os.Stat(modfile); !os.IsNotExist(err) { - t.Fatalf("temporary module file remains after wrapper: %v", err) - } - if _, err := os.Stat(filepath.Join(filepath.Dir(modfile), "releasekit.sum")); !os.IsNotExist(err) { - t.Fatalf("temporary sum file remains after wrapper: %v", err) - } - if info, err := os.Stat(filepath.Join(layout.repo, "bin", "releasekit")); err != nil || !info.Mode().IsRegular() { - t.Fatalf("ReleaseKit executable was not built in bin: %v", err) - } - - toolInvocation := readNULArguments(t, layout.argsLog) - wantToolInvocation := append([]string{layout.repo}, args...) - if !reflect.DeepEqual(toolInvocation, wantToolInvocation) { - t.Fatalf("tool invocation=%q, want %q", toolInvocation, wantToolInvocation) + wantTool := append([]string{layout.repo}, args...) + if got := readNULArguments(t, layout.argsLog); !reflect.DeepEqual(got, wantTool) { + t.Fatalf("tool invocation=%q, want %q", got, wantTool) } }) } @@ -166,8 +84,22 @@ func TestDeliveryWorkflowUsesModuleSelectedReleaseKit(t *testing.T) { if err := yaml.Unmarshal(data, &workflow); err != nil { t.Fatal(err) } - if len(workflow.Matrix.Include) != 1 || workflow.Matrix.Include[0]["GO_IMAGE"] != "golang:1.25.5" { - t.Fatalf("workflow Go image = %v, want existing Go 1.25.5 image", workflow.Matrix.Include) + goMod, err := modfile.Parse("go.mod", []byte(readText(t, "../go.mod")), nil) + if err != nil { + t.Fatal(err) + } + if goMod.Go == nil { + t.Fatal("module does not declare its Go minimum") + } + if len(workflow.Matrix.Include) != 1 { + t.Fatalf("workflow Go matrix = %v", workflow.Matrix.Include) + } + image := workflow.Matrix.Include[0]["GO_IMAGE"] + _, tag, _ := strings.Cut(image, ":") + patch, _, _ := strings.Cut(tag, "-") + imageVersion := "go" + patch + if !version.IsValid(imageVersion) || version.Compare(imageVersion, "go"+goMod.Go.Version) < 0 { + t.Fatalf("workflow Go image %q cannot satisfy module minimum %s", image, goMod.Go.Version) } if len(workflow.When) != 1 || workflow.When[0].Event != "tag" || workflow.When[0].Ref != "refs/tags/v*" { t.Fatalf("workflow trigger = %+v", workflow.When) @@ -183,41 +115,30 @@ func TestDeliveryWorkflowUsesModuleSelectedReleaseKit(t *testing.T) { } command := workflow.Steps[0].Commands[0] for _, fragment := range []string{ - "git fetch origin \"refs/tags/$${CI_COMMIT_TAG}:refs/tags/$${CI_COMMIT_TAG}\"", - "git checkout --detach \"$${tag_commit}\"", - "git status --porcelain --untracked-files=all", - "mktemp -d \"$${CI_WORKSPACE%/*}/.releasekit-bootstrap.XXXXXX\"", - "trap 'rm -rf \"$${bootstrap_dir}\"' EXIT", - "cp go.mod \"$${bootstrap_dir}/go.mod\"", - "cp go.sum \"$${bootstrap_dir}/go.sum\"", - "export GOWORK=off", - "export GOTOOLCHAIN=local", - "releasekit_module=gitea.maximumdirect.net/eric/ReleaseKit", - "go list -mod=mod -modfile=", - "-m \"$${releasekit_module}\"", - "go build -mod=mod -modfile=", - "-o \"$${bootstrap_dir}/releasekit\" \"$${releasekit_module}/cmd/releasekit\"", - "\"$${bootstrap_dir}/releasekit\" ci-deliver", - "--repo \"$${CI_WORKSPACE}\"", - "--commit \"$${CI_COMMIT_SHA}\"", + "git fetch origin", "refs/tags/$${CI_COMMIT_TAG}", + "git checkout --detach", "git status --porcelain", + "go list", "-modfile=", "-m", "go build", "$${releasekit_module}/cmd/releasekit", + "ci-deliver", "--repo \"$${CI_WORKSPACE}\"", "--commit \"$${CI_COMMIT_SHA}\"", "\"$${CI_COMMIT_TAG}\"", } { if !strings.Contains(command, fragment) { t.Fatalf("workflow command is missing %q: %s", fragment, command) } } - if strings.Index(command, "git checkout --detach") > strings.Index(command, "go list -mod=mod") || strings.Index(command, "git status --porcelain") > strings.Index(command, "go list -mod=mod") { + if strings.Index(command, "git checkout --detach") > strings.Index(command, "go list") || strings.Index(command, "git status --porcelain") > strings.Index(command, "go list") { t.Fatalf("workflow resolves the tool before verifying the tagged source: %s", command) } if strings.Contains(command, "RELEASEKIT_VERSION") || strings.Contains(command, "cmd/releasekit@") { t.Fatalf("workflow retains a separate tool version: %s", command) } - goMod, err := os.ReadFile("../go.mod") - if err != nil { - t.Fatal(err) + toolSelected := false + for _, tool := range goMod.Tool { + if tool.Path == "gitea.maximumdirect.net/eric/ReleaseKit/cmd/releasekit" { + toolSelected = true + } } - if !strings.Contains(string(goMod), "gitea.maximumdirect.net/eric/ReleaseKit v0.1.0") { - t.Fatalf("go.mod does not retain selected supported ReleaseKit version: %s", goMod) + if !toolSelected { + t.Fatal("module does not register the ReleaseKit CLI tool") } } @@ -227,10 +148,7 @@ func TestReleaseWrapperStopsWhenBootstrapFails(t *testing.T) { cmd.Dir = layout.caller cmd.Env = layout.environment(map[string]string{ "RELEASEKIT_ARGS_LOG": layout.argsLog, - "RELEASEKIT_BUILD_LOG": layout.buildLog, - "RELEASEKIT_LIST_LOG": layout.listLog, - "RELEASEKIT_MODULE_LOG": layout.moduleLog, - "RELEASEKIT_SUM_LOG": layout.sumLog, + "RELEASEKIT_GO_LOG": layout.goLog, "RELEASEKIT_BOOTSTRAP_STATUS": "42", }) output, err := cmd.CombinedOutput() @@ -240,17 +158,6 @@ func TestReleaseWrapperStopsWhenBootstrapFails(t *testing.T) { if _, err := os.Stat(layout.argsLog); !os.IsNotExist(err) { t.Fatalf("ReleaseKit executed after failed bootstrap; stat error=%v", err) } - if _, err := os.Stat(layout.buildLog); !os.IsNotExist(err) { - t.Fatalf("ReleaseKit build ran after module selection failed; stat error=%v", err) - } - listInvocation := readNULArguments(t, layout.listLog) - modfile := argumentValue(listInvocation[3:], "-modfile=") - if modfile == "" { - t.Fatalf("module selection did not use temporary metadata: %q", listInvocation) - } - if _, err := os.Stat(filepath.Dir(modfile)); !os.IsNotExist(err) { - t.Fatalf("temporary bootstrap directory remains after failed selection: %v", err) - } } func TestReleaseKitProfileDeclaresProductAndDeliverySettings(t *testing.T) { @@ -327,38 +234,28 @@ func TestReleaseKitTargetMembership(t *testing.T) { } type releaseTestLayout struct { - repo string - caller string - wrapper string - argsLog string - buildLog string - listLog string - moduleLog string - sumLog string - path string + repo string + caller string + wrapper string + argsLog string + goLog string + path string } func newReleaseTestLayout(t *testing.T) releaseTestLayout { t.Helper() root := t.TempDir() layout := releaseTestLayout{ - repo: filepath.Join(root, "repository"), - caller: filepath.Join(root, "caller"), - argsLog: filepath.Join(root, "tool-arguments"), - buildLog: filepath.Join(root, "build-arguments"), - listLog: filepath.Join(root, "list-arguments"), - moduleLog: filepath.Join(root, "temporary-go.mod"), - sumLog: filepath.Join(root, "temporary-go.sum"), - path: filepath.Join(root, "bin"), + repo: filepath.Join(root, "repository"), + caller: filepath.Join(root, "caller"), + argsLog: filepath.Join(root, "tool-arguments"), + goLog: filepath.Join(root, "go-arguments"), + path: filepath.Join(root, "bin"), } - if err := os.MkdirAll(filepath.Join(layout.repo, "scripts"), 0755); err != nil { - t.Fatal(err) - } - if err := os.MkdirAll(layout.caller, 0755); err != nil { - t.Fatal(err) - } - if err := os.MkdirAll(layout.path, 0755); err != nil { - t.Fatal(err) + for _, directory := range []string{filepath.Join(layout.repo, "scripts"), layout.caller, layout.path} { + if err := os.MkdirAll(directory, 0755); err != nil { + t.Fatal(err) + } } wrapper, err := os.ReadFile("release.sh") if err != nil { @@ -367,8 +264,6 @@ func newReleaseTestLayout(t *testing.T) releaseTestLayout { layout.wrapper = filepath.Join(layout.repo, "scripts", "release.sh") writeReleaseTestFile(t, layout.wrapper, wrapper, 0755) writeReleaseTestFile(t, filepath.Join(layout.path, "go"), []byte(goStub), 0755) - writeReleaseTestFile(t, filepath.Join(layout.repo, "go.mod"), []byte("module example.com/audita-fixture\n\ngo 1.25.5\n\nrequire gitea.maximumdirect.net/eric/ReleaseKit v0.1.0\n"), 0644) - writeReleaseTestFile(t, filepath.Join(layout.repo, "go.sum"), []byte("fixture checksum\n"), 0644) return layout } @@ -381,15 +276,6 @@ func readText(t *testing.T, path string) string { return string(data) } -func argumentValue(arguments []string, prefix string) string { - for _, argument := range arguments { - if strings.HasPrefix(argument, prefix) { - return strings.TrimPrefix(argument, prefix) - } - } - return "" -} - func (layout releaseTestLayout) environment(values map[string]string) []string { filtered := make([]string, 0, len(os.Environ())+len(values)+1) for _, value := range os.Environ() {