Skip to content

feat(aws-lambda): push a prebuilt image to ECR without Docker - #6063

Merged
miguel-heygen merged 6 commits into
mainfrom
feat/lambda-ecr-push
Oct 23, 2026
Merged

miguel-heygen merged 6 commits into
mainfrom
feat/lambda-ecr-push

Conversation

@miguel-heygen

Copy link
Copy Markdown
Collaborator

What

Adds two functions to @hyperframes/aws-lambda/sdk:

  • ensureEcrRepository({ ecr, repositoryName, tags? }) creates the ECR repository, or returns the existing one's URI when it already exists.
  • pushImageToEcr({ ecr, layoutDir, repositoryName, repositoryUri, tag, log? }) pushes a prebuilt single-platform OCI image layout (a directory) into that repository and returns { imageUri, digest, architecture, os }, where imageUri is <repositoryUri>@sha256:<manifest digest>.

Adds the @aws-sdk/client-ecr dependency on the same ^3.1146.0 line as the other AWS SDK clients. No CLI wiring yet; callers come in a follow-up.

Why

The arm64 video function will ship as a container image, and Lambda only runs images from the account's own private ECR. Users of the released CLI should not need Docker to get it there. The release will publish the image as an OCI layout; this function moves that layout into the user's ECR with the ECR API alone (no Docker, no registry HTTP calls).

Related work

First piece of the container-image deploy path. Nothing else touches ECR yet.

How

  • index.json must list exactly one image manifest (OCI or Docker v2 schema 2). An image index, attestation manifests, or zero or several manifests throw an error naming what was found.
  • Every blob (manifest, config, layers) is checked for size and streamed sha256 against its descriptor before any ECR call. A corrupt layout fails with nothing written.
  • The image config must name architecture and os; both are returned so the caller can assert the platform it expects.
  • BatchCheckLayerAvailability in batches of at most 100 digests; only blobs not AVAILABLE are uploaded: InitiateLayerUpload, parts of exactly partSize bytes read from disk one part at a time (inclusive partFirstByte/partLastByte), then CompleteLayerUpload. LayerAlreadyExistsException counts as uploaded.
  • PutImage sends the manifest's exact bytes, its media type, the tag and the digest. ImageAlreadyExistsException counts as success, so a redeploy of the same image is a no-op.
  • Uploads are sequential for now; bounded parallel layer uploads are the upgrade if push time on large layers matters.
  • The Lambda handler bundle does not import the SDK subpath, so the new dependency stays out of the handler zip.
  • bun.lock: besides the new entry, bun install refreshed the stale workspace version fields and dropped an unused date-fns entry.

Test plan

  • Unit tests added: pushImageToEcr.test.ts runs the real code path against an in-memory ECR that behaves like ECR where it matters. It tracks uploads per upload id, rejects out-of-order or non-contiguous parts, a wrong partLastByte, or a short non-final part, re-hashes the assembled bytes on CompleteLayerUpload, rejects PutImage when a referenced blob is missing, and caps availability checks at 100 digests. The fixture layout is built in a temp dir: one config, a 21-byte layer and a 17-byte layer, with a 7-byte part size.
    • bun test src/sdk/pushImageToEcr.test.ts: exit 0, 11 pass / 0 fail, three runs in a row.
    • bun test src/handler.test.ts: exit 0, 31 pass / 0 fail.
    • tsc --noEmit in packages/aws-lambda: exit 0. oxlint: 0 warnings, 0 errors. oxfmt --check: clean.
    • Each guarded behaviour was broken once to check that its test catches it (then reverted):
      • partLastByte off by one: the 5 tests that upload went red.
      • Skipping the pre-upload digest check: the tampered-blob test went red.
      • Raising the check batch size past 100: the batching test went red.
      • Rethrowing LayerAlreadyExists, ImageAlreadyExists or RepositoryAlreadyExists: in each case only the matching test went red.
      • Dropping the one-manifest check: the two-manifest test went red.
      • Dropping the architecture/os check: the config test went red.
      • Ignoring availability: the skip and redeploy tests went red.
  • Manual testing performed. Not exercised: no call to a real ECR, and Lambda pulling the pushed image is proven in a later PR.
  • Comments follow CONTRIBUTING.md "Comments"

@github-actions

github-actions Bot commented Oct 22, 2026 •

Copy link
Copy Markdown
Contributor

Edit accuracy: accurate 271 (base branch 271), smooth 196 of those

The gate passes.
Smoothness is reported in the artifact, not gated. A case fails only if it fails 2 of 3 runs.

Quarantined, measured but not gated (0)

@heygengenesis heygengenesis Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verdict: APPROVE · 0 blocker · 0 important · 2 nits · head 8e7f3c27

  • nit: pushImageToEcr.ts:230 — a 0-byte blob skips the part loop and calls CompleteLayerUpload with no parts, which ECR rejects (EmptyUploadException). Real image layouts don't contain 0-byte blobs, so only worth a clear error if you touch this again.
  • nit: the PR body's bun.lock note (refreshed workspace versions, dropped date-fns) no longer matches the diff, which is +3/−0 in bun.lock.
Scope

Read pushImageToEcr.ts end to end, plus the build.mjs external, package.json, sdk/index.ts exports and the lockfile entry; skimmed the test fake. Checked: the one-manifest gate, digest/size verification before any ECR call, de-duplicated layer digests, the 100-digest batching, inclusive part byte ranges, the file handle closed in finally, idempotent PutImage (a moved tag on an immutable repository still throws ImageTagAlreadyExists), and refusing to adopt a repository whose tags don't match. All 8 Actions workflows on this head passed. The PR adds an SDK primitive only; nothing in the release workflow calls it yet.

— Genesis

@miguel-heygen
miguel-heygen added this pull request to the merge queue Oct 23, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Oct 23, 2026
@miguel-heygen
miguel-heygen added this pull request to the merge queue Oct 23, 2026
@miguel-heygen
miguel-heygen removed this pull request from the merge queue due to a manual request Oct 23, 2026

@heygengenesis heygengenesis Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verdict: APPROVE · 0 blocker · 0 important · 0 nits · 8e7f3c27 → 39c003e7

Re-review after the rebase. Commits 1–5 have the same patches as the head I approved before (git range-diff). The main commits they now sit on don't touch build.mjs, sdk/index.ts or the dependency list, and they leave sha256File the same. The budget commit sets @hyperframes/aws-lambda to the measured size, the same way #6055 did, and verify:packed-manifests passes at this head. The two nits from the earlier review still apply and are optional.

Adds pushImageToEcr and ensureEcrRepository to the SDK subpath. The push
reads a single-platform OCI image layout, verifies every blob against its
descriptor before any ECR call, uploads only the blobs ECR lacks through
the layer-upload API, and puts the manifest under a tag, returning the
digest-pinned image URI.
Like the other AWS SDK clients, @aws-sdk/client-ecr is a dependency the consumer
resolves. Bundled into the ESM SDK it hits a dynamic require of node:https and
the packed package fails to import.
pushImageToEcr ships in dist (unminified chunk plus its .d.ts); no source maps, tests or release-only files are involved. Built dist, npm pack --dry-run, main versus main plus this branch: +2486 B packed, +8846 B unpacked. The merge-group run measured 73369 packed and 266805 unpacked, which are the new budgets (were 70874 and 258166).

@heygengenesis heygengenesis Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verdict: APPROVE · 0 blocker · 0 important · 0 nits · 39c003e7 → e9d37534

Rebase only. git range-diff shows commits 2–5 are identical. Commit 1 differs only in the sdk/index.ts export context: the prebuiltHandler exports from #6070 and the new pushImageToEcr exports are both kept. Commit 6 changes only its message. The net diff against the new base matches the approved head, and CI passes on e9d37534, including the package size check.

@miguel-heygen
miguel-heygen added this pull request to the merge queue Oct 23, 2026
Merged via the queue into main with commit 0df0889 Oct 23, 2026
77 checks passed
@miguel-heygen
miguel-heygen deleted the feat/lambda-ecr-push branch October 23, 2026 04:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant