Report JSON output failures from recovery commands
This commit is contained in:
@@ -102,14 +102,17 @@ func runRecovery(ctx context.Context, invocation cli.Invocation, stdout, stderr
|
|||||||
for _, item := range listed.Diagnostics {
|
for _, item := range listed.Diagnostics {
|
||||||
diagnostics = append(diagnostics, diagnostic(item))
|
diagnostics = append(diagnostics, diagnostic(item))
|
||||||
}
|
}
|
||||||
writeJSON(stdout, attemptsDocument{
|
if writeErr := writeJSON(stdout, attemptsDocument{
|
||||||
SchemaVersion: 1,
|
SchemaVersion: 1,
|
||||||
Command: "publication-attempts",
|
Command: "publication-attempts",
|
||||||
RecordDir: git.DisplayDiagnostic(listed.RecordDir),
|
RecordDir: git.DisplayDiagnostic(listed.RecordDir),
|
||||||
Attempts: attempts,
|
Attempts: attempts,
|
||||||
Diagnostics: diagnostics,
|
Diagnostics: diagnostics,
|
||||||
Diagnostic: diagnostic(err),
|
Diagnostic: diagnostic(err),
|
||||||
})
|
}); writeErr != nil {
|
||||||
|
fmt.Fprintf(stderr, "write recovery output: %v\n", git.DisplayError(writeErr))
|
||||||
|
return 1
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
fmt.Fprintf(stdout, "publication records in %s\n", git.DisplayDiagnostic(listed.RecordDir))
|
fmt.Fprintf(stdout, "publication records in %s\n", git.DisplayDiagnostic(listed.RecordDir))
|
||||||
for _, item := range listed.Attempts {
|
for _, item := range listed.Attempts {
|
||||||
@@ -149,7 +152,10 @@ func runReconcile(ctx context.Context, invocation cli.Invocation, stdout, stderr
|
|||||||
dto.ObservedCommit = result.ObservedCommit
|
dto.ObservedCommit = result.ObservedCommit
|
||||||
}
|
}
|
||||||
dto.Diagnostic = diagnostic(err)
|
dto.Diagnostic = diagnostic(err)
|
||||||
writeJSON(stdout, dto)
|
if writeErr := writeJSON(stdout, dto); writeErr != nil {
|
||||||
|
fmt.Fprintf(stderr, "write recovery output: %v\n", git.DisplayError(writeErr))
|
||||||
|
return 1
|
||||||
|
}
|
||||||
} else if err == nil {
|
} else if err == nil {
|
||||||
fmt.Fprintf(stdout, "reconciled %s module=%s tag=%s state=%s\n", git.DisplayDiagnostic(invocation.RecordPath), git.DisplayDiagnostic(evidence.ModulePath), git.DisplayDiagnostic(evidence.Tag), result.State)
|
fmt.Fprintf(stdout, "reconciled %s module=%s tag=%s state=%s\n", git.DisplayDiagnostic(invocation.RecordPath), git.DisplayDiagnostic(evidence.ModulePath), git.DisplayDiagnostic(evidence.Tag), result.State)
|
||||||
if result.State == releasekit.PublicationConflict {
|
if result.State == releasekit.PublicationConflict {
|
||||||
@@ -243,7 +249,7 @@ func humanDiagnostic(err error) string {
|
|||||||
return git.DisplayError(err).Error()
|
return git.DisplayError(err).Error()
|
||||||
}
|
}
|
||||||
|
|
||||||
func writeJSON(writer io.Writer, value any) {
|
func writeJSON(writer io.Writer, value any) error {
|
||||||
encoder := json.NewEncoder(writer)
|
encoder := json.NewEncoder(writer)
|
||||||
_ = encoder.Encode(value)
|
return encoder.Encode(value)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
@@ -14,6 +15,41 @@ import (
|
|||||||
"go.maximumdirect.net/releasekit/internal/publish"
|
"go.maximumdirect.net/releasekit/internal/publish"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type failingRecoveryWriter struct{ err error }
|
||||||
|
|
||||||
|
func (writer failingRecoveryWriter) Write([]byte) (int, error) { return 0, writer.err }
|
||||||
|
|
||||||
|
func TestRecoveryCommandsReportJSONWriteFailure(t *testing.T) {
|
||||||
|
repo := newCLIRepository(t)
|
||||||
|
cliGitRun(t, repo, "tag", "-a", "v1.2.3", "-m", "Release")
|
||||||
|
store := t.TempDir()
|
||||||
|
path, err := publish.SaveEvidence(t.Context(), repo, store, publish.Evidence{
|
||||||
|
RepositoryDir: repo, Endpoint: repo, Tag: "v1.2.3",
|
||||||
|
CandidateCommit: cliGitOutput(t, repo, "rev-parse", "HEAD"),
|
||||||
|
TagObject: cliGitOutput(t, repo, "rev-parse", "v1.2.3"),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
failure := errors.New("output unavailable: https://user:audit-secret@example.invalid/output")
|
||||||
|
for _, args := range [][]string{{"reconcile", "--record", path, "--json"}, {"publication-attempts", "--record-dir", store, "--json"}} {
|
||||||
|
var normal, stderr bytes.Buffer
|
||||||
|
if status := run(t.Context(), args, &normal, &stderr); status != 0 {
|
||||||
|
t.Fatalf("normal output status=%d: %s", status, &stderr)
|
||||||
|
}
|
||||||
|
stderr.Reset()
|
||||||
|
if status := run(t.Context(), args, failingRecoveryWriter{failure}, &stderr); status != 1 {
|
||||||
|
t.Fatalf("failed output status=%d: %s", status, &stderr)
|
||||||
|
}
|
||||||
|
if !strings.Contains(stderr.String(), "output unavailable") || strings.Contains(stderr.String(), "audit-secret") {
|
||||||
|
t.Fatalf("output diagnostic: %s", &stderr)
|
||||||
|
}
|
||||||
|
if args[0] == "reconcile" && !strings.Contains(normal.String(), `"state":"published"`) {
|
||||||
|
t.Fatalf("observation changed: %s", &normal)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestRecoveryDisplaysRedactCredentialPathsAndMetadata(t *testing.T) {
|
func TestRecoveryDisplaysRedactCredentialPathsAndMetadata(t *testing.T) {
|
||||||
secret := "audit-secret"
|
secret := "audit-secret"
|
||||||
raw := "https://audit-user:" + secret + "@example.invalid/attempt.json"
|
raw := "https://audit-user:" + secret + "@example.invalid/attempt.json"
|
||||||
|
|||||||
@@ -188,6 +188,11 @@ using current supported transport settings for its recorded remote name.
|
|||||||
| Discovery completes without record diagnostics, including an existing empty store | 0 |
|
| Discovery completes without record diagnostics, including an existing empty store | 0 |
|
||||||
| Discovery is incomplete or fails, including cancellation | 1 |
|
| Discovery is incomplete or fails, including cancellation | 1 |
|
||||||
| Discovery argument or usage error | 2 |
|
| Discovery argument or usage error | 2 |
|
||||||
|
| Either command cannot write its JSON output | 1 |
|
||||||
|
|
||||||
|
A JSON write failure reports a local output diagnostic on standard error. This
|
||||||
|
status describes reporting failure and leaves the observed publication state
|
||||||
|
unchanged.
|
||||||
|
|
||||||
The CLI JSON schema and safe evidence fields are documented in the
|
The CLI JSON schema and safe evidence fields are documented in the
|
||||||
[publication record format](integrations/publication-records.md). Recovery
|
[publication record format](integrations/publication-records.md). Recovery
|
||||||
|
|||||||
@@ -210,6 +210,11 @@ endpoint-only case. No new credential-storage policy is needed.
|
|||||||
|
|
||||||
## R06: Propagate structured-output write failures — P2
|
## R06: Propagate structured-output write failures — P2
|
||||||
|
|
||||||
|
**Completed:** Both recovery commands now report JSON encoder failures on
|
||||||
|
standard error and return status 1. Failing-writer regressions passed alongside
|
||||||
|
ordinary recovery status tests; observed publication state remains unchanged.
|
||||||
|
The CLI reference records the reporting-failure status.
|
||||||
|
|
||||||
**Relevant code:** `cmd/releasekit/recovery.go:242` and its two `writeJSON`
|
**Relevant code:** `cmd/releasekit/recovery.go:242` and its two `writeJSON`
|
||||||
call sites.
|
call sites.
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user