147 lines
7.0 KiB
Go
147 lines
7.0 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 independently of
|
|
// current repository state. Reconciliation uses RepositoryDir as Git's working
|
|
// directory, Tag and Endpoint as the observed ref and destination, and the
|
|
// captured object IDs to classify the observation. Endpoint may contain
|
|
// credentials; treat this value and saved records as sensitive.
|
|
type PublicationEvidence struct {
|
|
RepositoryDir string // Git working-directory context for relative endpoints; empty uses the caller's current directory.
|
|
ModuleDir string // physical module directory captured for this attempt.
|
|
ModulePath string // declared module path captured for this attempt.
|
|
Version string // semantic version selected for this attempt.
|
|
Tag string // complete Git tag ref selected for this attempt.
|
|
CandidateCommit string // candidate commit captured before publication.
|
|
TagObject string // annotated tag object ID captured before pushing, when available.
|
|
Endpoint string // exact effective push endpoint captured for this attempt; may contain credentials.
|
|
Remote string // remote name supplying supported transport configuration during later observation.
|
|
RecordedAt string // UTC RFC3339Nano time when this attempt's evidence was initialized, when available.
|
|
}
|
|
|
|
// PublicationReconciliationResult combines saved evidence with one later
|
|
// observation. It does not update local or remote state or modify the record.
|
|
type PublicationReconciliationResult struct {
|
|
Evidence PublicationEvidence // original selected evidence.
|
|
State PublicationState // published, not_published, conflict, or unknown.
|
|
ObservedTagObject string // observed remote tag-object ID, when available.
|
|
ObservedCommit string // observed peeled commit ID, when available.
|
|
}
|
|
|
|
// PublicationRecoveryError describes an unresolved recovery operation or a
|
|
// record/storage failure. Its Error text is credential-safe; Unwrap retains
|
|
// the underlying cause for errors.Is and errors.As.
|
|
// 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 // selected or affected record path, when applicable.
|
|
MissingFields []string // private-record field names required for the attempted classification.
|
|
cause error
|
|
}
|
|
|
|
// Error returns the credential-safe recovery diagnostic.
|
|
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
|
|
}
|
|
|
|
// Unwrap returns the underlying recovery or storage cause.
|
|
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)
|
|
}
|
|
|
|
// PublicationAttemptsRequest selects local publication-attempt storage.
|
|
type PublicationAttemptsRequest struct {
|
|
RepositoryDir string // repository used to resolve an empty or relative RecordDir; empty means the current directory.
|
|
RecordDir string // optional storage directory; relative paths use the repository root, and absolute paths can be listed outside a checkout.
|
|
}
|
|
|
|
// PublicationAttempt pairs one readable record path with the evidence it stores.
|
|
type PublicationAttempt struct {
|
|
RecordPath string // absolute path of the selected record.
|
|
Evidence PublicationEvidence // saved identity, including any partial fields.
|
|
}
|
|
|
|
// PublicationAttemptsResult contains readable attempts and per-record
|
|
// diagnostics from one local directory enumeration.
|
|
type PublicationAttemptsResult struct {
|
|
RecordDir string // resolved absolute directory selected for enumeration, when available.
|
|
Attempts []PublicationAttempt // readable records in deterministic path order.
|
|
Diagnostics []*PublicationRecoveryError // unreadable or unsupported records; other entries remain available.
|
|
}
|
|
|
|
// 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}
|
|
}
|