From aa03a784762d62a08e88b3b5740bdabfc7b146a9 Mon Sep 17 00:00:00 2001 From: Eric Rakestraw Date: Sun, 1 Nov 2026 17:52:38 +0000 Subject: [PATCH] Define the internal forge delivery contract --- docs/roadmap/implementation.md | 5 +- internal/forge/forge.go | 152 +++++++++++++++++++++++++++++++++ 2 files changed, 155 insertions(+), 2 deletions(-) create mode 100644 internal/forge/forge.go diff --git a/docs/roadmap/implementation.md b/docs/roadmap/implementation.md index 27e3d3a..2ad1376 100644 --- a/docs/roadmap/implementation.md +++ b/docs/roadmap/implementation.md @@ -1,6 +1,7 @@ # Implementation plan: neutral forge delivery and credential preflight -Status: ready for implementation; all stages are pending. +Status: implementation in progress; Stage 1 is complete and Stages 2–10 are +pending. ## Purpose and execution @@ -219,7 +220,7 @@ buffering of complete binaries. ## Stage 1: Establish the internal backend contract -Status: pending. +Status: complete. 1. Add `internal/forge` with the interface and logical values specified above. Define preflight classifications, neutral failures, and write-evidence types diff --git a/internal/forge/forge.go b/internal/forge/forge.go new file mode 100644 index 0000000..30e3721 --- /dev/null +++ b/internal/forge/forge.go @@ -0,0 +1,152 @@ +// Package forge defines the backend-neutral operations used by hosted delivery. +package forge + +import ( + "context" + "io" +) + +// Backend provides the hosted operations needed by the shared delivery +// workflow. Implementations own destination-specific rules and protocol +// details; the shared workflow uses only the logical values and evidence in +// this package. +type Backend interface { + // Preflight verifies credentials and access to the configured destination + // using read-only operations. It does not depend on a source tag or release. + Preflight(ctx context.Context) error + // TagCommit returns the commit observed for the complete tag. + TagCommit(ctx context.Context, tag string) (string, error) + // FindRelease returns the release for the complete tag. A missing release + // is reported with FailureNotFound. + FindRelease(ctx context.Context, tag string) (Release, error) + // CreateRelease creates the requested release and returns the protocol + // evidence even when an error occurs. + CreateRelease(ctx context.Context, request CreateReleaseRequest) (Release, WriteEvidence, error) + // ListAssets returns the assets attached to the release identified by handle. + ListAssets(ctx context.Context, releaseHandle any) ([]Asset, error) + // OpenAsset opens the remote asset identified by handle. The caller owns and + // must close the returned stream, including when reading it fails. + OpenAsset(ctx context.Context, assetHandle any) (io.ReadCloser, error) + // UploadAsset uploads content to the release identified by handle. The + // backend borrows content for the duration of this call and must finish any + // request or multipart work before returning; it must not close content. + UploadAsset(ctx context.Context, releaseHandle any, filename string, content io.Reader) (WriteEvidence, error) +} + +// Release contains the logical release properties used by shared delivery. +// Handle is opaque to shared code and is interpreted only by the backend that +// produced it. +type Release struct { + URL string + Tag string + Title string + Body string + Draft bool + Prerelease bool + Handle any +} + +// CreateReleaseRequest contains the release properties requested by shared +// delivery. +type CreateReleaseRequest struct { + Tag string + Commit string + Title string + Body string + Draft bool + Prerelease bool +} + +// Asset contains the remote properties needed to compare an attachment with a +// declared local asset. Handle is opaque to shared code and is interpreted only +// by the backend that produced it. +type Asset struct { + Filename string + Size int64 + Handle any +} + +// PreflightFailureKind classifies a failure to verify credentials or access to +// a delivery destination. +type PreflightFailureKind string + +const ( + PreflightMissingCredential PreflightFailureKind = "missing_credential" + PreflightInvalidCredential PreflightFailureKind = "invalid_credential" + PreflightInsufficientPermissions PreflightFailureKind = "insufficient_permissions" + PreflightInaccessibleDestination PreflightFailureKind = "inaccessible_destination" + PreflightUnavailable PreflightFailureKind = "unavailable" +) + +// PreflightFailure reports a classified credential or destination access +// failure. Err retains the underlying cause for diagnostics and cancellation +// identity. +type PreflightFailure struct { + Kind PreflightFailureKind + Err error +} + +func (f *PreflightFailure) Error() string { + if f.Err != nil { + return string(f.Kind) + ": " + f.Err.Error() + } + return string(f.Kind) +} + +// Unwrap returns the cause of the preflight failure, if any. +func (f *PreflightFailure) Unwrap() error { return f.Err } + +// FailureKind classifies a backend-neutral delivery operation failure. +type FailureKind string + +const ( + FailureInvalid FailureKind = "invalid_request" + FailureNotFound FailureKind = "not_found" + FailureUnauthorized FailureKind = "unauthorized" + FailureConflict FailureKind = "conflict" + FailureMalformed FailureKind = "malformed_response" + FailureUnverifiable FailureKind = "unverifiable" + FailureTransport FailureKind = "transport" + FailureRejected FailureKind = "rejected" + FailureUnknown FailureKind = "unknown" +) + +// Failure reports a backend-neutral operation failure. Backend-specific +// status and operation details belong in Err. Err is retained for errors.Is +// and errors.As inspection. +type Failure struct { + Kind FailureKind + Err error +} + +func (f *Failure) Error() string { + if f.Err != nil { + return string(f.Kind) + ": " + f.Err.Error() + } + return string(f.Kind) +} + +// Unwrap returns the cause of the operation failure, if any. +func (f *Failure) Unwrap() error { return f.Err } + +// WriteDisposition describes the backend's evidence about a write operation. +type WriteDisposition string + +const ( + WriteNotAttempted WriteDisposition = "not_attempted" + WriteAccepted WriteDisposition = "accepted" + WriteRejected WriteDisposition = "rejected" + WriteConflict WriteDisposition = "conflict" + WriteUncertain WriteDisposition = "uncertain" +) + +// WriteEvidence records whether a write was handed to the transport and what +// its acknowledgment establishes. Accepted means the backend received and +// decoded the required protocol acknowledgment; shared content checks still +// apply. Uncertain means the outcome could not be established. Attempted marks +// handoff to the transport and does not claim the server received the request. +// Evidence remains meaningful when the operation also returns an error. +type WriteEvidence struct { + Attempted bool + Disposition WriteDisposition +}