Preserve candidate failures when initializing publication records
This commit is contained in:
@@ -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.
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user