package scripts import ( "os" "os/exec" "path/filepath" "strings" "testing" ) // Real local Git repositories exercise publication; Go is stubbed at the slow // build/test boundary so these tests remain offline and cannot recurse. func TestReleaseWorkflow(t *testing.T) { for _, scenario := range []string{"check", "publish", "dirty", "tests-fail", "build-fail", "existing-remote-tag", "remote-ahead", "rejected-push", "source-changed", "missing-note", "invalid-version", "existing-local-tag", "wrong-branch", "head-changed"} { t.Run(scenario, func(t *testing.T) { root := t.TempDir() repo, remote := filepath.Join(root, "repo"), filepath.Join(root, "remote.git") if err := os.MkdirAll(repo, 0755); err != nil { t.Fatal(err) } env := append(os.Environ(), "GIT_CONFIG_NOSYSTEM=1", "GIT_CONFIG_GLOBAL="+os.DevNull, "GIT_AUTHOR_NAME=Release Test", "GIT_AUTHOR_EMAIL=test@example.invalid", "GIT_COMMITTER_NAME=Release Test", "GIT_COMMITTER_EMAIL=test@example.invalid", "GIT_TERMINAL_PROMPT=0") run := func(dir string, args ...string) string { t.Helper() c := exec.Command("git", args...) c.Dir = dir c.Env = env b, e := c.CombinedOutput() if e != nil { t.Fatalf("git %v: %v\n%s", args, e, b) } return strings.TrimSpace(string(b)) } write := func(path, data string, mode os.FileMode) { t.Helper() if e := os.MkdirAll(filepath.Dir(path), 0755); e != nil { t.Fatal(e) } if e := os.WriteFile(path, []byte(data), mode); e != nil { t.Fatal(e) } } run(root, "init", "--bare", "--initial-branch=main", remote) run(repo, "init", "--initial-branch=main") for _, name := range []string{"release.sh", "build-release.sh"} { b, e := os.ReadFile(name) if e != nil { t.Fatal(e) } write(filepath.Join(repo, "scripts", name), string(b), 0755) } write(filepath.Join(repo, "docs/releases/v1.2.3.md"), "# Audita v1.2.3\n\nA test release.\n", 0644) run(repo, "add", ".") run(repo, "commit", "-m", "Initial fixture") run(repo, "remote", "add", "origin", remote) run(repo, "push", "origin", "main") write(filepath.Join(repo, "change.txt"), "candidate\n", 0644) run(repo, "add", ".") run(repo, "commit", "-m", "Candidate fixture") candidate := run(repo, "rev-parse", "HEAD") log := filepath.Join(root, "go.log") stub := "#!/bin/sh\necho \"$* GOOS=$GOOS GOARCH=$GOARCH CGO_ENABLED=$CGO_ENABLED GOWORK=$GOWORK\" >> \"$GO_LOG\"\n" switch scenario { case "existing-local-tag": run(repo, "tag", "v1.2.3") case "wrong-branch": run(repo, "checkout", "-b", "feature") case "head-changed": stub += "if [ \"$1\" = test ]; then echo changed > change.txt; git add change.txt; git commit -m Changed; fi\n" case "dirty": write(filepath.Join(repo, "untracked"), "dirty", 0644) case "tests-fail": stub += "[ \"$1\" != test ] || exit 1\n" case "build-fail": stub += "[ \"$GOARCH\" != arm64 ] || exit 1\n" case "existing-remote-tag": run(remote, "tag", "v1.2.3", "main") case "remote-ahead": other := filepath.Join(root, "other") run(root, "clone", remote, other) write(filepath.Join(other, "other.txt"), "new remote commit", 0644) run(other, "add", ".") run(other, "commit", "-m", "Remote fixture") run(other, "push", "origin", "main") case "rejected-push": write(filepath.Join(remote, "hooks/pre-receive"), "#!/bin/sh\nexit 1\n", 0755) case "source-changed": stub += "[ \"$1\" != test ] || echo changed > change.txt\n" case "missing-note": run(repo, "rm", "docs/releases/v1.2.3.md") run(repo, "commit", "-m", "Remove fixture note") } stub += "exit 0\n" write(filepath.Join(root, "bin/go"), stub, 0755) mode, version := "--publish", "v1.2.3" if scenario == "check" { mode = "--check" run(repo, "remote", "remove", "origin") } if scenario == "invalid-version" { version = "v01.2.3" } c := exec.Command("bash", "scripts/release.sh", mode, version) c.Dir = repo c.Env = append(env, "PATH="+filepath.Join(root, "bin")+":"+os.Getenv("PATH"), "GO_LOG="+log) b, e := c.CombinedOutput() success := scenario == "check" || scenario == "publish" if (e == nil) != success { t.Fatalf("success=%v: %v\n%s", success, e, b) } tags := run(remote, "tag", "--list") if scenario == "publish" { if run(remote, "rev-parse", "refs/tags/v1.2.3") != candidate || run(remote, "rev-parse", "main") != candidate { t.Fatal("wrong source published") } if !strings.Contains(string(b), "CI is not awaited") { t.Fatal("missing asynchronous completion message") } } else if scenario != "existing-remote-tag" && tags != "" { t.Fatalf("unexpected remote tags: %s", tags) } if scenario == "rejected-push" && run(remote, "rev-parse", "main") == candidate { t.Fatal("rejected push published candidate") } if scenario == "check" && run(repo, "tag", "--list") != "" { t.Fatal("check created a tag") } if success { calls, e := os.ReadFile(log) if e != nil { t.Fatal(e) } lines := strings.Split(strings.TrimSpace(string(calls)), "\n") if len(lines) != 3 { t.Fatalf("expected one test and two builds: %s", calls) } if !strings.HasPrefix(lines[0], "test -mod=readonly ./...") || !strings.Contains(lines[1], "GOOS=linux GOARCH=amd64 CGO_ENABLED=0 GOWORK=off") || !strings.Contains(lines[2], "GOOS=linux GOARCH=arm64 CGO_ENABLED=0 GOWORK=off") { t.Fatalf("unexpected validation: %s", calls) } } }) } }