Extract publication recovery observation
This commit is contained in:
138
internal/publish/reconcile.go
Normal file
138
internal/publish/reconcile.go
Normal file
@@ -0,0 +1,138 @@
|
||||
package publish
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
"go.maximumdirect.net/releasekit/internal/git"
|
||||
)
|
||||
|
||||
// Evidence is the identity captured when one publication was attempted.
|
||||
type Evidence struct {
|
||||
RepositoryDir string
|
||||
ModuleDir string
|
||||
ModulePath string
|
||||
Version string
|
||||
Tag string
|
||||
CandidateCommit string
|
||||
TagObject string
|
||||
Endpoint string
|
||||
Remote string
|
||||
RecordedAt string
|
||||
}
|
||||
|
||||
// Observation combines the original evidence with authoritative remote state.
|
||||
type Observation struct {
|
||||
Evidence Evidence
|
||||
State State
|
||||
ObservedTagObject string
|
||||
ObservedCommit string
|
||||
}
|
||||
|
||||
// RecoveryError describes why a later publication observation is unresolved.
|
||||
type RecoveryError struct {
|
||||
Kind string
|
||||
MissingFields []string
|
||||
cause error
|
||||
}
|
||||
|
||||
func (e *RecoveryError) Error() string {
|
||||
if e == nil {
|
||||
return ""
|
||||
}
|
||||
message := "publication recovery " + e.Kind
|
||||
if e.cause != nil {
|
||||
return message + ": " + git.DisplayError(e.cause).Error()
|
||||
}
|
||||
return message
|
||||
}
|
||||
func (e *RecoveryError) Unwrap() error {
|
||||
if e == nil {
|
||||
return nil
|
||||
}
|
||||
return e.cause
|
||||
}
|
||||
|
||||
func classify(evidence Evidence, remoteTag git.RemoteRef) (Observation, error) {
|
||||
result := Observation{Evidence: evidence, State: Unknown, ObservedTagObject: remoteTag.Object, ObservedCommit: remoteTag.Peeled}
|
||||
missing := []string{}
|
||||
if evidence.Endpoint == "" {
|
||||
missing = append(missing, "endpoint")
|
||||
}
|
||||
if evidence.Tag == "" {
|
||||
missing = append(missing, "tag")
|
||||
}
|
||||
if len(missing) > 0 {
|
||||
return result, &RecoveryError{Kind: "missing_evidence", MissingFields: missing}
|
||||
}
|
||||
if remoteTag.State == git.RefAbsent {
|
||||
result.State = NotPublished
|
||||
return result, nil
|
||||
}
|
||||
if evidence.TagObject == "" {
|
||||
return result, &RecoveryError{Kind: "missing_evidence", MissingFields: []string{"tag_object"}}
|
||||
}
|
||||
if remoteTag.State != git.RefPresent {
|
||||
return result, &RecoveryError{Kind: "inconsistent_observation", cause: git.ErrRemoteStateUnreadable}
|
||||
}
|
||||
if !git.EqualObjectID(remoteTag.Object, evidence.TagObject) {
|
||||
result.State = Conflict
|
||||
return result, nil
|
||||
}
|
||||
if remoteTag.Peeled != "" {
|
||||
if evidence.CandidateCommit == "" {
|
||||
return result, &RecoveryError{Kind: "missing_evidence", MissingFields: []string{"candidate_commit"}}
|
||||
}
|
||||
if !git.EqualObjectID(remoteTag.Peeled, evidence.CandidateCommit) {
|
||||
result.State = Conflict
|
||||
return result, nil
|
||||
}
|
||||
}
|
||||
result.State = Published
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func observe(ctx context.Context, evidence Evidence) (Observation, error) {
|
||||
if err := ctx.Err(); err != nil {
|
||||
return Observation{Evidence: evidence, State: Unknown}, &RecoveryError{Kind: "canceled", cause: err}
|
||||
}
|
||||
if evidence.Endpoint == "" || evidence.Tag == "" {
|
||||
return classify(evidence, git.RemoteRef{})
|
||||
}
|
||||
if evidence.TagObject != "" && !git.ValidObjectID(evidence.TagObject) {
|
||||
return Observation{Evidence: evidence, State: Unknown}, &RecoveryError{Kind: "invalid_evidence"}
|
||||
}
|
||||
if evidence.CandidateCommit != "" && !git.ValidObjectID(evidence.CandidateCommit) {
|
||||
return Observation{Evidence: evidence, State: Unknown}, &RecoveryError{Kind: "invalid_evidence"}
|
||||
}
|
||||
valid, err := git.ValidateTagRef(ctx, evidence.RepositoryDir, evidence.Tag)
|
||||
if err != nil {
|
||||
return Observation{Evidence: evidence, State: Unknown}, &RecoveryError{Kind: "remote_observation", cause: err}
|
||||
}
|
||||
if !valid {
|
||||
return Observation{Evidence: evidence, State: Unknown}, &RecoveryError{Kind: "invalid_evidence"}
|
||||
}
|
||||
endpoint, err := git.ObserveEndpoint(ctx, evidence.RepositoryDir, evidence.Endpoint, evidence.Remote)
|
||||
if err != nil {
|
||||
if ctx.Err() != nil {
|
||||
return Observation{Evidence: evidence, State: Unknown}, &RecoveryError{Kind: "canceled", cause: ctx.Err()}
|
||||
}
|
||||
kind := "endpoint_changed"
|
||||
if errors.Is(err, git.ErrEndpointChanged) {
|
||||
return Observation{Evidence: evidence, State: Unknown}, &RecoveryError{Kind: kind, cause: err}
|
||||
}
|
||||
if evidence.Remote != "" {
|
||||
kind = "transport_configuration"
|
||||
}
|
||||
return Observation{Evidence: evidence, State: Unknown}, &RecoveryError{Kind: kind, cause: err}
|
||||
}
|
||||
ref, err := endpoint.QueryTag(ctx, evidence.RepositoryDir, evidence.Tag)
|
||||
if err != nil {
|
||||
return Observation{Evidence: evidence, State: Unknown}, &RecoveryError{Kind: "remote_observation", cause: err}
|
||||
}
|
||||
result, err := classify(evidence, ref)
|
||||
if err != nil && ctx.Err() != nil {
|
||||
return result, &RecoveryError{Kind: "canceled", cause: ctx.Err()}
|
||||
}
|
||||
return result, err
|
||||
}
|
||||
Reference in New Issue
Block a user