Make delivery asset outcomes disjoint
This commit is contained in:
@@ -230,6 +230,7 @@ func runDelivery(ctx context.Context, invocation cli.Invocation, stdout, stderr
|
||||
if err != nil {
|
||||
fmt.Fprintf(stderr, "delivery state=%s version=%s commit=%s url=%s: %v\n", result.State, result.Version, result.CandidateCommit, result.ReleaseURL, err)
|
||||
writeDeliveryAssets(stderr, result)
|
||||
writeDeliveryRecovery(stderr, result)
|
||||
if errors.Is(err, releasekit.ErrInvalidVersion) {
|
||||
return 2
|
||||
}
|
||||
@@ -257,6 +258,24 @@ func writeDeliveryAssets(output io.Writer, result releasekit.DeliveryResult) {
|
||||
}
|
||||
}
|
||||
|
||||
func writeDeliveryRecovery(output io.Writer, result releasekit.DeliveryResult) {
|
||||
switch result.State {
|
||||
case releasekit.DeliveryNotAttempted:
|
||||
fmt.Fprintln(output, "recovery: fix the local or preflight failure before retrying; no remote write was attempted")
|
||||
case releasekit.DeliveryPartial:
|
||||
fmt.Fprintln(output, "recovery: fix the reported cause, then retry this version")
|
||||
case releasekit.DeliveryConflict:
|
||||
fmt.Fprintln(output, "recovery: investigate conflicting hosted content; use a new version if the source or published contents need correction")
|
||||
case releasekit.DeliveryUnknown:
|
||||
fmt.Fprintln(output, "recovery: resolve the remote state before retrying")
|
||||
default:
|
||||
return
|
||||
}
|
||||
if len(result.Matching)+len(result.Uploaded)+len(result.Conflicting)+len(result.Uncertain) != 0 {
|
||||
fmt.Fprintln(output, "if rebuilding could change asset bytes already on the server, retain the original build artifacts and retry with deliver --assets DIR")
|
||||
}
|
||||
}
|
||||
|
||||
func buildResultFromReceipt(receipt cli.BuildReceipt) *releasekit.BuildResult {
|
||||
result := &releasekit.BuildResult{
|
||||
RepositoryDir: receipt.RepositoryDir, CandidateCommit: receipt.CandidateCommit, SourceVerified: receipt.SourceVerified,
|
||||
|
||||
@@ -317,6 +317,82 @@ func TestDeliverBuildsTaggedArtifactsAndReusesAssets(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeliverFailureReportsRecoveryForPartialConflictAndUnknownStates(t *testing.T) {
|
||||
for _, test := range []struct {
|
||||
name string
|
||||
state string
|
||||
status int
|
||||
recovery []string
|
||||
assetObservation string
|
||||
missingObservation string
|
||||
}{
|
||||
{
|
||||
name: "partial",
|
||||
status: 1,
|
||||
recovery: []string{"recovery: fix the reported cause", "retry this version"},
|
||||
assetObservation: "pending helper-v1.2.3-linux-amd64",
|
||||
},
|
||||
{
|
||||
name: "conflict",
|
||||
status: 1,
|
||||
recovery: []string{"investigate conflicting hosted content", "new version", "retain the original build artifacts"},
|
||||
assetObservation: "conflicting helper-v1.2.3-linux-amd64",
|
||||
missingObservation: "pending SHA256SUMS",
|
||||
},
|
||||
{
|
||||
name: "unknown",
|
||||
status: 3,
|
||||
recovery: []string{"resolve the remote state before retrying"},
|
||||
assetObservation: "uncertain helper-v1.2.3-linux-amd64",
|
||||
missingObservation: "pending SHA256SUMS",
|
||||
},
|
||||
} {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
repository := newCLIRepository(t)
|
||||
state := &deliveryServerState{}
|
||||
server := newDeliveryServer(t, state)
|
||||
defer server.Close()
|
||||
build := cli.BuildSettings{MainPackage: ".", BinaryName: "helper", Targets: []string{"linux/amd64"}}
|
||||
writeDeliveryProfile(t, repository, cli.Settings{
|
||||
Product: "Example Product",
|
||||
Build: &build,
|
||||
Delivery: &cli.DeliverySettings{BaseURL: server.URL, Owner: "owner", Repository: "example"},
|
||||
})
|
||||
state.commit = cliGitOutput(t, repository, "rev-parse", "HEAD")
|
||||
cliGitRun(t, repository, "tag", "v1.2.3")
|
||||
assetsDir, _ := writeDeliveryReceipt(t, repository, build)
|
||||
switch test.name {
|
||||
case "partial":
|
||||
state.failAssetName = "helper-v1.2.3-linux-amd64"
|
||||
case "conflict":
|
||||
state.assets["helper-v1.2.3-linux-amd64"] = []byte("different hosted bytes")
|
||||
case "unknown":
|
||||
state.ambiguousAssetName = "helper-v1.2.3-linux-amd64"
|
||||
}
|
||||
t.Setenv("RELEASEKIT_GITEA_TOKEN", "fixture-token")
|
||||
var stdout, stderr bytes.Buffer
|
||||
status := run(context.Background(), []string{"deliver", "--repo", repository, "--assets", assetsDir, "v1.2.3"}, &stdout, &stderr)
|
||||
if status != test.status || !strings.Contains(stderr.String(), "state="+test.name) {
|
||||
t.Fatalf("status=%d output=%q want status=%d state=%s", status, stderr.String(), test.status, test.name)
|
||||
}
|
||||
for _, phrase := range test.recovery {
|
||||
if !strings.Contains(stderr.String(), phrase) {
|
||||
t.Errorf("delivery output does not include recovery guidance %q: %s", phrase, stderr.String())
|
||||
}
|
||||
}
|
||||
if !strings.Contains(stderr.String(), test.assetObservation) {
|
||||
t.Errorf("delivery output does not include asset observation %q: %s", test.assetObservation, stderr.String())
|
||||
}
|
||||
if test.missingObservation != "" && !strings.Contains(stderr.String(), test.missingObservation) {
|
||||
t.Errorf("delivery output does not include later missing asset %q: %s", test.missingObservation, stderr.String())
|
||||
}
|
||||
if test.name == "unknown" && strings.Contains(stderr.String(), "pending helper-v1.2.3-linux-amd64") {
|
||||
t.Errorf("uncertain asset also appears pending: %s", stderr.String())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeliverRejectsWrongHostedTagBeforeReleaseWrite(t *testing.T) {
|
||||
repository := newCLIRepository(t)
|
||||
state := &deliveryServerState{remoteCommit: strings.Repeat("0", 40)}
|
||||
@@ -443,6 +519,8 @@ type deliveryServerState struct {
|
||||
malformedCreate bool
|
||||
failAssetListing bool
|
||||
failAssetDownloads bool
|
||||
failAssetName string
|
||||
ambiguousAssetName string
|
||||
commit string
|
||||
product string
|
||||
remoteCommit string
|
||||
@@ -543,6 +621,14 @@ func newDeliveryServer(t *testing.T, state *deliveryServerState) *httptest.Serve
|
||||
if _, exists := state.assets[name]; exists {
|
||||
t.Errorf("asset %q uploaded more than once", name)
|
||||
}
|
||||
if name == state.failAssetName {
|
||||
http.Error(w, "upload failed", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if name == state.ambiguousAssetName {
|
||||
http.Error(w, "upload outcome ambiguous", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
state.assets[name] = content
|
||||
state.assetUploads++
|
||||
_ = json.NewEncoder(w).Encode(map[string]any{"id": state.assetUploads, "name": name, "size": len(content)})
|
||||
@@ -751,6 +837,9 @@ func TestUnreadableAssetsDoNotClaimAnUnknownWrite(t *testing.T) {
|
||||
if status != 1 || !strings.Contains(stderr.String(), "state="+expectedState) || state.assetUploads != 0 || state.releaseCreates != expectedCreates {
|
||||
t.Fatalf("status=%d creates=%d uploads=%d %s", status, state.releaseCreates, state.assetUploads, stderr.String())
|
||||
}
|
||||
if existingPage && !strings.Contains(stderr.String(), "recovery: fix the local or preflight failure") {
|
||||
t.Fatalf("not-attempted recovery guidance missing: %s", stderr.String())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,6 +33,7 @@ func TestReleaseWrapperPreservesNativeExitStatuses(t *testing.T) {
|
||||
for _, path := range []string{
|
||||
"check.go", "publish.go", "internal/candidate/check.go", "internal/publish/publish.go",
|
||||
"build_test.go", "publish_test.go", "internal/candidate/check_test.go",
|
||||
"internal/gitea/upload.go", "internal/gitea/upload_test.go",
|
||||
"cmd/releasekit/main_test.go", "cmd/releasekit/release_wrapper_test.go",
|
||||
"cmd/releasekit/internal/cli/config_test.go", "examples/releasekit.json",
|
||||
} {
|
||||
|
||||
Reference in New Issue
Block a user