/** * The containers of a service's live task definition that run the service's * own build, which a code-only deploy, a rollback and a restart change and * record, and no others (FJL-1336). * * Every Fjall build of an application pushes into one repository, * `ecrRepoUri(account, region, appName)`. A container runs the service's * build when its image is in that repository and it is not the * `fjall-schema-gate` container. The choice reads each container's image, * never where it is listed: ECS lists one service's containers in either * order across revisions. The lanes gave every container the app's digest or * tag in that container's own repository, the schema gate and any sidecar * included, and looked the tag up in the repository of whichever container * ECS listed first. * * A roll also refuses a tag whose image a container it keeps runs from the * app's repository, since the containers that run the service's build would * take an image that is not theirs. Such a container is the schema gate, * when `schemaGate.image` names an image there, or one whose image there is * written `:@`, which the choice never reads as the * service's build. No other container a selection keeps runs an image from * the repository, and neither does a gate on its default mirror, so none of * them is looked up. The check compares digests, however either image is * written: the one ECR gives for the rolled tag, and the one each such * container's image names after `@`, or that ECR gives for its tag. When * either digest is unknown it refuses, since it cannot tell the two images * apart. * * Limits (2026-10-13): * - A container whose image is a fixed URL into the app's repository, * written with a tag of its own or a digest alone, such as a pinned * migration image, cannot be told from the service's build by its image, * so the service is refused as ambiguous unless that container runs the * same image. * - A container in another repository that the service's image-tag * parameter governs (an image named by repository name, or by a reference * the constructs do not read as a registry's) is kept as it runs. * - The choice reads an image written `:@` as * outside the app's repository, so a service whose build runs only such * images is refused as having none. Fjall never registers that form for * the containers that run the service's build; a container the roll keeps * can run one from the app's repository, and is checked by its digest. * - A rollback to a release recorded before the gate's image last changed. * With `schemaGate.image` in the app's repository, a restart through 39.0.7 * that saw the gate listed first recorded the gate's tag as the service's * image, and `serviceImageTagsFromRelease` admits that release, since the * image is in the app's repository. Once the gate runs another image, the * tag names an image no live container runs, so this check admits it too, * and the roll gives the gate's earlier image to the containers that run * the service's build. That image runs to completion and exits, and the * lane can end any of these ways: * (a) the task exits before it is RUNNING: ECS's circuit breaker, which * Fjall turns on by default, rolls the deployment back, and the lane fails; * (b) it exits after RUNNING but before ECS counts it healthy, or a load * balancer target never passes: the lane's 600 s poll times out, and the * crash-looping deployment is left running; * (c) it outlives 40 s after RUNNING, on a service with no health check or * with a minimum healthy percent of 0: the deployment can complete, the * lane reports success, and the hold reconciles the service's image tag to * the gate's. * TODO 2026-10-23, owner: Claude — tracked in FJL-1383 */ import type { ContainerDefinition } from "@aws-sdk/client-ecs"; import type { Result } from "@fjall/generator"; import { type EcrImageInspectorService } from "../../services/infrastructure/EcrImageInspectorService.js"; declare const appImageSelectionBrand: unique symbol; /** * The containers of one live revision that run the service's own build, all * running `liveImage`. Only `selectAppImageContainers` makes one, and it * carries the revision it chose from, so the containers it names are always * that revision's. */ export interface AppImageSelection { readonly kind: "found"; readonly appRepositoryUri: string; readonly liveImage: string; /** The chosen containers, by identity within `containerDefinitions`. */ readonly appContainers: ReadonlySet; /** Their names, for logs only. */ readonly appContainerNames: readonly string[]; /** The revision's containers, as ECS returned them. */ readonly containerDefinitions: readonly ContainerDefinition[]; readonly [appImageSelectionBrand]: true; } export type AppImageRefused = { readonly kind: "none"; readonly appRepositoryUri: string; /** Every container, and the repository its image is in. */ readonly containers: ReadonlyArray<{ readonly name: string; readonly repository: string | undefined; }>; } | { readonly kind: "ambiguous"; readonly appRepositoryUri: string; /** Every container in the app's repository, and the image it runs. */ readonly images: ReadonlyArray<{ readonly name: string; readonly image: string; }>; }; export type AppImageVerdict = AppImageSelection | AppImageRefused; /** * Whether `image` is in the app's image repository: the image less its tag or * digest is exactly `appRepositoryUri`. A repository the app's name only * prefixes, or one of that name in another account or region, is not. */ export declare function isInAppRepository(image: string, appRepositoryUri: string): boolean; /** * Choose the containers of a live revision that run the service's own build * from the app's repository. `none` when no container other than the schema * gate runs an image there; `ambiguous` when those that do run more than one * image, compared as written, so one image spelt by tag and by digest is two. */ export declare function selectAppImageContainers(containerDefinitions: readonly ContainerDefinition[], appRepositoryUri: string): AppImageVerdict; /** * The selection's revision with `image` given to the containers that run the * service's build. Every other container is the object ECS returned. */ export declare function withAppImage(selection: AppImageSelection, image: string): ContainerDefinition[]; /** What the lane learnt of an image's digest. */ export type ImageDigestAnswer = { readonly kind: "known"; readonly digest: string; } /** ECR holds no image under the tag: `why` is the lookup's own message. */ | { readonly kind: "absent"; readonly why: string; } /** The lookup failed any other way: `why` is its own message. */ | { readonly kind: "failed"; readonly why: string; } /** The image is in a registry the inspector reads no digest from. */ | { readonly kind: "unreadable"; }; /** * A container the roll keeps whose image is in the app's repository, and * what the lane learnt of that image's digest. */ export interface KeptAppRepositoryImage { readonly name: string; readonly image: string; readonly digest: ImageDigestAnswer; } /** * Why a roll to `imageTag` rolled nothing of a service, when the rolled image * may be one that a container the roll keeps runs from the app's repository: * the two have one digest, however each is written, or the rolled tag's * digest or that container's is unknown, so the two cannot be told apart. A * kept container never runs the service's build, so the roll would give the * build's containers an image that is not theirs. Each refusal ends with a * cure for what the lane learnt: another release or tag when the rolled tag * names a kept image or ECR holds no image under it; the kept image pushed * again, or its container given another image, when ECR holds none under a * kept image's tag, since a retry and every other tag make that same lookup; * a retry when a lookup failed any other way; and a full deploy when the * registry yields no digest at all. * `undefined` when the rolled digest and every kept image's digest are known * and differ, or the roll keeps no container in the app's repository. Not * masked, and it can carry a failed lookup's own message: each caller masks * at its sink. */ export declare function keptImageRefusal(serviceName: string, imageTag: string, rolled: ImageDigestAnswer, kept: readonly KeptAppRepositoryImage[]): string | undefined; /** * The refusal `keptImageRefusal` gives a roll of the selection's service to * `imageTag`, whose lookup in the app's repository answered `rolledDigest`, * once the digests of the images the roll keeps from that repository are * read, each lookup with `abortSignal`. Its callers make this check once * they have resolved the rolled digest, before they register anything. Not * masked: each caller masks at its sink. */ export declare function checkKeptImages(inspector: Pick, serviceName: string, selection: AppImageSelection, imageTag: string, rolledDigest: Result, abortSignal?: AbortSignal): Promise; /** * The containers a refusal names: for `none` each container and its * repository, the schema gate marked as such, for `ambiguous` each container * in the app's repository and its image. Identifier-only and not masked: each * caller masks at its sink. */ export declare function appImageEvidence(verdict: AppImageRefused): string; /** * Why a code-only deploy or rollback rolled nothing of a service. * Identifier-only and not masked: each caller masks at its sink. */ export declare function appImageRefusal(serviceName: string, taskDefinitionArn: string, verdict: AppImageRefused): string; export {};