diff --git a/cmd/releasekit/delivery_status_test.go b/cmd/releasekit/delivery_status_test.go new file mode 100644 index 0000000..6e1eaf9 --- /dev/null +++ b/cmd/releasekit/delivery_status_test.go @@ -0,0 +1,71 @@ +package main + +import ( + "bytes" + "context" + "encoding/json" + "net/http" + "net/http/httptest" + "strings" + "testing" + + "go.maximumdirect.net/releasekit/cmd/releasekit/internal/cli" +) + +func TestDeliveryFailureStatusesRespectPhase(t *testing.T) { + for _, test := range []struct { + name, rejectedPath, state string + cancelWrite bool + status int + }{ + {name: "preflight authorization", rejectedPath: "/api/v1/repos/owner/example", state: "not_attempted", status: 2}, + {name: "later authorization", rejectedPath: "/api/v1/repos/owner/example/tags/v1.2.3", state: "not_attempted", status: 1}, + {name: "canceled unresolved write", cancelWrite: true, state: "unknown", status: 3}, + } { + t.Run(test.name, func(t *testing.T) { + repository := newCLIRepository(t) + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + var commit string + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path == test.rejectedPath { + http.Error(w, "rejected credential", http.StatusUnauthorized) + return + } + switch r.URL.Path { + case "/api/v1/repos/owner/example", "/api/v1/repositories/1": + _ = json.NewEncoder(w).Encode(map[string]any{"id": 1, "name": "example", "full_name": "owner/example", "owner": map[string]string{"login": "owner"}}) + case "/api/v1/repos/owner/example/tags/v1.2.3": + _ = json.NewEncoder(w).Encode(map[string]any{"commit": map[string]string{"sha": commit}}) + case "/api/v1/repos/owner/example/releases/tags/v1.2.3": + http.NotFound(w, r) + case "/api/v1/repos/owner/example/releases": + if r.Method != http.MethodPost || !test.cancelWrite { + t.Errorf("unexpected release write: %s", r.Method) + } + cancel() + http.Error(w, "unacknowledged write", http.StatusServiceUnavailable) + default: + t.Errorf("unexpected request: %s", r.URL.Path) + http.NotFound(w, r) + } + })) + defer server.Close() + writeDeliveryProfile(t, repository, cli.Settings{ + Product: "Example Product", + Delivery: &cli.DeliverySettings{Forge: "gitea", BaseURL: server.URL, Owner: "owner", Repository: "example"}, + }) + commit = cliGitOutput(t, repository, "rev-parse", "HEAD") + cliGitRun(t, repository, "tag", "v1.2.3") + t.Setenv("RELEASEKIT_GITEA_TOKEN", "fixture-token") + var stdout, stderr bytes.Buffer + status := run(ctx, []string{"deliver", "--repo", repository, "v1.2.3"}, &stdout, &stderr) + if status != test.status || !strings.Contains(stderr.String(), "state="+test.state) { + t.Fatalf("status=%d stderr=%q, want status %d and state %s", status, stderr.String(), test.status, test.state) + } + if test.cancelWrite && !strings.Contains(stderr.String(), context.Canceled.Error()) { + t.Fatalf("cancellation cause missing: %s", stderr.String()) + } + }) + } +} diff --git a/docs/cli.md b/docs/cli.md index b42c6e4..f86af72 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -208,10 +208,14 @@ first and then run `deliver --assets DIR` with the token available. The ordinary `deliver` command does not download application dependencies or change the checkout. -The command returns 0 for complete delivery, 1 for an unavailable preflight, -cancellation, known delivery failure, conflict, or partial delivery, 2 for -usage, profile, forge-selection, credential, known permission deficiency, or -inaccessible-destination errors, and 3 when a remote write remains uncertain. +The command returns 0 for complete delivery, 1 for an unavailable preflight or +a known delivery failure, conflict, or partial delivery, and 2 for usage, +profile, forge-selection, local credential-loading, or classified preflight +configuration/access errors. A remote write whose outcome remains uncertain +returns 3, including when cancellation prevents reconciliation. Cancellation +without an unresolved write returns 1, including during preflight or +preparation. Later authorization failures follow +the delivery outcome: known failure returns 1; an unresolved write returns 3. The result lists matching, uploaded, pending, conflicting, and uncertain assets as disjoint current observations; early failures may leave some declared assets unreported. On failure, the CLI @@ -258,11 +262,12 @@ tag arguments. The event ID may identify an annotated tag object; see the [delivery guide](consumers/delivery.md#woodpecker-tag-workflows) for resolution and workflow preparation. The CLI does not read provider-specific environment variable names. Checkout and dependency -preparation failures, unavailable preflight, and cancellation return status 1; -usage, profile, forge-selection, credential, known permission deficiency, -inaccessible-destination, and invalid-version errors return status 2. Final -delivery uses the same statuses as `deliver`: 0 for complete, 1 for known -failure, conflict, or partial delivery, and 3 for uncertain remote state. +preparation failures, unavailable preflight, and cancellation during those +steps return status 1. Usage, profile, forge-selection, local credential-loading, +classified preflight configuration/access, and invalid-version errors return +status 2. Final delivery uses the same statuses as `deliver`: 0 for complete, +1 for known failure, conflict, or partial delivery, and 3 for uncertain remote +state, including when cancellation prevents reconciliation. The maintained module-based tag workflow verifies and checks out the event tag before resolving the consumer module. It builds the ReleaseKit version already diff --git a/docs/releases/v0.6.0.md b/docs/releases/v0.6.0.md index e2efd4e..7ae122e 100644 --- a/docs/releases/v0.6.0.md +++ b/docs/releases/v0.6.0.md @@ -26,8 +26,11 @@ selection and introduces authenticated, read-only credential preflight. establish release-write or asset-write permission. - The CLI keeps `RELEASEKIT_GITEA_TOKEN` as its default token environment setting and preserves configured environment/file precedence. Selection, - credential, and inaccessible-destination errors return status 2; unavailable - verification and cancellation return status 1. + local credential-loading, and classified preflight configuration/access + errors return status 2; unavailable preflight and cancellation during + preflight/preparation return status 1. Later authorization failures follow + delivery outcomes. An unresolved write returns status 3, including when + cancellation prevents reconciliation; known delivery failures return 1. - Binary `ci-deliver` checks credentials before application dependency downloads and checks again during delivery. The automatic check now precedes full local artifact size and digest verification; those checks still finish diff --git a/docs/roadmap/remediation.md b/docs/roadmap/remediation.md index 87a468c..09008c9 100644 --- a/docs/roadmap/remediation.md +++ b/docs/roadmap/remediation.md @@ -121,7 +121,13 @@ require a new public package restriction or a broader dependency policy. ## 3. Qualify phase-specific CLI exit-status documentation — P3 -Status: open. +Status: complete. + +**Completed:** Qualified the CLI reference and unreleased v0.6.0 note by phase: +local loading/classified preflight errors use status 2; later known failures +use 1; unresolved writes retain 3 even when canceled. Added a narrow CLI +regression for preflight versus later authorization failure and cancellation +after an attempted page write. Focused CLI preflight/recovery tests passed. **Evidence:** [CLI exit statuses](../cli.md), [v0.6.0 release note](../releases/v0.6.0.md),