Preserve page creation outcomes through recovery

This commit is contained in:
2026-10-06 00:53:33 +00:00
parent a1ee067b53
commit 91712fd900
6 changed files with 116 additions and 16 deletions

View File

@@ -111,7 +111,7 @@ func Deliver(ctx context.Context, request Request) (Result, error) {
inspection, err := (gitea.Client{}).InspectAssets(ctx, request.Destination, releaseResult.Release.ID, request.Assets)
if err != nil {
result.State = NotAttempted
if releaseResult.Created {
if releaseResult.WriteAttempted {
result.State = Partial
}
result.Uncertain = assetIdentities(request.Assets)

View File

@@ -75,8 +75,9 @@ type Release struct {
}
type Result struct {
Release Release
Created bool
Release Release
Created bool
WriteAttempted bool // a create request was issued, including reconciled creates.
}
type Client struct {
@@ -150,7 +151,12 @@ func (c Client) EnsureRelease(ctx context.Context, request Request) (Result, err
}
created, postStatus, postErr := c.postRelease(ctx, client, request.Destination.Token, apiRoot+"/releases", body)
if postErr == nil {
return acceptCreated(created, request)
result, verifyErr := acceptCreated(created, request)
if verifyErr == nil {
result.WriteAttempted = true
return result, nil
}
postErr = verifyErr
}
if !shouldReconcileWrite(postStatus, postErr) {
return Result{}, postErr
@@ -162,6 +168,7 @@ func (c Client) EnsureRelease(ctx context.Context, request Request) (Result, err
if retryErr == nil {
result, matchErr := acceptExisting(retry, request)
if matchErr == nil {
result.WriteAttempted = true
return result, nil
}
return Result{}, failure(FailureConflict, "resolve release creation", postStatus, matchErr)

View File

@@ -339,3 +339,57 @@ func failureKind(t *testing.T, err error) FailureKind {
func contains(value, substring string) bool {
return len(substring) > 0 && len(value) >= len(substring) && strings.Contains(value, substring)
}
func TestEnsureReleaseReconcilesInconsistentSuccessfulCreate(t *testing.T) {
for _, mode := range []string{"matching", "conflicting", "unavailable"} {
t.Run(mode, func(t *testing.T) {
posts, lookups := 0, 0
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch {
case strings.Contains(r.URL.Path, "/tags/") && !strings.Contains(r.URL.Path, "/releases/"):
writeTag(t, w, testCommit)
case strings.Contains(r.URL.Path, "/releases/tags/"):
lookups++
if posts == 0 {
http.NotFound(w, r)
return
}
if mode == "unavailable" {
http.Error(w, "unavailable", 503)
return
}
body := testNote
if mode == "conflicting" {
body = "other content"
}
writeRelease(t, w, releaseFixture{title: "Example v1.2.3", body: body})
case r.Method == http.MethodPost:
posts++
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(releaseResponse{ID: 10, TagName: "v1.2.3", Name: "inconsistent title", Body: testNote})
default:
t.Errorf("unexpected request %s", r.URL.Path)
}
}))
defer server.Close()
result, err := testClient(server).EnsureRelease(context.Background(), testRequest(server.URL))
if posts != 1 || lookups != 2 {
t.Fatalf("posts=%d lookups=%d", posts, lookups)
}
switch mode {
case "matching":
if err != nil || !result.WriteAttempted {
t.Fatalf("result=%#v err=%v", result, err)
}
case "conflicting":
if failureKind(t, err) != FailureConflict {
t.Fatal(err)
}
case "unavailable":
if failureKind(t, err) != FailureUnknown {
t.Fatal(err)
}
}
})
}
}