package main import ( "bytes" "context" "crypto/sha256" "encoding/hex" "encoding/json" "fmt" "io" "net/http" "net/http/httptest" "os" "os/exec" "path/filepath" "runtime" "strings" "sync" "testing" releasekit "gitea.maximumdirect.net/eric/ReleaseKit" "gitea.maximumdirect.net/eric/ReleaseKit/buildinfo" "gitea.maximumdirect.net/eric/ReleaseKit/cmd/releasekit/internal/cli" ) func TestVersionCommand(t *testing.T) { var stdout, stderr bytes.Buffer if status := run(context.Background(), []string{"version"}, &stdout, &stderr); status != 0 { t.Fatalf("run() status = %d, want 0; stderr = %q", status, stderr.String()) } if want := fmt.Sprintf("releasekit %s\n", buildinfo.Version()); stdout.String() != want { t.Fatalf("version output = %q, want %q", stdout.String(), want) } if stderr.Len() != 0 { t.Fatalf("version command wrote to stderr: %q", stderr.String()) } } func TestUnknownCommand(t *testing.T) { var stdout, stderr bytes.Buffer if status := run(context.Background(), []string{"unknown"}, &stdout, &stderr); status != 2 { t.Fatalf("run() status = %d, want 2", status) } if stdout.Len() != 0 { t.Fatalf("unknown command wrote to stdout: %q", stdout.String()) } if want := "unknown command \"unknown\"\n" + cli.Usage + "\n"; stderr.String() != want { t.Fatalf("usage output = %q, want %q", stderr.String(), want) } } func TestCheckCommandBindsConfiguredChecksToCapturedCandidate(t *testing.T) { raceSelected := racePrerequisitesAvailable() repository := newCLIRepository(t) moduleDir := filepath.Join(repository, "integrations", "ckconfig") writeRepositoryFile(t, repository, "integrations/ckconfig/go.mod", "module example.com/ckconfig\n\ngo 1.25.0\n") writeRepositoryFile(t, repository, "integrations/ckconfig/docs/releases/v1.2.3.md", "# Example Product v1.2.3\n\nNested release note.\n") writeRepositoryFile(t, repository, "integrations/ckconfig/fixture_test.go", "package ckconfig\nimport \"testing\"\nfunc TestFixture(t *testing.T) {}\n") writeRepositoryFile(t, repository, "integrations/ckconfig/race_fixture_test.go", "//go:build race\n\npackage ckconfig\n\nimport (\n\t\"os\"\n\t\"testing\"\n)\n\nfunc TestRaceBuildSelection(t *testing.T) {\n\tmarker := os.Getenv(\"RELEASEKIT_RACE_MARKER\")\n\tif marker == \"\" { return }\n\tif err := os.WriteFile(marker, []byte(\"race enabled\"), 0o600); err != nil { t.Fatal(err) }\n}\n") cliGitRun(t, repository, "add", "integrations/ckconfig") cliGitRun(t, repository, "commit", "-m", "add nested module candidate") profilePath := filepath.Join(t.TempDir(), "profile with spaces.json") orderPath := filepath.Join(t.TempDir(), "check-order") t.Setenv("RELEASEKIT_CHECK_ORDER_FILE", orderPath) executable, err := os.Executable() if err != nil { t.Fatal(err) } check := func(name string) cli.CheckCommand { return cli.CheckCommand{ Args: []string{executable, "-test.run=TestCandidateCheckHelper", "--", name}, Env: map[string]string{ "RELEASEKIT_CHECK_HELPER": "1", "RELEASEKIT_CHECK_NAME": name, "RELEASEKIT_VERSION": "profile version", "RELEASEKIT_CANDIDATE": "profile commit", }, } } settings := cli.Settings{ Product: "Example Product", ModuleDir: "integrations/ckconfig", Remote: "origin", Checks: []cli.CheckCommand{check("first"), check("second")}, } if raceSelected { settings.Validation = &cli.ValidationSettings{Go: cli.GoValidationSettings{Race: true}} } contents, err := json.Marshal(settings) if err != nil { t.Fatal(err) } if err := os.WriteFile(profilePath, contents, 0o600); err != nil { t.Fatal(err) } var stdout, stderr bytes.Buffer raceMarker := "" if raceSelected { raceMarker = filepath.Join(t.TempDir(), "race-enabled") t.Setenv("RELEASEKIT_RACE_MARKER", raceMarker) } args := []string{"check", "--repo", repository, "--config", profilePath, "v1.2.3"} if status := run(context.Background(), args, &stdout, &stderr); status != 0 { t.Fatalf("run() status = %d; stdout=%q stderr=%q", status, stdout.String(), stderr.String()) } commit := cliGitOutput(t, repository, "rev-parse", "HEAD") if got, err := os.ReadFile(orderPath); err != nil || string(got) != "first\nsecond\n" { t.Fatalf("configured check order = %q, error = %v", got, err) } if got, want := strings.Count(stderr.String(), "v1.2.3|"+commit), 2; got != want { t.Fatalf("configured checks received captured candidate %d times, want %d; stderr=%q", got, want, stderr.String()) } if !strings.Contains(stderr.String(), fmt.Sprintf("%s|%s|%s|example.com/ckconfig|integrations/ckconfig/v1.2.3", moduleDir, repository, moduleDir)) { t.Fatalf("configured checks did not receive the selected module directory and identity: %q", stderr.String()) } if want := fmt.Sprintf("local candidate module example.com/ckconfig version v1.2.3 tag integrations/ckconfig/v1.2.3 at %s is valid\n", commit); stdout.String() != want { t.Fatalf("stdout = %q, want %q", stdout.String(), want) } if raceSelected { if _, err := os.Stat(raceMarker); err != nil { t.Fatalf("race-enabled profile did not run: %v", err) } } } func TestCandidateRequestMapsGoValidation(t *testing.T) { limit := int64(2048) request := candidateRequest(cli.Invocation{Repository: "repository", Version: "v1.2.3"}, cli.Settings{ Product: "Example Product", ExpectedPushURL: "publication-only value", Validation: &cli.ValidationSettings{ GoFormat: true, Markdown: &cli.MarkdownSettings{EnforceCheckoutBoundary: true}, Go: cli.GoValidationSettings{Race: true, FullVet: true, CompileTargets: []string{"linux/amd64", "darwin/arm64"}}, ModuleDistribution: cli.ModuleDistributionSettings{WorkspaceFiles: true, LocalReplacements: true}, FileSize: &cli.FileSizeSettings{LimitBytes: &limit, Exclusions: []string{"cache"}}, }, }, nil) if !request.Validation.Go.Race || !request.Validation.Go.FullVet { t.Fatalf("candidate request validation = %#v, want Go race and full vet enabled", request.Validation) } if !request.Validation.GoFormat { t.Fatalf("candidate request GoFormat = false, want true") } if request.Validation.Markdown == nil || !request.Validation.Markdown.EnforceCheckoutBoundary { t.Fatalf("candidate request Markdown = %#v, want boundary enforcement enabled", request.Validation.Markdown) } if !request.Validation.ModuleDistribution.WorkspaceFiles || !request.Validation.ModuleDistribution.LocalReplacements { t.Fatalf("candidate request module distribution = %#v, want both checks enabled", request.Validation.ModuleDistribution) } if request.Validation.FileSize == nil || request.Validation.FileSize.LimitBytes == nil || *request.Validation.FileSize.LimitBytes != limit || len(request.Validation.FileSize.Exclusions) != 1 || request.Validation.FileSize.Exclusions[0] != "cache" { t.Fatalf("candidate request file size = %#v, want configured limit and exclusions", request.Validation.FileSize) } if request.ExpectedPushURL != "" { t.Fatalf("candidate request carried publication-only expected push URL %q", request.ExpectedPushURL) } wantTargets := []releasekit.Target{{OS: "linux", Arch: "amd64"}, {OS: "darwin", Arch: "arm64"}} if len(request.Validation.Go.CompileTargets) != len(wantTargets) { t.Fatalf("candidate request compile targets = %#v, want %#v", request.Validation.Go.CompileTargets, wantTargets) } for index := range wantTargets { if request.Validation.Go.CompileTargets[index] != wantTargets[index] { t.Fatalf("candidate request compile targets = %#v, want %#v", request.Validation.Go.CompileTargets, wantTargets) } } } func TestCandidateRequestPreservesModuleAndSharedFileSelection(t *testing.T) { for _, test := range []struct { name string shared []string wantNil bool wantValues []string }{ {name: "nil", wantNil: true}, {name: "empty", shared: []string{}}, {name: "selected", shared: []string{"README.md", "docs/policy.md"}, wantValues: []string{"README.md", "docs/policy.md"}}, } { t.Run(test.name, func(t *testing.T) { request := candidateRequest(cli.Invocation{Repository: "repository", Version: "v1.2.3"}, cli.Settings{ ModuleDir: "integrations/ckconfig", Validation: &cli.ValidationSettings{SharedFiles: test.shared}, }, nil) if request.ModuleDir != "integrations/ckconfig" { t.Fatalf("module selection = %q", request.ModuleDir) } if (request.Validation.SharedFiles == nil) != test.wantNil { t.Fatalf("mapped shared_files = %#v, want nil=%t", request.Validation.SharedFiles, test.wantNil) } if len(request.Validation.SharedFiles) != len(test.wantValues) { t.Fatalf("mapped shared_files = %#v, want %#v", request.Validation.SharedFiles, test.wantValues) } for i, path := range test.wantValues { if request.Validation.SharedFiles[i] != path { t.Fatalf("mapped shared_files = %#v, want %#v", request.Validation.SharedFiles, test.wantValues) } } }) } } func TestValidationSpecPreservesMarkdownObjectSelection(t *testing.T) { for _, test := range []struct { name string settings *cli.MarkdownSettings want bool }{ {name: "omitted"}, {name: "empty object", settings: &cli.MarkdownSettings{}, want: true}, } { t.Run(test.name, func(t *testing.T) { got := validationSpec(&cli.ValidationSettings{Markdown: test.settings}).Markdown != nil if got != test.want { t.Fatalf("Markdown selected = %t, want %t", got, test.want) } }) } } func TestPublishExpectedModuleRejectsMismatchBeforeProductCommands(t *testing.T) { repository := newCLIRepository(t) writeRepositoryFile(t, repository, "integrations/ckconfig/go.mod", "module example.com/ckconfig\n\ngo 1.25.0\n") marker := filepath.Join(t.TempDir(), "product-command-ran") profile, err := json.Marshal(cli.Settings{ Product: "Example Product", Checks: []cli.CheckCommand{{Args: []string{"sh", "-c", "touch " + marker}}}, }) if err != nil { t.Fatal(err) } writeRepositoryFile(t, repository, "releasekit.json", string(profile)) cliGitRun(t, repository, "add", "releasekit.json", "integrations/ckconfig/go.mod") cliGitRun(t, repository, "commit", "-m", "configure root release profile") var stdout, stderr bytes.Buffer status := run(context.Background(), []string{"publish", "--repo", repository, "--expected-module", "integrations/ckconfig", "v1.2.3"}, &stdout, &stderr) if status != 2 || !strings.Contains(stderr.String(), "profile module selection") { t.Fatalf("publish status=%d stdout=%q stderr=%q, want module-selection error", status, stdout.String(), stderr.String()) } if _, err := os.Stat(marker); !os.IsNotExist(err) { t.Fatalf("product command ran despite module mismatch: stat error=%v", err) } if err := exec.Command("git", "-C", repository, "show-ref", "--verify", "refs/tags/v1.2.3").Run(); err == nil { t.Fatal("module mismatch created a release tag") } } func TestCandidateCommandsRunMarkdownProfile(t *testing.T) { repository := newCLIRepository(t) profile, err := json.Marshal(cli.Settings{ Product: "Example Product", Validation: &cli.ValidationSettings{ Markdown: &cli.MarkdownSettings{}, }, }) if err != nil { t.Fatal(err) } writeRepositoryFile(t, repository, "releasekit.json", string(profile)) writeRepositoryFile(t, repository, "guide.md", "[missing](missing.md#top)\n") cliGitRun(t, repository, "add", "releasekit.json", "guide.md") cliGitRun(t, repository, "commit", "-m", "configure Markdown candidate validation") remote := filepath.Join(t.TempDir(), "origin.git") cliGitRun(t, repository, "init", "--bare", remote) cliGitRun(t, repository, "remote", "add", "origin", remote) cliGitRun(t, repository, "push", "origin", "HEAD:refs/heads/main") for _, command := range []string{"check", "publish"} { t.Run(command, func(t *testing.T) { var stdout, stderr bytes.Buffer args := []string{command, "--repo", repository, "v1.2.3"} if status := run(context.Background(), args, &stdout, &stderr); status != 1 { t.Fatalf("run() status = %d; stdout=%q stderr=%q, want validation failure", status, stdout.String(), stderr.String()) } if !strings.Contains(stderr.String(), "markdown-links") || !strings.Contains(stderr.String(), "guide.md:1:") || !strings.Contains(stderr.String(), "missing.md#top") { t.Fatalf("stderr = %q, want Markdown identity, source location, and target", stderr.String()) } }) } } func TestCandidateCommandsPresentValidationDiagnosticsOnce(t *testing.T) { repository := newCLIRepository(t) profile, err := json.Marshal(cli.Settings{Product: "Example Product"}) if err != nil { t.Fatal(err) } writeRepositoryFile(t, repository, "releasekit.json", string(profile)) cliGitRun(t, repository, "add", "releasekit.json") cliGitRun(t, repository, "commit", "-m", "configure candidate commands") remote := filepath.Join(t.TempDir(), "origin.git") cliGitRun(t, repository, "init", "--bare", remote) cliGitRun(t, repository, "remote", "add", "origin", remote) cliGitRun(t, repository, "push", "origin", "HEAD:refs/heads/main") goPath, err := exec.LookPath("go") if err != nil { t.Fatal(err) } goDirectory := t.TempDir() shim := filepath.Join(goDirectory, "go") if err := os.WriteFile(shim, []byte("#!/bin/sh\nif [ \"$1\" = test ]; then echo 'fixture Go suite failure' >&2; exit 1; fi\nexec \"$RELEASEKIT_REAL_GO\" \"$@\"\n"), 0o755); err != nil { t.Fatal(err) } t.Setenv("RELEASEKIT_REAL_GO", goPath) t.Setenv("PATH", goDirectory+string(os.PathListSeparator)+os.Getenv("PATH")) for _, command := range []struct { name string wantStatus int args []string }{ {name: "check", wantStatus: 1, args: []string{"check", "--repo", repository, "v1.2.3"}}, {name: "publish", wantStatus: 1, args: []string{"publish", "--repo", repository, "v1.2.3"}}, } { t.Run(command.name, func(t *testing.T) { var stdout, stderr bytes.Buffer if status := run(context.Background(), command.args, &stdout, &stderr); status != command.wantStatus { t.Fatalf("run() status = %d; stdout=%q stderr=%q", status, stdout.String(), stderr.String()) } if got := strings.Count(stderr.String(), "go-test validation failed"); got != 1 { t.Fatalf("validation diagnostic appeared %d times: %q", got, stderr.String()) } if !strings.Contains(stderr.String(), "fixture Go suite failure") { t.Fatalf("stderr omitted the validation cause: %q", stderr.String()) } if command.name == "publish" && !strings.Contains(stderr.String(), "state=not_attempted phase=validation") { t.Fatalf("publication validation error lost its state: %q", stderr.String()) } }) } } func TestCandidateFailurePresentationRetainsCallbackContext(t *testing.T) { diagnostic := &releasekit.ValidationError{ Check: "examples", Findings: []releasekit.ValidationFinding{{Path: "examples/run/main.go", Message: "unexpected output"}}, } err := fmt.Errorf("validate candidate captured-commit: %w", diagnostic) text := candidateFailureText(err) for _, part := range []string{"captured-commit", "examples", "examples/run/main.go", "unexpected output"} { if !strings.Contains(text, part) { t.Errorf("CLI presentation %q omits %q", text, part) } } } func TestPublishCommandPublishesConfiguredCandidate(t *testing.T) { raceSelected := racePrerequisitesAvailable() repository := newCLIRepository(t) writeRepositoryFile(t, repository, "docs/releases/v1.2.3.md", "# Example Product v1.2.3\n\n## Compatibility\n\nNo action required.\n") writeRepositoryFile(t, repository, "scripts/check-release-note.sh", `#!/bin/sh set -eu version=$RELEASEKIT_VERSION candidate=$RELEASEKIT_CANDIDATE test "$(git rev-parse HEAD)" = "$candidate" test "$(sed -n '1p' "docs/releases/$version.md")" = "# Example Product $version" grep -Fxq '## Compatibility' "docs/releases/$version.md" grep -Fxq 'No action required.' "docs/releases/$version.md" grep -Fxq 'module example.com/cli-candidate' go.mod printf '%s %s\n' "$version" "$candidate" >> "$RELEASEKIT_COMPATIBILITY_LOG" `) cliGitRun(t, repository, "add", "docs/releases/v1.2.3.md", "scripts/check-release-note.sh") cliGitRun(t, repository, "commit", "-m", "add compatibility note and check") checkLog := filepath.Join(t.TempDir(), "compatibility-check.log") remote := filepath.Join(t.TempDir(), "origin.git") settings := cli.Settings{ Product: "Example Product", Remote: "origin", ExpectedPushURL: remote, Checks: []cli.CheckCommand{{ Args: []string{"sh", "scripts/check-release-note.sh"}, Env: map[string]string{"RELEASEKIT_COMPATIBILITY_LOG": checkLog}, }}, } if raceSelected { settings.Validation = &cli.ValidationSettings{Go: cli.GoValidationSettings{Race: true}} } contents, err := json.Marshal(settings) if err != nil { t.Fatal(err) } if err := os.WriteFile(filepath.Join(repository, "releasekit.json"), contents, 0o600); err != nil { t.Fatal(err) } cliGitRun(t, repository, "add", "releasekit.json") cliGitRun(t, repository, "commit", "-m", "configure source release") cliGitRun(t, repository, "init", "--bare", remote) cliGitRun(t, repository, "remote", "add", "origin", remote) cliGitRun(t, repository, "push", "--set-upstream", "origin", "main") var stdout, stderr bytes.Buffer raceMarker := "" if raceSelected { raceMarker = filepath.Join(t.TempDir(), "race-enabled") t.Setenv("RELEASEKIT_RACE_MARKER", raceMarker) } args := []string{"publish", "--repo", repository, "v1.2.3"} if status := run(context.Background(), args, &stdout, &stderr); status != 0 { t.Fatalf("run() status = %d; stdout=%q stderr=%q", status, stdout.String(), stderr.String()) } commit := cliGitOutput(t, repository, "rev-parse", "HEAD") tagObject := cliGitOutput(t, repository, "rev-parse", "refs/tags/v1.2.3") if want := fmt.Sprintf("published v1.2.3 as v1.2.3 at %s (tag object %s)\n", commit, tagObject); stdout.String() != want { t.Fatalf("stdout = %q, want %q", stdout.String(), want) } if !bytes.Contains(stderr.Bytes(), []byte("validating and publishing module . version v1.2.3")) || !bytes.Contains(stderr.Bytes(), []byte("example.com/cli-candidate")) { t.Fatalf("stderr does not show publication progress and validation: %q", stderr.String()) } if got := cliGitOutput(t, remote, "rev-parse", "refs/tags/v1.2.3"); got != tagObject { t.Fatalf("remote tag object = %s, want %s", got, tagObject) } checkResult, err := os.ReadFile(checkLog) if err != nil { t.Fatalf("read product compatibility check result: %v", err) } if got, want := strings.Count(string(checkResult), "v1.2.3 "+commit), 1; got != want { t.Fatalf("product compatibility check ran %d times, want %d", got, want) } if raceSelected { if _, err := os.Stat(raceMarker); err != nil { t.Fatalf("race-enabled profile did not run: %v", err) } } } func TestBuildCommandUsesProfileModuleSelection(t *testing.T) { repository := newCLIRepository(t) writeRepositoryFile(t, repository, "integrations/ckconfig/go.mod", "module example.com/ckconfig\n\ngo 1.25.0\n") profile, err := json.Marshal(cli.Settings{ Product: "Example Product", ModuleDir: "integrations/ckconfig", Build: &cli.BuildSettings{}, }) if err != nil { t.Fatal(err) } writeRepositoryFile(t, repository, "releasekit.json", string(profile)) var stdout, stderr bytes.Buffer status := run(context.Background(), []string{"build", "--repo", repository, "v1.2.3"}, &stdout, &stderr) if status != 0 { t.Fatalf("build status=%d stdout=%q stderr=%q", status, stdout.String(), stderr.String()) } if !strings.Contains(stdout.String(), "no build targets selected for module example.com/ckconfig version v1.2.3 tag integrations/ckconfig/v1.2.3") { t.Fatalf("build did not use profile module selection: %q", stdout.String()) } } func TestProfileSharedFilesRequireCommittedFilesWithoutEnablingChecks(t *testing.T) { repository := newCLIRepository(t) profile, err := json.Marshal(cli.Settings{ Product: "Example Product", Validation: &cli.ValidationSettings{SharedFiles: []string{"docs/shared.md"}}, }) if err != nil { t.Fatal(err) } writeRepositoryFile(t, repository, "releasekit.json", string(profile)) cliGitRun(t, repository, "add", "releasekit.json") cliGitRun(t, repository, "commit", "-m", "configure an explicit shared file") var stdout, stderr bytes.Buffer status := run(context.Background(), []string{"check", "--repo", repository, "v1.2.3"}, &stdout, &stderr) if status != 1 || !strings.Contains(stderr.String(), "shared-files") || !strings.Contains(stderr.String(), "docs/shared.md") { t.Fatalf("check status=%d stdout=%q stderr=%q, want missing shared-files failure", status, stdout.String(), stderr.String()) } } func TestBuildCommandWritesValidatedReceiptForConfiguredArtifacts(t *testing.T) { repository := newCLIBuildRepository(t, `package main import ( "fmt" "os" "gitea.maximumdirect.net/eric/ReleaseKit/buildinfo" ) func main() { if len(os.Args) == 2 && os.Args[1] == "version" { fmt.Printf("fixture %s\n", buildinfo.Version()) } } `) var stdout, stderr bytes.Buffer args := []string{"build", "--repo", repository, "v1.2.3"} if status := run(context.Background(), args, &stdout, &stderr); status != 0 { t.Fatalf("run() status = %d; stdout=%q stderr=%q", status, stdout.String(), stderr.String()) } receiptPath := filepath.Join(repository, "dist", cli.BuildReceiptName) receipt, err := cli.LoadBuildReceipt(receiptPath) if err != nil { t.Fatal(err) } commit := cliGitOutput(t, repository, "rev-parse", "HEAD") settings := cli.BuildSettings{ MainPackage: "./cmd/app", BinaryName: "fixture", Targets: []string{fmt.Sprintf("%s/%s", runtime.GOOS, runtime.GOARCH)}, } if receipt.SchemaVersion != cli.BuildReceiptSchemaVersion || !receipt.MatchesBuild(repository, commit, "v1.2.3", settings) || len(receipt.Artifacts) != 1 { t.Fatalf("build receipt = %#v", receipt) } if !bytes.Contains(stdout.Bytes(), []byte(receipt.Artifacts[0].Filename)) || !bytes.Contains(stdout.Bytes(), []byte(receipt.Artifacts[0].SHA256)) { t.Fatalf("stdout does not report receipt artifacts: %q", stdout.String()) } if got := cliGitOutput(t, repository, "status", "--porcelain"); got != "" { t.Fatalf("build output dirtied candidate: %s", got) } } func TestBuildCommandLeavesNoReceiptAfterSmokeFailure(t *testing.T) { repository := newCLIBuildRepository(t, `package main import ( "fmt" "os" ) func main() { if len(os.Args) == 2 && os.Args[1] == "version" { fmt.Println("wrong") } } `) var stdout, stderr bytes.Buffer args := []string{"build", "--repo", repository, "v1.2.3"} if status := run(context.Background(), args, &stdout, &stderr); status != 1 { t.Fatalf("run() status = %d; stdout=%q stderr=%q, want 1", status, stdout.String(), stderr.String()) } if stdout.Len() != 0 || !bytes.Contains(stderr.Bytes(), []byte("native version output")) { t.Fatalf("stdout=%q stderr=%q; want smoke failure without final output", stdout.String(), stderr.String()) } if _, err := os.Stat(filepath.Join(repository, "dist", cli.BuildReceiptName)); !os.IsNotExist(err) { t.Fatalf("partial build wrote receipt: %v", err) } filename := fmt.Sprintf("fixture-v1.2.3-%s-%s", runtime.GOOS, runtime.GOARCH) if runtime.GOOS == "windows" { filename += ".exe" } if _, err := os.Stat(filepath.Join(repository, "dist", filename)); err != nil { t.Fatalf("completed artifact not reported or retained: %v; stderr=%q", err, stderr.String()) } } func TestCandidateCheckHelper(t *testing.T) { if os.Getenv("RELEASEKIT_CHECK_HELPER") != "1" { return } if orderPath := os.Getenv("RELEASEKIT_CHECK_ORDER_FILE"); orderPath != "" { file, err := os.OpenFile(orderPath, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0o600) if err != nil { fmt.Fprintln(os.Stderr, err) os.Exit(9) } if _, err := fmt.Fprintln(file, os.Getenv("RELEASEKIT_CHECK_NAME")); err != nil { _ = file.Close() fmt.Fprintln(os.Stderr, err) os.Exit(9) } if err := file.Close(); err != nil { fmt.Fprintln(os.Stderr, err) os.Exit(9) } } workdir, err := os.Getwd() if err != nil { fmt.Fprintln(os.Stderr, err) os.Exit(9) } fmt.Fprintf(os.Stdout, "%s|%s|%s|%s|%s|%s|%s", os.Getenv("RELEASEKIT_VERSION"), os.Getenv("RELEASEKIT_CANDIDATE"), workdir, os.Getenv("RELEASEKIT_REPOSITORY_DIR"), os.Getenv("RELEASEKIT_MODULE_DIR"), os.Getenv("RELEASEKIT_MODULE_PATH"), os.Getenv("RELEASEKIT_TAG")) os.Exit(0) } func newCLIRepository(t *testing.T) string { t.Helper() repository := filepath.Join(t.TempDir(), "candidate repository") if err := os.Mkdir(repository, 0o700); err != nil { t.Fatal(err) } cliGitRun(t, repository, "init", "--initial-branch=main") cliGitRun(t, repository, "config", "user.name", "ReleaseKit Test") cliGitRun(t, repository, "config", "user.email", "releasekit@example.test") cliGitRun(t, repository, "config", "commit.gpgsign", "false") write := func(name, value string) { t.Helper() path := filepath.Join(repository, filepath.FromSlash(name)) if err := os.MkdirAll(filepath.Dir(path), 0o700); err != nil { t.Fatal(err) } if err := os.WriteFile(path, []byte(value), 0o600); err != nil { t.Fatal(err) } } write("go.mod", "module example.com/cli-candidate\n\ngo 1.25.0\n") write("candidate_test.go", "package candidate\nimport \"testing\"\nfunc TestFixture(t *testing.T) {}\n") write("race_candidate_test.go", `//go:build race package candidate import ( "os" "testing" ) func TestRaceBuildSelection(t *testing.T) { marker := os.Getenv("RELEASEKIT_RACE_MARKER") if marker == "" { return } if err := os.WriteFile(marker, []byte("race enabled"), 0o600); err != nil { t.Fatal(err) } } `) write("docs/releases/v1.2.3.md", "# Example Product v1.2.3\n\nRelease note.\n") cliGitRun(t, repository, "add", "--all") cliGitRun(t, repository, "commit", "-m", "candidate") return repository } func racePrerequisitesAvailable() bool { supported := runtime.GOOS == "darwin" && (runtime.GOARCH == "amd64" || runtime.GOARCH == "arm64") || (runtime.GOOS == "freebsd" || runtime.GOOS == "linux") && (runtime.GOARCH == "amd64" || runtime.GOARCH == "arm64" || runtime.GOARCH == "ppc64le") || runtime.GOOS == "windows" && runtime.GOARCH == "amd64" if !supported { return false } cgo, err := exec.Command("go", "env", "CGO_ENABLED").Output() if err != nil || strings.TrimSpace(string(cgo)) != "1" { return false } cc, err := exec.Command("go", "env", "CC").Output() if err != nil || len(strings.Fields(string(cc))) == 0 { return false } if _, err := exec.LookPath(strings.Fields(string(cc))[0]); err != nil { return false } return true } func TestDeliverPageOnlyCreatesAndReusesRelease(t *testing.T) { repository := newCLIRepository(t) state := &deliveryServerState{} server := newDeliveryServer(t, state) defer server.Close() settings := cli.Settings{ Product: "Example Product", Validation: &cli.ValidationSettings{Go: cli.GoValidationSettings{CompileTargets: []string{"linux/amd64"}}}, Delivery: &cli.DeliverySettings{BaseURL: server.URL, Owner: "owner", Repository: "example"}, } writeDeliveryProfile(t, repository, settings) state.update(func(state *deliveryServerData) { state.commit = cliGitOutput(t, repository, "rev-parse", "HEAD") }) cliGitRun(t, repository, "tag", "v1.2.3") // Historical lightweight tags are valid delivery inputs. t.Setenv("RELEASEKIT_GITEA_TOKEN", "fixture-token") for attempt := 0; attempt < 2; attempt++ { var stdout, stderr bytes.Buffer status := run(context.Background(), []string{"deliver", "--repo", repository, "v1.2.3"}, &stdout, &stderr) if status != 0 { t.Fatalf("delivery attempt %d status=%d stdout=%q stderr=%q", attempt+1, status, stdout.String(), stderr.String()) } if !strings.Contains(stdout.String(), "state complete") || !strings.Contains(stdout.String(), "release: "+server.URL+"/releases/10") { t.Fatalf("delivery output = %q", stdout.String()) } } if state.snapshot().releaseCreates != 1 || state.snapshot().assetUploads != 0 || state.snapshot().assetListReads != 0 { t.Fatalf("page-only calls = creates:%d asset uploads:%d asset reads:%d", state.snapshot().releaseCreates, state.snapshot().assetUploads, state.snapshot().assetListReads) } if state.snapshot().noteBody != "# Example Product v1.2.3\n\nRelease note.\n" { t.Fatalf("hosted note body = %q", state.snapshot().noteBody) } } func TestDeliverBuildsTaggedArtifactsAndReusesAssets(t *testing.T) { repository := newDeliveryBuildRepository(t) temporaryRoot := t.TempDir() t.Setenv("TMPDIR", temporaryRoot) state := &deliveryServerState{} server := newDeliveryServer(t, state) defer server.Close() settings := cli.Settings{ Product: "Example Product", Build: &cli.BuildSettings{MainPackage: ".", BinaryName: "helper", Targets: []string{"linux/amd64"}}, Delivery: &cli.DeliverySettings{BaseURL: server.URL, Owner: "owner", Repository: "example"}, } writeDeliveryProfile(t, repository, settings) state.update(func(state *deliveryServerData) { state.commit = cliGitOutput(t, repository, "rev-parse", "HEAD") }) cliGitRun(t, repository, "tag", "v1.2.3") cliGitRun(t, repository, "checkout", "--detach", "v1.2.3") t.Setenv("RELEASEKIT_GITEA_TOKEN", "fixture-token") for attempt := 0; attempt < 2; attempt++ { var stdout, stderr bytes.Buffer status := run(context.Background(), []string{"deliver", "--repo", repository, "v1.2.3"}, &stdout, &stderr) if status != 0 { t.Fatalf("delivery attempt %d status=%d stdout=%q stderr=%q", attempt+1, status, stdout.String(), stderr.String()) } if attempt == 1 && (!strings.Contains(stdout.String(), "matching helper-v1.2.3-linux-amd64") || !strings.Contains(stdout.String(), "matching SHA256SUMS")) { t.Fatalf("second delivery did not report matching assets: %q", stdout.String()) } } observed := state.snapshot() if observed.releaseCreates != 1 || observed.assetUploads != 2 { t.Fatalf("delivery calls = creates:%d asset uploads:%d", observed.releaseCreates, observed.assetUploads) } if _, ok := observed.assets["helper-v1.2.3-linux-amd64"]; !ok { t.Fatalf("binary was not uploaded: %#v", observed.assets) } if _, ok := observed.assets["SHA256SUMS"]; !ok { t.Fatalf("checksum manifest was not uploaded: %#v", observed.assets) } if entries, err := os.ReadDir(temporaryRoot); err != nil || len(entries) != 0 { t.Fatalf("facade-owned build output remains after delivery: entries=%v error=%v", entries, err) } } func TestCIDeliverLoadsTaggedProfileAndSkipsUnavailablePageOnlyDependencies(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() writeDeliveryProfileAt(t, repository, "profiles/ci-delivery.json", cli.Settings{ Product: "Example Product", Delivery: &cli.DeliverySettings{BaseURL: server.URL, Owner: "owner", Repository: "example"}, }) expectedCommit := pushTaggedCLIRepository(t, repository) state.update(func(state *deliveryServerData) { state.commit = expectedCommit }) writeDeliveryProfileAt(t, repository, "profiles/ci-delivery.json", cli.Settings{Product: "Later Branch Profile"}) cliGitRun(t, repository, "checkout", "--detach", expectedCommit) cliGitRun(t, repository, "remote", "remove", "origin") t.Setenv("RELEASEKIT_GITEA_TOKEN", "fixture-token") t.Setenv("GOPROXY", "off") t.Setenv("GOSUMDB", "off") var stdout, stderr bytes.Buffer args := []string{"ci-deliver", "--repo", repository, "--config", "profiles/ci-delivery.json", "--prepared", "--commit", expectedCommit, "v1.2.3"} if status := run(context.Background(), args, &stdout, &stderr); status != 0 { t.Fatalf("run() status=%d stdout=%q stderr=%q", status, stdout.String(), stderr.String()) } if state.snapshot().releaseCreates != 1 || state.snapshot().apiRequests == 0 { t.Fatalf("page delivery did not reach the server: creates=%d requests=%d", state.snapshot().releaseCreates, state.snapshot().apiRequests) } if got := cliGitOutput(t, repository, "rev-parse", "HEAD"); got != expectedCommit { t.Fatalf("prepared HEAD = %s, want %s", got, expectedCommit) } } func TestCIDeliverRejectsWrongCommitBeforeGoOrHostedRequests(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", Build: &cli.BuildSettings{MainPackage: ".", BinaryName: "helper", Targets: []string{"linux/amd64"}}, Delivery: &cli.DeliverySettings{BaseURL: server.URL, Owner: "owner", Repository: "example"}, }) expectedCommit := pushTaggedCLIRepository(t, repository) state.update(func(state *deliveryServerData) { state.commit = expectedCommit }) shimDir := t.TempDir() markerPath := filepath.Join(shimDir, "go-invoked") goShim := "#!/bin/sh\n: > \"$RELEASEKIT_GO_MARKER\"\nexit 99\n" if err := os.WriteFile(filepath.Join(shimDir, "go"), []byte(goShim), 0o700); err != nil { t.Fatal(err) } t.Setenv("RELEASEKIT_GO_MARKER", markerPath) t.Setenv("PATH", shimDir+string(os.PathListSeparator)+os.Getenv("PATH")) t.Setenv("RELEASEKIT_GITEA_TOKEN", "fixture-token") var stdout, stderr bytes.Buffer wrongCommit := strings.Repeat("0", 40) args := []string{"ci-deliver", "--repo", repository, "--commit", wrongCommit, "v1.2.3"} if status := run(context.Background(), args, &stdout, &stderr); status != 1 { t.Fatalf("run() status=%d stdout=%q stderr=%q, want preparation failure", status, stdout.String(), stderr.String()) } if !strings.Contains(stderr.String(), "resolves to commit") { t.Fatalf("stderr=%q, want expected-commit mismatch", stderr.String()) } if _, err := os.Stat(markerPath); !os.IsNotExist(err) { t.Fatalf("Go was invoked before commit agreement: %v", err) } if state.snapshot().apiRequests != 0 { t.Fatalf("hosted API requests = %d, want 0", state.snapshot().apiRequests) } } func TestCIDeliverRejectsExpectedModuleMismatchBeforeDependencyPreparation(t *testing.T) { repository := newCLIRepository(t) if err := os.MkdirAll(filepath.Join(repository, "integrations", "other"), 0o700); err != nil { t.Fatal(err) } if err := os.MkdirAll(filepath.Join(repository, "integrations", "expected"), 0o700); err != nil { t.Fatal(err) } writeRepositoryFile(t, repository, "integrations/other/go.mod", "module example.com/other\n\ngo 1.25.0\n") writeRepositoryFile(t, repository, "integrations/expected/go.mod", "module example.com/expected\n\ngo 1.25.0\n") cliGitRun(t, repository, "add", "integrations") cliGitRun(t, repository, "commit", "-m", "add module profiles") state := &deliveryServerState{} server := newDeliveryServer(t, state) defer server.Close() writeDeliveryProfile(t, repository, cli.Settings{ Product: "Example Product", ModuleDir: "integrations/other", Build: &cli.BuildSettings{MainPackage: ".", BinaryName: "helper", Targets: []string{"linux/amd64"}}, Delivery: &cli.DeliverySettings{BaseURL: server.URL, Owner: "owner", Repository: "example"}, }) expectedCommit := pushTaggedCLIRepository(t, repository) state.update(func(state *deliveryServerData) { state.commit = expectedCommit }) shimDir := t.TempDir() markerPath := filepath.Join(shimDir, "go-invoked") goShim := "#!/bin/sh\n: > \"$RELEASEKIT_GO_MARKER\"\nexit 99\n" if err := os.WriteFile(filepath.Join(shimDir, "go"), []byte(goShim), 0o700); err != nil { t.Fatal(err) } t.Setenv("RELEASEKIT_GO_MARKER", markerPath) t.Setenv("PATH", shimDir+string(os.PathListSeparator)+os.Getenv("PATH")) t.Setenv("RELEASEKIT_GITEA_TOKEN", "fixture-token") var stdout, stderr bytes.Buffer args := []string{"ci-deliver", "--repo", repository, "--expected-module", "integrations/expected", "--commit", expectedCommit, "v1.2.3"} if status := run(context.Background(), args, &stdout, &stderr); status != 2 { t.Fatalf("run() status=%d stdout=%q stderr=%q, want profile-selection error", status, stdout.String(), stderr.String()) } if !strings.Contains(stderr.String(), "profile module selection") { t.Fatalf("stderr=%q, want profile-selection diagnostic", stderr.String()) } if _, err := os.Stat(markerPath); !os.IsNotExist(err) { t.Fatalf("Go was invoked before profile agreement: %v", err) } if state.snapshot().apiRequests != 0 { t.Fatalf("hosted API requests = %d, want 0", state.snapshot().apiRequests) } } func TestCIDeliverPreparesDependenciesAndBuildsConfiguredTargets(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"}, }) expectedCommit := pushTaggedCLIRepository(t, repository) state.update(func(state *deliveryServerData) { state.commit = expectedCommit }) t.Setenv("RELEASEKIT_GITEA_TOKEN", "fixture-token") var stdout, stderr bytes.Buffer args := []string{"ci-deliver", "--repo", repository, "--commit", expectedCommit, "v1.2.3"} if status := run(context.Background(), args, &stdout, &stderr); status != 0 { t.Fatalf("run() status=%d stdout=%q stderr=%q", status, stdout.String(), stderr.String()) } if state.snapshot().releaseCreates != 1 || state.snapshot().assetUploads != 2 { t.Fatalf("delivery counts = creates:%d uploads:%d", state.snapshot().releaseCreates, state.snapshot().assetUploads) } if _, ok := state.snapshot().assets["helper-v1.2.3-"+runtime.GOOS+"-"+runtime.GOARCH]; !ok { t.Fatalf("binary not uploaded: %#v", state.snapshot().assets) } } func TestCIDeliverValidatesCredentialsBeforePreparingDependencies(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{"linux/amd64"}}, Delivery: &cli.DeliverySettings{BaseURL: server.URL, Owner: "owner", Repository: "example"}, }) expectedCommit := pushTaggedCLIRepository(t, repository) state.update(func(state *deliveryServerData) { state.commit = expectedCommit }) shimDir := t.TempDir() markerPath := filepath.Join(shimDir, "go-invoked") goShim := "#!/bin/sh\n: > \"$RELEASEKIT_GO_MARKER\"\nexit 99\n" if err := os.WriteFile(filepath.Join(shimDir, "go"), []byte(goShim), 0o700); err != nil { t.Fatal(err) } t.Setenv("RELEASEKIT_GO_MARKER", markerPath) t.Setenv("PATH", shimDir+string(os.PathListSeparator)+os.Getenv("PATH")) t.Setenv("RELEASEKIT_GITEA_TOKEN", "") var stdout, stderr bytes.Buffer args := []string{"ci-deliver", "--repo", repository, "--commit", expectedCommit, "v1.2.3"} if status := run(context.Background(), args, &stdout, &stderr); status != 2 { t.Fatalf("run() status=%d stdout=%q stderr=%q, want credential error", status, stdout.String(), stderr.String()) } if !strings.Contains(stderr.String(), "non-empty token") { t.Fatalf("stderr=%q, want missing-credential error", stderr.String()) } if _, err := os.Stat(markerPath); !os.IsNotExist(err) { t.Fatalf("Go was invoked before credential validation: %v", err) } if state.snapshot().apiRequests != 0 { t.Fatalf("hosted API requests = %d, want 0", state.snapshot().apiRequests) } } func TestDeliverRejectsInvalidTaggedSourceBeforeManagedBuild(t *testing.T) { repository := newDeliveryBuildRepository(t) state := &deliveryServerState{} server := newDeliveryServer(t, state) defer server.Close() settings := cli.Settings{ Product: "Example Product", Build: &cli.BuildSettings{MainPackage: ".", BinaryName: "helper", Targets: []string{"linux/amd64"}}, Delivery: &cli.DeliverySettings{BaseURL: server.URL, Owner: "owner", Repository: "example"}, } writeDeliveryProfile(t, repository, settings) state.update(func(state *deliveryServerData) { state.commit = cliGitOutput(t, repository, "rev-parse", "HEAD") }) cliGitRun(t, repository, "tag", "v1.2.3") if err := os.WriteFile(filepath.Join(repository, "uncommitted.txt"), []byte("dirty"), 0600); err != nil { t.Fatal(err) } t.Setenv("RELEASEKIT_GITEA_TOKEN", "fixture-token") var stdout, stderr bytes.Buffer status := run(context.Background(), []string{"deliver", "--repo", repository, "v1.2.3"}, &stdout, &stderr) if status != 1 || state.snapshot().apiRequests != 0 || strings.Contains(stderr.String(), "building ") { t.Fatalf("invalid-source delivery status=%d requests=%d stdout=%q stderr=%q", status, state.snapshot().apiRequests, stdout.String(), stderr.String()) } } func TestDeliverFailureReportsRecoveryForPartialConflictAndUnknownStates(t *testing.T) { for _, test := range []struct { name string state string status int recovery []string assetObservation string missingObservation string }{ { name: "partial", status: 1, recovery: []string{"recovery: fix the reported cause", "retry module example.com/cli-candidate version v1.2.3 tag v1.2.3"}, assetObservation: "pending helper-v1.2.3-linux-amd64", }, { name: "conflict", status: 1, recovery: []string{"investigate conflicting hosted content", "new version", "retain the original build artifacts"}, assetObservation: "conflicting helper-v1.2.3-linux-amd64", missingObservation: "pending SHA256SUMS", }, { name: "unknown", status: 3, recovery: []string{"recovery: resolve the remote state for module example.com/cli-candidate version v1.2.3 tag v1.2.3"}, assetObservation: "uncertain helper-v1.2.3-linux-amd64", missingObservation: "pending SHA256SUMS", }, } { t.Run(test.name, func(t *testing.T) { repository := newCLIRepository(t) state := &deliveryServerState{} server := newDeliveryServer(t, state) defer server.Close() build := cli.BuildSettings{MainPackage: ".", BinaryName: "helper", Targets: []string{"linux/amd64"}} writeDeliveryProfile(t, repository, cli.Settings{ Product: "Example Product", Build: &build, Delivery: &cli.DeliverySettings{BaseURL: server.URL, Owner: "owner", Repository: "example"}, }) state.update(func(state *deliveryServerData) { state.commit = cliGitOutput(t, repository, "rev-parse", "HEAD") }) cliGitRun(t, repository, "tag", "v1.2.3") assetsDir, _ := writeDeliveryReceipt(t, repository, build) switch test.name { case "partial": state.update(func(state *deliveryServerData) { state.failAssetName = "helper-v1.2.3-linux-amd64" }) case "conflict": state.update(func(state *deliveryServerData) { state.assets["helper-v1.2.3-linux-amd64"] = []byte("different hosted bytes") }) case "unknown": state.update(func(state *deliveryServerData) { state.ambiguousAssetName = "helper-v1.2.3-linux-amd64" }) } t.Setenv("RELEASEKIT_GITEA_TOKEN", "fixture-token") var stdout, stderr bytes.Buffer status := run(context.Background(), []string{"deliver", "--repo", repository, "--assets", assetsDir, "v1.2.3"}, &stdout, &stderr) if status != test.status || !strings.Contains(stderr.String(), "state="+test.name) { t.Fatalf("status=%d output=%q want status=%d state=%s", status, stderr.String(), test.status, test.name) } for _, phrase := range test.recovery { if !strings.Contains(stderr.String(), phrase) { t.Errorf("delivery output does not include recovery guidance %q: %s", phrase, stderr.String()) } } if !strings.Contains(stderr.String(), test.assetObservation) { t.Errorf("delivery output does not include asset observation %q: %s", test.assetObservation, stderr.String()) } if test.missingObservation != "" && !strings.Contains(stderr.String(), test.missingObservation) { t.Errorf("delivery output does not include later missing asset %q: %s", test.missingObservation, stderr.String()) } if test.name == "unknown" && strings.Contains(stderr.String(), "pending helper-v1.2.3-linux-amd64") { t.Errorf("uncertain asset also appears pending: %s", stderr.String()) } }) } } func TestDeliverRejectsWrongHostedTagBeforeReleaseWrite(t *testing.T) { repository := newCLIRepository(t) state := &deliveryServerState{data: deliveryServerData{remoteCommit: strings.Repeat("0", 40)}} 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"}}) state.update(func(state *deliveryServerData) { state.commit = cliGitOutput(t, repository, "rev-parse", "HEAD") }) cliGitRun(t, repository, "tag", "v1.2.3") t.Setenv("RELEASEKIT_GITEA_TOKEN", "fixture-token") var stdout, stderr bytes.Buffer status := run(context.Background(), []string{"deliver", "--repo", repository, "v1.2.3"}, &stdout, &stderr) if status != 1 || state.snapshot().releaseCreates != 0 || state.snapshot().assetUploads != 0 { t.Fatalf("wrong hosted tag status=%d creates=%d uploads=%d stdout=%q stderr=%q", status, state.snapshot().releaseCreates, state.snapshot().assetUploads, stdout.String(), stderr.String()) } } func TestDeliverRequiresCredentialBeforeRemoteRequests(t *testing.T) { repository := newCLIRepository(t) 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", TokenEnv: "RELEASEKIT_TEST_DELIVERY_TOKEN"}}) state.update(func(state *deliveryServerData) { state.commit = cliGitOutput(t, repository, "rev-parse", "HEAD") }) cliGitRun(t, repository, "tag", "v1.2.3") t.Setenv("RELEASEKIT_TEST_DELIVERY_TOKEN", "") var stdout, stderr bytes.Buffer status := run(context.Background(), []string{"deliver", "--repo", repository, "v1.2.3"}, &stdout, &stderr) if status != 2 || state.snapshot().apiRequests != 0 || !strings.Contains(stderr.String(), "RELEASEKIT_TEST_DELIVERY_TOKEN") { t.Fatalf("missing credential status=%d requests=%d stdout=%q stderr=%q", status, state.snapshot().apiRequests, stdout.String(), stderr.String()) } } func TestDeliverRejectsCheckoutThatDiffersFromLocalTag(t *testing.T) { repository := newCLIRepository(t) 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"}}) state.update(func(state *deliveryServerData) { state.commit = cliGitOutput(t, repository, "rev-parse", "HEAD") }) cliGitRun(t, repository, "tag", "v1.2.3") if err := os.WriteFile(filepath.Join(repository, "later.txt"), []byte("later commit"), 0o600); err != nil { t.Fatal(err) } cliGitRun(t, repository, "add", "later.txt") cliGitRun(t, repository, "commit", "-m", "advance checkout") t.Setenv("RELEASEKIT_GITEA_TOKEN", "fixture-token") var stdout, stderr bytes.Buffer status := run(context.Background(), []string{"deliver", "--repo", repository, "v1.2.3"}, &stdout, &stderr) if status != 1 || state.snapshot().apiRequests != 0 || state.snapshot().releaseCreates != 0 { t.Fatalf("mismatched checkout status=%d requests=%d creates=%d stdout=%q stderr=%q", status, state.snapshot().apiRequests, state.snapshot().releaseCreates, stdout.String(), stderr.String()) } if got := cliGitOutput(t, repository, "rev-parse", "refs/tags/v1.2.3"); got != state.snapshot().commit { t.Fatalf("delivery moved tag from %s to %s", state.snapshot().commit, got) } } func TestDeliverRejectsTamperedAssetsAndStaleReceiptBeforeRemoteWrites(t *testing.T) { for _, mode := range []string{"valid receipt", "tampered", "missing artifact", "stale settings"} { t.Run(mode, func(t *testing.T) { repository := newCLIRepository(t) state := &deliveryServerState{} server := newDeliveryServer(t, state) defer server.Close() current := cli.BuildSettings{MainPackage: ".", BinaryName: "helper", Targets: []string{"linux/amd64"}} settings := cli.Settings{Product: "Example Product", Build: ¤t, Delivery: &cli.DeliverySettings{BaseURL: server.URL, Owner: "owner", Repository: "example"}} writeDeliveryProfile(t, repository, settings) state.update(func(state *deliveryServerData) { state.commit = cliGitOutput(t, repository, "rev-parse", "HEAD") }) cliGitRun(t, repository, "tag", "v1.2.3") receiptSettings := current if mode == "stale settings" { receiptSettings.BinaryName = "previous" } assetsDir, artifactPath := writeDeliveryReceipt(t, repository, receiptSettings) if mode == "tampered" { if err := os.WriteFile(artifactPath, []byte("tampered artifact"), 0o600); err != nil { t.Fatal(err) } } if mode == "missing artifact" { if err := os.Remove(artifactPath); err != nil { t.Fatal(err) } } if mode == "tampered" || mode == "missing artifact" { if _, err := cli.LoadBuildReceipt(filepath.Join(assetsDir, cli.BuildReceiptName)); err != nil { t.Fatalf("receipt loading performed artifact verification: %v", err) } } t.Setenv("RELEASEKIT_GITEA_TOKEN", "fixture-token") var stdout, stderr bytes.Buffer status := run(context.Background(), []string{"deliver", "--repo", repository, "--assets", assetsDir, "v1.2.3"}, &stdout, &stderr) if mode == "valid receipt" { if status != 0 || state.snapshot().releaseCreates != 1 || state.snapshot().assetUploads != 2 { t.Fatalf("valid receipt status=%d creates=%d uploads=%d stdout=%q stderr=%q", status, state.snapshot().releaseCreates, state.snapshot().assetUploads, stdout.String(), stderr.String()) } if _, err := os.Stat(artifactPath); err != nil { t.Fatalf("prebuilt artifact was not retained: %v", err) } return } if status != 1 || state.snapshot().apiRequests != 0 || state.snapshot().releaseCreates != 0 || state.snapshot().assetUploads != 0 { t.Fatalf("%s receipt status=%d requests=%d stdout=%q stderr=%q", mode, status, state.snapshot().apiRequests, stdout.String(), stderr.String()) } if mode == "tampered" || mode == "missing artifact" { if strings.Contains(stderr.String(), "load verified build receipt") { t.Fatalf("%s artifact was rejected while loading its receipt: %q", mode, stderr.String()) } } }) } } func writeDeliveryReceipt(t *testing.T, repository string, settings cli.BuildSettings) (string, string) { t.Helper() assetsDir := t.TempDir() filename := settings.BinaryName + "-v1.2.3-linux-amd64" artifactPath := filepath.Join(assetsDir, filename) content := []byte("fixture artifact") if err := os.WriteFile(artifactPath, content, 0o600); err != nil { t.Fatal(err) } resolvedRepository, err := filepath.Abs(repository) if err != nil { t.Fatal(err) } digest := sha256.Sum256(content) target := cli.ReceiptTarget{OS: "linux", Arch: "amd64"} receipt := cli.BuildReceipt{ SchemaVersion: cli.BuildReceiptSchemaVersion, RepositoryDir: resolvedRepository, ModuleDir: resolvedRepository, ModulePath: "example.com/cli-candidate", Tag: "v1.2.3", CandidateCommit: cliGitOutput(t, repository, "rev-parse", "HEAD"), SourceVerified: true, Version: "v1.2.3", MainPackage: settings.MainPackage, BinaryName: settings.BinaryName, OutputDir: assetsDir, Targets: []cli.ReceiptTarget{target}, Compiler: "go version fixture", Artifacts: []cli.ReceiptArtifact{{Path: artifactPath, Filename: filename, Target: target, SHA256: hex.EncodeToString(digest[:])}}, } if _, err := cli.WriteBuildReceipt(assetsDir, receipt); err != nil { t.Fatal(err) } return assetsDir, artifactPath } type deliveryServerState struct { mu sync.Mutex data deliveryServerData } func (state *deliveryServerState) update(change func(*deliveryServerData)) { state.mu.Lock() defer state.mu.Unlock() change(&state.data) } func (state *deliveryServerState) snapshot() deliveryServerData { state.mu.Lock() defer state.mu.Unlock() result := state.data result.assets = make(map[string][]byte, len(state.data.assets)) for key, value := range state.data.assets { result.assets[key] = append([]byte(nil), value...) } return result } type deliveryServerData struct { tag string malformedCreate bool failAssetListing bool failAssetDownloads bool failAssetName string ambiguousAssetName string commit string product string remoteCommit string apiRequests int pageExists bool releaseCreates int assetUploads int assetListReads int noteBody string assets map[string][]byte } func newDeliveryServer(t *testing.T, state *deliveryServerState) *httptest.Server { t.Helper() data := &state.data if data.tag == "" { data.tag = "v1.2.3" } if data.assets == nil { data.assets = make(map[string][]byte) } handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { state.mu.Lock() defer state.mu.Unlock() if strings.HasPrefix(r.URL.Path, "/api/") { data.apiRequests++ if r.Header.Get("Authorization") != "token fixture-token" { t.Errorf("authorization = %q", r.Header.Get("Authorization")) } } switch r.URL.Path { case "/api/v1/repos/owner/example/tags/" + data.tag: if data.commit == "" { t.Error("tag source commit was not initialized") } commit := data.remoteCommit if commit == "" { commit = data.commit } _, _ = fmt.Fprintf(w, `{"name":%q,"commit":{"sha":%q}}`, data.tag, commit) case "/api/v1/repos/owner/example/releases/tags/" + data.tag: if !data.pageExists { http.NotFound(w, r) return } writeDeliveryRelease(w, serverURL(r), data) case "/api/v1/repos/owner/example/releases": if r.Method != http.MethodPost { t.Errorf("release request method = %s", r.Method) http.Error(w, "method", 405) return } var payload struct { Tag string `json:"tag_name"` Target string `json:"target_commitish"` Name string `json:"name"` Body string `json:"body"` } if err := json.NewDecoder(r.Body).Decode(&payload); err != nil { t.Errorf("decode create payload: %v", err) } if payload.Tag != data.tag || payload.Target != data.commit { t.Errorf("release source payload = %#v", payload) } data.pageExists, data.releaseCreates, data.noteBody = true, data.releaseCreates+1, payload.Body if data.malformedCreate { w.WriteHeader(http.StatusCreated) _ = json.NewEncoder(w).Encode(map[string]any{"id": 10, "tag_name": data.tag, "name": "inconsistent response", "body": payload.Body}) return } writeDeliveryRelease(w, serverURL(r), data) case "/api/v1/repos/owner/example/releases/10/assets": if r.Method == http.MethodGet { data.assetListReads++ if data.failAssetListing { http.Error(w, "unavailable", 503) return } list := make([]map[string]any, 0, len(data.assets)) for name, content := range data.assets { list = append(list, map[string]any{"id": len(list) + 1, "name": name, "size": len(content), "browser_download_url": serverURL(r) + "/files/" + name}) } _ = json.NewEncoder(w).Encode(list) return } if r.Method != http.MethodPost { t.Errorf("asset request method = %s", r.Method) http.Error(w, "method", 405) return } name := r.URL.Query().Get("name") file, _, err := r.FormFile("attachment") if err != nil { t.Errorf("read asset upload %q: %v", name, err) http.Error(w, "multipart", 400) return } content, err := io.ReadAll(file) _ = file.Close() if err != nil { t.Errorf("read asset bytes: %v", err) } if _, exists := data.assets[name]; exists { t.Errorf("asset %q uploaded more than once", name) } if name == data.failAssetName { http.Error(w, "upload failed", http.StatusBadRequest) return } if name == data.ambiguousAssetName { http.Error(w, "upload outcome ambiguous", http.StatusInternalServerError) return } data.assets[name] = content data.assetUploads++ _ = json.NewEncoder(w).Encode(map[string]any{"id": data.assetUploads, "name": name, "size": len(content)}) default: if strings.HasPrefix(r.URL.Path, "/files/") { if data.failAssetDownloads { http.Error(w, "unavailable", 503) return } name := strings.TrimPrefix(r.URL.Path, "/files/") content, ok := data.assets[name] if !ok { http.NotFound(w, r) return } _, _ = w.Write(content) return } t.Errorf("unexpected delivery request %s %s", r.Method, r.URL.String()) http.NotFound(w, r) } }) server := httptest.NewServer(handler) return server } // writeDeliveryRelease reads data while the request handler holds state.mu. func writeDeliveryRelease(w http.ResponseWriter, baseURL string, data *deliveryServerData) { w.Header().Set("Content-Type", "application/json") product := data.product if product == "" { product = "Example Product" } _ = json.NewEncoder(w).Encode(map[string]any{"id": 10, "html_url": baseURL + "/releases/10", "tag_name": data.tag, "target_commitish": data.commit, "name": product + " v1.2.3", "body": data.noteBody, "draft": false, "prerelease": false}) } func writeDeliveryProfile(t *testing.T, repository string, settings cli.Settings) { writeDeliveryProfileAt(t, repository, "releasekit.json", settings) } func writeDeliveryProfileAt(t *testing.T, repository, name string, settings cli.Settings) { t.Helper() contents, err := json.Marshal(settings) if err != nil { t.Fatal(err) } path := filepath.Join(repository, filepath.FromSlash(name)) if err := os.MkdirAll(filepath.Dir(path), 0o700); err != nil { t.Fatal(err) } if err := os.WriteFile(path, contents, 0o600); err != nil { t.Fatal(err) } cliGitRun(t, repository, "add", name) cliGitRun(t, repository, "commit", "-m", "configure delivery") } func newDeliveryBuildRepository(t *testing.T) string { t.Helper() repository := filepath.Join(t.TempDir(), "tagged application") if err := os.Mkdir(repository, 0o700); err != nil { t.Fatal(err) } cliGitRun(t, repository, "init", "--initial-branch=main") cliGitRun(t, repository, "config", "user.name", "ReleaseKit Test") cliGitRun(t, repository, "config", "user.email", "releasekit@example.test") files := map[string]string{ "go.mod": "module example.com/tagged-app\n\ngo 1.25.0\n", "main.go": "package main\nimport \"fmt\"\nfunc main() { fmt.Println(\"helper v1.2.3\") }\n", "docs/releases/v1.2.3.md": "# Example Product v1.2.3\n\nRelease note.\n", } for name, contents := range files { path := filepath.Join(repository, filepath.FromSlash(name)) if err := os.MkdirAll(filepath.Dir(path), 0o700); err != nil { t.Fatal(err) } if err := os.WriteFile(path, []byte(contents), 0o600); err != nil { t.Fatal(err) } } cliGitRun(t, repository, "add", "--all") cliGitRun(t, repository, "commit", "-m", "tagged candidate") return repository } func newCLIBuildRepository(t *testing.T, mainSource string) string { t.Helper() repository := newCLIRepository(t) root, err := filepath.Abs(filepath.Join("..", "..")) if err != nil { t.Fatal(err) } module := fmt.Sprintf("module example.com/cli-candidate\n\ngo 1.26.0\n\nrequire gitea.maximumdirect.net/eric/ReleaseKit v0.0.0\n\nreplace gitea.maximumdirect.net/eric/ReleaseKit => %s\n", filepath.ToSlash(root)) write := func(name, contents string) { t.Helper() path := filepath.Join(repository, filepath.FromSlash(name)) if err := os.MkdirAll(filepath.Dir(path), 0o700); err != nil { t.Fatal(err) } if err := os.WriteFile(path, []byte(contents), 0o600); err != nil { t.Fatal(err) } } write("go.mod", module) write("cmd/app/main.go", mainSource) write("releasekit.json", fmt.Sprintf(`{"product":"Example Product","build":{"main_package":"./cmd/app","binary_name":"fixture","targets":[%q]}}`, fmt.Sprintf("%s/%s", runtime.GOOS, runtime.GOARCH))) write(".gitignore", "dist/\n") cliGitRun(t, repository, "add", "--all") cliGitRun(t, repository, "commit", "-m", "configure build") return repository } func cliGitRun(t *testing.T, repository string, args ...string) { t.Helper() commandArgs := append([]string{"git"}, args...) command := exec.Command(commandArgs[0], commandArgs[1:]...) command.Dir = repository output, err := command.CombinedOutput() if err != nil { t.Fatalf("%v: %v\n%s", commandArgs, err, output) } } func writeRepositoryFile(t *testing.T, repository, name, contents string) { t.Helper() path := filepath.Join(repository, filepath.FromSlash(name)) if err := os.MkdirAll(filepath.Dir(path), 0o700); err != nil { t.Fatal(err) } if err := os.WriteFile(path, []byte(contents), 0o600); err != nil { t.Fatal(err) } } func pushTaggedCLIRepository(t *testing.T, repository string) string { t.Helper() cliGitRun(t, repository, "tag", "v1.2.3") commit := cliGitOutput(t, repository, "rev-parse", "v1.2.3^{commit}") remote := filepath.Join(t.TempDir(), "origin.git") cliGitRun(t, repository, "init", "--bare", remote) cliGitRun(t, repository, "remote", "add", "origin", remote) cliGitRun(t, repository, "push", "origin", "refs/tags/v1.2.3") return commit } func cliGitOutput(t *testing.T, repository string, args ...string) string { t.Helper() commandArgs := append([]string{"git"}, args...) command := exec.Command(commandArgs[0], commandArgs[1:]...) command.Dir = repository output, err := command.Output() if err != nil { t.Fatalf("%v: %v", commandArgs, err) } return strings.TrimSpace(string(output)) } func TestDeliverRejectsReceiptBuiltFromDirtySourceAfterCleanup(t *testing.T) { repository := newDeliveryBuildRepository(t) state := &deliveryServerState{} server := newDeliveryServer(t, state) defer server.Close() settings := cli.Settings{Product: "Example Product", Build: &cli.BuildSettings{MainPackage: ".", BinaryName: "helper", Targets: []string{"linux/amd64"}}, Delivery: &cli.DeliverySettings{BaseURL: server.URL, Owner: "owner", Repository: "example"}} writeDeliveryProfile(t, repository, settings) state.update(func(state *deliveryServerData) { state.commit = cliGitOutput(t, repository, "rev-parse", "HEAD") }) cliGitRun(t, repository, "tag", "v1.2.3") mainPath := filepath.Join(repository, "main.go") original, err := os.ReadFile(mainPath) if err != nil { t.Fatal(err) } if err := os.WriteFile(mainPath, append(original, []byte("\n// uncommitted build\n")...), 0600); err != nil { t.Fatal(err) } assets := t.TempDir() var stdout, stderr bytes.Buffer if status := run(context.Background(), []string{"build", "--repo", repository, "--output", assets, "v1.2.3"}, &stdout, &stderr); status != 0 { t.Fatalf("local build status=%d %s", status, stderr.String()) } receipt, err := cli.LoadBuildReceipt(filepath.Join(assets, cli.BuildReceiptName)) if err != nil { t.Fatal(err) } if receipt.SourceVerified { t.Fatal("dirty build receipt claims verified source") } cliGitRun(t, repository, "restore", "main.go") t.Setenv("RELEASEKIT_GITEA_TOKEN", "fixture-token") stdout.Reset() stderr.Reset() status := run(context.Background(), []string{"deliver", "--repo", repository, "--assets", assets, "v1.2.3"}, &stdout, &stderr) if status != 1 || state.snapshot().apiRequests != 0 || !strings.Contains(stderr.String(), "build source") { t.Fatalf("status=%d requests=%d %s", status, state.snapshot().apiRequests, stderr.String()) } } func TestRecoveredPageCreatePreservesPartialDeliveryOnInspectionFailure(t *testing.T) { repository := newCLIRepository(t) state := &deliveryServerState{data: deliveryServerData{malformedCreate: true, failAssetListing: true}} server := newDeliveryServer(t, state) defer server.Close() build := cli.BuildSettings{MainPackage: ".", BinaryName: "helper", Targets: []string{"linux/amd64"}} writeDeliveryProfile(t, repository, cli.Settings{Product: "Example Product", Build: &build, Delivery: &cli.DeliverySettings{BaseURL: server.URL, Owner: "owner", Repository: "example"}}) state.update(func(state *deliveryServerData) { state.commit = cliGitOutput(t, repository, "rev-parse", "HEAD") }) cliGitRun(t, repository, "tag", "v1.2.3") assets, _ := writeDeliveryReceipt(t, repository, build) t.Setenv("RELEASEKIT_GITEA_TOKEN", "fixture-token") var stdout, stderr bytes.Buffer status := run(context.Background(), []string{"deliver", "--repo", repository, "--assets", assets, "v1.2.3"}, &stdout, &stderr) if status != 1 || !strings.Contains(stderr.String(), "state=partial") || state.snapshot().releaseCreates != 1 || state.snapshot().assetListReads != 1 || state.snapshot().assetUploads != 0 { t.Fatalf("status=%d state=%#v %s", status, state.snapshot(), stderr.String()) } } func TestUnreadableAssetsDoNotClaimAnUnknownWrite(t *testing.T) { for _, existingPage := range []bool{false, true} { t.Run(fmt.Sprintf("existing-page-%t", existingPage), func(t *testing.T) { repository := newCLIRepository(t) state := &deliveryServerState{data: deliveryServerData{pageExists: existingPage, noteBody: "# Example Product v1.2.3\n\nRelease note.\n", failAssetDownloads: true, assets: map[string][]byte{"helper-v1.2.3-linux-amd64": []byte("fixture artifact")}}} server := newDeliveryServer(t, state) defer server.Close() build := cli.BuildSettings{MainPackage: ".", BinaryName: "helper", Targets: []string{"linux/amd64"}} writeDeliveryProfile(t, repository, cli.Settings{Product: "Example Product", Build: &build, Delivery: &cli.DeliverySettings{BaseURL: server.URL, Owner: "owner", Repository: "example"}}) state.update(func(state *deliveryServerData) { state.commit = cliGitOutput(t, repository, "rev-parse", "HEAD") }) cliGitRun(t, repository, "tag", "v1.2.3") assets, _ := writeDeliveryReceipt(t, repository, build) t.Setenv("RELEASEKIT_GITEA_TOKEN", "fixture-token") var stdout, stderr bytes.Buffer status := run(context.Background(), []string{"deliver", "--repo", repository, "--assets", assets, "v1.2.3"}, &stdout, &stderr) expectedState := "partial" expectedCreates := 1 if existingPage { expectedState = "not_attempted" expectedCreates = 0 } if status != 1 || !strings.Contains(stderr.String(), "state="+expectedState) || state.snapshot().assetUploads != 0 || state.snapshot().releaseCreates != expectedCreates { t.Fatalf("status=%d creates=%d uploads=%d %s", status, state.snapshot().releaseCreates, state.snapshot().assetUploads, stderr.String()) } if existingPage && !strings.Contains(stderr.String(), "recovery: fix the local or preflight failure") { t.Fatalf("not-attempted recovery guidance missing: %s", stderr.String()) } }) } } func TestFailedRebuildInvalidatesPreviousReceipt(t *testing.T) { const source = `package main import("fmt";"gitea.maximumdirect.net/eric/ReleaseKit/buildinfo") func main(){fmt.Println("fixture",buildinfo.Version())} ` repository := newCLIBuildRepository(t, source) var stdout, stderr bytes.Buffer args := []string{"build", "--repo", repository, "v1.2.3"} for attempt := 0; attempt < 2; attempt++ { if status := run(context.Background(), args, &stdout, &stderr); status != 0 { t.Fatalf("build %d: status=%d %s", attempt, status, stderr.String()) } } path := filepath.Join(repository, "cmd/app/main.go") if err := os.WriteFile(path, []byte("package main\nfunc main(){}\n"), 0600); err != nil { t.Fatal(err) } if status := run(context.Background(), args, &stdout, &stderr); status != 1 { t.Fatalf("failed rebuild status=%d", status) } if _, err := os.Stat(filepath.Join(repository, "dist", cli.BuildReceiptName)); !os.IsNotExist(err) { t.Fatalf("stale receipt remains: %v", err) } } func TestTemporaryDeliveryBuildDoesNotRequireWritableRepositoryParent(t *testing.T) { if runtime.GOOS == "windows" || os.Getuid() == 0 { t.Skip("requires ordinary Unix permission checks") } 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{"linux/amd64"}}, Delivery: &cli.DeliverySettings{BaseURL: server.URL, Owner: "owner", Repository: "example"}}) state.update(func(state *deliveryServerData) { state.commit = cliGitOutput(t, repository, "rev-parse", "HEAD") }) cliGitRun(t, repository, "tag", "v1.2.3") parent := t.TempDir() moved := filepath.Join(parent, "repo") if err := os.Rename(repository, moved); err != nil { t.Fatal(err) } if err := os.Chmod(parent, 0555); err != nil { t.Fatal(err) } t.Cleanup(func() { os.Chmod(parent, 0700) }) t.Setenv("RELEASEKIT_GITEA_TOKEN", "fixture-token") var stdout, stderr bytes.Buffer if status := run(context.Background(), []string{"deliver", "--repo", moved, "v1.2.3"}, &stdout, &stderr); status != 0 { t.Fatalf("status=%d %s", status, stderr.String()) } if status := cliGitOutput(t, moved, "status", "--porcelain"); status != "" { t.Fatalf("source dirtied: %s", status) } }