Files
releasekit/recovery.go

132 lines
4.6 KiB
Go

package releasekit
import (
"context"
"errors"
"fmt"
"go.maximumdirect.net/releasekit/internal/git"
"go.maximumdirect.net/releasekit/internal/publish"
)
// PublicationEvidence identifies one attempted publication. Endpoint may
// contain credentials; treat this value and saved records as sensitive.
type PublicationEvidence struct {
RepositoryDir string
ModuleDir string
ModulePath string
Version string
Tag string
CandidateCommit string
TagObject string
Endpoint string
Remote string
RecordedAt string
}
// PublicationReconciliationResult combines evidence with current remote state.
type PublicationReconciliationResult struct {
Evidence PublicationEvidence
State PublicationState
ObservedTagObject string
ObservedCommit string
}
// PublicationRecoveryError describes an unresolved recovery operation.
// Kind includes missing_evidence, invalid_evidence, canceled, endpoint_changed,
// transport_configuration, remote_observation, inconsistent_observation,
// record_format, record_read, record_write, and storage_enumeration.
type PublicationRecoveryError struct {
Kind string
RecordPath string
MissingFields []string
cause error
}
func (e *PublicationRecoveryError) Error() string {
if e == nil {
return ""
}
message := fmt.Sprintf("publication recovery %s", e.Kind)
if e.RecordPath != "" {
message += fmt.Sprintf(" for %q", e.RecordPath)
}
if e.cause != nil {
message += ": " + git.DisplayError(e.cause).Error()
}
return message
}
func (e *PublicationRecoveryError) Unwrap() error {
if e == nil {
return nil
}
return e.cause
}
// ReconcilePublication observes the recorded endpoint without running candidate
// validation or changing local or remote state.
func ReconcilePublication(ctx context.Context, evidence PublicationEvidence) (PublicationReconciliationResult, error) {
observation, err := publish.Reconcile(ctx, internalEvidence(evidence))
return publicObservation(observation), publicRecoveryError(err, "")
}
// LoadPublicationEvidence loads one explicit private attempt record. Relative
// paths are interpreted from the caller's current directory; no Git lookup is
// performed.
func LoadPublicationEvidence(path string) (PublicationEvidence, error) {
evidence, err := publish.LoadEvidence(path)
return publicEvidence(evidence), publicRecoveryError(err, path)
}
type PublicationAttemptsRequest struct {
RepositoryDir string
RecordDir string
}
type PublicationAttempt struct {
RecordPath string
Evidence PublicationEvidence
}
type PublicationAttemptsResult struct {
RecordDir string
Attempts []PublicationAttempt
Diagnostics []*PublicationRecoveryError
}
// ListPublicationAttempts discovers saved local records without querying a
// remote or inferring which attempt should be reconciled.
func ListPublicationAttempts(ctx context.Context, request PublicationAttemptsRequest) (PublicationAttemptsResult, error) {
listed, err := publish.ListAttempts(ctx, request.RepositoryDir, request.RecordDir)
result := PublicationAttemptsResult{RecordDir: listed.RecordDir, Attempts: make([]PublicationAttempt, 0, len(listed.Attempts)), Diagnostics: make([]*PublicationRecoveryError, 0, len(listed.Diagnostics))}
for _, attempt := range listed.Attempts {
result.Attempts = append(result.Attempts, PublicationAttempt{RecordPath: attempt.RecordPath, Evidence: publicEvidence(attempt.Evidence)})
}
for _, diagnostic := range listed.Diagnostics {
converted := publicRecoveryError(diagnostic, diagnostic.RecordPath)
if recovery, ok := converted.(*PublicationRecoveryError); ok {
result.Diagnostics = append(result.Diagnostics, recovery)
}
}
return result, publicRecoveryError(err, "")
}
func internalEvidence(e PublicationEvidence) publish.Evidence { return publish.Evidence(e) }
func publicEvidence(e publish.Evidence) PublicationEvidence { return PublicationEvidence(e) }
func publicObservation(o publish.Observation) PublicationReconciliationResult {
return PublicationReconciliationResult{Evidence: publicEvidence(o.Evidence), State: PublicationState(o.State), ObservedTagObject: o.ObservedTagObject, ObservedCommit: o.ObservedCommit}
}
func publicRecoveryError(err error, path string) error {
if err == nil {
return nil
}
var recovery *publish.RecoveryError
if errors.As(err, &recovery) {
recordPath := path
if recordPath == "" {
recordPath = recovery.RecordPath
}
return &PublicationRecoveryError{Kind: recovery.Kind, RecordPath: recordPath, MissingFields: append([]string(nil), recovery.MissingFields...), cause: recovery}
}
return &PublicationRecoveryError{Kind: "remote_observation", RecordPath: path, cause: err}
}