package releasekit import ( "context" "errors" "fmt" "io" "strings" internalbuild "go.maximumdirect.net/releasekit/internal/build" internaldelivery "go.maximumdirect.net/releasekit/internal/delivery" "go.maximumdirect.net/releasekit/internal/forge" "go.maximumdirect.net/releasekit/internal/gitea" ) // DeliveryState describes the result of one delivery invocation. type DeliveryState string const ( // DeliveryNotAttempted means this invocation made no remote write. DeliveryNotAttempted DeliveryState = "not_attempted" // DeliveryComplete means the release page and all declared assets match. DeliveryComplete DeliveryState = "complete" // DeliveryPartial means a page or some assets were delivered, with work pending. DeliveryPartial DeliveryState = "partial" // DeliveryConflict means existing hosted content differs from the declaration. DeliveryConflict DeliveryState = "conflict" // DeliveryUnknown means a remote write may have succeeded but could not be resolved. DeliveryUnknown DeliveryState = "unknown" ) // Forge selects the hosted delivery backend. A delivery request must select a // supported value explicitly; the destination URL does not select a backend. type Forge string const ( // ForgeGitea selects Gitea for hosted delivery. ForgeGitea Forge = "gitea" ) // DeliveryDestination supplies an explicitly selected hosted destination. type DeliveryDestination struct { // Forge selects the adapter that interprets the destination fields. Forge Forge BaseURL string Owner string Repository string Token string } // DeliveryPreflightFailureKind classifies a failure while checking destination // settings, credentials, and repository access. type DeliveryPreflightFailureKind string const ( // DeliveryPreflightInvalidDestination means the destination settings fail local validation. DeliveryPreflightInvalidDestination DeliveryPreflightFailureKind = "invalid_destination" // DeliveryPreflightMissingCredential means no nonblank credential was supplied. DeliveryPreflightMissingCredential DeliveryPreflightFailureKind = "missing_credential" // DeliveryPreflightInvalidCredential means the selected service rejected the credential. DeliveryPreflightInvalidCredential DeliveryPreflightFailureKind = "invalid_credential" // DeliveryPreflightInsufficientPermissions means a required delivery permission is known to be absent. DeliveryPreflightInsufficientPermissions DeliveryPreflightFailureKind = "insufficient_permissions" // DeliveryPreflightInaccessibleDestination means the credential cannot access the selected repository. DeliveryPreflightInaccessibleDestination DeliveryPreflightFailureKind = "inaccessible_destination" // DeliveryPreflightUnavailable means the service response could not verify access. DeliveryPreflightUnavailable DeliveryPreflightFailureKind = "unavailable" ) // DeliveryPreflightError reports a classified failure to verify a destination. type DeliveryPreflightError struct { Kind DeliveryPreflightFailureKind Err error } func (err *DeliveryPreflightError) Error() string { if err.Err == nil { return fmt.Sprintf("delivery preflight %s", err.Kind) } return fmt.Sprintf("delivery preflight %s: %v", err.Kind, err.Err) } func (err *DeliveryPreflightError) Unwrap() error { return err.Err } // DeliveryAsset identifies a hosted asset by filename and SHA-256 digest. type DeliveryAsset struct { Filename string SHA256 string } // DeliveryRequest identifies a local tagged checkout and its explicit // destination. Build selects a managed build; Prebuilt supplies caller-owned // build output instead. They cannot both be set. Either may be nil for // page-only delivery when both are nil. Build with no targets, or an empty // Prebuilt result, also performs page-only delivery. type DeliveryRequest struct { RepositoryDir string // directory within the tagged source Git checkout. // ModuleDir selects a repository-relative module directory; empty selects // the repository root. ModuleDir string Product string Version string // ExpectedTag optionally requires the full CI event tag to match the // selected module's derived tag before building or making hosted requests. ExpectedTag string Destination DeliveryDestination // Build selects binaries to build in temporary storage outside the checkout. Build *BuildSpec // Prebuilt supplies an existing build whose files remain caller-owned. Prebuilt *BuildResult // Output receives progress from a managed build. Output io.Writer } // DeliveryResult reports hosted content and the state reached by this // invocation. A partial result may accompany an error. The asset slices are // current observations: an asset appears in at most one slice, and early // failures may leave some declared assets unreported. type DeliveryResult struct { RepositoryDir string ModuleDir string ModulePath string Tag string // complete derived Git tag. Version string // selected stable semantic version. CandidateCommit string ReleaseURL string State DeliveryState // Matching lists existing attachments with the declared content. Matching []DeliveryAsset // Uploaded lists assets whose uploads were confirmed during this invocation. Uploaded []DeliveryAsset // Pending lists assets known to be missing. Pending []DeliveryAsset // Conflicting lists assets with detected content conflicts. Conflicting []DeliveryAsset // Uncertain lists assets whose remote state could not be resolved. Uncertain []DeliveryAsset } // Deliver requires an explicit supported destination forge. It checks the local // request and source, then performs read-only credential preflight before a // managed build or full artifact verification. It verifies the selected local // tag and hosted source, then creates or finds its release page and adds any // missing declared build assets. Prebuilt artifacts remain caller-owned. Local // artifact contents are verified after preflight and before hosted writes. // Module selection and declaration links resolve from the captured commit. // Page-only delivery can use a sparse checkout with available committed objects; // managed builds require their selected source files in the working tree. // Preflight failures return a [DeliveryPreflightError] and leave State at // [DeliveryNotAttempted]. func Deliver(ctx context.Context, request DeliveryRequest) (DeliveryResult, error) { result := DeliveryResult{Version: request.Version, State: DeliveryNotAttempted} backend, err := deliveryBackend(request.Destination) if err != nil { return result, err } var prebuilt *internalbuild.Result if request.Prebuilt != nil { prebuilt = internalBuildResult(*request.Prebuilt) } internalResult, err := internaldelivery.Deliver(ctx, internaldelivery.Request{ RepositoryDir: request.RepositoryDir, ModuleDir: request.ModuleDir, Product: request.Product, Version: request.Version, ExpectedTag: request.ExpectedTag, Backend: backend, Build: internalBuildSpec(request.Build), Prebuilt: prebuilt, Output: request.Output, }) result.RepositoryDir = internalResult.RepositoryDir result.ModuleDir = internalResult.ModuleDir result.ModulePath = internalResult.ModulePath result.Tag = internalResult.Tag result.CandidateCommit = internalResult.CandidateCommit result.ReleaseURL = internalResult.ReleaseURL result.State = DeliveryState(internalResult.State) result.Matching = deliveryAssets(internalResult.Matching) result.Uploaded = deliveryAssets(internalResult.Uploaded) result.Pending = deliveryAssets(internalResult.Pending) result.Conflicting = deliveryAssets(internalResult.Conflicting) result.Uncertain = deliveryAssets(internalResult.Uncertain) return result, publicPreflightError(err) } // PreflightDelivery checks destination credentials and repository access using // read-only backend operations. It does not require a checkout or published tag. // The result verifies only the access and permission evidence exposed by the // selected backend; it does not guarantee that a later write will succeed. // Failures are returned as [DeliveryPreflightError] values when classified by // the backend, and retain their underlying cause for errors.Is and errors.As. func PreflightDelivery(ctx context.Context, destination DeliveryDestination) error { backend, err := deliveryBackend(destination) if err != nil { return err } return publicPreflightError(backend.Preflight(ctx)) } func deliveryBackend(destination DeliveryDestination) (forge.Backend, error) { selection := Forge(strings.TrimSpace(string(destination.Forge))) switch selection { case ForgeGitea: return gitea.NewBackend(gitea.Destination{ BaseURL: destination.BaseURL, Owner: destination.Owner, Repository: destination.Repository, Token: destination.Token, }, nil), nil case "": return nil, errors.New("delivery forge must be selected explicitly") default: return nil, fmt.Errorf("unsupported delivery forge %q", destination.Forge) } } func publicPreflightError(err error) error { if err == nil { return nil } var failure *forge.PreflightFailure if !errors.As(err, &failure) { return err } kind := DeliveryPreflightUnavailable switch failure.Kind { case forge.PreflightInvalidDestination: kind = DeliveryPreflightInvalidDestination case forge.PreflightMissingCredential: kind = DeliveryPreflightMissingCredential case forge.PreflightInvalidCredential: kind = DeliveryPreflightInvalidCredential case forge.PreflightInsufficientPermissions: kind = DeliveryPreflightInsufficientPermissions case forge.PreflightInaccessibleDestination: kind = DeliveryPreflightInaccessibleDestination case forge.PreflightUnavailable: kind = DeliveryPreflightUnavailable } return &DeliveryPreflightError{Kind: kind, Err: failure.Err} } func deliveryAssets(assets []internaldelivery.AssetIdentity) []DeliveryAsset { result := make([]DeliveryAsset, 0, len(assets)) for _, asset := range assets { result = append(result, DeliveryAsset{Filename: asset.Filename, SHA256: asset.SHA256}) } return result }