Move release page recovery into shared delivery

This commit is contained in:
2026-11-01 18:04:30 +00:00
parent ddac9e3c73
commit 88a79c3561
3 changed files with 297 additions and 2 deletions

View File

@@ -1,6 +1,6 @@
# Implementation plan: neutral forge delivery and credential preflight
Status: implementation in progress; Stages 1–5 are complete and Stages 6–10
Status: implementation in progress; Stages 1–6 are complete and Stages 7–10
are pending.
## Purpose and execution
@@ -357,7 +357,7 @@ orchestration as its callers move rather than retaining a parallel policy path.
## Stage 6: Move hosted page policy and recovery
Status: pending.
Status: complete.
1. Add shared page coordination in `internal/delivery` using `forge.Backend`.
After credential preflight and local source/build verification, obtain the

91
internal/delivery/page.go Normal file
View File

@@ -0,0 +1,91 @@
package delivery
import (
"context"
"errors"
"fmt"
"strings"
"go.maximumdirect.net/releasekit/internal/forge"
)
func coordinateRelease(ctx context.Context, backend forge.Backend, tag, commit, note string) (forge.Release, forge.WriteEvidence, error) {
observedCommit, err := backend.TagCommit(ctx, tag)
if err != nil {
return forge.Release{}, forge.WriteEvidence{Disposition: forge.WriteNotAttempted}, err
}
if observedCommit != commit {
return forge.Release{}, forge.WriteEvidence{Disposition: forge.WriteNotAttempted}, &forge.Failure{
Kind: forge.FailureConflict,
Err: fmt.Errorf("hosted tag %q targets %q, want %q", tag, observedCommit, commit),
}
}
existing, err := backend.FindRelease(ctx, tag)
if err == nil {
if !matchesExistingRelease(existing, tag, note) {
return forge.Release{}, forge.WriteEvidence{Disposition: forge.WriteNotAttempted}, &forge.Failure{
Kind: forge.FailureConflict,
Err: errors.New("existing release does not match the committed note and stable tag"),
}
}
return existing, forge.WriteEvidence{Disposition: forge.WriteNotAttempted}, nil
}
if !isForgeFailure(err, forge.FailureNotFound) {
return forge.Release{}, forge.WriteEvidence{Disposition: forge.WriteNotAttempted}, err
}
request := forge.CreateReleaseRequest{
Tag: tag, Commit: commit, Title: releaseNoteTitle(note), Body: note,
}
created, evidence, createErr := backend.CreateRelease(ctx, request)
if createErr == nil && matchesCreatedRelease(created, request) {
return created, evidence, nil
}
if createErr == nil {
createErr = &forge.Failure{Kind: forge.FailureMalformed, Err: errors.New("created release does not match the requested stable release")}
}
if evidence.Disposition != forge.WriteConflict && evidence.Disposition != forge.WriteUncertain && !isForgeFailure(createErr, forge.FailureMalformed) {
return forge.Release{}, evidence, createErr
}
return reconcileReleaseCreate(ctx, backend, tag, note, evidence, createErr)
}
func reconcileReleaseCreate(ctx context.Context, backend forge.Backend, tag, note string, evidence forge.WriteEvidence, createErr error) (forge.Release, forge.WriteEvidence, error) {
existing, lookupErr := backend.FindRelease(ctx, tag)
if lookupErr == nil {
if matchesExistingRelease(existing, tag, note) {
return existing, evidence, nil
}
return forge.Release{}, evidence, &forge.Failure{Kind: forge.FailureConflict, Err: errors.Join(createErr, errors.New("reconciled release does not match the committed note and stable tag"))}
}
if isForgeFailure(lookupErr, forge.FailureNotFound) {
if evidence.Disposition == forge.WriteConflict {
return forge.Release{}, evidence, &forge.Failure{Kind: forge.FailureConflict, Err: createErr}
}
return forge.Release{}, evidence, &forge.Failure{Kind: forge.FailureUnknown, Err: createErr}
}
return forge.Release{}, evidence, &forge.Failure{Kind: forge.FailureUnknown, Err: errors.Join(createErr, lookupErr)}
}
func matchesExistingRelease(release forge.Release, tag, note string) bool {
return release.Tag == tag && release.Body == note && !release.Draft && !release.Prerelease
}
func matchesCreatedRelease(release forge.Release, request forge.CreateReleaseRequest) bool {
return release.Tag == request.Tag && release.Body == request.Body && release.Title == request.Title && !release.Draft && !release.Prerelease
}
func releaseNoteTitle(note string) string {
first := strings.SplitN(note, "\n", 2)[0]
if !strings.HasPrefix(first, "# ") {
return ""
}
return strings.TrimSpace(strings.TrimPrefix(first, "# "))
}
func isForgeFailure(err error, kind forge.FailureKind) bool {
var failure *forge.Failure
return errors.As(err, &failure) && failure.Kind == kind
}

View File

@@ -0,0 +1,204 @@
package delivery
import (
"context"
"errors"
"io"
"strings"
"testing"
"go.maximumdirect.net/releasekit/internal/forge"
)
func TestCoordinateReleaseAcceptsExistingPageAndRetainsItsTitle(t *testing.T) {
backend := &pageBackend{
commit: "commit",
findResults: []pageFindResult{{release: forge.Release{
Tag: "v1.2.3", Title: "Maintained title", Body: "# Release\n\nNotes", Handle: "release",
}}},
}
release, evidence, err := coordinateRelease(context.Background(), backend, "v1.2.3", "commit", "# Release\n\nNotes")
if err != nil || release.Title != "Maintained title" || evidence.Attempted {
t.Fatalf("coordinateRelease() = %#v, evidence %#v, error %v", release, evidence, err)
}
if backend.createCalls != 0 || backend.findCalls != 1 {
t.Fatalf("calls = create %d, find %d", backend.createCalls, backend.findCalls)
}
}
func TestCoordinateReleaseRejectsWrongHostedTagAndExistingPage(t *testing.T) {
t.Run("wrong hosted tag", func(t *testing.T) {
backend := &pageBackend{commit: "other"}
_, evidence, err := coordinateRelease(context.Background(), backend, "v1.2.3", "commit", "# Release")
if !isForgeFailure(err, forge.FailureConflict) || evidence.Attempted || backend.findCalls != 0 || backend.createCalls != 0 {
t.Fatalf("coordinateRelease() evidence %#v, error %v, calls find=%d create=%d", evidence, err, backend.findCalls, backend.createCalls)
}
})
t.Run("missing hosted tag", func(t *testing.T) {
backend := &pageBackend{tagErr: &forge.Failure{Kind: forge.FailureNotFound, Err: errors.New("tag missing")}}
_, _, err := coordinateRelease(context.Background(), backend, "v1.2.3", "commit", "# Release")
if !isForgeFailure(err, forge.FailureNotFound) || backend.findCalls != 0 || backend.createCalls != 0 {
t.Fatalf("coordinateRelease() error = %v, calls find=%d create=%d", err, backend.findCalls, backend.createCalls)
}
})
for _, release := range []forge.Release{
{Tag: "v9.9.9", Body: "# Release", Title: "Release"},
{Tag: "v1.2.3", Body: "different", Title: "Release"},
{Tag: "v1.2.3", Body: "# Release", Title: "Release", Draft: true},
{Tag: "v1.2.3", Body: "# Release", Title: "Release", Prerelease: true},
} {
backend := &pageBackend{commit: "commit", findResults: []pageFindResult{{release: release}}}
_, _, err := coordinateRelease(context.Background(), backend, "v1.2.3", "commit", "# Release")
if !isForgeFailure(err, forge.FailureConflict) || backend.createCalls != 0 {
t.Fatalf("coordinateRelease(%#v) error = %v, create calls = %d", release, err, backend.createCalls)
}
}
}
func TestCoordinateReleaseCreatesWithFullNoteAndVerifiesRequestedTitle(t *testing.T) {
backend := &pageBackend{
commit: "commit",
findResults: []pageFindResult{{err: &forge.Failure{Kind: forge.FailureNotFound}}},
createResult: pageCreateResult{
release: forge.Release{Tag: "v1.2.3", Title: "Release", Body: "# Release\n\nFull note", Handle: "release"},
evidence: forge.WriteEvidence{Attempted: true, Disposition: forge.WriteAccepted},
},
}
release, evidence, err := coordinateRelease(context.Background(), backend, "v1.2.3", "commit", "# Release\n\nFull note")
if err != nil || release.Handle != "release" || evidence.Disposition != forge.WriteAccepted || !evidence.Attempted {
t.Fatalf("coordinateRelease() = %#v, evidence %#v, error %v", release, evidence, err)
}
if got := backend.createRequest; got.Tag != "v1.2.3" || got.Commit != "commit" || got.Title != "Release" || got.Body != "# Release\n\nFull note" || got.Draft || got.Prerelease {
t.Fatalf("CreateRelease request = %#v", got)
}
}
func TestCoordinateReleaseReconcilesCreateOutcomes(t *testing.T) {
createErr := &forge.Failure{Kind: forge.FailureTransport, Err: errors.New("connection lost")}
lookupErr := errors.New("lookup unavailable")
matching := forge.Release{Tag: "v1.2.3", Title: "Original", Body: "# Release\n\nNotes", Handle: "release"}
wrong := forge.Release{Tag: "v1.2.3", Body: "different", Handle: "other"}
notFound := &forge.Failure{Kind: forge.FailureNotFound}
for _, test := range []struct {
name string
create pageCreateResult
followup pageFindResult
wantKind forge.FailureKind
wantRecovered bool
wantCause error
}{
{
name: "matching page after conflict",
create: pageCreateResult{err: createErr, evidence: forge.WriteEvidence{Attempted: true, Disposition: forge.WriteConflict}},
followup: pageFindResult{release: matching}, wantRecovered: true,
},
{
name: "different page after uncertain write",
create: pageCreateResult{err: createErr, evidence: forge.WriteEvidence{Attempted: true, Disposition: forge.WriteUncertain}},
followup: pageFindResult{release: wrong}, wantKind: forge.FailureConflict,
},
{
name: "missing after conflict",
create: pageCreateResult{err: createErr, evidence: forge.WriteEvidence{Attempted: true, Disposition: forge.WriteConflict}},
followup: pageFindResult{err: notFound}, wantKind: forge.FailureConflict,
},
{
name: "missing after uncertain write",
create: pageCreateResult{err: createErr, evidence: forge.WriteEvidence{Attempted: true, Disposition: forge.WriteUncertain}},
followup: pageFindResult{err: notFound}, wantKind: forge.FailureUnknown, wantCause: createErr,
},
{
name: "unavailable follow-up",
create: pageCreateResult{err: createErr, evidence: forge.WriteEvidence{Attempted: true, Disposition: forge.WriteUncertain}},
followup: pageFindResult{err: lookupErr}, wantKind: forge.FailureUnknown, wantCause: lookupErr,
},
{
name: "inconsistent accepted response reconciled",
create: pageCreateResult{release: wrong, evidence: forge.WriteEvidence{Attempted: true, Disposition: forge.WriteAccepted}},
followup: pageFindResult{release: matching}, wantRecovered: true,
},
{
name: "inconsistent accepted response missing",
create: pageCreateResult{release: wrong, evidence: forge.WriteEvidence{Attempted: true, Disposition: forge.WriteAccepted}},
followup: pageFindResult{err: notFound}, wantKind: forge.FailureUnknown,
},
} {
t.Run(test.name, func(t *testing.T) {
backend := &pageBackend{
commit: "commit",
findResults: []pageFindResult{{err: notFound}, test.followup},
createResult: test.create,
}
release, evidence, err := coordinateRelease(context.Background(), backend, "v1.2.3", "commit", "# Release\n\nNotes")
if test.wantRecovered {
if err != nil || release.Handle != "release" || !evidence.Attempted || backend.findCalls != 2 {
t.Fatalf("recovered result = %#v, evidence %#v, error %v, find calls %d", release, evidence, err, backend.findCalls)
}
return
}
if !isForgeFailure(err, test.wantKind) || !evidence.Attempted {
t.Fatalf("result evidence %#v, error %v; want kind %s", evidence, err, test.wantKind)
}
if test.wantCause != nil && !errors.Is(err, test.wantCause) {
t.Fatalf("error %v does not retain cause %v", err, test.wantCause)
}
})
}
}
type pageFindResult struct {
release forge.Release
err error
}
type pageCreateResult struct {
release forge.Release
evidence forge.WriteEvidence
err error
}
type pageBackend struct {
commit string
tagErr error
findResults []pageFindResult
findCalls int
createResult pageCreateResult
createCalls int
createRequest forge.CreateReleaseRequest
}
func (backend *pageBackend) Preflight(context.Context) error { return nil }
func (backend *pageBackend) TagCommit(context.Context, string) (string, error) {
return backend.commit, backend.tagErr
}
func (backend *pageBackend) FindRelease(context.Context, string) (forge.Release, error) {
index := backend.findCalls
backend.findCalls++
if index >= len(backend.findResults) {
return forge.Release{}, &forge.Failure{Kind: forge.FailureNotFound}
}
result := backend.findResults[index]
return result.release, result.err
}
func (backend *pageBackend) CreateRelease(_ context.Context, request forge.CreateReleaseRequest) (forge.Release, forge.WriteEvidence, error) {
backend.createCalls++
backend.createRequest = request
return backend.createResult.release, backend.createResult.evidence, backend.createResult.err
}
func (*pageBackend) ListAssets(context.Context, any) ([]forge.Asset, error) { return nil, nil }
func (*pageBackend) OpenAsset(context.Context, any) (io.ReadCloser, error) {
return io.NopCloser(strings.NewReader("")), nil
}
func (*pageBackend) UploadAsset(context.Context, any, string, io.Reader) (forge.WriteEvidence, error) {
return forge.WriteEvidence{}, nil
}