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 {
|
||||
t.Fatal(err)
|
||||
}
|
||||
ignore, err := os.OpenFile(filepath.Join(repository, ".gitignore"), os.O_APPEND|os.O_WRONLY, 0)
|
||||
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, "add", "docs/releases/"+version+".md")
|
||||
cliGitRun(t, repository, "commit", "-m", "prepare source-only delivery")
|
||||
|
||||
state := &deliveryServerState{product: "ReleaseKit"}
|
||||
@@ -78,16 +68,11 @@ func TestReleaseKitSourceDeliveryWorkflowFromTaggedCheckout(t *testing.T) {
|
||||
cliGitRun(t, repository, "remote", "set-url", "origin", remote)
|
||||
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"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
buildBlock, err := extractWoodpeckerCommand(string(template), "build-tool")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
deliveryBlock, err := extractWoodpeckerCommand(string(template), "deliver-page")
|
||||
block, err := extractWoodpeckerCommand(string(template), "deliver")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -96,19 +81,14 @@ func TestReleaseKitSourceDeliveryWorkflowFromTaggedCheckout(t *testing.T) {
|
||||
"CI_COMMIT_SHA": state.commit,
|
||||
"CI_COMMIT_TAG": version,
|
||||
"RELEASEKIT_GITEA_TOKEN": "fixture-token",
|
||||
"GOTOOLCHAIN": "local",
|
||||
"GOPROXY": "off",
|
||||
"GOSUMDB": "off",
|
||||
}
|
||||
runWoodpeckerBlock(t, strings.ReplaceAll(buildBlock, "$${", "${"), repository, env)
|
||||
if got := cliGitOutput(t, repository, "rev-parse", "HEAD"); got != state.commit {
|
||||
t.Fatalf("template checkout = %s, want %s", got, state.commit)
|
||||
}
|
||||
command := strings.ReplaceAll(block, "$${", "${")
|
||||
|
||||
state.remoteCommit = strings.Repeat("f", 40)
|
||||
failure := exec.Command(tool, "deliver", "--repo", repository, version)
|
||||
failure.Dir = repository
|
||||
failure.Env = append(os.Environ(), "RELEASEKIT_GITEA_TOKEN=fixture-token")
|
||||
if output, err := failure.CombinedOutput(); err == nil {
|
||||
if output, err := executeWoodpeckerBlock(command, repository, env); err == nil {
|
||||
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 {
|
||||
@@ -117,7 +97,7 @@ func TestReleaseKitSourceDeliveryWorkflowFromTaggedCheckout(t *testing.T) {
|
||||
state.remoteCommit = ""
|
||||
|
||||
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") {
|
||||
t.Fatalf("delivery attempt %d output = %q", attempt+1, output)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"errors"
|
||||
"fmt"
|
||||
"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 {
|
||||
t.Helper()
|
||||
_, sourceFile, _, ok := runtime.Caller(0)
|
||||
@@ -195,7 +325,16 @@ func installGoBootstrapShim(t *testing.T) (string, string) {
|
||||
goLog := filepath.Join(shimDir, "go-commands")
|
||||
shim := `#!/bin/sh
|
||||
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 {
|
||||
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")
|
||||
}
|
||||
|
||||
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) {
|
||||
t.Helper()
|
||||
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 {
|
||||
t.Helper()
|
||||
contents, err := os.ReadFile(path)
|
||||
|
||||
Reference in New Issue
Block a user