// Package upload provides producer-facing helpers for submitting distributor // source bundles to the HTTP upload API. // // The package is intended for Go producer applications that already create // reports or other Markdown bundle contents and want to hand those bundles to a // running distributor server. It builds on pkg/bundle for manifest generation, // path validation, digest calculation, local bundle writing, and local bundle // validation. It does not expose distributor internals, server configuration, // storage backends, destination state, or publish behavior. // // # Client Construction // // NewClient creates a Client from ClientOptions. Endpoint is required and must // be an http or https distributor server base URL without userinfo, query, or // fragment. The client derives /v1/pipelines//upload for // submissions and /runs/ for status checks. Token is required and is // sent as Authorization: Bearer . Token values are redacted from errors // produced by the client. // // HTTPClient is optional. When omitted, the package uses a client with a // conservative timeout. Retry is optional; zero values select safe defaults. // RetryOptions.MaxAttempts, BaseDelay, and MaxDelay must not be negative, and // MaxDelay must be greater than or equal to BaseDelay. // // # Upload Workflows // // UploadBundle uploads an existing local source bundle root to the configured // PipelineID. PipelineID is required and must match the server's slug-like // pipeline id syntax. The root must contain manifest.json. By default, // UploadBundle loads the manifest and validates the complete local bundle with // pkg/bundle before making any HTTP request. The generated gzip-compressed tar // archive contains manifest.json and exactly the manifest-listed files; // unlisted files are not uploaded. // // UploadFiles is the convenience workflow for producer applications that have // generated files but have not yet assembled a bundle directory. PipelineID is // required and selects the configured distributor workflow. UploadFiles uses // pkg/bundle to create a temporary complete bundle from explicit // bundle.BundleFile values, validates it by default, archives it, uploads it, // and removes temporary files when the call returns. UploadFiles does not write // into producer source directories. A zero Created timestamp follows // pkg/bundle defaulting behavior. // // The producer contract has four separate identifiers: the bearer token // authenticates the client, PipelineID selects the distributor workflow, the // source manifest ID identifies the logical artifact within that workflow, and // IdempotencyKey identifies one producer run and retry group. // // Validation is enabled by default. Set DisableValidation when the application // has already performed equivalent local validation and wants to skip the // package's validation step. Validate and DisableValidation must not both be // true. // // # Idempotency And Retry // // Every upload request includes Idempotency-Key. If UploadBundleOptions or // UploadFilesOptions provides IdempotencyKey, the client validates and uses // that value. Otherwise, it generates a random 128-bit lowercase hexadecimal // key once for that upload operation and reuses it for all retries from that // call. // // Generated idempotency keys are useful for retrying transient failures within // a single process call. Producers that need cross-process retry safety should // provide their own stable key, such as a key derived from the producer job id // or report id. Valid keys are non-empty ASCII strings up to 128 bytes using // letters, digits, '.', '_', '-', and ':'. // // The client retries only safe cases: 503 Service Unavailable, temporary // network errors, and ambiguous mid-upload failures. Retries use the same // idempotency key and replayable gzip archive body. The client does not retry // 400, 401, 409, 413, 415, or any response after 202 Accepted. Context // cancellation is honored before each attempt and while waiting between // retries. // // # Results And Errors // // Result represents upload admission. A successful UploadBundle or UploadFiles // call means the server accepted the upload and returned a run id; it does not // mean the asynchronous distribution run has finished successfully. // // Status fetches the current server status for a run id and returns RunStatus. // This is a separate polling helper; upload calls do not wait for publication // completion. // // Non-2xx upload and status responses return *HTTPError when the server status // can be represented as an HTTP failure. HTTPError includes the numeric status // code, HTTP status string, response message, and server retryable flag when // present. A 409 Conflict response is returned as *IdempotencyConflictError, // which wraps HTTPError and can be detected with errors.As. // // Example: upload producer files with a stable idempotency key. // // ctx := context.Background() // client, err := upload.NewClient(upload.ClientOptions{ // Endpoint: "https://distributor.example.com", // Token: os.Getenv("DISTRIBUTOR_UPLOAD_TOKEN"), // }) // if err != nil { // return err // } // // result, err := client.UploadFiles(ctx, upload.UploadFilesOptions{ // PipelineID: "reports.daily", // ID: "reports.daily.2026-06-06", // IdempotencyKey: "reports.daily.2026-06-06", // Files: []bundle.BundleFile{ // {SourcePath: "/tmp/report.md", Path: "report.md"}, // {SourcePath: "/tmp/summary.txt", Path: "summary.txt"}, // }, // }) // if err != nil { // var conflict *upload.IdempotencyConflictError // if errors.As(err, &conflict) { // return fmt.Errorf("upload conflicts with an earlier different bundle: %w", err) // } // return err // } // // status, err := client.Status(ctx, result.RunID) // if err != nil { // return err // } // _ = status // // Example: upload an existing bundle root. // // result, err := client.UploadBundle(ctx, upload.UploadBundleOptions{ // PipelineID: "reports.daily", // Root: "/var/lib/reports/daily-2026-06-06", // IdempotencyKey: "reports.daily.2026-06-06", // }) // if err != nil { // return err // } // _ = result package upload