Preserve candidate failures when initializing publication records

This commit is contained in:
2026-11-02 22:17:29 +00:00
parent da1839c6d2
commit 0d19a5546f
3 changed files with 72 additions and 6 deletions

View File

@@ -22,6 +22,10 @@ cover these failure combinations.
## R01: Preserve candidate failures and keep storage failures separate — P1
**Completed:** Capture and storage errors now remain separate. Focused publication
regressions passed for invalid/missing notes, retained Git causes, saved partial
evidence, and successful publication after storage lookup failure.
**Relevant code:** `internal/publish/publish.go:110`, especially the assignment
at line 120 and the error check at line 127.

View File

@@ -107,7 +107,7 @@ func Publish(ctx context.Context, request Request) (result Result, resultErr err
AdditionalChecks: request.AdditionalChecks,
Output: request.Output,
}
captured, err := candidate.Capture(ctx, checks)
captured, captureErr := candidate.Capture(ctx, checks)
result.RepositoryDir = captured.RepositoryDir
result.ModuleDir = captured.ModuleDir
result.ModulePath = captured.ModulePath
@@ -117,15 +117,16 @@ func Publish(ctx context.Context, request Request) (result Result, resultErr err
if captured.RepositoryDir != "" {
initialized = true
result.Evidence.RecordedAt = time.Now().UTC().Format(time.RFC3339Nano)
recordDir, repositoryRoot, err = resolveRecordDir(captured.RepositoryDir, request.RecordDir)
if err != nil {
result.RecordError = &RecoveryError{Kind: "record_write", cause: err}
var storageErr error
recordDir, repositoryRoot, storageErr = resolveRecordDir(captured.RepositoryDir, request.RecordDir)
if storageErr != nil {
result.RecordError = &RecoveryError{Kind: "record_write", cause: storageErr}
recordDir = ""
recorded = true
}
}
if err != nil {
return result, fmt.Errorf("capture release candidate: %w", err)
if captureErr != nil {
return result, fmt.Errorf("capture release candidate: %w", captureErr)
}
endpoint, err := git.Resolve(ctx, captured.RepositoryDir, remoteName)

View File

@@ -19,6 +19,67 @@ import (
const releaseNote = "# Sample App v1.2.3\n\nA source release.\n\nUpgrade notes stay intact.\n"
func TestPublishStopsOnReleaseNoteCaptureFailure(t *testing.T) {
for _, missing := range []bool{false, true} {
t.Run(fmt.Sprintf("missing=%t", missing), func(t *testing.T) {
repo := newPublishFixture(t)
if missing {
gitRun(t, repo.work, "rm", "docs/releases/v1.2.3.md")
} else {
writeFile(t, repo.work, "docs/releases/v1.2.3.md", "# Wrong Product v1.2.3\n")
gitRun(t, repo.work, "add", ".")
}
gitRun(t, repo.work, "commit", "-m", "Invalid note")
called := false
result, err := releasekit.Publish(t.Context(), releasekit.ReleaseRequest{
RepositoryDir: repo.work, Product: "Sample App", Version: "v1.2.3",
AdditionalChecks: func(context.Context, releasekit.Candidate) error { called = true; return nil },
})
if err == nil || called || result.State != releasekit.PublicationNotAttempted {
t.Fatalf("rejected candidate = %#v, %v; callback=%t", result, err, called)
}
if missing {
var exitErr *exec.ExitError
if !errors.As(err, &exitErr) {
t.Fatalf("missing note lost Git cause: %v", err)
}
}
if result.RecordError != nil || result.RecordPath == "" || result.Evidence.CandidateCommit == "" {
t.Fatalf("partial evidence was not saved: %#v", result)
}
if tags := gitText(t, repo.work, "tag", "--list", "v1.2.3"); tags != "" {
t.Fatalf("created tag for rejected candidate: %s", tags)
}
if tags := gitText(t, repo.work, "ls-remote", "origin", "refs/tags/v1.2.3"); tags != "" {
t.Fatalf("pushed tag for rejected candidate: %s", tags)
}
})
}
}
func TestPublishStorageResolutionFailureIsNonfatal(t *testing.T) {
repo := newPublishFixture(t)
realGit := mustLookPath(t, "git")
bin := t.TempDir()
wrapper := "#!/bin/sh\nfor arg in \"$@\"; do\n [ \"$arg\" = --git-common-dir ] && exit 71\ndone\nexec " + shellQuote(realGit) + " \"$@\"\n"
if err := os.WriteFile(filepath.Join(bin, "git"), []byte(wrapper), 0o755); err != nil {
t.Fatal(err)
}
t.Setenv("PATH", bin+string(os.PathListSeparator)+os.Getenv("PATH"))
called := false
result, err := releasekit.Publish(t.Context(), releasekit.ReleaseRequest{
RepositoryDir: repo.work, Product: "Sample App", Version: "v1.2.3",
AdditionalChecks: func(context.Context, releasekit.Candidate) error { called = true; return nil },
})
var exitErr *exec.ExitError
if err != nil || !called || result.State != releasekit.PublicationPublished || result.RecordPath != "" || !errors.As(result.RecordError, &exitErr) || exitErr.ExitCode() != 71 {
t.Fatalf("publication with failed storage lookup = %#v, %v; callback=%t", result, err, called)
}
if got := gitText(t, repo.work, "ls-remote", "origin", "refs/tags/v1.2.3"); !strings.Contains(got, result.TagObject) {
t.Fatalf("published object missing: %s", got)
}
}
func TestPublishPreservesHookDirectoryEnvironment(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("directory-derived PWD is a POSIX behavior")