Skip to content

docs(create-app): issue-resolution spec for local Docker start failure and the resulting permanent UVE 403 (#37262) - #37263

Merged
fmontes merged 11 commits into
mainfrom
issue-37262-create-app-docker-uve-spec
Sep 1, 2026
Merged

docs(create-app): issue-resolution spec for local Docker start failure and the resulting permanent UVE 403 (#37262)#37263
fmontes merged 11 commits into
mainfrom
issue-37262-create-app-docker-uve-spec

Conversation

@fmontes

@fmontes fmontes commented Aug 28, 2026

Copy link
Copy Markdown
Member

Proposed Changes

Spec-only PR (PR 1 of 2) for #37262, per the repo's spec-kit flow. No code changes — this PR is the approval gate for the approach. Implementation lands in the stacked PR on top of this branch.

  • Adds specs/37262-create-app-docker-uve/spec.md — an issue-resolution spec for the @dotcms/create-app local-Docker failure
  • Points .specify/feature.json at the new feature

What's broken (three compounding defects):

  1. dotCMS never starts. single-node-demo-site/docker-compose.yml lets dotcms boot before Postgres accepts connections and gives it no restart policy, so it exits and stays exited. The CLI reports "containers started successfully" and burns its retry budget on a dead container. Users get past this only by pressing ▶ in Docker Desktop.
  2. A non-essential config call aborts the run. Token and default site both resolve fine; the UVE app-config POST then returns a transient 403 and the CLI calls process.exit(1). Because UVE setup runs before scaffolding, the user is left with an empty directory and the working token and site ID are discarded without ever being printed.
  3. Recovery is blocked by the CLI's own side effects. The port pre-check hard-fails on the now-running dotCMS's ports, and the directory-clearing prompt would delete the docker-compose.yml needed to tear it down.

Verified against the tree before writing — every claim in the issue holds:

  • compose: dotcms has depends_on with no condition, no restart:, no healthcheck, and does not publish 8090; opensearch has no healthcheck; db's healthcheck exists but nothing consumes it
  • src/index.ts:370 exits before the scaffolding at :377
  • src/index.ts:597 tests if (!result) against a truthy { ok: false, val }, making the npm-install failure branch unreachable
  • checkPortsAvailability() (src/utils/index.ts:479) hard-fails on exactly the ports a successful run holds
  • the package contains no spec files at all — this fix establishes the test harness

Deliberately deferred as explicit non-goals, so the P0 fix stays bounded:

  • user.isAdmin() swallowing exceptions into a false "not admin" — real defect, but legacy Liferay code on a hot permission path with wide blast radius, and the P0 fix doesn't depend on it
  • Pinning the image tag alongside CUSTOM_STARTER_URL — intersects binding ADR-0019 (date-lockstep SDK versioning), so it deserves its own decision rather than riding along in a bug fix

Reviewers — and two questions for you

.github/CODEOWNERS does not cover docker/, so the highest-blast-radius file in this fix gets no automatic reviewer. Reviewers below are derived from git blame on the exact hunks the implementation will change, weighting design ownership over line count (the two largest raw counts are a bulk restore, #27432, and a pgvector bump, #29915 — both mechanical).

Reviewer Why
@wezell OpenSearch 1.x + SSL in this file (#27754) and the Postgres 18 upgrade (#34236); owns the db/opensearch shape we're adding healthchecks to. Most senior still-active owner of docker/.
@erickgonzalez Most recent semantic change to the dotcms service (#36490); closest to the CUSTOM_STARTER_URL / starter-import behavior the readiness race depends on
@jcastro-dotcms Not from blame on this file — added because blame's strongest signal is unavailable (below). Second-most-active contributor to docker/ over the last 12 months.

Blame's two strongest signals can't be assigned. spbolton holds dominant blame on every hunk in scope and authored lgtm-observability (#32980) — the exact condition: service_healthy pattern this change copies — and dcolina holds part of the opensearch block (#29915). Both are no longer collaborators on this repo (last commits 2026-03-30 and 2026-04-07), so GitHub rejects the review request. Flagging it rather than quietly dropping them: the person who designed the pattern we're copying is not available to check that we're copying it faithfully. If anyone has context on the lgtm-observability healthcheck choices, that's the gap to fill.

⚠️ Updated — the diagnosis changed

Planning tested this spec's own root cause 2 and disproved it. The 403 is not a transient
startup race. It is permanent damage caused by root cause 1, and the two "independent bugs"
are one causal chain. Details in Root-Cause Hypothesis; summary below.

Experiment 1 — a clean boot has no settling window. Measured on an M5/64GB host with dotCMS constrained to 2 CPUs / 4G:

Signal First success
POST api-token 46s
GET apps/dotema-config-v2/{site} 46s
/dotmgt/livez, /dotmgt/readyz 48s
/api/v1/appconfiguration (current CLI probe) 49s

The UVE endpoint is usable two seconds before readyz goes green. The starter import (T+20s) and ES reindex (T+44s) both complete inside Tomcat startup, and the connector accepts no traffic until after them. The hypothesised race cannot happen.

Experiment 2 — the reporter's actual path reproduces it, permanently. Killing dotCMS mid starter-import and hand-starting it (reproduction step 4):

T+39s  appconfiguration 200 — CLI proceeds
T+41s  api-token 200 ・ defaultSite 200 ・ UVE GET 403 ・ UVE POST 403
   … 193 consecutive attempts over ~7 minutes, zero successes …
T+440s UVE GET 403 ・ UVE POST 403

The reported log, line for line. Server-side:

DotSecurityException: User 'Admin User [dotcms.org.1][admin@dotcms.com]'
  does not have READ permissions on Site 'demo.dotcms.com'

The interrupted import never wrote the site's permission rows; the restart re-runs Task00004LoadStarter, reports a clean startup, and the permissions never appear. The instance does not recover.

What changed in the spec as a result:

  • AC-005 no longer polls until 200 — it would never terminate. Retry is restricted to 5xx; 403 explicitly does not retry.
  • The failure guidance is rewritten. "Configure UVE manually at this URL" is wrong advice — manual setup fails identically. The CLI must say the instance is unrecoverable and to run docker compose down -v.
  • The P2 backend non-goal is re-pointed. Not user.isAdmin() swallowing an exception (there is nothing to swallow — the data is absent), but a larger defect: any interrupted first boot silently bricks the instance while reporting success. Filed separately as An interrupted first boot silently bricks the instance: starter import leaves site permissions unwritten and every Apps API call 403s forever #37268.
  • Title and Reproducibility corrected — the 403 is deterministic once the crash has happened, not timing-dependent.

This raises US1's value. Fixing the compose file doesn't just stop a crash — it removes the 403 entirely, because it removes the interrupted boot that causes it.

Evidence limits: one host, one starter, one image, kill point fixed at 25s. Which kill-points corrupt is unmapped.


Updated after planning. Verifying the spec's claims against the tree turned up four that didn't hold, and I've folded the corrections back in before sign-off so you're approving what will actually be built:

  • "Six siblings use condition: service_healthy" — there are three, and none of them gates dotcms on OpenSearch being healthy (all use service_started). So doing both is a deliberate deviation from every precedent, not the house pattern the spec implied. Kept, with the reasoning stated, using single-node-os-migration's proven probe.
  • The flapping risk I flagged can't happen. Compose restart policies react to container exit, not health status. Replaced with the real exposure: a wrong probe blocks docker compose up --wait until timeout.
  • 8090:8090127.0.0.1:8090:8090. InfrastructureManagementFilter authorizes purely by arrival port — no credential check, no IP allowlist — so a wildcard binding puts /dotmgt/health and /dotmgt/metrics on the local network. New AC-011 asserts it's refused on the LAN address.
  • The Jest harness already exists; only spec files are missing. The spec said this fix establishes it.

Also added AC-012 for a landmine found while reading: updateDockerComposeStarterUrl rewrites the compose file with a regex and throws on no match, so reformatting CUSTOM_STARTER_URL would break --starter for every already-installed CLI, with no release able to reach them.

What's left for you to settle (the 8090 question is now answered above):

  1. Gating dotcms on opensearch: service_healthy goes further than all four existing examples. Right call for a stack driven by an unattended CLI, or should it match the house pattern? (My recommendation: keep the stricter gate.)
  2. Is start_period: 180s enough for a cold demo-starter import on a slow machine? Above both precedents (lgtm 120s, metrics-monitoring 20s). Too short and --wait blocks to timeout.

Note on blast radius

The compose file is fetched from main at runtime by every installed @dotcms/create-app. A change ships instantly and unversioned to all existing CLI users — including older versions that won't know about port 8090. That cuts both ways: it's why the fix is P0 and reaches users without a release, and it's the single largest regression risk here. Worth a careful look.

Checklist

  • Tests — n/a for a spec-only PR; the spec defines the Jest specs and cold-start compose verification that PR 2 must land first (constitution Principle V: Red before Green)
  • Translations — n/a
  • Security Implications Contemplated — publishing management port 8090 on the host is called out in the spec's Regression Risk and raised as review question 1 above

Additional Info

  • Follows the spec-kit flow in CLAUDE.md: /speckit-specify-fixPR 1 approved/speckit-plan/speckit-tasks/speckit-implement → PR 2
  • ADR consultation ran as the before_plan hook. ADR-0019 (accepted, binding) is recorded in the spec; ADR-0016 matched on keywords but is an unfilled template and imposes nothing. No conflicts with an accepted ADR.
  • Related: e2e: Add E2E test suite for @dotcms/create-app CLI #35096 (E2E suite for @dotcms/create-app) — owns the fault-injection coverage, out of scope here

Refs #37262

This PR fixes: #37262

This PR fixes: #37262

@fmontes fmontes changed the title docs(create-app): add issue-resolution spec for #37262 docs(create-app): issue-resolution spec for local Docker start failure and transient UVE 403 (#37262) Aug 28, 2026
@claude

claude Bot commented Aug 28, 2026

Copy link
Copy Markdown
Contributor

Claude finished @fmontes's task in 2m 8s —— View job


Spec review — claims verified against the tree

I checked every load-bearing claim in the spec against the working tree rather than re-reading the prose. The two prior reviews (zJaaal, nicobytes) are substantially incorporated already: the unawaited async moves now appear in Cause 3 / AC-008 / the P1 list; the OpenSearch "prior art" framing is withdrawn to "deliberate deviation from every non-migration example"; --wait and AC-009 are re-tiered to P0; AC-013 gained a post-build/npm pack --dry-run gate. Good.

  • Read spec.md
  • Verify CLI claims (src/index.ts, utils, asks, git)
  • Verify packaging claims (package.json, project.json, verify-cold-start.sh)
  • Verify compose claims
  • Post review

New Issues

  • 🟡 Medium: specs/37262-create-app-docker-uve/spec.md:542-549 (AC-012) — the spec asserts, in emphatic present tense, that core-web/libs/sdk/create-app/scripts/verify-cold-start.sh "exists, it ships in this package, and it was already written against the bundled asset … What was genuinely wrong was the path." It does not exist anywhere in the tree — find . -name "verify-cold-start*" returns nothing, and neither assets/ nor scripts/ exists under core-web/libs/sdk/create-app/. The script is a plan artifact (fix(create-app): design contracts for local Docker start failure and transient UVE 403 (#37262) #37264), not something committed on this branch. This is the same class of "phantom reference" the paragraph claims to be correcting, just relocated. Since AC-012's whole verification gate rests on this file, either soften to future tense (the plan/PR-2 must create it) or make explicit it is a plan-phase deliverable, so a reviewer approving the spec isn't approving a gate anchored to a file that isn't there. Fix this →

Minor (non-blocking)

  • Cause 3, orphaned-compose bullet (spec.md:248-251): says startScaffoldingFrontEnd() exits at :588, :599. Verified line numbers are :588 and :601 (:601 is the process.exit(1) inside the if (!result) install-failure block; :599 is spinner.fail). Trivial, but the surrounding text is precise about line numbers so it's worth correcting.

Verified clean

Everything else the spec cites holds against the tree:

Claim Verified
Existing-instance fatal block :226-228 (exit at :228), startScaffoldingFrontEnd :232
Local-Docker fatal block :369-371 (fail :370, exit :371); compose move :376, scaffold :377, move-back :378
Truthy-Result bug — if (!result) at :597 against Err() = {ok:false,...}
Both moves async (git/index.ts:76, :82, each await fs.rename) and called without await
checkPortsAvailability() at utils/index.ts:479, ports 8082/8443/9200/9600
prepareDirectory() at asks.ts:180
project.json: passWithNoTests: true (:75), esbuild assets block (:38)
package.json files: ["*.js", "README.md"] — flat glob won't match assets/docker-compose.yml (AC-013 correctly flags this)
Shared compose: dotcms depends_on: [db, opensearch] with no condition/restart/healthcheck, no 8090; opensearch no healthcheck; only db has a healthcheck (:13-17, + a restart: unless-stopped at :18)
CUSTOM_STARTER_URL present (:60); opensearch on floating :1 (:21)

One small factual addition on the last row: the shared db service already carries restart: unless-stopped (L18) in addition to its unused healthcheck — the spec mentions the unused healthcheck but not the existing restart policy. Doesn't change any conclusion.

Verdict: Approvable as a spec. The AC-012 over-claim is the only thing I'd want tightened before sign-off, since the spec is otherwise scrupulous about distinguishing verified-present from to-be-built and this one line breaks that discipline. It's a doc-only PR (610 additions, no code), so nothing here blocks the spec-kit gate on its own.
· issue-37262-create-app-docker-uve-spec

@fmontes
fmontes requested a review from jcastro-dotcms August 28, 2026 11:38
@fmontes fmontes changed the title docs(create-app): issue-resolution spec for local Docker start failure and transient UVE 403 (#37262) docs(create-app): issue-resolution spec for local Docker start failure and the resulting permanent UVE 403 (#37262) Aug 28, 2026
fmontes and others added 7 commits August 28, 2026 14:37
Spec-only PR 1 for the create-app local-Docker failure. Covers the compose
ordering/restart defect, the transient UVE 403, and the CLI's discarding of
recoverable state.

Verified every claim in the issue against the tree before writing:
- single-node-demo-site compose: dotcms has no depends_on condition, no
  restart policy, no healthcheck, and does not publish 8090; opensearch has
  no healthcheck; db's healthcheck exists but nothing consumes it
- src/index.ts:370 exits before the scaffolding at :377
- src/index.ts:597 tests `if (!result)` against a truthy `{ ok: false, val }`
- checkPortsAvailability() hard-fails on the ports a successful run holds
- the package contains no spec files, so this establishes the harness

Defers two P2 items as explicit non-goals: the user.isAdmin() exception
swallowing (legacy Liferay, hot permission path, wide blast radius) and
image-tag pinning (intersects binding ADR-0019). Neither blocks the P0 fix.

Names compose reviewers from git blame, since .github/CODEOWNERS does not
cover docker/ and the runtime-fetched compose file is the highest-blast-radius
part of this change.

Refs #37262

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The two strongest git-blame signals on the compose file — spbolton (dominant
blame on every hunk in scope, and author of the lgtm-observability stack whose
service_healthy pattern this change copies) and dcolina — are no longer
collaborators on dotCMS/core, so GitHub rejects review requests for them.

Replaces them with jcastro-dotcms (second-most-active docker/ contributor over
the last 12 months) and records the resulting coverage gap explicitly: nobody
currently assignable designed the pattern being copied, so the plan phase should
read lgtm-observability/docker-compose.yml as the specification rather than rely
on a reviewer to catch a faithful-copy error.

Refs #37262

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Planning verified the spec's claims against the tree and four did not hold.
Correcting them here, before sign-off, so reviewers approve what we will
actually build.

- "Six siblings use condition: service_healthy" — there are three, and NONE
  gates dotcms on opensearch being healthy; all use service_started. Gating on
  both is therefore a deliberate deviation from every precedent, not the house
  pattern the spec implied. Kept, with the rationale stated and using
  os-migration's proven probe.

- The stated risk that a bad healthcheck would make restart: unless-stopped
  flap the container cannot happen: Compose restart policies react to container
  exit, not health status. Replaced with the real exposure — a wrong probe
  blocks `docker compose up --wait` until timeout — and required an explicit
  --wait-timeout.

- Publishing 8090 changed from "8090:8090, acceptable for a local stack" to
  "127.0.0.1:8090:8090". InfrastructureManagementFilter authorizes purely by
  arrival port: no credential check, no IP allowlist, so a wildcard binding puts
  /dotmgt/health and /dotmgt/metrics on the local network. Added AC-011.

- The package has no spec files but the Jest harness already exists, so this
  fix adds specs rather than establishing a harness. Recorded pnpm install as a
  Red-gate prerequisite and warned that passWithNoTests makes an empty run
  green.

Also adds AC-012 for a compose/CLI compatibility landmine found while reading:
updateDockerComposeStarterUrl rewrites the file with a regex and throws on no
match, so reformatting CUSTOM_STARTER_URL would break --starter for every
installed CLI. Notes that .env is new behavior, not a restoration, and downgrades
the /dotmgt/livez-on-latest assumption to partly-verified with a gating check.

Two of the three open review questions are now settled or reframed.

Refs #37262

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… not a race

Planning tested the spec's own hypothesis and disproved it. Correcting before
sign-off so reviewers approve the real diagnosis.

The spec claimed a transient startup race in which the CLI writes while roles
and permissions are still settling, with user.isAdmin() swallowing an exception
via Try.of(...).getOrElse(false). Two experiments (M5/64GB host, dotcms
constrained to 2 CPUs / 4G) say otherwise.

Clean boot has no settling window. The UVE endpoint is usable at 46s — two
seconds BEFORE /dotmgt/readyz goes green — because the starter import (T+20s)
and ES reindex (T+44s) both finish inside Tomcat startup and the connector
accepts no traffic until after them. The hypothesised race cannot occur.

The reporter's actual path does reproduce it, and permanently. Killing dotcms
mid starter-import and hand-starting it (reproduction step 4) yields the
reported log exactly — token 200, defaultSite 200, UVE 403 — and then 403 on
193 consecutive attempts over ~7 minutes with zero successes. The server says
the admin user lacks READ permission on demo.dotcms.com: the interrupted import
never wrote the site's permission rows, and the restart does not repair them.

So cause 2 is a consequence of cause 1, not an independent defect, and fixing
the compose file removes it.

Design consequences, not just narrative:
- AC-005 no longer polls until 200. A poll would never terminate; retry is
  restricted to 5xx and 403 explicitly does not retry.
- On 403 the CLI must tell the user the instance is unrecoverable and to run
  `docker compose down -v` — offering manual UVE setup steps is wrong advice,
  since manual configuration fails identically.
- The P2 backend non-goal is re-pointed: not isAdmin() exception swallowing,
  but the larger defect that any interrupted first boot silently bricks the
  instance while reporting a clean startup. Filed separately.

Title and Reproducibility updated: the 403 is deterministic once the crash has
happened, not timing-dependent.

Refs #37262

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The interrupted-first-boot corruption behind the 403 is filed as #37268.

Refs #37262

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
AC-005 was corrected when the diagnosis changed, but the matching Fix Scope
bullet was not — leaving the spec contradicting itself: one section said "poll
GET until 200, retry on 401/403/5xx" while the other forbade exactly that.
Caught by /speckit-analyze.

Fix Scope now specifies a single probe, retry on 5xx only, and no retry on 403.

Also splits the non-fatal UVE guidance in two, because the cases need opposite
advice. On 403 the instance's permissions were never written, so manual UVE
setup fails identically — pointing the user at the configuration guide would
send them down a path that cannot work. That case tells them to recreate the
instance and references #37268. Every other failure keeps the guide link plus
host, site ID and app key.

Refs #37262, #37268

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The fix no longer modifies
docker/docker-compose-examples/single-node-demo-site/docker-compose.yml. The CLI
gets its own file, bundled in the npm package, and the shared example is left
exactly as it is for README readers and installed CLIs.

Reason: every hardening step this fix wanted was otherwise a behavior change
shipped unversioned to consumers who never asked for it, because that file is
fetched from main at runtime. Gating dotcms on opensearch health was the sharpest
case — it introduces a way for dotCMS to never start if the probe later breaks
(an opensearch:1 -> :2 bump invalidating admin:admin), where today it starts
regardless. Owning the file makes strictness free.

This removes what the spec itself called the single largest regression risk in
the work. Two smaller risks replace it, both recorded: the bundled asset must be
listed in package.json `files` AND project.json esbuild `assets` or it ships
missing and every local-Docker run fails at step one (new AC-013); and strict
gating means a future broken opensearch probe stops dotCMS starting, contained to
this CLI's own stack.

Accepted consequence: users on <=1.2.5 keep the old shared file and are not
repaired. This starts fresh local instances rather than serving CI, no known
users have it in CI, and npx resolves to the latest published version.

AC-009 now requires continuous feedback for the whole wait, not just visible pull
progress — ten minutes of frozen spinner is the failure this issue was reported
for. AC-010 inverts to asserting docker/docker-compose-examples/* is UNCHANGED,
verified by diff.

Reviewer rationale updated: the blame-derived reviewers were chosen for a shared
file this work no longer touches.

Refs #37262

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@fmontes
fmontes force-pushed the issue-37262-create-app-docker-uve-spec branch from 09da5c7 to 5d4cf45 Compare August 28, 2026 20:38
Review against the tree turned up five places where the spec and the code
disagreed, and left four questions for the reviewer to decide. All are now
resolved in the spec so PR 1 is approving what will actually be built.

Decisions:

* start_period is 180s, not the two conflicting values the spec carried
  (120s in Fix Scope, 180s in the review question). ~4x the measured ~46s
  boot and above both in-repo precedents.
* The stricter opensearch: service_healthy gate is kept, using
  single-node-os-migration's proven probe. A credential-free variant was
  considered and rejected: the admin:admin coupling is contained by the
  major-version tag pin, and a proven probe beats an unproven one on the
  critical path.
* The two duplicated UVE call sites collapse into one configureUVE()
  owner that contains no process.exit, rather than being patched in place.
* AC-012 is enforced by a Jest spec against the real bundled asset. The
  scripts/verify-cold-start.sh reference is removed - that file does not
  exist and nothing tasked its creation.

Corrections found while applying them:

* Two wrong premises about start_period. A too-short window does not make
  `docker compose up --wait` block until timeout; it marks the container
  unhealthy and makes --wait abort early, abandoning an instance that
  would have been healthy, and restart: unless-stopped cannot rescue it
  because restart policies react to exit, not health. This is why
  overshooting start_period is free.
* The 403 guidance cannot be shared between the two paths. On the local
  stack it is the bricked boot and `docker compose down -v` is the fix; on
  a user-supplied server there is no stack to recreate and a 403 means the
  token lacks permission on the site, where manual UVE setup does work.
  configureUVE() now takes a mode and AC-005 specifies both messages.
* The compose verification method targeted the shared single-node-demo-site
  file that AC-010 requires to be unchanged, and which has no dotcms
  healthcheck for --wait to assess. Retargeted at the bundled asset.
* Sibling counts corrected: four files use condition: service_healthy and
  three gate dotcms directly. single-node-os-migration gates dotcms on
  OpenSearch health transitively via provision jobs, so the stricter gate
  has prior art here and is not the clean break from precedent the spec
  claimed.

One item remains open and is blocking: /dotmgt/livez is unconfirmed on the
released dotcms/dotcms:latest image, and the whole compose design depends
on it.

Refs #37262

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…scope

The blame-derived reviewer list was selected against the shared
single-node-demo-site/docker-compose.yml, back when this work modified that
file. It no longer does: the compose file that ships lives in
core-web/libs/sdk/create-app/assets/ and is reviewed as SDK code with the
rest of the CLI change. With no infrastructure change to gate on its owners,
the list is dropped rather than carried as courtesy CCs.

Two points from that section survive on their own merits and are kept:

* .github/CODEOWNERS covers neither docker/ nor the SDK tree, so PR 2 draws
  no automatic reviewer and one has to be requested by hand.
* lgtm-observability/docker-compose.yml should be read directly as the
  reference for correct condition: service_healthy usage - its author is no
  longer a collaborator, so the file is the specification. It binds 8090 on
  the wildcard, which this design deliberately does not copy.

Section renamed from "Required reviewers for the compose change" to
"Compose design decisions". The four settled decisions and the /dotmgt/livez
confirmation are unchanged.

Refs #37262

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@fmontes
fmontes removed the request for review from jcastro-dotcms August 31, 2026 17:24
Both were written into the spec during review and both turned out to be wrong.
Correcting them in the spec rather than leaving the implementation to silently
disagree with it.

AC-012 described scripts/verify-cold-start.sh as a phantom reference that nothing
created, and replaced it with a Jest guard. The script exists. It ships in this
package, it was already written against the bundled asset rather than the shared
example, and it already carries a --static mode that runs the config-only checks
with no Docker daemon - measured at 9 passed / 0 failed in 0.3s. The claim was
made after checking only the spec branch, where the file genuinely is absent
because it lands one PR up the stack. What WAS wrong is the path: the script is
under the package's own scripts/, not the repository root.

Both guards are now named, because they assert different things and neither
subsumes the other: --static checks that the FILE still matches the shape
installed CLIs depend on, while the Jest spec runs applyStarterUrl() and checks
the FUNCTION's output. Keeping only one leaves a real gap.

The image-tag deferral cited ADR-0019 as the reason it "deserves its own
decision". Reading ADR-0019 in full, that is backwards: the ADR's motivating
problem is precisely a client pinned to `latest` against a mismatched instance
producing a cryptic runtime failure, and under date-lockstep the SDK version IS
the release version - so it supplies the tag to pin rather than complicating the
choice. The deferral still stands, but on scope: `latest` is what ships today, so
leaving it preserves the status quo rather than introducing new drift inside a
bug fix.

Refs #37262

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

@nicobytes nicobytes left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Two questions from a /speckit-clarify ambiguity pass over the spec. Both are gaps that change task decomposition or test design, so they are worth settling before /speckit-plan output is treated as final — I checked #37264 and neither is resolved there. Details inline on the two ACs they affect.

Comment thread specs/37262-create-app-docker-uve/spec.md Outdated
Comment thread specs/37262-create-app-docker-uve/spec.md Outdated
@zJaaal

zJaaal commented Sep 1, 2026

Copy link
Copy Markdown
Member

Review: spec is approvable, with one missed defect and two claims to soften

I checked the spec's claims against the tree rather than just reading it. Everything cited verifies. Details below, in the order I'd act on them.

1. Missed defect: two unawaited async calls (src/index.ts:376, :378)

moveDockerComposeOneLevelUp() and moveDockerComposeBack() are both async (src/git/index.ts:76, :82, each doing await fs.rename), and both are called without await:

// required since git requires empty directory
moveDockerComposeOneLevelUp(finalDirectory);
await startScaffoldingFrontEnd({ spinner, selectedFramework, finalDirectory });
moveDockerComposeBack(finalDirectory);

The comment states the very requirement the missing await breaks: the rename may not have landed before the git clone runs against a directory that must be empty. AC-008 covers this file but only asks for try/finally, which does not fix a floating promise. Suggest AC-008 also require the awaits.

Related wording slip in Cause 3: the spec says moveDockerComposeOneLevelUp() "calls process.exit(1) internally". It does not. The exits are in startScaffoldingFrontEnd at :588 and :599. The orphaned-compose-file conclusion still holds, only the attribution is wrong.

2. The OpenSearch "prior art" is thinner than written

The spec leans on single-node-os-migration twice: for the probe (L61-65, verified verbatim) and for "transitive prior art" that dotcms gates on OpenSearch health. That file is the tester harness for the unreleased OpenSearch 3.x migration (DOT_FEATURE_FLAG_OPEN_SEARCH_PHASE, phases 0-3), and its gate exists because provision jobs must create search users before dotCMS connects, not as a boot-ordering practice.

The three non-migration examples that gate dotcms all use db: service_healthy + opensearch: service_started (lgtm L90/L92, metrics-monitoring L88/L90, experiments L139/L141), exactly as the spec states. So the stricter gate is a deliberate deviation from every non-migration precedent. Regression Risk already owns the new failure mode, and since the file is now CLI-owned the blast radius is contained, so this is not a design objection. I'd just drop the "not the clean break from precedent an earlier draft claimed" framing: the earlier draft was closer to right.

Footnote on the same probe: single-node-os-migration pins opensearchproject/opensearch:1.3.20, while the demo stack uses floating :1. The independent verification on this stack at ~15s is what actually matters, so this is minor, but "proven in single-node-os-migration" was proven against a different image.

3. Question on AC-010

AC-010 freezes docker/docker-compose-examples/single-node-demo-site/docker-compose.yml, which leaves everyone on CLI <=1.2.5 and every README reader on the broken file indefinitely. Worth deciding explicitly rather than by omission: should a one-line db: condition: service_healthy land there separately? The healthcheck already exists at L13 and nothing consumes it, so that single change addresses the actual root cause for existing users with none of the risk the rescope removed: no OpenSearch health gate, no 8090 publishing, no new failure mode.

What verified clean

Every cited line and count holds: src/index.ts:226-228 and :369-371 are byte-identical fatal blocks; :597's if (!result) against a truthy Err() is real; src/utils/index.ts:479 and src/asks.ts:180 are correct; the CUSTOM_STARTER_URL regex sits at :490 and throws at :496; fetchWithRetry accepts any 2xx (utils:55) while isDotcmsRunning demands exactly 200 (index.ts:507); zero spec files present, Jest harness in place, passWithNoTests: true at project.json:75.

AC-013 is well aimed and I'd treat it as the highest-value AC in the list. package.json files is ["*.js", "README.md"] and the esbuild assets block lists only README and package.json, so a bundled compose asset ships missing unless both are edited.

Compose precedents also check out: four files use condition: service_healthy, lgtm binds 8090 on the wildcard at L195, and both /dotmgt/livez probers run dotcms/dotcms-test:1.0.0-SNAPSHOT with start_period 120s and 20s respectively. The "confirm /dotmgt/livez on the released image before the healthcheck depends on it" prerequisite is correctly flagged as unverified, and I agree it gates the compose design.

One incidental find while verifying, not worth an AC: checkPortsAvailability() still labels 9200/9600 as "Elasticsearch HTTP" and "Elasticsearch Transport" while the stack has run OpenSearch since #27754. Cosmetic, but it is the kind of thing that makes readers doubt which search backend the demo actually uses.


Reviewed by Claude Code on @zJaaal's behalf; the claim verification above was run against the working tree.

zJaaal
zJaaal previously approved these changes Sep 1, 2026
Four findings from @nicobytes and @zJaaal, all verified against the tree before
acting.

--wait was listed under "CLI (P1)" while AC-001 and AC-002 are P0 and have no
other stated mechanism (@nicobytes). Shipping P0 without it would leave both
unsatisfiable. Promoted to P0, and AC-009's continuous feedback travels with it
for the same reason - a ten-minute silent wait is the symptom this issue was
reported for. AC-009 previously carried no tier tag at all. Also stated
explicitly, since the spec never did: all P1 items are in scope for this fix, not
a follow-up; the tiers say what must land for the fix to be coherent versus what
makes it good, not how it is split into PRs.

AC-013 had no verification method (@nicobytes), which the spec itself calls the
most likely way to break the release. The gate is now named, and named carefully:
it asserts the BUILD OUTPUT, not the source tree. Checking the two manifests is
necessary but not sufficient - a wrong `output:` in the esbuild assets entry
satisfies a manifest check and still puts the file where the package does not
carry it. Likewise a Jest spec or `verify-cold-start.sh --static` resolving the
asset relative to src/ passes identically whether or not it ever ships, which is
precisely the failure AC-013 exists to catch. The gate is a post-build assertion
over dist/ plus `npm pack --dry-run` tarball contents.

The OpenSearch "prior art" framing was mine and it was wrong (@zJaaal).
single-node-os-migration is the tester harness for the unreleased OpenSearch 3.x
migration; its gate exists so provision jobs create search users before dotCMS
connects, not as boot ordering. All three non-migration examples use
`opensearch: service_started`. So the stricter gate IS a deliberate deviation from
every non-migration precedent, and calling it "prior art, one step removed" made a
deviation sound like house practice. Withdrawn. (The reviewer also notes that file
pins opensearch:1.3.20 while the demo stack floats on :1, so "proven in
single-node-os-migration" was proven against a different image - the independent
~15s measurement on this stack is what actually carries the probe.)

Cause 3 said moveDockerComposeOneLevelUp() "calls process.exit(1) internally"
(@zJaaal). It does not - the exits are in startScaffoldingFrontEnd at :588 and
:599. Conclusion unaffected, attribution corrected. The same finding surfaced a
real defect the spec had missed: both moves are `async` and called WITHOUT await,
so the rename may not have landed before the clone runs against a directory that
must be empty. try/finally does not fix a floating promise. AC-008 now requires
the awaits as well.

Still open for a decision, not actioned here: @zJaaal asks whether a one-line
`db: condition: service_healthy` should land in the shared single-node-demo-site
example after all. AC-010 freezes it, which leaves every CLI <=1.2.5 user and
every README reader on the broken file indefinitely, and that one change carries
none of the risk the rescope removed.

Refs #37263, #37262

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
fmontes added a commit that referenced this pull request Sep 1, 2026
)

Closes the gap @nicobytes raised on #37263: AC-013 is the one criterion the spec
itself calls "the most likely way to break the release", and nothing actually
enforced it.

What existed asserted the wrong thing. packaging.spec.ts reads package.json and
project.json from the SOURCE TREE, and verify-cold-start.sh --static resolves the
asset relative to src/. Both pass identically whether or not the file ever ships.
A wrong `output:` in the esbuild assets entry satisfies every one of them and
still publishes a package with no compose file - and then every local-Docker run
fails at its first step, which is precisely what AC-013 exists to prevent. The
packaging had in fact been verified once, by hand, with npm pack --dry-run; there
was no gate.

scripts/verify-package.sh asserts the artifact instead:

  * the compose asset is in dist/libs/sdk/create-app at the path the CLI resolves
    at runtime (resolveComposeSource walks up from the bundle entry, so it must
    sit beside index.js exactly as it does in the source tree), and
  * npm pack --dry-run lists it in the tarball contents.

Those are two independent failures and both are checked, because either alone
ships a broken package: esbuild copies the file but `files` omits it, or `files`
is right and the copy never happened.

Confirmed to FAIL on both modes rather than assumed: removing dist/assets fails
both checks; restoring the asset and reverting `files` to its pre-fix
["*.js", "README.md"] passes the first and fails the second, naming package.json.

Wired as the nx target `verify-package` with dependsOn build, deliberately NOT
folded into `test` - tests must stay fast and must not require a build.

Also tags T052 with AC-011. That criterion (8090 answers on loopback, refused on
the LAN address) was already enforced by verify-cold-start.sh's T007 check but
referenced by no task, so the only security criterion in the list read as
uncovered. Acceptance-criteria coverage is now 13/13.

144 tests, 11 suites; build, lint and both verification scripts clean.

Refs #37262, #37263

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
fmontes added a commit that referenced this pull request Sep 1, 2026
Neither script ran in CI. The Maven build drives nx with exactly four
invocations - `nx affected -t lint`, `nx format:check`, `nx run-many -t build`,
`nx affected -t test` - so `verify-package` was never reached, and
verify-cold-start.sh was not an nx target at all. Both were manual, which for
AC-013 in particular means the criterion the spec calls "the most likely way to
break the release" had a gate nobody ran.

Both now hang off this project's `test` target, so `nx affected -t test` picks
them up. That keeps the change inside libs/sdk/create-app/project.json rather
than editing core-web/pom.xml, which every project in the monorepo shares.

New target `verify-compose-static` runs verify-cold-start.sh --static: the
config-only half, no Docker, ~0.3s. It is what catches a reformatted
CUSTOM_STARTER_URL line silently breaking --starter for every installed CLI. The
runtime half (cold start, restart recovery, LAN exposure) needs a real daemon and
a multi-minute starter import and stays manual.

Both targets get `cache: true` with narrow inputs - the compose asset, the
manifests and the scripts themselves - so they do not re-run on unrelated edits.

Measured: 4s cold, 1s warm, against 144 Jest tests. This reverses the "keep tests
fast and buildless" call made when verify-package was added; that rule earns its
keep on a large Angular library, not on a CLI whose build is 1.3s.

Verified the chain actually fails rather than assumed: reverting `files` in
package.json to its pre-fix ["*.js", "README.md"] fails the packaging gate and
aborts the run before Jest executes.

Refs #37262, #37263

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@fmontes
fmontes added this pull request to the merge queue Sep 1, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Sep 1, 2026
@fmontes
fmontes added this pull request to the merge queue Sep 1, 2026
Merged via the queue into main with commit e73aa33 Sep 1, 2026
44 checks passed
@fmontes
fmontes deleted the issue-37262-create-app-docker-uve-spec branch September 1, 2026 19:11
fmontes added a commit that referenced this pull request Sep 1, 2026
)

Closes the gap @nicobytes raised on #37263: AC-013 is the one criterion the spec
itself calls "the most likely way to break the release", and nothing actually
enforced it.

What existed asserted the wrong thing. packaging.spec.ts reads package.json and
project.json from the SOURCE TREE, and verify-cold-start.sh --static resolves the
asset relative to src/. Both pass identically whether or not the file ever ships.
A wrong `output:` in the esbuild assets entry satisfies every one of them and
still publishes a package with no compose file - and then every local-Docker run
fails at its first step, which is precisely what AC-013 exists to prevent. The
packaging had in fact been verified once, by hand, with npm pack --dry-run; there
was no gate.

scripts/verify-package.sh asserts the artifact instead:

  * the compose asset is in dist/libs/sdk/create-app at the path the CLI resolves
    at runtime (resolveComposeSource walks up from the bundle entry, so it must
    sit beside index.js exactly as it does in the source tree), and
  * npm pack --dry-run lists it in the tarball contents.

Those are two independent failures and both are checked, because either alone
ships a broken package: esbuild copies the file but `files` omits it, or `files`
is right and the copy never happened.

Confirmed to FAIL on both modes rather than assumed: removing dist/assets fails
both checks; restoring the asset and reverting `files` to its pre-fix
["*.js", "README.md"] passes the first and fails the second, naming package.json.

Wired as the nx target `verify-package` with dependsOn build, deliberately NOT
folded into `test` - tests must stay fast and must not require a build.

Also tags T052 with AC-011. That criterion (8090 answers on loopback, refused on
the LAN address) was already enforced by verify-cold-start.sh's T007 check but
referenced by no task, so the only security criterion in the list read as
uncovered. Acceptance-criteria coverage is now 13/13.

144 tests, 11 suites; build, lint and both verification scripts clean.

Refs #37262, #37263

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
fmontes added a commit that referenced this pull request Sep 1, 2026
Neither script ran in CI. The Maven build drives nx with exactly four
invocations - `nx affected -t lint`, `nx format:check`, `nx run-many -t build`,
`nx affected -t test` - so `verify-package` was never reached, and
verify-cold-start.sh was not an nx target at all. Both were manual, which for
AC-013 in particular means the criterion the spec calls "the most likely way to
break the release" had a gate nobody ran.

Both now hang off this project's `test` target, so `nx affected -t test` picks
them up. That keeps the change inside libs/sdk/create-app/project.json rather
than editing core-web/pom.xml, which every project in the monorepo shares.

New target `verify-compose-static` runs verify-cold-start.sh --static: the
config-only half, no Docker, ~0.3s. It is what catches a reformatted
CUSTOM_STARTER_URL line silently breaking --starter for every installed CLI. The
runtime half (cold start, restart recovery, LAN exposure) needs a real daemon and
a multi-minute starter import and stays manual.

Both targets get `cache: true` with narrow inputs - the compose asset, the
manifests and the scripts themselves - so they do not re-run on unrelated edits.

Measured: 4s cold, 1s warm, against 144 Jest tests. This reverses the "keep tests
fast and buildless" call made when verify-package was added; that rule earns its
keep on a large Angular library, not on a CLI whose build is 1.3s.

Verified the chain actually fails rather than assumed: reverting `files` in
package.json to its pre-fix ["*.js", "README.md"] fails the packaging gate and
aborts the run before Jest executes.

Refs #37262, #37263

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
wezell pushed a commit that referenced this pull request Sep 3, 2026
)

Closes the gap @nicobytes raised on #37263: AC-013 is the one criterion the spec
itself calls "the most likely way to break the release", and nothing actually
enforced it.

What existed asserted the wrong thing. packaging.spec.ts reads package.json and
project.json from the SOURCE TREE, and verify-cold-start.sh --static resolves the
asset relative to src/. Both pass identically whether or not the file ever ships.
A wrong `output:` in the esbuild assets entry satisfies every one of them and
still publishes a package with no compose file - and then every local-Docker run
fails at its first step, which is precisely what AC-013 exists to prevent. The
packaging had in fact been verified once, by hand, with npm pack --dry-run; there
was no gate.

scripts/verify-package.sh asserts the artifact instead:

  * the compose asset is in dist/libs/sdk/create-app at the path the CLI resolves
    at runtime (resolveComposeSource walks up from the bundle entry, so it must
    sit beside index.js exactly as it does in the source tree), and
  * npm pack --dry-run lists it in the tarball contents.

Those are two independent failures and both are checked, because either alone
ships a broken package: esbuild copies the file but `files` omits it, or `files`
is right and the copy never happened.

Confirmed to FAIL on both modes rather than assumed: removing dist/assets fails
both checks; restoring the asset and reverting `files` to its pre-fix
["*.js", "README.md"] passes the first and fails the second, naming package.json.

Wired as the nx target `verify-package` with dependsOn build, deliberately NOT
folded into `test` - tests must stay fast and must not require a build.

Also tags T052 with AC-011. That criterion (8090 answers on loopback, refused on
the LAN address) was already enforced by verify-cold-start.sh's T007 check but
referenced by no task, so the only security criterion in the list read as
uncovered. Acceptance-criteria coverage is now 13/13.

144 tests, 11 suites; build, lint and both verification scripts clean.

Refs #37262, #37263

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
wezell pushed a commit that referenced this pull request Sep 3, 2026
Neither script ran in CI. The Maven build drives nx with exactly four
invocations - `nx affected -t lint`, `nx format:check`, `nx run-many -t build`,
`nx affected -t test` - so `verify-package` was never reached, and
verify-cold-start.sh was not an nx target at all. Both were manual, which for
AC-013 in particular means the criterion the spec calls "the most likely way to
break the release" had a gate nobody ran.

Both now hang off this project's `test` target, so `nx affected -t test` picks
them up. That keeps the change inside libs/sdk/create-app/project.json rather
than editing core-web/pom.xml, which every project in the monorepo shares.

New target `verify-compose-static` runs verify-cold-start.sh --static: the
config-only half, no Docker, ~0.3s. It is what catches a reformatted
CUSTOM_STARTER_URL line silently breaking --starter for every installed CLI. The
runtime half (cold start, restart recovery, LAN exposure) needs a real daemon and
a multi-minute starter import and stays manual.

Both targets get `cache: true` with narrow inputs - the compose asset, the
manifests and the scripts themselves - so they do not re-run on unrelated edits.

Measured: 4s cold, 1s warm, against 144 Jest tests. This reverses the "keep tests
fast and buildless" call made when verify-package was added; that rule earns its
keep on a large Angular library, not on a CLI whose build is 1.3s.

Verified the chain actually fails rather than assumed: reverting `files` in
package.json to its pre-fix ["*.js", "README.md"] fails the packaging gate and
aborts the run before Jest executes.

Refs #37262, #37263

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: No status

Development

Successfully merging this pull request may close these issues.

create-app: local Docker run never starts dotCMS, then a transient UVE 403 aborts the CLI and discards the project

3 participants