Skip to content

CMS editor image: build-time reads, sharp in standalone images, arm64 pairing, image smoke test - #331

Merged
jpslav merged 65 commits into
int-202609-afrom
int-202609-cms-image
Sep 14, 2026
Merged

jpslav merged 65 commits into
int-202609-afrom
int-202609-cms-image

Conversation

@jpslav

@jpslav jpslav commented Sep 13, 2026

Copy link
Copy Markdown
Contributor

Merges the CMS editor image epic (int-202609-cms-image) into int-202609-a. An adopter's first image built from Dockerfile.cms.template failed at next build, then answered 500 on every route, and nothing kept the image platform and Lambda architecture in step. Spec, decisions and as-built notes: .claude/future-tasks/cms-image-build-epic.md. Each PR below had its own review rounds and claim-check.

Merged

#332: the scaffolded CDK app is type-checked again

#328's infrastructure/ exclusion left the CDK app unchecked, because cdk.json runs it through tsx. init-deploy aws now scaffolds infrastructure/tsconfig.json, which extends the project's tsconfig.json and turns off verbatimModuleSyntax, exactOptionalPropertyTypes, noPropertyAccessFromIndexSignature, incremental and composite. The workflow template runs npx tsc --noEmit -p infrastructure before AWS credentials and also triggers on tsconfig.json. scaffold-synth.test.ts runs that command and expects TS2561 on a misspelled prop. README says to make withCanopy() the outermost config wrapper. Left open: scaffold-cdk-typecheck-published-shape.md.

For adopters

docs/adopter-migration.md has one entry. It is breaking for a stack that sets platform on fromImageAsset without architecture.

Known limitation (pre-existing)

A webpack build with pnpm bundles sharp, so image transforms fail; Turbopack, Next 16's default, is unaffected (webpack-standalone-sharp-bundled.md).

Review status

  • Code review (round 1, Fable, whole diff at d63d4367): no finding at MEDIUM or above. One LOW (the smoke test's sitemap check can't tell the working-tree copy from the branch copy) is filed as item 5 of cms-image-pr5-review-followups.md.
  • Claim-check over the whole diff: three verifiers; every correction landed as one commit, c32410eb, via docs: claims pass over the CMS image epic #335 (merge 6777c8e9, CI green). No code defects. The unmeasured pnpm-workspace.yam[l] wildcard COPY was measured and works without the file.
  • Mechanical re-check of that commit: clean. One leftover table row fixed via docs: mechanical re-check of the claims pass (#335) #336 (5fd4869e, merge af979ee1, CI green).
  • Base merged in: int-202609-a (89620302, 10 PRs since 8b854c18) was merged into the integration branch with a merge commit via Merge int-202609-a into int-202609-cms-image #341 (540ad4ed). Four conflicts, all resolved keeping both sides; two clean-merge interactions fixed (a duplicate node:path import in cms-deploy.test.ts, and an arm64 test reading a cdk.out another test re-synths); the three duplicate examples/aws-deployment drift tasks consolidated into example-aws-deployment-drift-from-template.md. Merge int-202609-a into int-202609-cms-image #341 had its own code review (clean) and claim-check.
  • CI on this PR: green at 540ad4ed — 11/11, including all three standalone-image legs (pnpm and npm on ubuntu-latest, pnpm on ubuntu-24.04-arm) and the four E2E shards.
  • In progress: a post-merge review of the interactions between the two epics (the files both touched and their callers), then a claim-check.

Commit messages with wrong claims (history not rewritten)

bd9c7653 (corrected by 7807bcf0), 3d7434e0 (by 6d6812c1), 7240ab1a (see the #328 notes), b4d7eb1e (a docs(tasks) subject on a commit that also carries code).

Further notes from PR 5 (#328)

  • Commit 7240ab1a overclaims in its message: it says the CI path filter covered "all source and packaging inputs" (the tsconfigs, root package.json and .nvmrc came later, in d10a5b0b) and that the container log tail prints on every exit path (it prints on failure or early stop). Later commits corrected the code and docs; history was not rewritten.
  • Both red-before-green defects (base-branch build failure; untraced libvips) were reproduced on local arm64 Docker only. The amd64 legs have only been run green.
  • turbopack: null in an adopter config is treated as unset and gets {}, matching Next's own guard and withCanopy's existing isSet rule.
  • Still unmeasured: what an in-workspace (monorepo) build does with sharp.

Sanitized epic spec for the six-PR CMS editor image epic, cut from the
adopter build-failure/runtime-500/architecture-mismatch investigation:

1. build-time reads come from the working tree (fix/build-reads-working-tree)
2. sharp loads lazily (fix/sharp-lazy-load)
3. withCanopy traces libvips into standalone output (fix/sharp-standalone-tracing)
4. CanopyCmsService always passes a resolved architecture, arm64 default (fix/cms-service-architecture)
5. CI image smoke test (ci/standalone-image-smoke)
6. adopter-answer docs (docs/cms-image-adopter-answers)

Cross-links added from the four existing task files this epic addresses.
CanopyCmsService passed `props.architecture` straight through, so by default
the function got none. DockerImageFunction binds its image code with that
architecture, and for `DockerImageCode.fromImageAsset` that is what sets the
Docker build platform: with none, CDK set no platform and Docker built for
whatever machine ran `cdk deploy`. An Apple Silicon deploy produced an arm64
image for an x86_64 function, which deploys clean and fails at invoke.

- Resolve `architecture` to ARM_64 when unset and always pass it, matching
  the arm64 worker and transform Lambda. No new API.
- Scaffold stack template and example stack: drop `platform` and its import;
  `architecture` is the single source. The example also gains the
  `NEXT_PUBLIC_CANOPY_MODE: 'prod'` build arg the template already had.
- Generated deploy workflow (template and example): run on
  `ubuntu-24.04-arm`, so the arm64 image builds natively instead of needing
  QEMU on an x86 runner.
- Tests: arm64 default; asset-manifest assertions that `fromImageAsset`
  without `platform` synthesizes linux/arm64 by default and linux/amd64 with
  X86_64, and that an explicit platform still wins; the scaffold synth asserts
  the image and its Lambda agree on arm64.
- docs/deploying-to-aws.md: a "Where the image is built" section.
…s platform

ARCHITECTURE.md gains a Key Design Decisions entry for why CanopyCmsService
always resolves an architecture. DEVELOPING.md notes that a Docker build's
platform lives in the asset manifest, not the template, and points at the
Dockerfile-only fixture that lets a synth exercise it without Docker.
A claim-check pass over the whole branch, after a code-only review round
found no code defects.

- Name the error an architecture mismatch actually produces at invoke,
  Runtime.InvalidEntrypoint, in every copy, and cite it once in the deploy
  guide (with execve(2) for why a wrong-architecture binary cannot run).
- Deploy guide: sharp and libvips come from the package install, not a
  dedicated Dockerfile step; cite the arm64 runner's private-repo date and
  Docker's docs for QEMU emulation and its cost; note that the workflow's own
  dependency install now runs on arm64 Linux; say CDK, not CanopyCmsService,
  derives the platform.
- Workflow comments: an x86 runner's build would run under QEMU, which
  Docker's docs warn can be much slower, rather than asserting it is slow.
- Test helper doc: the asset manifest records the platform the image is
  built for, per the schema, rather than a CLI flag nothing here observes.
- Example stack: its workflow is deploy-cms.yml, not a generated one.
- File the pre-existing drift between examples/aws-deployment and the
  init-deploy templates as a P3 future task.
fix(cdk): always pass the resolved Lambda architecture, arm64 by default
…work

transform.ts imported sharp statically, and it sits under canopycms/server
and, via api/assets.ts, canopycms/http and canopycms-next. Turbopack emits a
top-level await of the external, so importing that graph dlopens libvips:
an adopter's standalone editor image with the libvips .so missing returned
500 on every route, 404s included. The static import also defeated
pipeline.ts's deliberate fail-open dynamic import.

- assets/sharp-loader.ts: loadSharp(), the package's only runtime load of
  sharp. Memoized, including a rejection (a failed dlopen cannot heal
  in-process), logging one error per process.
- transform.ts: type-only sharp import. applyTransform keeps the 400 for an
  unsupported format first, then awaits loadSharp() outside its try, so a
  load failure throws (500) and is never the 422 for undecodable input.
- pipeline.ts: uses loadSharp(); still fails open.
- eslint: @typescript-eslint/no-restricted-imports rejects a static value
  import of sharp under packages/canopycms/src outside tests.

Tests: server.sharp-unavailable (importing ./server and ./http with sharp
unloadable), transform.sharp-unavailable, sharp-loader. Each was
break-and-rerun against the pre-fix transform.ts and four single-point
breaks.

Docs: DEVELOPING.md, ARCHITECTURE.md, CODEBASE_GUIDE.md, assets/AGENTS.md.
Files admin-status-image-processing-availability.md (P3, deferred).
CI typecheck failed with TS2550: the package targets ES2021, whose lib
types Error without a cause property. The local typecheck ran before the
cause-chain helper was added.
… too

Two LOW findings from review round 1:

- A first loadSharp() call that nobody awaits (a warm-up) left a rejected
  promise with no handler, which Node treats as fatal by default. Attach a
  no-op catch to the memoized promise; every caller still gets the
  rejection. New test; removing the catch makes vitest report an unhandled
  rejection.
- The static-sharp-import lint rule now also covers
  packages/canopycms-next/src, which sits in the same adopter server graph.
A claim-check pass over the whole diff, each claim run or opened:

- eslint.config.mjs said the typescript-eslint rule variant was chosen for
  allowTypeImports. Core no-restricted-imports (ESLint 9.39) supports it too
  and flags the same forms, so that reason is gone. The comment also said the
  canopycms-next scope stops a dependency being added (it stops an import),
  and the rule message pointed canopycms-next at a module it cannot import.
- The Turbopack claim is now scoped to sharp being external, matching what
  the adopter image's server chunk shows: an async module awaiting the
  external's load.
- DEVELOPING.md: no NON-TEST module imports sharp statically; tests do.
- ARCHITECTURE.md: finalize does resize (a throwaway decode check); it stores
  no resized variants.
- assets/AGENTS.md, CODEBASE_GUIDE.md: a static import is a lint error
  outside tests, and it was transform.ts's import that reached every importer
  of canopycms/server and canopycms/http, not any import under src/.
- The admin-status task and its index row now list the per-request handler
  error among the signals.
- Two stale 'PR 7' forward references to the transform Lambda are gone.
fix(assets): load sharp lazily so a missing libvips fails only image work
- deploying-to-aws.md: document <ClerkProvider publishableKey> read from a
  run-time variable, with `dynamic = 'force-dynamic'` in the editor layout
  and no clerkMiddleware, as a supported shape (not yet run live). Present
  the NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY build arg as the clerkMiddleware
  path. Restate the Security Model's Clerk note: CanopyCMS's Lambda auth
  needs only CLERK_JWT_KEY; clerkMiddleware is what needs the secret.
- Operating mode: set NEXT_PUBLIC_CANOPY_MODE=prod as a constant build
  value, expect the override warning, never derive config.mode from either
  variable. Remove the false "hides the pull-request UI" claim; no client
  check does that.
- middleware templates: the Clerk middleware is optional, and what it costs.
- dual-build fixture: the /edit layout reads its key per request. The
  `dynamic` export moves from the 'use client' page, where Next ignored it
  and prerendered /edit at build. The test builds without the key, serves
  with it, and asserts the served /edit carries it.
- Backlog: clerk-middleware-runtime-key-unverified,
  deploy-test-lambda-plaintext-clerk-secret and pr229-review-followups §1
  updated to match.
claim-check pass on PR 325:
- Security Model: the middleware shape was deploy-tested in 2026-07;
  only the shape without it is unproven.
- deploy doc: the ignored `dynamic` export was measured on a 'use client'
  page (the fixture's), not on the scaffold's page.
- Generated canopy.ts comment (templates.ts, mirrored in example1): a
  missing CLERK_JWT_KEY throws at the first authenticated request;
  CLERK_SECRET_KEY is read only where Clerk's backend API is called.
- assertAuthPluginAllowedForMode's message names CLERK_JWT_KEY, not
  CLERK_SECRET_KEY.
- README, ARCHITECTURE.md, adopter-migration.md: a CMS server needs the
  secret only if it also runs clerkMiddleware.
- Backlog: file editor-operatingmode-option-unused (P3).
PR 4 copied cms-stack.ts.template's build-args comment into
examples/aws-deployment, including "and the prod feature flags". This
branch corrected that claim in the template (no client-side check varies
by mode except the edit page's auth selection); match it here.
docs: one CMS image for every Clerk tier; what adopters need for NEXT_PUBLIC_CANOPY_MODE
A CMS editor image built from Dockerfile.cms.template failed during
`next build` page-data collection with "Cannot initialize local simulated
remote: base branch '<name>' does not exist locally". isBuildMode() changed
WHO a build-time read runs as, not WHERE: for deployedAs 'server' every read
still provisioned a branch clone, which needs the configured base branch to
exist in the builder's synthesized snapshot repo. The same root cause made a
local `next build` silently read git-committed state from .canopy-dev.

Every build-time read now comes from the working tree, in every mode, as the
static path already did:

- build-mode.ts: readsFromCheckout(config) = isDeployedStatic || isBuildMode
  decides WHERE; the existing checks keep deciding WHO.
- loadOrCreateBranchContext short-circuits on it (covering context.ts,
  content-reader.ts and ai/resolve-branch.ts); content-reader,
  resolve-branch and services.ts's HEAD detection use the same predicate.
- Dockerfile.cms.template: the builder loses its git install and snapshot
  commit and sets CANOPY_BUILD_MODE=true; the runner keeps git.
- CI: example1's `git checkout -B main` step is removed, so its build on a
  detached HEAD is the live proof.
- Comments and docs that described the old behaviour are corrected; the
  provisioning lock stays, with its rationale reworded.
- Backlog: resolves dev-mode-build-reads-branch-clone-not-working-tree;
  files build-canopy-scripts-outside-next-build and
  dev-content-watcher-relative-sourceroot.
Builds no longer provision workspaces, so the codebase guide, the
concurrency doc and the content-write-lock task no longer cite build
workers as provisioning contenders. The codebase guide also documents
readsFromCheckout and the new build-mode-reads integration test.
Branch identity no longer detects git HEAD during a build, the dev
content-divergence section covers every build rather than only static
ones, README notes that editor saves reach a build only after sync pull,
and ARCHITECTURE gains a short design note on why builds read the
checkout. The open build-canopy task now also names README's AI-content
sentence that depends on its decision.
A real Next 16.1.7 pnpm scaffold built through init + init-deploy aws +
docker build hit two scaffold gaps unrelated to this branch: the
generated infrastructure/ breaks the app's next build type-check, and
the Dockerfile's pnpm path never copies pnpm-workspace.yaml, so pnpm 11
allowBuilds approvals are lost in the builder.
- init.test.ts: the stage split consumed the runner's own FROM line, so
  a template edit to FROM builder AS runner -- which would carry the
  builder's CANOPY_BUILD_MODE=true into the deployed CMS -- stayed green.
  Assert the runner's base image is not the builder.
- branch-workspace.test.ts: drop a .canopy-meta absence assertion from
  the new build-mode twin that no implementation could fail.
- NEXT_PHASE is not yet set when next build loads next.config (the
  earlier 'while next.config is evaluated' overreached); cite the Next
  versions checked, 15.5.21 and 16.1.7.
- Dockerfile.cms sets CANOPY_BUILD_MODE in its builder stage ahead of the
  build command, not for the whole stage.
- 'builds never provision' narrowed to build-time content reads, in every
  copy.
- mode-env.ts cites where the prod-only checks come from; the integration
  test header states the matrix it covers; the init-deploy gaps task
  quotes the CLI's actual wording and the verified es5-ext chain.
The isBuildMode JSDoc named only dist/build/index.js; Next 15.5.21 and
16.1.7 carry the same assignment in dist/esm/build/index.js too.
…NOPY_MODE

Build-time reads now come from the working tree in either mode, so a
build that resolves prod no longer reads anything differently; what it
meets is the prod-mode checks the dev literal keeps out of it, as the
Operating mode section says.
fix: build-time reads come from the working tree
sharp loads libvips with dlopen through its native binding's rpath, so
nothing requires the shared library and import-following tracers never see
it. Next's JS tracer special-cases only sharp 0.34's lib/index.js entry,
and a Next 16.1.7 Turbopack standalone build traced libvips's package.json
and the rpath symlink but not lib/libvips-cpp.so.*, so the server failed
every sharp load with ERR_DLOPEN_FAILED (vercel/next.js#97973).

New sharp-tracing.ts finds each installed @img/sharp-libvips-* package's
real lib/ directory, reached from the canopycms the project resolves and
from any sharp the project resolves directly, looking beside sharp and
beside each native binding. It walks the node_modules hierarchy the way a
bundler does: not require.resolve, which exports maps defeat, and not
require.resolve.paths, which adds global folders. It mirrors Next's tracing
root inference, refuses directories outside that root (Turbopack fails the
build on a glob that climbs above it) and paths containing glob
metacharacters, and never throws.

withCanopy merges the result into outputFileTracingIncludes['/**'] for any
build except a static export, under experimental before Next 15, keeping
the adopter's own includes. A standalone build that finds nothing, or runs
from a directory with no next.config, warns with a manual snippet.
…heck

docs/deploying-to-aws.md gets the manual outputFileTracingIncludes snippet
for configs that do not use withCanopy. README, ARCHITECTURE and
CODEBASE_GUIDE describe the new behaviour. New P3
upstream-next-sharp-tracing-recheck.md says how to tell when Next traces
the library itself, and what to delete then.
…ows bindings

Two defects found by review round 1, sharing one cause. Next 15 and 16
still accept the legacy experimental spellings, and copy any that is
present over the top-level key:

- experimental.outputFileTracingRoot (and, on Next 15 only,
  experimental.turbo.root) was not read. withCanopy computed a wider root
  than Next used, so a monorepo include could climb above Next's root,
  which fails a Turbopack build.
- experimental.outputFileTracingIncludes was not read. Next copied it over
  the top-level include withCanopy wrote, silently dropping libvips again.

withCanopy now reads and writes each option where Next reads it.

Also from the same round:
- An installed native binding that lists no libvips package contributes its
  own lib/. sharp 0.35's Windows bindings ship libvips-42.dll and
  libvips-cpp-8.18.3.dll there and list none, so a Windows standalone build
  previously got no include and a warning.
- isFile and isDirectory answer false on any failed stat, so hasNextConfig
  can no longer throw out of the adopter's next.config.
- New fixture: a project directory reached through a symlink. It pins
  realpathSync(projectDir) on platforms whose tmpdir is not a symlink.
Review round 2 found the round-1 fix's `in` checks counted a legacy
experimental key set to undefined or null as present. Next's
assignDefaults drops such values before copying legacy keys to the top
level, so withCanopy could:

- write the libvips include under experimental, where Next never reads it,
  and drop the adopter's own top-level includes from the result;
- treat a null value as malformed and skip the include with no warning;
- ignore a real top-level outputFileTracingRoot.

Now every value is checked with isSet, and every read goes through
readProperty, so `experimental: true` in an untyped config cannot throw.

Also from the same round:
- On Next 15, any root key on turbopack wins over experimental.turbo.root,
  even an empty one, matching Next's `{ ...turbo, ...turbopack }`.
- On Next 13 and 14 the tracing root passed to the lookup is
  `experimental.outputFileTracingRoot || projectDir`. Those versions infer
  no root from lockfiles (Next 14.2.25 build/index.ts), so a lockfile-
  inferred root could let an include climb out of the directory Next copies
  from.
… an unknown version

Correction to 4761a20. Its message says Next 13 and 14 infer no tracing
root from lockfiles. That is wrong: assignDefaults in their
server/config.ts sets experimental.outputFileTracingRoot to findRootDir(dir),
the directory of the CLOSEST lockfile (checked in Next 13.5.7 and 14.2.25).
The `|| projectDir` fallback that commit added made withCanopy refuse a
valid include in any Next 13/14 workspace whose lockfile sits above the app.

The root lookup now takes lockfileRoot 'closest' (Next 13/14, with Next
14.2.25's lockfile names, which predate bun.lock) or 'outermost' (Next 15
and later, the default). withCanopy passes the one matching the installed
Next.

Also, per JP's decision on the Next 13/14 key: when the installed Next
version cannot be read, the include still falls back to the top-level key,
but a standalone build now says so and tells Next 13/14 users to move it
under experimental. When nothing could be traced either, the nothing-found
warning carries the same note. No version warning fires when a legacy
experimental include decides the key, because every Next version reads that
one.
…nd unguarded

The review of 91f4bda ran five mutations that no test caught. Each now
has a test, and each mutation now fails exactly that test:

- the unreadable-version warning fires only once per module instance;
- an unreadable Next version uses the outermost-lockfile root, as Next 15
  and later do;
- lockfileRoot 'closest' recognises bun.lockb, which Next 14 looks for;
- lockfileRoot 'closest' falls back to the project directory when no
  lockfile exists;
- the outermost walk continues past a nearer lockfile to an outer bun.lock,
  which Next 14's list does not know.
Every claim in the change, its comments, docs and backlog files was run,
opened or mutated against the code, CI logs and the local evidence. Wrong
copies were fixed everywhere they appeared, including:
- DEVELOPING.md's smoke section: the path filter, the 14 checks (two
  titles, request-time vs build-time reads), --keep, --work-dir and the
  container log;
- the tsconfig skip cases, which now include an `extends` tsconfig with no
  exclude of its own (README, deploying-to-aws, CODEBASE_GUIDE, backlog);
- withCanopy's turbopack default, which adds no key for an unreadable
  Next version (README, JSDoc, citations into Next 16.1.7 and 15.5.21);
- leg timings, the Node floor, the runner stage's copies, and claims no
  run measured, which were deleted or given a citation.

Two warnings in init.ts said less than the code does. "Not plain JSON"
also covers an `exclude` that is not a list of strings, and the kept
.dockerignore warning now names infrastructure alongside .env* and vendor/.
CI: build, boot and check the generated CMS image (CMS image epic PR 5)

Adds the `standalone-image` CI job and scripts/smoke/standalone-image.mjs, which run the image
`canopycms init-deploy aws` generates against real requests. Includes three scaffold fixes the
job's first builds found: withCanopy's answer to Next 16's default-Turbopack guard,
init-deploy's tsconfig/.dockerignore exclusion of infrastructure/, and the pnpm COPY carrying
pnpm-workspace.yaml.

Commit history note: b4d7eb1 is titled "docs(tasks): …" but also contains the original
withCanopy Turbopack fix, the CLI scaffold fixes, the CI job and script, and their docs. A
pre-commit hook rejected the four commits intended to precede it, and their staged files were
swept into it. The later commits are each what their subject says; 3cf2007 refines the
Turbopack fix.
PR 5 excluded infrastructure/ from the app's tsconfig.json and the image
context so `next build` would stop failing on aws-cdk-lib, which left the
CDK app with no type-check at all: cdk.json runs it through tsx, which
strips types without checking them. A misspelled CanopyCmsService prop
was dropped silently and the deploy used the prop's default.

- `init-deploy aws` scaffolds infrastructure/tsconfig.json
  (cdk-tsconfig.json.template): bundler resolution, which accepts the
  extensionless imports tsx does whatever the package "type"; strict;
  noEmit; include limited to bin/ and lib/.
- deploy-cms.yml.template runs `npx tsc --noEmit -p infrastructure` after
  the dependency check and before AWS credentials, failing first with a
  named error when typescript is missing (otherwise npx downloads npm's
  deprecated `tsc` package).
- scaffold-synth.test.ts reads that command out of the generated workflow
  and runs it: it passes on the scaffold, lists only the CDK app and
  canopycms.config.ts, and fails with TS2561 on a misspelled memorySize.
- examples/aws-deployment gets the same tsconfig and step.
…igration entry

- README and deploying-to-aws.md: the scaffolded infrastructure/tsconfig.json
  and the workflow's `tsc --noEmit -p infrastructure` step, including the
  local `cdk deploy` sequence; README says withCanopy() should be the
  outermost config wrapper, since a plugin wrapped around it adds its
  webpack after withCanopy() chose to add `turbopack: {}`.
- adopter-migration.md: one entry for adopters who scaffolded before the CMS
  image epic or hand-copied Dockerfile.cms.template: no git or snapshot repo
  in the builder, CANOPY_BUILD_MODE, the pnpm-workspace.yaml COPY, the
  infrastructure/ exclusions and the new type-check, the ARM_64 default and
  omitting `platform`, and withCanopy's tracing and turbopack additions.
- DEVELOPING.md and CODEBASE_GUIDE.md: the new scaffold-synth tests and the
  new template.
- future-tasks: items 2 and 3 of cms-image-pr5-review-followups.md resolved
  (1 and 4 stay open); pr229-review-followups' row now says the PR-UI claim
  was corrected and the runtime mode check remains open.
Review round 1: tsx runs the CDK app with the project's tsconfig.json and
honours its `paths` and `jsx`, but the scaffolded infrastructure/tsconfig.json
stood alone. An `@/` import in canopycms.config.ts (create-next-app's alias)
synthesized and then failed the deploy's type-check with TS2307; so did a
`.ts`-extension import.

infrastructure/tsconfig.json now extends ../tsconfig.json and overrides only
what the CDK app needs: bundler resolution, allowImportingTsExtensions,
types: ["node"], skipLibCheck, noEmit. Two overrides are load-bearing:
- `exclude: []`, because the inherited exclude resolves against the project
  directory and names infrastructure/, which leaves no inputs (TS18003);
- `incremental: false`, or the app's inherited `incremental: true` writes
  infrastructure/tsconfig.tsbuildinfo.

With no project tsconfig.json the type-check now fails, so init-deploy's
existing "No tsconfig.json found" warning says so.

The scaffold-synth type-check tests get their own scaffold with a
create-next-app-shaped tsconfig.json, and a test that an `@/` alias import
resolves. Removing `extends`, `exclude: []` or `incremental: false` from the
template each fails them.
Review round 2 found the round-1 fix inheriting too much. tsx resolves the
project's `paths` in every file, but compiles files the project's
tsconfig.json excludes -- infrastructure/, since PR 5 -- without its
compiler options. Extending that tsconfig therefore failed the generated
stack on code tsx runs:
- verbatimModuleSyntax: TS1484 on `import { Stack, StackProps }`, while the
  synth passes;
- exactOptionalPropertyTypes: TS2375/TS2379 in app.ts and cms-stack.ts;
- noPropertyAccessFromIndexSignature: TS4111 on each `process.env.X`;
- composite: TS6379 against the `incremental: false` override.

A sweep of 33 project-tsconfig options over a real scaffold found no other
option that fails a project file under the template (the rest either pass or
fail only the workspace's own canopycms src/, which adopters get as .d.ts
behind skipLibCheck; isolatedDeclarations flags only the adopter's
canopycms.config.ts, which `next build` checks under the same option). The
template resets those four, leaving the adopter's other options inherited.

The deploy workflow now also triggers on tsconfig.json: infrastructure/
tsconfig.json extends it, so a commit changing only it can fail the
type-check, and would otherwise surface on the next unrelated deploy.

A new scaffold-synth test sets all four in the app's tsconfig.json and
expects the type-check to pass; removing any one reset fails it with that
option's error.
…ounds

The claims pass over the whole PR. The docs, the backlog and several
comments still described infrastructure/tsconfig.json as the standalone
file the first commit wrote; it now extends the project's tsconfig.json
and resets four options.

- README, deploying-to-aws.md, the adopter-migration entry, the examples
  README and the follow-ups file say it extends the project's tsconfig.json,
  so the type-check fails until the project has one, and that the workflow
  also runs on a change to tsconfig.json. The follow-ups resolution now
  lists what the template actually overrides and resets, and cites PR #332.
- The template comment says the resets reach canopycms.config.ts as well,
  which `next build` still checks with the project's own options (verified:
  tsx applies the project's verbatimModuleSyntax to canopycms.config.ts and
  not to a file under infrastructure/). Its `exclude` comment says that
  `init-deploy aws` is what adds infrastructure/ to the inherited list.
- init-deploy's missing-tsconfig warning says "as init-deploy writes it",
  since a kept infrastructure/tsconfig.json may not extend anything.
- A test comment no longer calls `incremental` inherited, which the template
  turns off; another says tsx applies none of the four options to
  infrastructure/, not that none changes how tsx runs anything.
- The migration entry says a hand-copied Dockerfile.cms builds as it did,
  rather than that nothing requires the new lines.

Commit 3d7434e's message says tsx honours the project tsconfig's `paths`
and `jsx` for the CDK app. That holds for `paths` only; 6d6812c records why.
Two things PR #332 deliberately did not do, so they don't read as done:
CI runs the scaffolded CDK app's type-check only against workspace src/,
never the published .d.ts adopters install (checked once by hand); and a
project tsconfig.json with a typeRoots that leaves out node_modules/@types,
or a narrower rootDir, fails the check loudly on code tsx runs.
fix(cli): type-check the scaffolded CDK app again
Corrects the claims the three claim-check verifiers found wrong or
unverifiable across the epic's diff. Comments, JSDoc, docs, test comments,
smoke-check names and error strings only; no program or test logic changes.

- Turbopack vs webpack: withCanopy's libvips include fixes Turbopack
  standalone builds, while a pnpm webpack build (seen on Next 15.5.21)
  bundles sharp. Every copy now says so: the withCanopy JSDoc,
  sharp-tracing, the smoke script, ci.yml, README, ARCHITECTURE,
  CODEBASE_GUIDE, DEVELOPING, and the deploy and migration guides.
- Stale rationales: "a prod-mode build opens an EFS branch workspace"
  (cms-service, scaffold-synth test, generated canopycms.config) and
  "the app need not install aws-cdk-lib" (init, dockerignore, init tests).
- Architecture mismatch: name Runtime.InvalidEntrypoint only for the
  direction the cited source covers, and drop "deploys clean".
- Citations: Next 16.1.7 line numbers and renamed functions, Clerk 6.39.5
  lines, the optional pnpm-workspace.yaml COPY glob (measured on Docker
  Engine 29.6.2, BuildKit v0.31.2), and CI leg durations with run IDs.
- Smaller fixes: init flags in CODEBASE_GUIDE, init-deploy's overwrite
  prompt, withCanopy's warning conditions and Turbopack guard, stale
  "this PR" references, the `npx tsc` note, the pnpm/action-setup version,
  and smoke-check names that claimed more than they assert.
- Backlog: the epic doc records #332 and its follow-up task; new P3
  sharp-loader-rejection-test-strength.md; item 5 (the sitemap check) in
  cms-image-pr5-review-followups.md.
docs: mechanical re-check of the claims pass (#335)
Brings in the worker-credential epic that landed on int-202609-a since this
branch was cut at 8b854c1: #322, #321, #329, #334, #330, #333, #338, #339,
#340, #337.

Conflicts, each resolved by keeping both sides:

- examples/aws-deployment/infrastructure/lib/cms-stack.ts: both sides had
  added the NEXT_PUBLIC_CANOPY_MODE build-arg comment to the example. Took
  this branch's wording, which the claims pass corrected against the client
  code and which the merged template already carries.
- packages/canopycms-cdk/src/scaffold-synth.test.ts: kept this branch's
  linux/arm64 image-platform test and int-202609-a's four JSON-field and
  GitHub App tests. The App test re-synths into the shared cdk.out and says
  every other test reads state captured in beforeAll, but the arm64 test
  read the asset manifests from disk at test time. Moved that read into
  beforeAll (imagePlatforms) so the claim holds whatever order the tests run in.
- CODEBASE_GUIDE.md: CLI commands table keeps this branch's init-deploy aws
  row and adds int-202609-a's init-github-app row; test-files list keeps
  int-202609-a's permission drift guard bullet and this branch's extended
  scaffold synth bullet. Table re-aligned by prettier.
- .claude/future-tasks/index.md: every row from both sides in P2, P3 and
  Resolved, int-202609-a's (newer) rows first.

Also: the two sides filed the same examples/aws-deployment drift on
2026-09-12 (example-aws-deployment-drift-from-template.md and
example-deploy-workflow-drifted.md there, examples-aws-deployment-drift.md
here). All three kept, each now links the other two.
int-202609-a (the JSON-field and App tests) and int-202609-cms-image (the image-asset platform tests) each added `import path from 'node:path'` to cms-deploy.test.ts. Git merged the two lines cleanly, and tsc rejects them with TS2300.
The same examples/aws-deployment drift was filed three times on 2026-09-12, twice on int-202609-a and once on int-202609-cms-image, and the base merge brought all three together. example-aws-deployment-drift-from-template.md, the P1 filing, is now canonical and carries every distinct fact from the other two, re-checked against the merged tree: five template/example pairs and their current state, the workflow and stack drift, the guards that exist and what each pins, and the three fix options with the seven placeholders. example-deploy-workflow-drifted.md and examples-aws-deployment-drift.md move to resolved/ as duplicates. Do-next #16 now points at the canonical file, its P3 row is gone, and the epic spec links the resolved copy.
- scaffold-synth.test.ts: the imagePlatforms comment and the GitHub App test's comment now state the invariant precisely: no other test reads cdk.out after beforeAll. Before, the App test's comment said every other assertion reads beforeAll state, but the prod-mode and workflow tests read scaffold files from disk.
- example-aws-deployment-drift-from-template.md: PR #323 edited more of the example than runs-on and build args, and it added NEXT_PUBLIC_CANOPY_MODE independently of #322. Names the test that pins it, calls the cross-copy checks per-feature pins rather than textual ones (the asset-support check instantiates AssetSupport), and adds that the example's tsconfig.json extends a ../tsconfig.json that examples/aws-deployment/ does not have.
- index.md: Do-next #16 and the P1 row use the same wording for the cross-copy checks.
- cms-image-pr5-review-followups.md: item 6, from the base merge's code review. The file-level beforeAll in scaffold-synth.test.ts also runs, and can fail, for the type-check describe, which has a scaffold of its own.
Merge int-202609-a into int-202609-cms-image
@jpslav
jpslav merged commit c25035d into int-202609-a Sep 14, 2026
11 checks passed
@jpslav
jpslav deleted the int-202609-cms-image branch September 14, 2026 00:11
jpslav added a commit that referenced this pull request Sep 14, 2026
…aims

docs: post-merge claims pass over the CMS image epic (#331)
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