package main import ( "fmt" "os" "os/exec" "path/filepath" "runtime" "strings" "testing" "gitea.maximumdirect.net/eric/ReleaseKit/cmd/releasekit/internal/cli" ) func TestWoodpeckerTagDeliveryTemplate(t *testing.T) { root := cliRepositoryRoot(t) templatePath := filepath.Join(root, "examples", "woodpecker", "tag-delivery.yml") contents, err := os.ReadFile(templatePath) if err != nil { t.Fatal(err) } pin, err := woodpeckerMatrixValue(string(contents), "RELEASEKIT_VERSION") if err != nil { t.Fatal(err) } block, err := extractWoodpeckerCommand(string(contents), "deliver") if err != nil { t.Fatal(err) } command := strings.ReplaceAll(block, "${RELEASEKIT_VERSION}", pin) command = strings.ReplaceAll(command, "$${", "${") executable := filepath.Join(t.TempDir(), "releasekit") build := exec.Command("go", "build", "-o", executable, "./cmd/releasekit") build.Dir = root if output, err := build.CombinedOutput(); err != nil { t.Fatalf("build local ReleaseKit executable: %v\n%s", err, output) } module := "gitea.maximumdirect.net/eric/ReleaseKit/cmd/releasekit@" + pin t.Run("source-only", func(t *testing.T) { repository := newCLIRepository(t) writeRepositoryFile(t, repository, "go.mod", "module example.com/cli-candidate\n\ngo 1.25.0\n\nrequire example.com/unavailable/module v1.2.3\n") cliGitRun(t, repository, "add", "go.mod") cliGitRun(t, repository, "commit", "-m", "declare unavailable dependency") state := &deliveryServerState{} server := newDeliveryServer(t, state) defer server.Close() writeDeliveryProfile(t, repository, cli.Settings{ Product: "Example Product", Delivery: &cli.DeliverySettings{BaseURL: server.URL, Owner: "owner", Repository: "example"}, }) commit := pushTaggedCLIRepository(t, repository) state.commit = commit shimDir, goLog := installGoBootstrapShim(t) env := templateEnvironment(repository, commit, executable, module, shimDir, goLog) for attempt := 0; attempt < 2; attempt++ { output := runWoodpeckerBlock(t, command, repository, env) if !strings.Contains(output, "state complete") { t.Fatalf("delivery attempt %d output=%q", attempt+1, output) } } if state.releaseCreates != 1 || state.assetUploads != 0 { t.Fatalf("source-only calls = creates:%d uploads:%d", state.releaseCreates, state.assetUploads) } if log, err := os.ReadFile(goLog); err != nil || strings.Contains(string(log), "mod download") { t.Fatalf("source-only command downloaded application dependencies: log=%q error=%v", log, err) } }) t.Run("binary", func(t *testing.T) { repository := newDeliveryBuildRepository(t) state := &deliveryServerState{} server := newDeliveryServer(t, state) defer server.Close() writeDeliveryProfile(t, repository, cli.Settings{ Product: "Example Product", Build: &cli.BuildSettings{ MainPackage: ".", BinaryName: "helper", Targets: []string{fmt.Sprintf("%s/%s", runtime.GOOS, runtime.GOARCH)}, }, Delivery: &cli.DeliverySettings{BaseURL: server.URL, Owner: "owner", Repository: "example"}, }) commit := pushTaggedCLIRepository(t, repository) state.commit = commit shimDir, goLog := installGoBootstrapShim(t) env := templateEnvironment(repository, commit, executable, module, shimDir, goLog) for attempt := 0; attempt < 2; attempt++ { output := runWoodpeckerBlock(t, command, repository, env) if !strings.Contains(output, "state complete") { t.Fatalf("delivery attempt %d output=%q", attempt+1, output) } if attempt == 1 && !strings.Contains(output, "matching SHA256SUMS") { t.Fatalf("second delivery did not reuse matching assets: %q", output) } } if state.releaseCreates != 1 || state.assetUploads != 2 { t.Fatalf("binary calls = creates:%d uploads:%d", state.releaseCreates, state.assetUploads) } if _, ok := state.assets["helper-v1.2.3-"+runtime.GOOS+"-"+runtime.GOARCH]; !ok { t.Fatalf("binary asset was not delivered: %#v", state.assets) } if _, ok := state.assets["SHA256SUMS"]; !ok { t.Fatalf("checksum manifest was not delivered: %#v", state.assets) } if log, err := os.ReadFile(goLog); err != nil || !strings.Contains(string(log), "mod download") { t.Fatalf("binary command did not prepare application dependencies: log=%q error=%v", log, err) } }) } func cliRepositoryRoot(t *testing.T) string { t.Helper() _, sourceFile, _, ok := runtime.Caller(0) if !ok { t.Fatal("locate integration test source") } return filepath.Clean(filepath.Join(filepath.Dir(sourceFile), "../..")) } func extractWoodpeckerCommand(contents, stepName string) (string, error) { lines := strings.Split(contents, "\n") stepHeader := " - name: " + stepName for i, line := range lines { if strings.TrimSpace(line) != strings.TrimSpace(stepHeader) { continue } for j := i + 1; j < len(lines); j++ { trimmed := strings.TrimSpace(lines[j]) if strings.HasPrefix(lines[j], " - name:") { break } if trimmed != "- |" { continue } var block []string for k := j + 1; k < len(lines); k++ { current := lines[k] if strings.TrimSpace(current) == "" { block = append(block, "") continue } indent := len(current) - len(strings.TrimLeft(current, " ")) if indent < 8 { break } block = append(block, current[8:]) } return strings.TrimSpace(strings.Join(block, "\n")), nil } return "", fmt.Errorf("step %q has no literal command block", stepName) } return "", fmt.Errorf("step %q was not found", stepName) } func woodpeckerMatrixValue(contents, key string) (string, error) { for _, line := range strings.Split(contents, "\n") { field, value, ok := strings.Cut(strings.TrimSpace(line), ":") if ok && field == key { return strings.TrimSpace(value), nil } } return "", fmt.Errorf("matrix value %q was not found", key) } func installGoBootstrapShim(t *testing.T) (string, string) { t.Helper() shimDir := t.TempDir() goLog := filepath.Join(shimDir, "go-commands") shim := `#!/bin/sh if [ "$1" = "run" ]; then shift [ "$1" = "$RELEASEKIT_EXPECTED_MODULE" ] || { echo "unexpected module: $1" >&2; exit 90; } shift [ "$1" = "ci-deliver" ] || { echo "unexpected command: $1" >&2; exit 91; } [ "$2" = "--repo" ] && [ "$3" = "$CI_WORKSPACE" ] || { echo "unexpected repository arguments" >&2; exit 92; } [ "$4" = "--commit" ] && [ "$5" = "$CI_COMMIT_SHA" ] || { echo "unexpected commit arguments" >&2; exit 93; } [ "$6" = "$CI_COMMIT_TAG" ] && [ "$#" = 6 ] || { echo "unexpected version argument" >&2; exit 94; } printf '%s\n' "run $RELEASEKIT_EXPECTED_MODULE" >> "$RELEASEKIT_GO_LOG" exec "$RELEASEKIT_LOCAL_EXECUTABLE" "$@" fi printf '%s\n' "$*" >> "$RELEASEKIT_GO_LOG" exec "$RELEASEKIT_REAL_GO" "$@" ` if err := os.WriteFile(filepath.Join(shimDir, "go"), []byte(shim), 0o700); err != nil { t.Fatal(err) } return shimDir, goLog } func templateEnvironment(repository, commit, executable, module, shimDir, goLog string) map[string]string { realGo, _ := exec.LookPath("go") return map[string]string{ "CI_WORKSPACE": repository, "CI_COMMIT_SHA": commit, "CI_COMMIT_TAG": "v1.2.3", "RELEASEKIT_GITEA_TOKEN": "fixture-token", "RELEASEKIT_EXPECTED_MODULE": module, "RELEASEKIT_LOCAL_EXECUTABLE": executable, "RELEASEKIT_REAL_GO": realGo, "RELEASEKIT_GO_LOG": goLog, "GOTOOLCHAIN": "local", "GOPROXY": "off", "GOSUMDB": "off", "GOFLAGS": "", "PATH": shimDir + string(os.PathListSeparator) + os.Getenv("PATH"), } } func runWoodpeckerBlock(t *testing.T, block, repository string, environment map[string]string) string { t.Helper() command := exec.Command("sh", "-e", "-c", block) command.Dir = repository command.Env = os.Environ() for key, value := range environment { command.Env = setEnvironment(command.Env, key, value) } output, err := command.CombinedOutput() if err != nil { t.Fatalf("Woodpecker command failed: %v\ncommand: %s\noutput: %s", err, block, output) } return string(output) } func setEnvironment(environment []string, key, value string) []string { prefix := key + "=" for i, entry := range environment { if strings.HasPrefix(entry, prefix) { environment[i] = prefix + value return environment } } return append(environment, prefix+value) }