import type { Result } from "@fjall/generator"; import type { DockerBuild } from "@fjall/util/manifest/schemas"; import type { BuildxProgressEvent } from "@fjall/util/docker"; export interface DockerServiceConfig { name: string; docker: DockerBuild; } /** * A build-time secret RESOLVED for buildx — the value has been fetched JIT and * is now addressed either by a 0600 temp-file path (`kind: "file"`) or a * build-host env-var name (`kind: "env"`). This shape carries NO secret value * itself; it points buildx at where to read one for a `--secret` mount. It maps * 1:1 onto `BuildxBuildArgs.secrets[].source` (the providers project it verbatim). */ export interface ResolvedBuildSecret { id: string; source: { kind: "file"; path: string; } | { kind: "env"; name: string; }; } export interface DockerBuildParams { appName: string; appPath: string; region: string; accountId: string; imageTag?: string; services?: DockerServiceConfig[]; platform?: string; /** * When provided, the provider constructs registry-mode buildx cache args * (`--cache-from type=registry,ref=:` and * `--cache-to type=registry,ref=:,mode=`) so * incremental builds reuse layers across CI runs and developer machines. * The cache-to mode defaults to `max`; providers may resolve it per * environment (the CLI provider honours `FJALL_BUILD_CACHE_MODE` via * `resolveBuildCacheMode` at `@fjall/util`, omitting `--cache-to` entirely * for `off` while keeping `--cache-from` — reading a cache is free). * The orchestrator is responsible for ensuring the `-cache` repo exists * before invoking the provider; missing repos surface as a buildx push error. */ cacheRepoUri?: string; /** * Per-service identifier used as the cache-tag suffix when `cacheRepoUri` * is set. When omitted the cache tag falls back to `appName.toLowerCase()`, * which keeps single-service builds working without orchestrator changes. */ serviceName?: string; /** Optional Dockerfile target stage (used as part of the build identity). */ target?: string; /** Optional explicit Dockerfile path (defaults to `/Dockerfile`). */ dockerfilePath?: string; /** Optional explicit build context path (defaults to `appPath`). */ buildContext?: string; /** * Optional explicit ECR tag list. When provided, the provider pushes every * tag in one `buildxBuild` invocation (avoiding rebuilds for retag-only * cases). When omitted, the provider falls back to a single tag derived * from `imageTag` / `appName`. */ imageTags?: readonly string[]; /** * Push the image to ECR addressed by digest only, creating no tag. The * caller derives a content-hash tag from `DockerBuildResult.imageDigest` and * applies it via `tagByDigest`. This is the deploy path's default: it keeps * the ECR repo IMMUTABLE (a moving `-latest` tag would collide on the second * push; digest-only pushes and unique content-hash tags never do). When set, * `imageTags` is ignored. */ pushByDigest?: boolean; /** Optional buildArgs forwarded to buildx. */ buildArgs?: Readonly>; /** * Optional build-time SECRETS, already RESOLVED just-in-time by deploy-core's * `resolveBuildSecrets`. Each entry points buildx at a 0600 temp file or a * build-host env var for a `--secret` mount — never a `--build-arg`, so the * value never enters an image layer, `docker history`, or provenance. The * provider projects these 1:1 onto `BuildxBuildArgs.secrets`. */ buildSecrets?: readonly ResolvedBuildSecret[]; /** * True when this build group contains at least one container-Lambda image. * Lambda's `CreateFunction`/`UpdateFunctionCode` rejects an OCI image index * with attestation manifests — the shape buildx produces by default when * pushing (`--provenance=true`/`--sbom=true`) — so the provider must * disable both for a Lambda-targeted build. ECS tolerates either shape, so * a mixed ECS+Lambda group (sharing one Dockerfile/context/target) disables * both for the whole group rather than trying to push two variants. */ targetsLambda?: boolean; /** * Caller abort signal (deploy cancel, worker SIGTERM, wave sibling-failure * controller). CANONICAL RATIONALE — providers thread this to the underlying * buildx subprocess so an in-flight build/push can be interrupted instead of * running minutes past the cancellation. Killing buildx mid-push is safe: * ECR deduplicates blobs so a retry re-uploads only what is missing, and the * registry cache tag is mutable by design. Absent → the build runs to * completion (pre-2.34 behaviour). */ abortSignal?: AbortSignal; } export interface DockerBuildResult { imageUri: string; imageTag: string; /** * Content-addressed digest of the pushed buildx artifact. Required: every * consumer (CLI + worker `DockerProvider`s) MUST surface this so the * orchestrator can derive content-hash tags via * `deriveContentHashTag(digest, serviceName, target?)` without * re-inspecting the registry. */ imageDigest: string; services?: Array<{ name: string; imageUri: string; }>; } export interface ECRInitParams { appName: string; region: string; accountId: string; } export interface ECRInitResult { repositoryUri: string; } /** * Parameters for tagging existing ECR images for ECS services. * Used on the non-Dockerfile welcome-image path: a create-once, * identical-manifest copy of the seeded `:latest` image to * `:-latest`, safe on IMMUTABLE repositories. Those tags are * read only by stacks synthesised before 2026-05-13 — newer stacks default * their ImageTag parameter to `:latest`. */ export interface TagImagesParams { appName: string; appPath: string; region: string; accountId: string; } export interface TagImagesResult { taggedServices: string[]; } /** Callback for Docker build/push progress reporting. */ export type DockerProgressCallback = (message: string, percentage?: number, layerId?: string, status?: string) => void; /** * Parameters for adding tags to an already-pushed image digest without * re-uploading layers. Used by the orchestrator to apply the content-hash * tag derived from a buildx push. Implementations must carbon-copy the * source's flat manifest (not wrap it in a new image index) — Lambda * rejects an index outright, and fjall never combines multiple * platform-specific digests under one tag here. */ export interface TagByDigestParams { /** Fully-qualified ECR repository URI without a tag suffix. */ sourceImage: string; /** sha256:... digest returned by `buildAndPush`. */ digest: string; /** Fully-qualified tag references to alias onto the digest. */ tags: readonly string[]; } /** * Interface for Docker operations (build, push, ECR initialisation). * CLI provides CliDockerProvider, worker provides WorkerDockerProvider — * both wrap @fjall/util DockerCli (buildx CLI subprocess). */ export interface DockerProvider { /** * Optional one-shot environment preflight, called ONCE at the top of a * multi-group run before any group builds. Implementations verify the docker * daemon is reachable and buildx is available, failing closed so a missing * toolchain surfaces once up-front rather than mid-wave. Absent → skipped * (today's behaviour). The CLI provider implements it; the webapp worker * counterpart is a release-coupled follow-up (worker repo). */ preflight?(): Promise>; /** * Optional ECR session bracket. When present, the orchestrator opens ONE ECR * session for the whole docker phase — `beginEcrSession` once before the group * loop, `endEcrSession` once after — so every `buildAndPush` / `tagByDigest` * in the phase reuses a single login instead of each op self-scoping its own * (which, on macOS, re-touches the osxkeychain and can hit the -25299 stale- * entry block). Absent → each op self-scopes (today's behaviour); the * optional-chained call makes the addition backward-compatible while a * consumer that hasn't yet implemented these ships. Providers implement both * or neither. Token acquisition stays provider-side (the params carry only the * account/region the provider needs to mint its own ECR authorisation). */ beginEcrSession?(params: { accountId: string; region: string; /** * Declared from birth per robustness-standards § "Optional-capability seam * methods carry `abortSignal?`" — implementers compose it with their SDK * timeouts via `composeSdkAbortSignal`; ignoring it is permitted. */ abortSignal?: AbortSignal; }): Promise>; endEcrSession?(): Promise; buildAndPush(params: DockerBuildParams, onProgress?: DockerProgressCallback, onBuildxEvent?: (event: BuildxProgressEvent) => void): Promise>; initialiseECR(params: ECRInitParams): Promise>; /** * Apply additional tags to a pushed digest, as a flat-manifest carbon * copy (never an image index). The orchestrator uses this to attach the * content-hash tag immediately after `buildAndPush` returns, without * re-uploading layers. */ tagByDigest(params: TagByDigestParams): Promise>; /** Tag existing ECR images for ECS services (non-Dockerfile apps). Optional. */ tagImages?(params: TagImagesParams, onProgress?: DockerProgressCallback): Promise>; }