Verify tag-selected tool versions
This commit is contained in:
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
10
docs/cli.md
10
docs/cli.md
@@ -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.
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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}"
|
||||||
|
|||||||
Reference in New Issue
Block a user