// Package publish coordinates candidate validation and exact tag publication. package publish import ( "context" "errors" "fmt" "io" "gitea.maximumdirect.net/eric/ReleaseKit/internal/candidate" "gitea.maximumdirect.net/eric/ReleaseKit/internal/git" ) type State string const ( NotAttempted State = "not_attempted" NotPublished State = "not_published" Published State = "published" Conflict State = "conflict" Unknown State = "unknown" ) type Phase string const ( Preflight Phase = "preflight" Validation Phase = "validation" TagCreation Phase = "tag_creation" Push Phase = "push" Complete Phase = "complete" ) type Request struct { RepositoryDir string Product string Version string Remote string Validate func(context.Context, candidate.Candidate) error Output io.Writer } type Result struct { Version string CandidateCommit string Endpoint string Phase Phase TagObject string State State } // 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. func Publish(ctx context.Context, request Request) (Result, error) { result := Result{Version: request.Version, Phase: Preflight, State: NotAttempted} remoteName := request.Remote if remoteName == "" { remoteName = "origin" } checks := candidate.Request{ RepositoryDir: request.RepositoryDir, Product: request.Product, Version: request.Version, Validate: request.Validate, Output: request.Output, } captured, err := candidate.Capture(ctx, checks) if err != nil { return result, fmt.Errorf("capture release candidate: %w", err) } result.CandidateCommit = captured.Commit endpoint, err := git.Resolve(ctx, captured.RepositoryDir, remoteName) if err != nil { return result, fmt.Errorf("resolve publication endpoint: %w", err) } result.Endpoint = endpoint.PushURL if err := endpoint.Recheck(ctx, captured.RepositoryDir); err != nil { return result, fmt.Errorf("pre-validation endpoint guard: %w", err) } if _, err := endpoint.RequireCandidate(ctx, captured.RepositoryDir, captured.Version, captured.Commit); err != nil { if isRemoteConflict(err) { result.State = Conflict } return result, fmt.Errorf("pre-validation remote guard: %w", err) } if err := candidate.Recheck(ctx, captured, ""); err != nil { return result, fmt.Errorf("pre-validation local guard: %w", err) } result.Phase = Validation if err := candidate.Validate(ctx, checks, captured); err != nil { return result, err } if err := candidate.Recheck(ctx, captured, ""); err != nil { return result, fmt.Errorf("post-validation local guard: %w", err) } if err := endpoint.Recheck(ctx, captured.RepositoryDir); err != nil { return result, fmt.Errorf("pre-publication endpoint guard: %w", err) } if _, err := endpoint.RequireCandidate(ctx, captured.RepositoryDir, captured.Version, captured.Commit); err != nil { if isRemoteConflict(err) { result.State = Conflict } return result, fmt.Errorf("pre-publication remote guard: %w", err) } result.Phase = TagCreation result.State = NotPublished tagObject, err := git.CreateAnnotatedTag(ctx, captured.RepositoryDir, captured.Version, captured.Commit, []byte(captured.Note)) if err != nil { return result, err } result.TagObject = tagObject if err := candidate.Recheck(ctx, captured, tagObject); err != nil { return result, fmt.Errorf("pre-push local guard: %w", err) } if err := endpoint.Recheck(ctx, captured.RepositoryDir); err != nil { return result, fmt.Errorf("pre-push endpoint guard: %w", err) } if _, err := endpoint.RequireCandidate(ctx, captured.RepositoryDir, captured.Version, captured.Commit); err != nil { if isRemoteConflict(err) { result.State = Conflict } return result, fmt.Errorf("pre-push remote guard: %w", err) } result.Phase = Push result.State = Unknown if err := endpoint.PushTag(ctx, captured.RepositoryDir, captured.Version, tagObject); err != nil { return result, err } result.Phase = Complete result.State = Published return result, nil } func isRemoteConflict(err error) bool { return errors.Is(err, git.ErrRemoteMainMissing) || errors.Is(err, git.ErrRemoteMainConflict) || errors.Is(err, git.ErrRemoteTagConflict) }