Commit Graph

38 Commits

Author SHA1 Message Date
Jordan Ritter 3dc43b9ca9 feat(showcase): bring prod autoUpdates under drift-gate management
Prod autoUpdates is now "disabled" (was "unmanaged") for every service, so the
drift gate enforces prod as well as staging. Paired with disabling autoUpdates
on the live prod Railway services. Regenerates the SSOT JSON.
2026-07-20 16:31:28 -07:00
Jordan Ritter a33e313df8 feat(showcase): make autoUpdates per-env for staging-first rollout
autoUpdates is now per-env: staging is enforced "disabled" while prod is
"unmanaged" (the drift gate skips it) so prod stays untouched until a later
migration. The gate enforces managed envs and skips unmanaged ones; the
zero-checked floor applies only to managed envs. Regenerates the SSOT JSON.
2026-07-20 15:31:23 -07:00
Jordan Ritter 2ecba43d2f feat(showcase): track autoUpdates in SSOT, disabled fleet-wide
autoUpdates was tracked nowhere and had drifted (24 services minor / 17 none).
Add an explicit disabled autoUpdates policy to every service in railway-envs.ts
and regenerate railway-envs.generated.json, making CI-explicit redeploy the
single deploy path instead of Railway's registry auto-watch.
2026-07-20 15:08:34 -07:00
Jordan Ritter e3b3d2f14c fix(showcase): pin ANTHROPIC_BASE_URL aimock serviceRef for claude-sdk-python (SSOT drift-proof)
The claude-sdk-python agent routes its LLM traffic through ANTHROPIC_BASE_URL
(see src/agents/claude_agent_sdk_adapter.py and the aimock-wiring probe's
claude-sdk pattern), but the railway-envs SSOT only declared an OPENAI_BASE_URL
serviceRef. Add the ANTHROPIC_BASE_URL -> aimock serviceRef so the Stage-2 Ruby
promote preflight asserts it prod->prod (never copies) and refuses a cross-env
leak. The var is already set correctly on the live service; this is SSOT
hygiene that makes the pin drift-proof. Regenerated railway-envs.generated.json
via the repo generator (oxfmt-canonical, emit --check clean).
2026-07-18 11:16:09 -07:00
Jordan Ritter 3f36822f9b fix: route showcase demos to aimock over private Railway networking (egress fix)
Add an env-scoped `internalDomain` (showcase-aimock.railway.internal) to the
aimock SSOT entry in both envs and emit it as `internalDomains` in the
generated JSON. Railway bills public *.up.railway.app traffic as egress even
intra-project, while *.railway.internal private networking is free and
env-scoped. aimock is ~89% of showcase egress; routing the ~20 demo backends'
LLM traffic at the private host over http://showcase-aimock.railway.internal:4010
eliminates the billed path. The public `domain` is retained for health probes.

Serviceref host resolution + assertions to follow in subsequent commits on
this branch.
2026-07-13 20:42:48 -07:00
Jordan Ritter 2225a09ff9 feat(showcase): bring deploy rollover (overlap/draining) under SSOT
Layer (c) deploy-rollover config for the harness-workers fleet — pure
Railway config, no custom rolling-restart code. Declares overlapSeconds=45
(capacity floor: old deployment serves until new workers register+claim, so
no staleness dip) and drainingSeconds=180 (SIGTERM->SIGKILL window >=
PLATFORM_STOP_GRACE_MS so the layer-(b) 3s+90s composed worker-drain finishes)
for both prod and staging in the railway-envs SSOT, regenerates the JSON
snapshot, and extends the harness-workers drift gate so CI fails if either
field drifts between SSOT and snapshot. Documents both knobs, their rationale
(incl. that Railway's draining default is 0s = immediate SIGKILL), the
composition with drain layers a+b, and how to apply them (GraphQL
serviceInstanceUpdate / dashboard) in RAILWAY.md.
2026-06-27 22:22:00 -07:00
Jordan Ritter 2362599d19 fix(showcase): SSOT models effective multiRegionConfig replica count + reconciled prod=6 parity
The harness-workers SSOT modeled only the top-level numReplicas, whose drift
gate watched a field that does not drive the live replica count. harness-workers
is single-region (us-west2); Railway derives the live count from
multiRegionConfig.us-west2.numReplicas.

- WorkerProvisioning gains effectiveReplicas (= multiRegionConfig.us-west2.
  numReplicas), the authoritative field the drift gate now asserts. Top-level
  numReplicas is retained as a documented mirror.
- Declared values reflect current reconciled reality (verified live via the
  Railway GraphQL environment.config staged-config read): prod and staging both
  at effectiveReplicas=6 (parity achieved by B-reconcile scaling prod 3 -> 6 in
  both the top-level field and multiRegionConfig). BROWSER_POOL_MAX_CONTEXTS is
  40 on both envs (verified live).
- Regenerated railway-envs.generated.json; drift-gate test asserts
  effectiveReplicas (RED-GREEN proven). RAILWAY.md documents multiRegionConfig
  as the effective knob and the achieved parity.
2026-06-26 14:13:14 -07:00
Jordan Ritter 893104c377 feat(showcase): bring harness-workers provisioning under SSOT
Add WorkerProvisioning interface and workerProvisioning field to the
harness-workers ServiceEntry in railway-envs.ts. Declares current live
reality: prod=3 replicas, staging=6 replicas, BROWSER_POOL_MAX_CONTEXTS=40
per worker (both envs).

Worker model: 1-worker-per-replica (Railway runs one process per
container, keyed on HOSTNAME). HARNESS_POOL_COUNT is informational
only — not a fork factor. Authoritative concurrency knob per worker is
BROWSER_POOL_MAX_CONTEXTS.

Comments flag: staging config-field drift (Railway field=2, live=6) as
a follow-up item; and the prod/staging parity decision (prod=3 vs
staging=6) as deliberately deferred.

Extends emit-railway-envs-json.ts to emit workerProvisioning into the
generated JSON snapshot. Adds drift-gate test
(harness-workers-provisioning.test.ts) that fails if SSOT numReplicas
diverges from the committed JSON snapshot — no live Railway API calls.
Red-green-red-green verified locally.

Updates RAILWAY.md with the 1-worker-per-replica model, declared
values, manual apply procedure, and drift-gate reference.

The existing tooling is verify-only for numReplicas; applying a replica
count change to Railway remains a manual operation (Railway Dashboard or
GraphQL API).
2026-06-26 13:41:36 -07:00
Jordan Ritter e88d01a99f fix(showcase): backfill prod harness-workers into SSOT so image rebuilds bounce it
The prod `harness-workers` fleet worker (serviceId
c2aa8a0b-350e-4b76-8541-3012dfac41d0, instance
7c48ee43-6df4-457b-b977-10f1f1ac1680) runs the shared `showcase-harness`
image (`imageOf: "harness"`) but had NO `prod` env entry in the
railway-envs SSOT. `expandImageConsumers` is env-aware — a consumer only
joins an env's redeploy scope if it declares that env — so a rebuilt
`showcase-harness:latest` bounced the prod control-plane but SILENTLY
SKIPPED the prod worker, leaving it pinned to a stale 2026-06-19 image.

That stale worker image carries a 1-demo `registry.json` for
`ms-agent-harness-dotnet` (only `beautiful-chat`), so the hourly
`e2e_demos` driver running on it produced only 1 of 39 `e2e:` rows in
prod PocketBase. The other 38 feature rows were absent → `resolveD3`
exists=false → `UI` badge omitted → broken D3 rung → D0.

Backfill the live prod worker as a `prod` env entry (real
serviceInstance ID), flip `gateIgnore` off, and set `gateValidated:
true` so the env-aware `imageOf` expansion now pulls the prod worker
into the prod redeploy scope on every `showcase-harness` rebuild.
Regenerate the emitted JSON + golden fixture and update the two
gate-count assertions accordingly.
2026-06-25 22:46:32 -07:00
Jordan Ritter 27dcf4a404 fix(showcase): promote strands-typescript to production (dual-env SSOT)
The showcase-strands-typescript integration was staging-only: it had no
production Railway serviceInstance, so the prod D6 dashboard column showed
a uniform false-red (every cell errorClass=goto-error, backendUrl="") —
the probe navigated a bare relative path because the harness had no prod
health record / backendUrl to discover.

Provisions the prod serviceInstance (8a50728e-6119-43c4-b59c-d9535b6717a4,
domain showcase-strands-typescript-production.up.railway.app, healthcheck
/api/health, image pinned to the GHCR @sha256 digest, OPENAI_BASE_URL at
prod aimock) and brings the SSOT to the dual-env showcase-strands shape:

- railway-envs.ts: add the prod env entry with the real instanceId,
  gateValidated:true, drop gateIgnore, remove the legacyJsonCompat
  prod-domain placeholder.
- railway-envs.generated.json: regenerated (prod instanceId/domain, probe.prod
  true, prod healthcheck; moved into the promote closure, tier 2).
- railway-envs.golden.json: regenerated to include the new prod (service,env)
  pair (intentional behavior change, not a refactor regression).
- showcase_promote.yml: dropdown regenerated to list strands-typescript.
- verify-railway-image-refs.test.ts / redeploy-env.test.ts: update the
  gateValidated/scope counts (39->40 gate targets, prod default 38->39) and the
  now-stale staging-only comments.

RED->GREEN (live prod): BEFORE /api/health 404, prod PocketBase
health:strands-typescript totalItems:0, the 3 named D6 cells all
errorClass=goto-error backendUrl="". AFTER /api/health 200, prod PocketBase
health:strands-typescript present (status:200, valid url),
verify-railway-image-refs OK 80 instances.
2026-06-25 13:22:31 -07:00
Ran Shem Tov d779f71468 feat(showcase): deploy strands-typescript integration to staging
Wire the strands-typescript showcase integration for staging deployment,
mirroring how the Python strands integration is deployed.

- manifest: flip deployed: true so the shell lists it in the integration menu
- railway-envs.ts: add showcase-strands-typescript SSOT entry (staging-only
  for now: prod instance not yet provisioned, so it omits the prod env and is
  gateIgnore'd until promoted dual-env); regenerate railway-envs.generated.json
- showcase_build.yml + showcase_build_check.yml: add the strands-typescript
  build matrix entry, change-detection filter, and dispatch option (railway_id
  is the new Railway service id)
- golden fixture + image-ref-gate inventory tests updated for the new service

Railway staging service showcase-strands-typescript provisioned
(showcase-strands-typescript-staging.up.railway.app, health /api/health,
OpenAI-via-aimock env). Prod is added later via the promote pipeline.
2026-06-24 19:39:36 +02:00
Jordan Ritter ec646bbf4f Merge remote-tracking branch 'origin/main' into chore/remove-harness-legacy-ssot
# Conflicts:
#	showcase/scripts/railway-envs.generated.json
#	showcase/scripts/railway-envs.ts
2026-06-23 17:56:18 -07:00
Jordan Ritter 0809c25dac chore(showcase): remove redundant harness-legacy service from SSOT and fixtures
The pool-fleet migration is complete: the control-plane harness plus the
prod workers (deployed 2026-06-19, HARNESS_ROLE=worker, pool count 2) now
cover every probe dimension the interim `harness-legacy` fleet-migration
bridge was holding live, so `harness-legacy` is dead config.

This is the code-side cleanup only:
- Remove the `harness-legacy` entry from the railway-envs SSOT and the
  now-dead `key === "harness-legacy"` special-case in computePromoteClosure.
- Regenerate railway-envs.generated.json (41 -> 40 services).
- Drop harness-legacy from the golden snapshot, the gateIgnore expectation
  sets, the promote-notify fixtures, and the redeploy-env doc comments;
  update the service-count assertions (41 -> 40).
- Fix the stale "STAGING-ONLY" harness-workers comment: prod workers are
  live on Railway, though this SSOT entry still models the staging
  instance only (no prod env backfilled here yet).

The live Railway `harness-legacy` service is torn down separately as a
follow-up infra step.
2026-06-23 17:05:26 -07:00
Jordan Ritter 59e18693eb feat(showcase): honest promote-notify message + durable healthcheckPath SSOT tracking
Promote-notify Slack message: name the promoted AND failed services (one
Failed: header + bullets), legible "(N): <names>" count, real wall-clock
elapsed (integer-coerced), and drop the constant verify-prod legend line.

Durable healthcheckPath: track it per-service/env in the SSOT (railway-envs),
re-assert it on the promote pin path (omit-when-absent, never null), and route
deploy-to-railway provisioning through isTrackedService/resolveProvisionHealthcheck
so a tracked-null service omits the healthcheck while an untracked one keeps the
/api/health default — fixing the silent prod-healthcheck drift that refused aimock.

Tests: ruby pin-reassert spec + deploy-to-railway healthcheck spec + emit/golden/accessor.
2026-06-23 16:56:16 -07:00
Jordan Ritter bc784a1412 feat(showcase): make starter fleet always-on (sleepApplication=false, staging probe on)
Flip the 12 starter-* services to behave like every other managed
showcase service: always-on and staging-probed.

- railway-envs SSOT: staging probe false -> true for all 12 starters
  (prod was already on); refresh the stale "staging probe OFF" /
  fail-loud-placeholder doc-comments to reflect baseline-driver verification.
- provision-starter-fleet: sleepApplication true -> false so the
  provisioner creates always-on (not sleepable) Railway instances; update
  the "the whole point — sleepable" header/inline comments + log lines.
- Regenerate railway-envs.generated.json (emit-railway-envs-json.ts) and
  the golden fixture; update the SSOT probe + provisioner sleep assertions.
2026-06-23 12:00:53 -07:00
Jordan Ritter 4d953e5ed2 fix(showcase): standalone promote services (no deps, never gated) — flag docs
A `shell-docs` promote expanded its dependency closure to include the
always-on Tier-1 verification set (harness, dashboard, …); harness then
tripped a P6 env-divergence WARN-refusal and the tier barrier gated docs
as NOT-ATTEMPTED. docs has no runtime dependency on the control plane.

Introduce a declarative `standalone` service class: a leaf that neither
depends on anything nor gates on anything. computePromoteClosure (TS) and
the resolve-promote-targets jq both skip the Tier-1 union when the request
is entirely standalone, so a standalone request promotes ONLY itself; the
fleet runner promotes `s:`-marked services ungated (never NOT-ATTEMPTED on
an unrelated failure, never gating a tier). Flag `docs` standalone.

Red-green: railway-envs.test.ts (4), resolve-promote-targets.bats (5),
promote-fleet.bats (3) fail on origin/main (docs closure pulls harness;
`s:` is an unknown tier) and pass after the fix.
2026-06-22 17:05:25 -07:00
Jordan Ritter 808bc4741d feat(showcase): promote-closure SSOT (tiers/runtimeDeps/serviceRefs) + computePromoteClosure + 12 starter entries + oxfmt-canonical emit 2026-06-19 12:23:22 -07:00
Jordan Ritter 2e675a7bdd docs(showcase): correct webhooks-bounce/imageOf/serviceEnvPairs claims, test hygiene (redeploy scripts)
The webhooks SSOT comment claimed the push-driven default scope is
"guaranteed" to leave webhooks untouched — false: a push touching the
build workflow files trips the workflow_config paths-filter disjunct,
which selects every matrix slot (webhooks included; its skip_build slot
still reports success and enters the redeploy CSV). Reworded to state
the actual behavior. imageOf doc now states the enforced NON-EMPTY
subset constraint; serviceEnvPairs doc now truthfully says it has no
consumers yet; file header notes the probe flag default and
bin/railway's Ruby-only "stage" synonym.

Test hygiene: drop the stale bin/railway line-number citation from a
test name, the _envConfigTypeAnchor (EnvironmentConfig is genuinely
referenced by the shape-compile test), a dead eslint-disable, and a
dead `as never` cast (env is an open string); align the webhooks
dispatch-name pin regex with the extraction regex's whitespace
tolerance.
2026-06-10 09:28:10 -07:00
Jordan Ritter a0ef5e7ef2 fix(showcase): close env-registry cross-wire/orphan/case/prototype gaps, trim REDEPLOY_SUMMARY_JSON
assertEnvRegistryConsistent gains four clauses: (iv) a key present in
both ENV_IDS and ENV_ID_BY_NAME must carry the same env-id (ENV_IDS.prod
drifted to the staging id previously passed every clause while
resolveEnv("prod") silently returned staging); (v) every ENV_IDS env-id
must be carried by a canonical name (was only caught lazily in
resolveEnv); (vi) registry keys must be trim().toLowerCase()-normalized
(resolveEnv lowercases input, so a non-lowercase spelling is registered
but unreachable); (vii) no registry key may be an Object.prototype
property name.

expandImageConsumers' per-entry env skip-check becomes an own-property
test (Object.hasOwn) for uniformity with every other lookup in the file.

REDEPLOY_SUMMARY_JSON is trimmed before the set-but-empty branch so a
whitespace-only value hits the loud warn path instead of attempting a
JSON write against a garbage path.
2026-06-10 09:28:09 -07:00
Jordan Ritter e56c4db155 docs(showcase): correct SSOT field/comment claims in railway-envs
- ciBuilt field doc: pocketbase IS showcase-CI-built — only webhooks
  remains out-of-band; keep the MUST-NOT-touch claim for webhooks only.
- gateValidated field doc: true for every service EXCEPT the two
  gateIgnore entries (harness-workers, harness-legacy), not "every
  service".
- CI_BUILT_SERVICES comment: also names the excluded non-CI-built
  harness-workers and harness-legacy alongside webhooks.
- legacyJsonCompat doc: it is bin/railway's EXPECTED_DOMAINS derivation
  that filters out *.up.railway.app hosts (no "parity test rejects"
  claim contradicting the placeholder data below).
- webhooks entry comment: a manual service=all build dispatch MAY
  bounce webhooks staging (the skip_build slot still reports success,
  entering the matrix ∩ success-set redeploy scope); only the
  push-driven default scope is guaranteed to leave webhooks untouched.
2026-06-10 09:06:55 -07:00
Jordan Ritter 3cf06a7e49 fix(showcase): uniform own-property accessors, new SSOT invariants, fail-loud exit default (redeploy scripts)
- railway-envs: route envsFor/instanceIdFor/domainFor/probeEnabled (and
  repoNameFor) through shared getEntry/getEnvCfg own-property helpers so
  inherited Object.prototype keys on either axis produce the curated
  error (or probeEnabled's contract false) instead of raw TypeErrors,
  silent undefined, or a spurious probe=true.
- railway-envs: two new module-load invariants (synthetic-map
  injectable): assertEnvRegistryConsistent (per-service env keys are
  registered canonical names; ENV_ID_BY_NAME env-ids unique; every
  canonical name has an ENV_IDS spelling) and
  assertServiceAndInstanceIdsUnique (serviceId unique per entry,
  instanceId globally unique).
- redeploy-env: invert the exit-code policy to fail-loud by default —
  any env except the documented staging carve-out exits non-zero on
  per-service failure (a future preview/canary env inherits fatal
  semantics instead of silently swallowing failures).
- redeploy-env: sanitize per-service THROWN error messages through
  sanitizeErrorBody before recording; flatten bare \r in the summary
  table escape.
- redeploy-env: warn on set-but-empty REDEPLOY_SUMMARY_JSON; reject
  flag-like --services CSV parts (both forms) and a flag-like first
  argument (missing env); derive usage env lists from ENV_IDS.
- tests: prototype-key sweep across all accessors, invariant
  positive/negative coverage, third-env exit-code pin, sanitization and
  CLI-guard coverage, makeLiveRedeploy !res.ok and non-true mutation
  branches.
2026-06-10 09:06:55 -07:00
Jordan Ritter bee835c4b0 chore(showcase): SSOT invariant hardening + doc accuracy (redeploy scripts) 2026-06-10 08:28:16 -07:00
Jordan Ritter c71d183247 fix(showcase): unify env-name authority + fail-loud SSOT accessors (redeploy scripts)
Confirmation-CR bucket-(a) fixes for PR #5353 — one coherent concern:
env-name resolution has exactly ONE authority (the ENV_IDS /
ENV_ID_BY_NAME registries) and SSOT accessors fail loud instead of
silently returning wrong values.

- runRedeploy: resolve envId via ENV_ID_BY_NAME with an Object.hasOwn
  guard + fail-loud throw listing the registered envs. Removes the
  hardcoded `prod`/`staging` pair check and PRODUCTION/STAGING ternary
  that contradicted the SSOT's documented open-env contract ("a new env
  needs only a registry entry"); the registry lookup subsumes it.
- resolveEnv: derive resolution entirely from the registries (ENV_IDS
  spellings -> env-id -> canonical ENV_ID_BY_NAME name) instead of its
  own hardcoded synonym chain. Behavior identical for
  prod/production/staging; still throws on unknowns, and now also
  throws on a mis-wired registry (a spelling whose env-id has no
  canonical name).
- serviceForDispatchName: fix the docstring's false "CI-built service"
  claim — it does no ciBuilt filtering and tests pin the unfiltered
  behavior (the non-CI-built webhooks resolves).
- repoNameFor: fail loud (consistent with instanceIdFor/domainFor)
  instead of silently echoing the service name — the exact
  silently-wrong-GHCR-name class this PR's hardening targets. Throws on
  unknown service, on an env not registered in ENV_ID_BY_NAME
  (unnormalized synonyms like "production"), and on a registered env
  the service does not declare. Keeps the documented default (the
  service name) for declared envs without an override.
  Call-site enumeration confirming nothing relies on the old fallback:
    - verify-railway-image-refs.ts:523 — iterates the entry's DECLARED
      environments keys, registry-filtered, SSOT-matched service names
    - __tests__/railway-envs.golden.test.ts:81 — iterates envsFor(name)
      (declared envs only) over real SSOT keys
    - railway-envs.test.ts repoNameFor cases — dual-env services,
      prod/staging only
    - __tests__/verify-railway-image-refs.test.ts:264 — FIVE_NEW keys,
      all dual-env
- resolveTargetServices: throw when an explicitly-provided services
  list resolves to zero entries (whitespace-only programmatic input)
  instead of letting runRedeploy exit 0 having redeployed nothing; the
  default undefined -> full CI-built scope is unchanged.
- makeLiveRedeploy: add signal: AbortSignal.timeout(30s) so a hung
  Railway API records a per-service FAIL instead of stalling CI, and
  pass GraphQL errors[].message through sanitizeErrorBody for
  consistency with the HTTP-error path. Exported for direct unit tests.

Red-green: 11 new tests (open-env registry resolution incl. a
runtime-registered hypothetical env, repoNameFor negatives,
empty-resolution throw, abort-signal presence, GraphQL error
sanitization) all failed against the old code; full showcase/scripts
suite green (50 files, 1807 tests) + tsc --noEmit clean.
2026-06-10 08:28:16 -07:00
Jordan Ritter 26a43a794f fix(showcase): reject prototype-key service names; sync redeploy scope docs 2026-06-09 18:09:01 -07:00
Jordan Ritter 6f3f9ef736 fix(showcase): validate imageOf env overlap, prototype-safe target lookup; clarify expansion docs 2026-06-09 18:00:46 -07:00
Jordan Ritter 2af5d691ef fix(showcase): redeploy image consumers (harness-workers) when their shared image is rebuilt
harness-workers runs the SAME showcase-harness GHCR image as the harness
scheduler but has ciBuilt:false (it has no build slot of its own), and the
CI staging redeploy scope was derived purely from ciBuilt — so a main-merge
rebuild of showcase-harness:latest only bounced the scheduler while the
workers silently kept running the stale image (PR #5352's worker-side fixes
never reached staging).

Model image consumption explicitly in the SSOT instead:

- railway-envs.ts: new optional `imageOf` field on ServiceEntry — the SSOT
  key of the ciBuilt service whose image this entry runs. Set
  `imageOf: "harness"` on harness-workers. New module-load invariant
  `assertImageConsumersValid` (fail-loud, same style as
  assertDispatchNamesUnique): imageOf must name an existing SSOT key, the
  target must be ciBuilt, and the consumer itself must not be ciBuilt.
- redeploy-env.ts: new `expandImageConsumers(names, env)` applied inside
  runRedeploy — the redeploy set becomes the resolved scope PLUS any
  service whose imageOf points at a service already in scope. Env-aware:
  a consumer only joins envs it declares, so the staging-only worker
  never enters a prod redeploy (prod behavior unchanged).

No workflow change needed: showcase_build.yml keeps passing the
built-and-successful dispatch_names; the script expands them. Gate
behavior (gateIgnore / image-ref gate), the generated JSON
(emit --check passes byte-identical), and the promote dropdown are all
untouched. harness-legacy deliberately gets no imageOf (pinned pre-fleet
digest; must not follow rebuilds).
2026-06-09 17:40:41 -07:00
Jordan Ritter da03e1626a fix(showcase): reconcile harness-workers SSOT key with the Railway service name
The pool-fleet worker's Railway service is named `harness-workers` (PLURAL),
but the SSOT keyed it `showcase-harness-worker` (singular). The image-ref gate
matches SSOT keys to Railway service names verbatim, so the gate reported
`harness-workers` as an untracked Railway service AND the stale singular key
matched nothing. Rename the SSOT key (and every test/fixture reference) to the
exact Railway name `harness-workers`.

It stays the staging-only, domainless, probe-disabled worker that runs the
shared `showcase-harness` image: serviceId c2aa8a0b-…, staging instance
362c1e37-…, ciBuilt:false, gateIgnore:true, no build slot (so no dispatchName),
single `staging` env with no domain. Add a focused test pinning that shape.
Counts are unchanged (29 services / 26 CI_BUILT) — this is a rename, not an
addition; both harness workers already existed on main.

Verified LOCALLY against Railway: verify-railway-image-refs reports
`54 env-scoped instances verified (2 skipped)` — 0 violations, 0 missing, 0
untracked (harness-workers reconciled, harness-workers + harness-legacy the 2
gateIgnore'd skips). emit --check zero drift, Ruby parity green (borrowed
.up.railway.app host is parity-excluded), full scripts suite + typecheck green.
2026-06-06 07:51:00 -07:00
Jordan Ritter 9b76a2d2a4 refactor(showcase): unify railway-envs SSOT into per-env environments map (Option C)
Replace ServiceEntry's parallel prodInstanceId/stagingInstanceId/domains/probe/
repoNameOverride fields with a single environments: Record<string, {instanceId,
domain?, probe?, repoName?}> map plus a hoisted env-independent probeDriver.
EnvName becomes an open string backed by an ENV_ID_BY_NAME registry so
accessors resolve arbitrary env names; a single-env service (the staging-only
showcase-harness-worker) now simply omits the absent env instead of carrying a
placeholder ID/borrowed host.

Accessors instanceIdFor/domainFor/repoNameFor index environments[env]
(domainFor still throws on missing/scheme); add envsFor(name),
serviceEnvPairs(), and probeEnabled(name, env). Generalize the image-ref gate
(iterate each service's declared environments, resolve env-id via the
registry, sum/iterate missingByEnv over registry env names) and verify-deploy's
host->env reverse-map (envForTarget iterates environments).

Pure TS-internal: emit-railway-envs-json.ts projects the env-map back onto the
FROZEN legacy JSON shape (prodInstanceId/stagingInstanceId/domains/probe/
repoNameOverride) via a documented legacyJsonCompat shim for the two domainless
harness workers, so railway-envs.generated.json stays byte-identical and Ruby
(bin/railway) + workflow jq + the parity test are untouched. Verified: emit
--check zero drift, Ruby test_expected_domains_parity green, golden snapshot
toEqual proves byte-identical resolution for every real (service, env) pair,
full scripts vitest suite green, showcase scripts typecheck clean.
2026-06-06 07:48:14 -07:00
Jordan Ritter fcf1a33d73 fix(showcase): track interim harness-legacy service in railway-envs SSOT (unblock harness builds)
The showcase_build verify-image-refs gate (SSOT = showcase/scripts/railway-envs.ts)
was failing with "1 untracked Railway services" because the interim
harness-legacy staging service (the legacy all-probe harness kept live during
the pool-fleet migration) exists on Railway but had no SSOT entry. That
Railway->SSOT drift check skips the build, so nothing deploys.

Adds a harness-legacy SERVICES entry mirroring the showcase-harness-worker
precedent (PR #5280): ciBuilt:false (not built by showcase_build, runs a pinned
out-of-band digest) and gateIgnore:true (deliberately-untracked for the image-ref
gate). findUntrackedServices treats any SSOT entry as known, so this clears the
untracked failure; gateValidated:false keeps findMissingServices from flagging
it. Real serviceInstance IDs for both envs recorded from Railway GraphQL.
Regenerates railway-envs.generated.json and updates the service-count /
gate-ignored carve-out assertions (28->29 services).

Verified: live verify-railway-image-refs.ts now exits 0 ("54 env-scoped
instances verified, 2 skipped"); without the entry it exits 1 with the
harness-legacy untracked failure. Full scripts test suite green (1771 passed).
2026-06-05 15:37:26 -07:00
Jordan Ritter 959d42b585 fix(showcase): add showcase-harness-worker to Railway SSOT (railway-envs.ts)
The pool-fleet cutover manually created the staging-only
`showcase-harness-worker` Railway service (HARNESS_ROLE=worker, 2
replicas) and flipped the existing `harness` service to
HARNESS_ROLE=control-plane. The new service was untracked in the SSOT,
so verify-railway-image-refs.ts failed the "Showcase: Build & Push"
workflow on every push to main (1 untracked Railway service), which
skipped the harness `build` job and blocked harness image rebuilds.

Add the worker to SERVICES as a staging-only, domain-less queue worker:
- ciBuilt:false — it runs the SAME `showcase-harness` image the existing
  harness build slot produces; there is no separate worker build.
- gateIgnore:true / gateValidated:false — no prod instance and no public
  domain, so it does not fit the symmetric dual-env shape the image-ref
  gate validates. gateIgnore clears the "untracked Railway service"
  failure (any SSOT entry counts as known) without tripping a false
  "missing from prod" failure.
- repoNameOverride → showcase-harness so the image-ref shape resolves.
- probe disabled in both envs (no externally-reachable health endpoint).

Regenerate railway-envs.generated.json and update the SSOT-count and
gate-coverage test invariants (27→28 services; worker is the sole
intentional gateIgnore/gateValidated:false entry).
2026-06-05 12:59:51 -07:00
Jordan Ritter 3c138ec450 ci(showcase): build & push the PocketBase image from main
PocketBase had no CI build path: `ghcr.io/copilotkit/showcase-pocketbase`
was a stale April `:latest`, and there was no way to ship pb_migrations /
pb_hooks changes without an ad-hoc manual build. Add a `pocketbase` slot to
showcase_build.yml's build matrix, mirroring the harness/aimock entries:
  - dispatch_name `showcase-pocketbase`, context `showcase/pocketbase`, its
    own Dockerfile, health `/api/health`, railway_id from the SSOT.
  - a paths-filter key gated to `showcase/pocketbase/**` so the slot only
    rebuilds when PB's own files change (the image is self-contained — no
    shared-module copy), not on every showcase push.
  - the workflow_dispatch service choice so PB is human-targetable.

Flip the SSOT entry (railway-envs.ts) to `ciBuilt: true` with
`dispatchName: "showcase-pocketbase"` so it is built+pushed (`:sha` +
`:latest`) and joins the default staging-redeploy scope; the build's
redeploy step only touches the matrix-intersect-success set, so PB still
only redeploys when its own files change. Regenerate
railway-envs.generated.json and the showcase_promote.yml service dropdown,
and update the SSOT/redeploy tests that pinned PB as out-of-band
(CI_BUILT_SERVICES 25 -> 26; webhooks stays the only non-CI-built service).
2026-06-04 12:23:05 -07:00
Jordan Ritter b64058a270 fix(showcase): domainFor scheme guard checks :// not startsWith http 2026-05-29 11:45:12 -07:00
Jordan Ritter 262007d808 feat(showcase): flip the five Phase-2 services into the image-ref gate
Flip dashboard, docs, dojo, shell, and harness from
gateValidated: false to gateValidated: true and simultaneously add
the corresponding repoNameOverride for both envs:

  dashboard -> showcase-shell-dashboard
  docs      -> showcase-shell-docs
  dojo      -> showcase-shell-dojo
  shell     -> showcase-shell
  harness   -> showcase-harness

These two halves MUST land in the same commit. Flipping
gateValidated without the override would make the gate look up
ghcr.io/copilotkit/<railway-name> (e.g.
ghcr.io/copilotkit/dashboard:latest) which does not exist — the
gate would fail on first run. Adding the override without flipping
gateValidated is dead code: main() short-circuits unvalidated
services before consulting the override. Only the union is correct.

Also remove the Phase-2 deferral comments and refresh the
ServiceEntry.gateValidated JSDoc — there are no Phase-2 holdouts
left. All 27 services are now gate-validated; gateIgnore remains
the sole escape hatch and is unused by every current entry.
2026-05-29 11:45:05 -07:00
Jordan Ritter 4edc41ca17 feat(showcase): add gateIgnore opt-out field to ServiceEntry SSOT
Widen `ServiceEntry` with an optional `gateIgnore: boolean` field
(default false / unset) so the image-ref gate can deliberately exclude
a Railway service from BOTH coverage-direction checks. No behavioural
change in this commit; the field is consumed by the next commit which
flips the Railway->SSOT direction from warn-only to hard-fail.

Stub-export `findUntrackedServices` from verify-railway-image-refs so
the unit test for the new direction can compile. Behavioural wiring
into main() lands in the next commit.

Adds verify-railway-image-refs.test.ts with the first unit tests
covering the new field surface and the helper contract.
2026-05-29 11:45:05 -07:00
Jordan Ritter 2a11edeb7e feat(showcase): add per-env domains + probe config to railway-envs SSOT
Adds Domains, ProbeDriver, ProbeConfig types + domainFor() helper that
throws on unknown service/env. Populates domains.{staging,prod} and
probe.{staging,prod,driver} on every SERVICES entry. Adds
emit-railway-envs-json.ts to serialize the SSOT for the Ruby side and
the workflow consumers.

Refs spec §3 / §3a.
2026-05-29 11:45:04 -07:00
Jordan Ritter 5f6bbb593a feat(showcase): enforce dispatchName uniqueness invariant on SSOT load
serviceForDispatchName iterates Object.entries(SERVICES) and returns the
first match — a silent dispatchName collision would route a redeploy to
whichever entry happens to iterate first. We now fail loud at module load
via assertDispatchNamesUnique(), and ship synthetic-input tests proving the
invariant fires on a real collision (and stays quiet for entries without a
dispatchName, which is legitimate for out-of-band services).

[BLITZ:L3-wf] E-7a/E-7b wiring.
2026-05-29 11:45:04 -07:00
Jordan Ritter 1a51fb1316 feat(showcase): add webhooks dispatch entry to build + verify workflows
Adds 'webhooks' as a workflow_dispatch choice in both showcase_build.yml
and showcase_deploy.yml so humans can redeploy/verify the webhooks service
on demand. webhooks' GHCR image (showcase-eval-webhook) is built by a
separate release workflow in the showcase-eval-webhook repo, so:

  - paths-filter uses a sentinel that cannot match any in-tree path,
    keeping push-driven runs from ever including webhooks.
  - The build matrix entry carries skip_build: true; the Build and push
    step skips the Depot build for that slot. The per-slot result still
    publishes (job.status == success) so the redeploy path proceeds and
    redeploy-env.ts picks the existing :latest from GHCR.

The SSOT entry in railway-envs.ts gains dispatchName: 'webhooks' so the
forward/reverse round-trip tests cover it. New tests pin that the SSOT
dispatchName is mirrored in both workflow files' dispatch choice lists
AND ALL_SERVICES JSON.

[BLITZ:L3-wf] E-6a/E-6b/E-6c/E-6d wiring.
2026-05-29 11:45:04 -07:00
Jordan Ritter e08051c79c feat(showcase): add railway-envs SSOT for env/service IDs and image helpers
Single source of truth mapping SSOT service keys to Railway service/env IDs,
dispatchName values, ciBuilt/gateValidated flags, and per-env repo-name overrides
(aimock and pocketbase/webhooks map to their showcase-* wrapper repos in both envs).
Adds dispatchName round-trip + workflow YAML forward-guard tests.
2026-05-29 11:45:01 -07:00