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} }