Verify tag-selected tool versions

This commit is contained in:
2026-10-07 23:41:25 +00:00
parent b80d4ee071
commit eee979db7b
7 changed files with 314 additions and 63 deletions

View File

@@ -47,17 +47,7 @@ func TestReleaseKitSourceDeliveryWorkflowFromTaggedCheckout(t *testing.T) {
if err := os.WriteFile(notePath, []byte(note), 0o600); err != nil { if err := os.WriteFile(notePath, []byte(note), 0o600); err != nil {
t.Fatal(err) t.Fatal(err)
} }
ignore, err := os.OpenFile(filepath.Join(repository, ".gitignore"), os.O_APPEND|os.O_WRONLY, 0) cliGitRun(t, repository, "add", "docs/releases/"+version+".md")
if err != nil {
t.Fatal(err)
}
if _, err := ignore.WriteString("\n/.releasekit/\n"); err != nil {
t.Fatal(err)
}
if err := ignore.Close(); err != nil {
t.Fatal(err)
}
cliGitRun(t, repository, "add", "docs/releases/"+version+".md", ".gitignore")
cliGitRun(t, repository, "commit", "-m", "prepare source-only delivery") cliGitRun(t, repository, "commit", "-m", "prepare source-only delivery")
state := &deliveryServerState{product: "ReleaseKit"} state := &deliveryServerState{product: "ReleaseKit"}
@@ -78,16 +68,11 @@ func TestReleaseKitSourceDeliveryWorkflowFromTaggedCheckout(t *testing.T) {
cliGitRun(t, repository, "remote", "set-url", "origin", remote) cliGitRun(t, repository, "remote", "set-url", "origin", remote)
cliGitRun(t, repository, "push", "origin", "HEAD:refs/heads/main", "refs/tags/"+version) cliGitRun(t, repository, "push", "origin", "HEAD:refs/heads/main", "refs/tags/"+version)
tool := filepath.Join(repository, ".releasekit", "bin", "releasekit")
template, err := os.ReadFile(filepath.Join(root, "examples", "woodpecker", "releasekit-source-delivery.yml")) template, err := os.ReadFile(filepath.Join(root, "examples", "woodpecker", "releasekit-source-delivery.yml"))
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
buildBlock, err := extractWoodpeckerCommand(string(template), "build-tool") block, err := extractWoodpeckerCommand(string(template), "deliver")
if err != nil {
t.Fatal(err)
}
deliveryBlock, err := extractWoodpeckerCommand(string(template), "deliver-page")
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@@ -96,19 +81,14 @@ func TestReleaseKitSourceDeliveryWorkflowFromTaggedCheckout(t *testing.T) {
"CI_COMMIT_SHA": state.commit, "CI_COMMIT_SHA": state.commit,
"CI_COMMIT_TAG": version, "CI_COMMIT_TAG": version,
"RELEASEKIT_GITEA_TOKEN": "fixture-token", "RELEASEKIT_GITEA_TOKEN": "fixture-token",
"GOTOOLCHAIN": "local",
"GOPROXY": "off", "GOPROXY": "off",
"GOSUMDB": "off", "GOSUMDB": "off",
} }
runWoodpeckerBlock(t, strings.ReplaceAll(buildBlock, "$${", "${"), repository, env) command := strings.ReplaceAll(block, "$${", "${")
if got := cliGitOutput(t, repository, "rev-parse", "HEAD"); got != state.commit {
t.Fatalf("template checkout = %s, want %s", got, state.commit)
}
state.remoteCommit = strings.Repeat("f", 40) state.remoteCommit = strings.Repeat("f", 40)
failure := exec.Command(tool, "deliver", "--repo", repository, version) if output, err := executeWoodpeckerBlock(command, repository, env); err == nil {
failure.Dir = repository
failure.Env = append(os.Environ(), "RELEASEKIT_GITEA_TOKEN=fixture-token")
if output, err := failure.CombinedOutput(); err == nil {
t.Fatalf("delivery with a mismatched hosted tag succeeded: %s", output) t.Fatalf("delivery with a mismatched hosted tag succeeded: %s", output)
} }
if state.releaseCreates != 0 || cliGitOutput(t, repository, "rev-parse", "refs/tags/"+version+"^{commit}") != state.commit { if state.releaseCreates != 0 || cliGitOutput(t, repository, "rev-parse", "refs/tags/"+version+"^{commit}") != state.commit {
@@ -117,7 +97,7 @@ func TestReleaseKitSourceDeliveryWorkflowFromTaggedCheckout(t *testing.T) {
state.remoteCommit = "" state.remoteCommit = ""
for attempt := 0; attempt < 2; attempt++ { for attempt := 0; attempt < 2; attempt++ {
output := runWoodpeckerBlock(t, strings.ReplaceAll(deliveryBlock, "$${", "${"), repository, env) output := runWoodpeckerBlock(t, command, repository, env)
if !strings.Contains(output, "state complete") { if !strings.Contains(output, "state complete") {
t.Fatalf("delivery attempt %d output = %q", attempt+1, output) t.Fatalf("delivery attempt %d output = %q", attempt+1, output)
} }

View File

@@ -1,6 +1,7 @@
package main package main
import ( import (
"archive/zip"
"errors" "errors"
"fmt" "fmt"
"os" "os"
@@ -145,6 +146,135 @@ func TestWoodpeckerTagDeliveryTemplate(t *testing.T) {
}) })
} }
func TestWoodpeckerTagDeliveryUsesTaggedModuleVersion(t *testing.T) {
root := cliRepositoryRoot(t)
contents, err := os.ReadFile(filepath.Join(root, "examples", "woodpecker", "tag-delivery.yml"))
if err != nil {
t.Fatal(err)
}
block, err := extractWoodpeckerCommand(string(contents), "deliver")
if err != nil {
t.Fatal(err)
}
command := strings.ReplaceAll(block, "$${", "${")
proxy := writeReleaseKitModuleProxy(t)
t.Run("older tag retry uses its committed module selection", func(t *testing.T) {
goDir, goLog := installGoBootstrapShim(t)
repository := newCLIRepository(t)
const module = "gitea.maximumdirect.net/eric/ReleaseKit"
writeRepositoryFile(t, repository, "go.mod", "module example.com/cli-candidate\n\ngo 1.25.0\n\nrequire "+module+" v0.2.197\n")
cliGitRun(t, repository, "add", "go.mod")
cliGitRun(t, repository, "commit", "-m", "select the earlier ReleaseKit version")
oldCommit := cliGitOutput(t, repository, "rev-parse", "HEAD")
cliGitRun(t, repository, "tag", "v1.2.3")
writeRepositoryFile(t, repository, "go.mod", "module example.com/cli-candidate\n\ngo 1.25.0\n\nrequire "+module+" v0.2.198\n")
cliGitRun(t, repository, "add", "go.mod")
cliGitRun(t, repository, "commit", "-m", "upgrade the default branch tool")
cliGitRun(t, repository, "init", "--bare", filepath.Join(filepath.Dir(repository), "origin.git"))
remote := filepath.Join(filepath.Dir(repository), "origin.git")
cliGitRun(t, repository, "remote", "add", "origin", remote)
cliGitRun(t, repository, "push", "origin", "refs/heads/main", "refs/tags/v1.2.3")
cliGitRun(t, repository, "checkout", "--detach", oldCommit)
if mainModule := cliGitOutput(t, repository, "show", "main:go.mod"); !strings.Contains(mainModule, module+" v0.2.198") {
t.Fatalf("default branch module = %q, want upgraded tool selection", mainModule)
}
moduleBefore := readWoodpeckerFixtureFile(t, filepath.Join(repository, "go.mod"))
commit := oldCommit
env := templateEnvironment(repository, commit, goDir, goLog)
env["GOPROXY"] = "file://" + filepath.ToSlash(proxy)
sumLog := filepath.Join(t.TempDir(), "tool-only-sums")
env["RELEASEKIT_SUM_LOG"] = sumLog
output := runWoodpeckerBlock(t, command, repository, env)
if !strings.Contains(output, "tool-version=v0.2.197") || !strings.Contains(output, "helper=tool-only dependency") || strings.Contains(output, "tool-version=v0.2.198") {
t.Fatalf("retry selected the wrong tool version: %q", output)
}
assertModuleBootstrap(t, repository, goLog, moduleBefore)
assertToolOnlyChecksums(t, sumLog)
})
t.Run("versioned replacement selects its declared source", func(t *testing.T) {
goDir, goLog := installGoBootstrapShim(t)
repository := newCLIRepository(t)
const module = "gitea.maximumdirect.net/eric/ReleaseKit"
goMod := "module example.com/cli-candidate\n\ngo 1.25.0\n\nrequire " + module + " v0.2.197\n\nreplace " + module + " v0.2.197 => " + module + " v0.2.198\n"
writeRepositoryFile(t, repository, "go.mod", goMod)
cliGitRun(t, repository, "add", "go.mod")
cliGitRun(t, repository, "commit", "-m", "select a versioned tool replacement")
commit := pushTaggedCLIRepository(t, repository)
moduleBefore := readWoodpeckerFixtureFile(t, filepath.Join(repository, "go.mod"))
env := templateEnvironment(repository, commit, goDir, goLog)
env["GOPROXY"] = "file://" + filepath.ToSlash(proxy)
sumLog := filepath.Join(t.TempDir(), "tool-only-sums")
env["RELEASEKIT_SUM_LOG"] = sumLog
output := runWoodpeckerBlock(t, command, repository, env)
if !strings.Contains(output, "tool-version=v0.2.198") || !strings.Contains(output, "helper=tool-only dependency") {
t.Fatalf("versioned replacement was not selected: %q", output)
}
assertModuleBootstrap(t, repository, goLog, moduleBefore)
assertToolOnlyChecksums(t, sumLog)
})
}
func TestWoodpeckerSourceDeliveryTemplateSelfBootstraps(t *testing.T) {
root := cliRepositoryRoot(t)
contents, err := os.ReadFile(filepath.Join(root, "examples", "woodpecker", "releasekit-source-delivery.yml"))
if err != nil {
t.Fatal(err)
}
block, err := extractWoodpeckerCommand(string(contents), "deliver")
if err != nil {
t.Fatal(err)
}
command := strings.ReplaceAll(block, "$${", "${")
repository := newCLIRepository(t)
writeRepositoryFile(t, repository, "cmd/releasekit/main.go", "package main\n\nimport (\"fmt\"; \"os\")\n\nfunc main() {\n\tfmt.Println(\"self-bootstrap\")\n\tfmt.Printf(\"tool-args=%q\\n\", os.Args[1:])\n}\n")
cliGitRun(t, repository, "add", "cmd/releasekit/main.go")
cliGitRun(t, repository, "commit", "-m", "add the self-bootstrapped command")
commit := pushTaggedCLIRepository(t, repository)
goDir, goLog := installGoBootstrapShim(t)
env := templateEnvironment(repository, commit, goDir, goLog)
output := runWoodpeckerBlock(t, command, repository, env)
if !strings.Contains(output, "self-bootstrap") || !strings.Contains(output, `"`+commit+`"`) {
t.Fatalf("self-bootstrap output = %q", output)
}
if !strings.Contains(output, `"ci-deliver"`) || !strings.Contains(output, `"`+repository+`"`) || !strings.Contains(output, `"v1.2.3"`) {
t.Fatalf("self-bootstrapped command arguments = %q", output)
}
log := readWoodpeckerFixtureFile(t, goLog)
if !strings.Contains(log, "GOWORK=off GOTOOLCHAIN=local") || !strings.Contains(log, "build -mod=readonly -o ") || !strings.Contains(log, "./cmd/releasekit") {
t.Fatalf("self-bootstrap Go command = %q", log)
}
var executable string
for index, field := range strings.Fields(log) {
if field == "-o" && index+1 < len(strings.Fields(log)) {
executable = strings.Fields(log)[index+1]
break
}
}
if executable == "" {
t.Fatalf("self-bootstrap output path missing from Go command: %q", log)
}
if relative, err := filepath.Rel(repository, executable); err != nil || relative == "." || (!strings.HasPrefix(relative, ".."+string(filepath.Separator)) && relative != "..") {
t.Fatalf("self-bootstrap executable %q is not outside source %q: relative=%q error=%v", executable, repository, relative, err)
}
if status := cliGitOutput(t, repository, "status", "--porcelain", "--untracked-files=all"); status != "" {
t.Fatalf("self-bootstrap changed the tagged source: %q", status)
}
if branch := cliGitOutput(t, repository, "rev-parse", "--abbrev-ref", "HEAD"); branch != "HEAD" {
t.Fatalf("self-bootstrap checkout branch = %q, want detached HEAD", branch)
}
entries, err := os.ReadDir(filepath.Dir(repository))
if err != nil {
t.Fatal(err)
}
for _, entry := range entries {
if strings.HasPrefix(entry.Name(), ".releasekit-source.") {
t.Fatalf("self-bootstrap temporary directory remains: %s", entry.Name())
}
}
}
func cliRepositoryRoot(t *testing.T) string { func cliRepositoryRoot(t *testing.T) string {
t.Helper() t.Helper()
_, sourceFile, _, ok := runtime.Caller(0) _, sourceFile, _, ok := runtime.Caller(0)
@@ -195,7 +325,16 @@ func installGoBootstrapShim(t *testing.T) (string, string) {
goLog := filepath.Join(shimDir, "go-commands") goLog := filepath.Join(shimDir, "go-commands")
shim := `#!/bin/sh shim := `#!/bin/sh
printf 'cwd=%s GOWORK=%s GOTOOLCHAIN=%s %s\n' "$PWD" "$GOWORK" "$GOTOOLCHAIN" "$*" >> "$RELEASEKIT_GO_LOG" printf 'cwd=%s GOWORK=%s GOTOOLCHAIN=%s %s\n' "$PWD" "$GOWORK" "$GOTOOLCHAIN" "$*" >> "$RELEASEKIT_GO_LOG"
exec "$RELEASEKIT_REAL_GO" "$@" "$RELEASEKIT_REAL_GO" "$@"
status=$?
if [ "$status" -eq 0 ] && [ "$1" = "build" ] && [ -n "${RELEASEKIT_SUM_LOG:-}" ]; then
modfile=
for argument in "$@"; do
case "$argument" in -modfile=*) modfile=${argument#-modfile=} ;; esac
done
cp "${modfile%.mod}.sum" "$RELEASEKIT_SUM_LOG"
fi
exit "$status"
` `
if err := os.WriteFile(filepath.Join(shimDir, "go"), []byte(shim), 0o700); err != nil { if err := os.WriteFile(filepath.Join(shimDir, "go"), []byte(shim), 0o700); err != nil {
t.Fatal(err) t.Fatal(err)
@@ -299,6 +438,80 @@ func selectReleaseKitModule(t *testing.T, repository, releaseKitSource string, i
cliGitRun(t, repository, "commit", "-m", "select ReleaseKit from module configuration") cliGitRun(t, repository, "commit", "-m", "select ReleaseKit from module configuration")
} }
func writeReleaseKitModuleProxy(t *testing.T) string {
t.Helper()
const module = "gitea.maximumdirect.net/eric/ReleaseKit"
const toolDependency = "example.com/releasekit-helper"
proxyDir := t.TempDir()
toolModule := "module " + module + "\n\ngo 1.25.0\n\nrequire " + toolDependency + " v1.0.0\n"
for _, version := range []string{"v0.2.197", "v0.2.198"} {
files := map[string]string{
"cmd/releasekit/main.go": "package main\n\nimport (\"fmt\"; \"os\"; tooldep \"example.com/releasekit-helper\")\n\nfunc main() {\n\tfmt.Println(\"tool-version=" + version + " helper=\" + tooldep.Version)\n\tfmt.Printf(\"tool-args=%q\\n\", os.Args[1:])\n}\n",
}
writeModuleProxyVersion(t, proxyDir, module, version, toolModule, files)
}
writeModuleProxyVersion(t, proxyDir, toolDependency, "v1.0.0", "module "+toolDependency+"\n\ngo 1.25.0\n", map[string]string{"tooldep.go": "package tooldep\n\nconst Version = \"tool-only dependency\"\n"})
return proxyDir
}
func writeModuleProxyVersion(t *testing.T, proxyDir, module, version, moduleFile string, files map[string]string) {
t.Helper()
var escaped strings.Builder
for _, character := range module {
if character >= 'A' && character <= 'Z' {
escaped.WriteByte('!')
character += 'a' - 'A'
}
escaped.WriteRune(character)
}
moduleVersions := filepath.Join(proxyDir, filepath.FromSlash(escaped.String()), "@v")
if err := os.MkdirAll(moduleVersions, 0o700); err != nil {
t.Fatal(err)
}
if _, err := os.Stat(filepath.Join(moduleVersions, "list")); os.IsNotExist(err) {
if err := os.WriteFile(filepath.Join(moduleVersions, "list"), []byte(version+"\n"), 0o600); err != nil {
t.Fatal(err)
}
} else {
listPath := filepath.Join(moduleVersions, "list")
list := readWoodpeckerFixtureFile(t, listPath)
if !strings.Contains(list, version+"\n") {
if err := os.WriteFile(listPath, []byte(list+version+"\n"), 0o600); err != nil {
t.Fatal(err)
}
}
}
if err := os.WriteFile(filepath.Join(moduleVersions, version+".mod"), []byte(moduleFile), 0o600); err != nil {
t.Fatal(err)
}
info := fmt.Sprintf("{\"Version\":%q,\"Time\":\"2026-10-01T00:00:00Z\"}\n", version)
if err := os.WriteFile(filepath.Join(moduleVersions, version+".info"), []byte(info), 0o600); err != nil {
t.Fatal(err)
}
archive, err := os.Create(filepath.Join(moduleVersions, version+".zip"))
if err != nil {
t.Fatal(err)
}
writer := zip.NewWriter(archive)
prefix := module + "@" + version + "/"
files["go.mod"] = moduleFile
for name, contents := range files {
file, err := writer.Create(prefix + name)
if err != nil {
t.Fatal(err)
}
if _, err := file.Write([]byte(contents)); err != nil {
t.Fatal(err)
}
}
if err := writer.Close(); err != nil {
t.Fatal(err)
}
if err := archive.Close(); err != nil {
t.Fatal(err)
}
}
func assertModuleBootstrap(t *testing.T, repository, goLog, moduleBefore string, sumBefore ...string) { func assertModuleBootstrap(t *testing.T, repository, goLog, moduleBefore string, sumBefore ...string) {
t.Helper() t.Helper()
assertGoSelection(t, repository, goLog) assertGoSelection(t, repository, goLog)
@@ -371,6 +584,14 @@ func assertNoCommands(t *testing.T, goLog string) {
} }
} }
func assertToolOnlyChecksums(t *testing.T, path string) {
t.Helper()
sums := readWoodpeckerFixtureFile(t, path)
if !strings.Contains(sums, "example.com/releasekit-helper v1.0.0 h1:") {
t.Fatalf("temporary module sum file omitted the tool-only dependency checksum: %q", sums)
}
}
func readWoodpeckerFixtureFile(t *testing.T, path string) string { func readWoodpeckerFixtureFile(t *testing.T, path string) string {
t.Helper() t.Helper()
contents, err := os.ReadFile(path) contents, err := os.ReadFile(path)

View File

@@ -220,3 +220,13 @@ preparation failures return status 1; usage, profile, credential, and invalid
version errors return status 2. Final delivery uses the same statuses as version errors return status 2. Final delivery uses the same statuses as
`deliver`: 0 for complete, 1 for known failure, conflict, or partial delivery, `deliver`: 0 for complete, 1 for known failure, conflict, or partial delivery,
and 3 for uncertain remote state. and 3 for uncertain remote state.
The maintained module-based tag workflow verifies and checks out the event tag
before resolving the consumer module. It builds the ReleaseKit version already
selected by that tagged `go.mod`, using temporary alternate module files outside
the source checkout. This tool bootstrap is separate from `ci-deliver`'s
application dependency preparation for binary profiles. ReleaseKit's own
source workflow self-bootstraps `./cmd/releasekit`; consumers without a declared
ReleaseKit module dependency can use the separately installed-tool command.
See the [delivery guide](consumers/delivery.md#woodpecker-tag-workflows) for
compatible Go environments, retries, and preparation details.

View File

@@ -181,6 +181,14 @@ dependencies. This preparation may access the network and may update
independent of Woodpecker environment variable names. See the independent of Woodpecker environment variable names. See the
[CLI contract](cli.md) for the invocation and exit statuses. [CLI contract](cli.md) for the invocation and exit statuses.
This application dependency preparation is separate from bootstrapping the
ReleaseKit executable. The maintained module-based tag workflow verifies the
tag first, then uses temporary copies of that tag's module files to build the
ReleaseKit version selected by the consumer's `go.mod`. The
[delivery guide](consumers/delivery.md#woodpecker-tag-workflows) describes that
bootstrap, the self-bootstrapped source workflow, and the separately installed
tool option.
ReleaseKit's source-only profile is ReleaseKit's source-only profile is
[releasekit.json](../releasekit.json). [releasekit.json](../releasekit.json).
See the [source-only profile](../examples/releasekit.json) and See the [source-only profile](../examples/releasekit.json) and

View File

@@ -150,28 +150,44 @@ exit codes, and transport behavior.
Use the maintained [tag-delivery workflow](../../examples/woodpecker/tag-delivery.yml) Use the maintained [tag-delivery workflow](../../examples/woodpecker/tag-delivery.yml)
in the consumer repository's `.woodpecker/` directory. Set `GO_IMAGE` to an in the consumer repository's `.woodpecker/` directory. Set `GO_IMAGE` to an
image with Go 1.25 or newer, Git, and a Go version compatible with the consumer image with Go 1.25 or newer, Git, and a local Go version compatible with the
module. Replace `RELEASEKIT_VERSION` with an immutable ReleaseKit revision or consumer module. It uses `GOTOOLCHAIN=local`; Go does not download a different
published version. The example selects the initial `v0.1.0` baseline; update toolchain. The tag's `go.mod` must select ReleaseKit as a module dependency.
that pin when adopting a later release. Configure the Woodpecker repository secret Configure the Woodpecker repository secret `releasekit_gitea_token`; the
`releasekit_gitea_token`; the template maps it to `RELEASEKIT_GITEA_TOKEN` only template maps it to `RELEASEKIT_GITEA_TOKEN` for delivery. The repository needs
for the delivery step. The repository needs a `delivery` profile. Page-only and a `delivery` profile. Page-only and binary behavior both use this template; the
binary behavior both use this template; the profile's build target list selects profile's build target list selects whether application binaries are built and
whether binaries are built and delivered. delivered.
The command runs versioned source directly with `go run`, passing Before running Go, the template fetches the explicit event tag, compares its
`CI_WORKSPACE`, `CI_COMMIT_SHA`, and `CI_COMMIT_TAG` as command arguments. peeled commit with the provider's full commit ID without regard to hex case,
`ci-deliver` fetches and verifies the event tag against its commit before checks out that commit detached, and confirms the source tree is clean. It then
checking out the commit detached. It then loads the profile from that source, copies the tagged `go.mod` and any `go.sum` to a temporary directory outside
validates the destination and token, and downloads application dependencies the checkout. `go list -m` confirms that the module selects ReleaseKit; Go uses
only for profiles with build targets. The facade builds and delivers those an alternate `-modfile` to prepare dependencies and build the selected
targets with the note page. Source-only profiles skip dependency downloads and `cmd/releasekit` package without changing the tagged module files. The template
binary work. The workflow does not repeat checkout guards or maintain a invokes that executable with `CI_WORKSPACE`, `CI_COMMIT_SHA`, and
separate build/receipt/delivery sequence. `CI_COMMIT_TAG`. A cleanup trap removes the temporary metadata and executable.
Tool bootstrap and application artifact preparation are separate. The bootstrap
resolves and builds ReleaseKit from the tagged consumer's module configuration.
Afterward, `ci-deliver` loads the profile and downloads application dependencies
only when build targets are selected; it builds and delivers those targets with
the note page. Source-only profiles skip application dependency downloads and
binary work. Dependency preparation for binaries may use the network and may
update an uncommitted checksum file in the checkout. A retry still uses the
same immutable tag and that tag's module settings, even after the default branch
selects a newer ReleaseKit version.
If the application does not declare ReleaseKit as a module dependency, install
the executable separately and invoke the same `ci-deliver` command. For
ReleaseKit's own source workflow, the maintained
[source-delivery template](../../examples/woodpecker/releasekit-source-delivery.yml)
checks out the tag and self-bootstraps `./cmd/releasekit` into temporary storage.
The normal Go module and build caches are sufficient. Cache mounts may be added The normal Go module and build caches are sufficient. Cache mounts may be added
as deployment configuration when they help the runner; they are not needed by as deployment configuration when they help the runner; they are not needed by
the template. A locally retained executable can be installed with the template. A separately installed executable can be installed with
`go install gitea.maximumdirect.net/eric/ReleaseKit/cmd/releasekit@PIN`, then run `go install gitea.maximumdirect.net/eric/ReleaseKit/cmd/releasekit@PIN`, then run
with the same arguments: with the same arguments:
@@ -179,9 +195,10 @@ with the same arguments:
releasekit ci-deliver --repo "$CI_WORKSPACE" --commit "$CI_COMMIT_SHA" "$CI_COMMIT_TAG" releasekit ci-deliver --repo "$CI_WORKSPACE" --commit "$CI_COMMIT_SHA" "$CI_COMMIT_TAG"
``` ```
Upgrade by changing the selected immutable The module-based workflow follows the ReleaseKit version selected by the tagged
`RELEASEKIT_VERSION` (or the `@PIN` used by `go install`) and validating the consumer's `go.mod`, so retries use that source's original selection. Update the
workflow against a tag fixture. The command and profile behavior are documented consumer module configuration when adopting a newer ReleaseKit version and
verify it with a tag fixture. The command and profile behavior are documented
in the [CLI contract](../cli.md) and [configuration reference](../config.md). in the [CLI contract](../cli.md) and [configuration reference](../config.md).
Woodpecker runs delivery after the tag event, independently from the source Woodpecker runs delivery after the tag event, independently from the source

View File

@@ -547,6 +547,15 @@ and propagation of its exit status. Existing delivery tests own hosted behavior.
## Stage 13: Verify upgrades, retries, and bootstrap variants ## Stage 13: Verify upgrades, retries, and bootstrap variants
**Completion:** Complete. The tag workflow is exercised against a local module
proxy with two tool versions, an older tagged configuration after the default
branch upgrades, and a versioned replacement. Missing tool-only dependency
checksums are prepared in temporary module metadata. The ReleaseKit source
workflow verifies its tag before self-bootstrap and writes the executable
outside the checkout. Delivery, configuration, and CLI guidance now distinguish
tool bootstrap from application dependency preparation and describe
immutable-source retries and installed-tool use.
**Outcome:** Tool selection follows the triggering release across supported uses. **Outcome:** Tool selection follows the triggering release across supported uses.
1. Extend Stage 12's bootstrap fixtures with a local module proxy for two tool 1. Extend Stage 12's bootstrap fixtures with a local module proxy for two tool

View File

@@ -3,27 +3,33 @@ when:
ref: refs/tags/v* ref: refs/tags/v*
steps: steps:
- name: build-tool - name: deliver
image: golang:1.25.0-bookworm
commands:
- |
test -n "$${CI_COMMIT_TAG}"
test -n "$${CI_COMMIT_SHA}"
git fetch origin "refs/tags/$${CI_COMMIT_TAG}:refs/tags/$${CI_COMMIT_TAG}"
test "$(git rev-parse "refs/tags/$${CI_COMMIT_TAG}^{commit}")" = "$${CI_COMMIT_SHA}"
git checkout --detach "$${CI_COMMIT_SHA}"
test "$(git rev-parse HEAD)" = "$${CI_COMMIT_SHA}"
mkdir -p "$${CI_WORKSPACE}/.releasekit/bin"
GOTOOLCHAIN=local GOPROXY=off GOSUMDB=off go build -o "$${CI_WORKSPACE}/.releasekit/bin/releasekit" ./cmd/releasekit
- name: deliver-page
image: golang:1.25.0-bookworm image: golang:1.25.0-bookworm
environment: environment:
RELEASEKIT_GITEA_TOKEN: RELEASEKIT_GITEA_TOKEN:
from_secret: releasekit_gitea_token from_secret: releasekit_gitea_token
commands: commands:
- | - |
"$${CI_WORKSPACE}/.releasekit/bin/releasekit" ci-deliver \ set -eu
test -n "$${CI_COMMIT_TAG}"
test -n "$${CI_COMMIT_SHA}"
git check-ref-format "refs/tags/$${CI_COMMIT_TAG}"
git fetch origin "refs/tags/$${CI_COMMIT_TAG}:refs/tags/$${CI_COMMIT_TAG}"
tag_commit=$(git rev-parse "refs/tags/$${CI_COMMIT_TAG}^{commit}")
expected_commit=$(printf '%s' "$${CI_COMMIT_SHA}" | tr '[:upper:]' '[:lower:]')
test "$(printf '%s' "$${tag_commit}" | tr '[:upper:]' '[:lower:]')" = "$${expected_commit}"
git checkout --detach "$${tag_commit}"
test "$(git rev-parse HEAD | tr '[:upper:]' '[:lower:]')" = "$${expected_commit}"
test -z "$(git status --porcelain --untracked-files=all)"
bootstrap_dir=$(mktemp -d "$${CI_WORKSPACE%/*}/.releasekit-source.XXXXXX")
trap 'rm -rf "$${bootstrap_dir}"' EXIT
export GOWORK=off
export GOTOOLCHAIN=local
export GOPROXY=off
export GOSUMDB=off
go build -mod=readonly -o "$${bootstrap_dir}/releasekit" ./cmd/releasekit
"$${bootstrap_dir}/releasekit" ci-deliver \
--repo "$${CI_WORKSPACE}" \ --repo "$${CI_WORKSPACE}" \
--commit "$${CI_COMMIT_SHA}" \ --commit "$${CI_COMMIT_SHA}" \
"$${CI_COMMIT_TAG}" "$${CI_COMMIT_TAG}"