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
|
## 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
|
**Relevant code:** `internal/publish/publish.go:110`, especially the assignment
|
||||||
at line 120 and the error check at line 127.
|
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,
|
AdditionalChecks: request.AdditionalChecks,
|
||||||
Output: request.Output,
|
Output: request.Output,
|
||||||
}
|
}
|
||||||
captured, err := candidate.Capture(ctx, checks)
|
captured, captureErr := candidate.Capture(ctx, checks)
|
||||||
result.RepositoryDir = captured.RepositoryDir
|
result.RepositoryDir = captured.RepositoryDir
|
||||||
result.ModuleDir = captured.ModuleDir
|
result.ModuleDir = captured.ModuleDir
|
||||||
result.ModulePath = captured.ModulePath
|
result.ModulePath = captured.ModulePath
|
||||||
@@ -117,15 +117,16 @@ func Publish(ctx context.Context, request Request) (result Result, resultErr err
|
|||||||
if captured.RepositoryDir != "" {
|
if captured.RepositoryDir != "" {
|
||||||
initialized = true
|
initialized = true
|
||||||
result.Evidence.RecordedAt = time.Now().UTC().Format(time.RFC3339Nano)
|
result.Evidence.RecordedAt = time.Now().UTC().Format(time.RFC3339Nano)
|
||||||
recordDir, repositoryRoot, err = resolveRecordDir(captured.RepositoryDir, request.RecordDir)
|
var storageErr error
|
||||||
if err != nil {
|
recordDir, repositoryRoot, storageErr = resolveRecordDir(captured.RepositoryDir, request.RecordDir)
|
||||||
result.RecordError = &RecoveryError{Kind: "record_write", cause: err}
|
if storageErr != nil {
|
||||||
|
result.RecordError = &RecoveryError{Kind: "record_write", cause: storageErr}
|
||||||
recordDir = ""
|
recordDir = ""
|
||||||
recorded = true
|
recorded = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if err != nil {
|
if captureErr != nil {
|
||||||
return result, fmt.Errorf("capture release candidate: %w", err)
|
return result, fmt.Errorf("capture release candidate: %w", captureErr)
|
||||||
}
|
}
|
||||||
|
|
||||||
endpoint, err := git.Resolve(ctx, captured.RepositoryDir, remoteName)
|
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"
|
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) {
|
func TestPublishPreservesHookDirectoryEnvironment(t *testing.T) {
|
||||||
if runtime.GOOS == "windows" {
|
if runtime.GOOS == "windows" {
|
||||||
t.Skip("directory-derived PWD is a POSIX behavior")
|
t.Skip("directory-derived PWD is a POSIX behavior")
|
||||||
|
|||||||
Reference in New Issue
Block a user