139 lines
4.3 KiB
Go
139 lines
4.3 KiB
Go
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
|
|
}
|