Record publication evidence before tag pushes

This commit is contained in:
2026-11-02 19:08:07 +00:00
parent b11c6d1788
commit 2a04938a96
5 changed files with 87 additions and 10 deletions

View File

@@ -48,6 +48,7 @@ type Request struct {
Version string
Remote string
ExpectedPushURL string
RecordDir string
Validation validation.Settings
AdditionalChecks func(context.Context, candidate.Candidate) error
AfterValidate func(context.Context, candidate.Candidate, *gotool.Owner) error
@@ -65,6 +66,9 @@ type Result struct {
Phase Phase
TagObject string
State State
Evidence Evidence
RecordPath string
RecordError error
}
// Publish captures a candidate, resolves and checks the optional expected push
@@ -74,8 +78,22 @@ type Result struct {
// A failed push receives one bounded authoritative tag query. Exact
// object equality confirms publication; absence and conflict are classified,
// while an unreadable result remains Unknown and preserves the push error.
func Publish(ctx context.Context, request Request) (Result, error) {
result := Result{Version: request.Version, Phase: Preflight, State: NotAttempted}
func Publish(ctx context.Context, request Request) (result Result, resultErr error) {
result = Result{Version: request.Version, Phase: Preflight, State: NotAttempted}
var recordDir, repositoryRoot string
initialized, recorded := false, false
defer func() {
if !initialized || recorded || result.RecordError != nil {
return
}
recorded = true
path, err := saveEvidenceInDir(result.RepositoryDir, repositoryRoot, request.RecordDir, recordDir, result.Evidence)
if err != nil {
result.RecordError = err
return
}
result.RecordPath = path
}()
remoteName := request.Remote
if remoteName == "" {
remoteName = "origin"
@@ -95,6 +113,17 @@ func Publish(ctx context.Context, request Request) (Result, error) {
result.ModulePath = captured.ModulePath
result.Tag = captured.Tag
result.CandidateCommit = captured.Commit
result.Evidence = Evidence{RepositoryDir: captured.RepositoryDir, ModuleDir: captured.ModuleDir, ModulePath: captured.ModulePath, Version: captured.Version, Tag: captured.Tag, CandidateCommit: captured.Commit, Remote: remoteName}
if captured.RepositoryDir != "" {
initialized = true
result.Evidence.RecordedAt = time.Now().UTC().Format(time.RFC3339Nano)
recordDir, repositoryRoot, err = resolveRecordDir(captured.RepositoryDir, request.RecordDir)
if err != nil {
result.RecordError = &RecoveryError{Kind: "record_write", cause: err}
recordDir = ""
recorded = true
}
}
if err != nil {
return result, fmt.Errorf("capture release candidate: %w", err)
}
@@ -104,21 +133,22 @@ func Publish(ctx context.Context, request Request) (Result, error) {
return result, fmt.Errorf("resolve publication endpoint: %w", err)
}
result.Endpoint = endpoint.PushURL
result.Evidence.Endpoint = endpoint.PushURL
if request.ExpectedPushURL != "" && request.ExpectedPushURL != endpoint.PushURL {
return result, fmt.Errorf("%w: expected %q, resolved %q", ErrUnexpectedPushURL, git.DisplayURL(request.ExpectedPushURL), git.DisplayURL(endpoint.PushURL))
}
if err := endpoint.Recheck(ctx, captured.RepositoryDir); err != nil {
return result, fmt.Errorf("pre-validation endpoint guard: %w", err)
}
if err := candidate.Recheck(ctx, captured, ""); err != nil {
return result, fmt.Errorf("pre-validation local guard: candidate worktree and index must remain clean: %w", err)
}
if _, err := endpoint.RequireCandidate(ctx, captured.RepositoryDir, captured.Tag, captured.Commit); err != nil {
if isRemoteConflict(err) {
result.State = Conflict
}
return result, fmt.Errorf("pre-validation remote guard: %w", err)
}
if err := candidate.Recheck(ctx, captured, ""); err != nil {
return result, fmt.Errorf("pre-validation local guard: %w", err)
}
result.Phase = Validation
checks.ToolOwner = gotool.NewOwner(captured.ModuleDir)
@@ -148,6 +178,7 @@ func Publish(ctx context.Context, request Request) (Result, error) {
result.State = NotPublished
tagObject, err := git.CreateAnnotatedTag(ctx, captured.RepositoryDir, captured.Tag, captured.Commit, []byte(captured.Note))
result.TagObject = tagObject
result.Evidence.TagObject = tagObject
if err != nil {
return result, err
}
@@ -166,6 +197,15 @@ func Publish(ctx context.Context, request Request) (Result, error) {
result.Phase = Push
result.State = Unknown
if !recorded {
recorded = true
path, recordErr := saveEvidenceInDir(result.RepositoryDir, repositoryRoot, request.RecordDir, recordDir, result.Evidence)
if recordErr != nil {
result.RecordError = recordErr
} else {
result.RecordPath = path
}
}
if err := endpoint.PushTag(ctx, captured.RepositoryDir, captured.Tag, tagObject); err != nil {
return reconcilePush(endpoint, captured, tagObject, result, err)
}

View File

@@ -55,6 +55,10 @@ func SaveEvidence(repositoryDir, override string, evidence Evidence) (string, er
if err != nil {
return "", &RecoveryError{Kind: "record_write", cause: err}
}
return saveEvidenceInDir(repositoryDir, root, override, recordDir, evidence)
}
func saveEvidenceInDir(repositoryDir, root, override, recordDir string, evidence Evidence) (string, error) {
if evidence.RecordedAt == "" {
evidence.RecordedAt = time.Now().UTC().Format(time.RFC3339Nano)
}