Reconcile uncertain tag pushes
This commit is contained in:
@@ -6,6 +6,7 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"time"
|
||||
|
||||
"gitea.maximumdirect.net/eric/ReleaseKit/internal/candidate"
|
||||
"gitea.maximumdirect.net/eric/ReleaseKit/internal/git"
|
||||
@@ -24,13 +25,16 @@ const (
|
||||
type Phase string
|
||||
|
||||
const (
|
||||
Preflight Phase = "preflight"
|
||||
Validation Phase = "validation"
|
||||
TagCreation Phase = "tag_creation"
|
||||
Push Phase = "push"
|
||||
Complete Phase = "complete"
|
||||
Preflight Phase = "preflight"
|
||||
Validation Phase = "validation"
|
||||
TagCreation Phase = "tag_creation"
|
||||
Push Phase = "push"
|
||||
Reconciliation Phase = "reconciliation"
|
||||
Complete Phase = "complete"
|
||||
)
|
||||
|
||||
const recoveryTimeout = 30 * time.Second
|
||||
|
||||
type Request struct {
|
||||
RepositoryDir string
|
||||
Product string
|
||||
@@ -50,8 +54,9 @@ type Result struct {
|
||||
}
|
||||
|
||||
// Publish validates one captured candidate and pushes only its annotated tag
|
||||
// object. A failed push remains Unknown until authoritative reconciliation is
|
||||
// added by the following implementation stage.
|
||||
// object. A failed push receives one bounded authoritative tag query. Exact
|
||||
// object equality confirms publication; absence and conflict are classified,
|
||||
// while an unreadable result remains Unknown and preserves the push error.
|
||||
func Publish(ctx context.Context, request Request) (Result, error) {
|
||||
result := Result{Version: request.Version, Phase: Preflight, State: NotAttempted}
|
||||
remoteName := request.Remote
|
||||
@@ -129,13 +134,42 @@ func Publish(ctx context.Context, request Request) (Result, error) {
|
||||
result.Phase = Push
|
||||
result.State = Unknown
|
||||
if err := endpoint.PushTag(ctx, captured.RepositoryDir, captured.Version, tagObject); err != nil {
|
||||
return result, err
|
||||
return reconcilePush(endpoint, captured, tagObject, result, err)
|
||||
}
|
||||
result.Phase = Complete
|
||||
result.State = Published
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func reconcilePush(endpoint git.Endpoint, captured candidate.Candidate, tagObject string, result Result, pushErr error) (Result, error) {
|
||||
result.Phase = Reconciliation
|
||||
recoveryCtx, cancel := context.WithTimeout(context.Background(), recoveryTimeout)
|
||||
defer cancel()
|
||||
remoteTag, queryErr := endpoint.QueryTag(recoveryCtx, captured.RepositoryDir, captured.Version)
|
||||
if queryErr != nil || remoteTag.State == git.RefUnreadable || remoteTag.State == git.RefConflict {
|
||||
result.State = Unknown
|
||||
if queryErr == nil {
|
||||
if remoteTag.State == git.RefConflict {
|
||||
queryErr = errors.New("authoritative tag query returned inconsistent exact ref records")
|
||||
} else {
|
||||
queryErr = git.ErrRemoteStateUnreadable
|
||||
}
|
||||
}
|
||||
return result, fmt.Errorf("push annotated tag %s failed; authoritative reconciliation failed (%v): %w", captured.Version, queryErr, pushErr)
|
||||
}
|
||||
if remoteTag.State == git.RefAbsent {
|
||||
result.State = NotPublished
|
||||
return result, fmt.Errorf("push annotated tag %s failed and the authoritative tag is absent: %w", captured.Version, pushErr)
|
||||
}
|
||||
if remoteTag.State == git.RefPresent && remoteTag.Object == tagObject && (remoteTag.Peeled == "" || remoteTag.Peeled == captured.Commit) {
|
||||
result.Phase = Complete
|
||||
result.State = Published
|
||||
return result, nil
|
||||
}
|
||||
result.State = Conflict
|
||||
return result, fmt.Errorf("push annotated tag %s failed and the authoritative tag identifies object %s, not %s: %w", captured.Version, remoteTag.Object, tagObject, pushErr)
|
||||
}
|
||||
|
||||
func isRemoteConflict(err error) bool {
|
||||
return errors.Is(err, git.ErrRemoteMainMissing) ||
|
||||
errors.Is(err, git.ErrRemoteMainConflict) ||
|
||||
|
||||
Reference in New Issue
Block a user