Run API-only recovery independently of consumer vendoring

This commit is contained in:
2026-11-03 00:05:32 +00:00
parent 16d1c072e3
commit cd08a2892a
6 changed files with 165 additions and 24 deletions

View File

@@ -14,9 +14,106 @@ import (
"testing"
"go.maximumdirect.net/releasekit/cmd/releasekit/internal/cli"
"go.maximumdirect.net/releasekit/internal/gotool"
"go.maximumdirect.net/releasekit/internal/publish"
)
func TestVendoredAPIOnlyPublicationRecovery(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("fixture executes POSIX recovery commands")
}
repo, remote := newSimplePublishRepository(t, false)
source := repositoryRoot(t)
relative, err := filepath.Rel(repo, source)
if err != nil {
t.Fatal(err)
}
writeRepositoryFile(t, repo, "go.mod", "module example.com/cli-candidate\n\ngo 1.26.0\n\nrequire go.maximumdirect.net/releasekit v0.0.0\n\nreplace go.maximumdirect.net/releasekit => "+strconv.Quote(relative)+"\n")
writeRepositoryFile(t, repo, "uses_releasekit.go", "package candidate\nimport _ \"go.maximumdirect.net/releasekit\"\n")
sums, err := os.ReadFile(filepath.Join(source, "go.sum"))
if err != nil {
t.Fatal(err)
}
writeRepositoryFile(t, repo, "go.sum", string(sums))
goBinary := filepath.Join(runtime.GOROOT(), "bin", "go")
for _, args := range [][]string{{"mod", "tidy"}, {"mod", "vendor"}} {
command := exec.Command(goBinary, args...)
command.Dir = repo
if out, err := command.CombinedOutput(); err != nil {
t.Fatalf("prepare vendored API: %v: %s", err, out)
}
}
cliGitRun(t, repo, "add", ".")
cliGitRun(t, repo, "commit", "-m", "Vendor API dependency")
cliGitRun(t, repo, "push", "origin", "main")
before := cliGitOutput(t, repo, "rev-parse", "HEAD^{tree}")
if err := os.WriteFile(filepath.Join(remote, "hooks", "pre-receive"), []byte("#!/bin/sh\nexit 1\n"), 0700); err != nil {
t.Fatal(err)
}
var out, diagnostics bytes.Buffer
if status := run(t.Context(), []string{"publish", "--repo", repo, "v1.2.3"}, &out, &diagnostics); status != 1 {
t.Fatalf("publication status=%d: %s", status, &diagnostics)
}
command := outputLineValue(diagnostics.String(), "recovery command: ")
if command == "" {
t.Fatalf("no handoff: %s", &diagnostics)
}
recovery := exec.Command("sh", "-c", command)
recovery.Dir = t.TempDir()
recovered, err := recovery.CombinedOutput()
var exitError *exec.ExitError
if !errors.As(err, &exitError) || exitError.ExitCode() != 1 || !strings.Contains(string(recovered), "module=example.com/cli-candidate tag=v1.2.3 state=not_published") {
t.Fatalf("vendored recovery: %v: %s", err, recovered)
}
if status := cliGitOutput(t, repo, "status", "--porcelain"); status != "" {
t.Fatalf("handoff changed consumer files: %s", status)
}
if after := cliGitOutput(t, repo, "write-tree"); after != before {
t.Fatalf("handoff changed consumer index: %s != %s", after, before)
}
}
func TestRecoveryToolPreservesVersionedReplacement(t *testing.T) {
proxy := writeReleaseKitModuleProxy(t)
environment := make(map[string]string)
useSyntheticModuleProxy(t, environment, "file://"+filepath.ToSlash(proxy))
for key, value := range environment {
t.Setenv(key, value)
}
module := t.TempDir()
data := "module example.com/recovery-consumer\n\ngo 1.26.0\n\nrequire go.maximumdirect.net/releasekit v0.2.197\n\nreplace go.maximumdirect.net/releasekit v0.2.197 => go.maximumdirect.net/releasekit v0.2.198\n"
if err := os.WriteFile(filepath.Join(module, "go.mod"), []byte(data), 0600); err != nil {
t.Fatal(err)
}
args, err := gotool.RecoveryTool(t.Context(), module, t.TempDir())
if err != nil {
t.Fatal(err)
}
command := exec.Command(filepath.Join(runtime.GOROOT(), "bin", "go"), args...)
command.Dir = t.TempDir()
output, err := command.CombinedOutput()
if err != nil || !strings.Contains(string(output), "tool-version=v0.2.198 helper=tool-only dependency") {
t.Fatalf("versioned recovery selection: %v: %s", err, output)
}
after, err := os.ReadFile(filepath.Join(module, "go.mod"))
if err != nil || string(after) != data {
t.Fatalf("consumer metadata changed: %q, %v", after, err)
}
}
func TestRecoveryToolSupportsReleaseKitSourceModule(t *testing.T) {
args, err := gotool.RecoveryTool(t.Context(), repositoryRoot(t), t.TempDir())
if err != nil {
t.Fatal(err)
}
command := exec.Command(filepath.Join(runtime.GOROOT(), "bin", "go"), append(args, "version")...)
command.Dir = t.TempDir()
output, err := command.CombinedOutput()
if err != nil || !strings.HasPrefix(string(output), "releasekit ") {
t.Fatalf("source module recovery entry point: %v: %s", err, output)
}
}
func TestColdGoToolRecoverySurvivesLauncherExit(t *testing.T) {
if runtime.GOOS != "linux" {
t.Skip("fixture observes the cold executable through /proc")