249 lines
9.9 KiB
Go
249 lines
9.9 KiB
Go
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
|
|
}
|