Run consumer release commands through Go tools

This commit is contained in:
2026-11-02 14:46:42 +00:00
parent 6fab30337a
commit 691926d839
9 changed files with 148 additions and 103 deletions

View File

@@ -2,11 +2,13 @@ package main
import (
"bytes"
"errors"
"fmt"
"io/fs"
"os"
"os/exec"
"path/filepath"
"strconv"
"strings"
"testing"
)
@@ -80,20 +82,13 @@ func TestMultiModuleLauncherUsesSelectedModuleAndProfile(t *testing.T) {
set -eu
printf 'cwd=%s args=%s\n' "$PWD" "$*" >> "$RELEASEKIT_TEST_GO_LOG"
modfile=
output=
previous=
for argument in "$@"; do
case "$argument" in -modfile=*) modfile=${argument#-modfile=} ;; esac
if [ "$previous" = -o ]; then output=$argument; fi
previous=$argument
done
if [ -n "$modfile" ]; then cat "$modfile" >> "$RELEASEKIT_TEST_GO_LOG"; fi
if [ "$1" = build ]; then
cat > "$output" <<'TOOL'
#!/bin/sh
printf '<%s>\n' "$@" > "$RELEASEKIT_TEST_TOOL_ARGS"
TOOL
chmod +x "$output"
if [ "$1" = tool ]; then
shift 3
printf '<%s>\n' "$@" > "$RELEASEKIT_TEST_TOOL_ARGS"
fi
`
if err := os.WriteFile(filepath.Join(shimDir, "go"), []byte(shim), 0o700); err != nil {
@@ -134,7 +129,7 @@ fi
}
}
func TestMultiModuleLauncherBuildsToolFromSelectedDependency(t *testing.T) {
func TestMultiModuleLauncherRunsToolFromSelectedDependency(t *testing.T) {
example := filepath.Join(cliRepositoryRoot(t), "examples", "multi-module")
repository := newCLIRepository(t)
copyExampleTree(t, example, repository)
@@ -161,27 +156,50 @@ func TestMultiModuleLauncherBuildsToolFromSelectedDependency(t *testing.T) {
if err != nil {
t.Fatal(err)
}
command := exec.Command(filepath.Join(repository, "release.sh"), "ckconfig", "v1.2.3")
output, err := command.CombinedOutput()
if err != nil {
t.Fatalf("launcher error: %v output=%q", err, output)
}
for _, expected := range []string{
"tool-version=v0.2.198 helper=tool-only dependency",
`"publish"`, `"--repo"`, `"` + repository + `"`,
`"--config"`, `"releasekit-ckconfig.json"`,
`"--expected-module"`, `"integrations/ckconfig"`, `"v1.2.3"`,
} {
if !strings.Contains(string(output), expected) {
t.Errorf("launcher output %q omits %q", output, expected)
}
}
if strings.Contains(string(output), "tool-version=v0.2.197") {
t.Fatalf("launcher built the root-selected ReleaseKit version: %s", output)
}
moduleAfter, err := os.ReadFile(filepath.Join(repository, "integrations", "ckconfig", "go.mod"))
if err != nil || !bytes.Equal(moduleAfter, moduleBefore) {
t.Fatalf("selected module go.mod changed: got %q want %q error=%v", moduleAfter, moduleBefore, err)
for _, status := range []int{0, 1, 2, 3} {
t.Run(strconv.Itoa(status), func(t *testing.T) {
command := exec.Command(filepath.Join(repository, "release.sh"), "ckconfig", "v1.2.3")
command.Env = setEnvironment(os.Environ(), "RELEASEKIT_TOOL_EXIT", strconv.Itoa(status))
output, err := command.CombinedOutput()
if status == 0 && err != nil {
t.Fatalf("launcher error: %v output=%q", err, output)
}
if status != 0 {
var exitError *exec.ExitError
if !errors.As(err, &exitError) || exitError.ExitCode() != status {
t.Fatalf("launcher error = %v, want exit %d; output=%q", err, status, output)
}
}
for _, expected := range []string{
"tool-version=v0.2.198 helper=tool-only dependency",
`"publish"`, `"--repo"`, `"` + repository + `"`,
`"--config"`, `"releasekit-ckconfig.json"`,
`"--expected-module"`, `"integrations/ckconfig"`, `"v1.2.3"`,
} {
if !strings.Contains(string(output), expected) {
t.Errorf("launcher output %q omits %q", output, expected)
}
}
if strings.Contains(string(output), "tool-version=v0.2.197") {
t.Fatalf("launcher ran the root-selected ReleaseKit version: %s", output)
}
moduleAfter, err := os.ReadFile(filepath.Join(repository, "integrations", "ckconfig", "go.mod"))
if err != nil || !bytes.Equal(moduleAfter, moduleBefore) {
t.Fatalf("selected module go.mod changed: got %q want %q error=%v", moduleAfter, moduleBefore, err)
}
if status := cliGitOutput(t, repository, "status", "--porcelain", "--untracked-files=all"); status != "" {
t.Fatalf("launcher changed source: %q", status)
}
entries, err := os.ReadDir(filepath.Dir(repository))
if err != nil {
t.Fatal(err)
}
for _, entry := range entries {
if strings.HasPrefix(entry.Name(), ".releasekit-bootstrap.") {
t.Fatalf("temporary bootstrap directory remains: %s", entry.Name())
}
}
})
}
}

View File

@@ -236,6 +236,26 @@ func TestWoodpeckerTagDeliveryUsesTaggedModuleVersion(t *testing.T) {
assertToolOnlyChecksums(t, sumLog)
})
t.Run("vendored tool dependency", func(t *testing.T) {
goDir, goLog := installGoBootstrapShim(t)
repository := newCLIRepository(t)
const module = "go.maximumdirect.net/releasekit"
writeRepositoryFile(t, repository, "go.mod", "module example.com/cli-candidate\n\ngo 1.25.0\n\nrequire "+module+" v0.2.197\n\ntool "+module+"/cmd/releasekit\n")
env := templateEnvironment(repository, "", goDir, goLog)
useSyntheticModuleProxy(t, env, "file://"+filepath.ToSlash(proxy))
runWoodpeckerBlock(t, "go mod tidy\ngo mod vendor", repository, env)
cliGitRun(t, repository, "add", "go.mod", "go.sum", "vendor")
cliGitRun(t, repository, "commit", "-m", "vendor the selected release tool")
env["CI_COMMIT_SHA"] = pushTaggedCLIRepository(t, repository)
moduleBefore := readWoodpeckerFixtureFile(t, filepath.Join(repository, "go.mod"))
sumBefore := readWoodpeckerFixtureFile(t, filepath.Join(repository, "go.sum"))
output := runWoodpeckerBlock(t, command, repository, env)
if !strings.Contains(output, "tool-version=v0.2.197 helper=tool-only dependency") {
t.Fatalf("vendored tool was not selected: %q", output)
}
assertModuleBootstrap(t, repository, goLog, moduleBefore, sumBefore)
})
t.Run("versioned replacement selects its declared source", func(t *testing.T) {
goDir, goLog := installGoBootstrapShim(t)
repository := newCLIRepository(t)
@@ -400,7 +420,7 @@ func installGoBootstrapShim(t *testing.T) (string, string) {
printf 'cwd=%s GOWORK=%s GOTOOLCHAIN=%s %s\n' "$PWD" "$GOWORK" "$GOTOOLCHAIN" "$*" >> "$RELEASEKIT_GO_LOG"
"$RELEASEKIT_REAL_GO" "$@"
status=$?
if [ "$status" -eq 0 ] && [ "$1" = "build" ] && [ -n "${RELEASEKIT_SUM_LOG:-}" ]; then
if [ "$status" -eq 0 ] && [ "$1" = "tool" ] && [ -n "${RELEASEKIT_SUM_LOG:-}" ]; then
modfile=
for argument in "$@"; do
case "$argument" in -modfile=*) modfile=${argument#-modfile=} ;; esac
@@ -433,20 +453,12 @@ case "$1" in
list)
printf '%s v0.2.0\n' "$RELEASEKIT_MODULE"
;;
build)
output=
previous=
for argument in "$@"; do
if [ "$previous" = "-o" ]; then output=$argument; break; fi
previous=$argument
done
[ -n "$output" ] || { echo 'go build omitted output path' >&2; exit 90; }
cat > "$output" <<'TOOL'
#!/bin/sh
printf '<%s>\n' "$@" > "$RELEASEKIT_TOOL_ARGS_LOG"
exit "${RELEASEKIT_TOOL_EXIT:-0}"
TOOL
chmod +x "$output"
mod)
;;
tool)
shift 3
printf '<%s>\n' "$@" > "$RELEASEKIT_TOOL_ARGS_LOG"
exit "${RELEASEKIT_TOOL_EXIT:-0}"
;;
*)
echo "unexpected Go command: $1" >&2
@@ -532,7 +544,7 @@ func writeReleaseKitModuleProxy(t *testing.T) string {
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",
"cmd/releasekit/main.go": "package main\n\nimport (\"fmt\"; \"os\"; \"strconv\"; 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\tstatus, _ := strconv.Atoi(os.Getenv(\"RELEASEKIT_TOOL_EXIT\"))\n\tos.Exit(status)\n}\n",
}
writeModuleProxyVersion(t, proxyDir, module, version, toolModule, files)
}
@@ -633,16 +645,16 @@ func assertGoSelection(t *testing.T, repository, goLog string) {
t.Helper()
log := readWoodpeckerFixtureFile(t, goLog)
list := "list -mod=mod -modfile="
build := "build -mod=mod -modfile="
tool := "tool -modfile="
module := "go.maximumdirect.net/releasekit"
if !strings.Contains(log, "GOWORK=off GOTOOLCHAIN=local") || !strings.Contains(log, list) || !strings.Contains(log, "-m "+module) {
t.Fatalf("Go module selection command is missing required settings: %q", log)
}
if !strings.Contains(log, build) || !strings.Contains(log, module+"/cmd/releasekit") || strings.Contains(log, "@v0.2.0") {
t.Fatalf("Go build did not use the selected module without a version suffix: %q", log)
if !strings.Contains(log, tool) || !strings.Contains(log, " releasekit ci-deliver ") || strings.Contains(log, "@v0.2.0") {
t.Fatalf("Go tool did not run the module-selected ReleaseKit: %q", log)
}
if strings.Index(log, list) > strings.Index(log, build) {
t.Fatalf("Go build ran before module selection: %q", log)
if strings.Index(log, list) > strings.Index(log, tool) {
t.Fatalf("Go tool ran before module selection: %q", log)
}
var modfile string
for _, field := range strings.Fields(log) {