Skip to content

Commit ca620d0

Browse files
authored
fix(showcase): track interim harness-legacy service in railway-envs SSOT (unblock harness builds) (CopilotKit#5286)
## Summary The `showcase_build` workflow's pre-build `verify-image-refs` gate (SSOT = `showcase/scripts/railway-envs.ts`) was **failing all staging deploys** with `1 untracked Railway services`. The interim `harness-legacy` staging service (id `11279eba-97eb-417e-82a5-7cb4254eb147`, project `showcase`, env `staging`) exists on Railway but had no SSOT entry, so the Railway→SSOT drift check failed → build skipped → nothing deployed (this blocked PR CopilotKit#5283's merge build). `harness-legacy` is the **interim legacy all-probe harness** (`HARNESS_ROLE` unset) stood up to keep non-d6 coverage live during the fleet migration. It is **not CI-built** (runs a pinned pre-fleet image digest set out-of-band) and will be torn down at migration end. ## Change - Adds a `harness-legacy` entry to `SERVICES` mirroring the `showcase-harness-worker` precedent (PR CopilotKit#5280): - `ciBuilt: false` — not built by `showcase_build`; no dedicated build slot. - `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. - `repoNameOverride` → `showcase-harness` for both envs (same image-ref shape as the control-plane harness). - Real serviceInstance IDs recorded for both envs (resolved via Railway GraphQL); probe disabled in both envs. - Regenerates `railway-envs.generated.json`. - Updates service-count and gate-ignored carve-out assertions across the three affected test files (28 → 29 services; `harness-legacy` added to the two `GATE_IGNORED` sets). ## Verification - **Live gate (red→green):** without the entry, `verify-railway-image-refs.ts` exits **1** with `harness-legacy is not in the SSOT`; with the entry it exits **0** — `✓ 54 env-scoped instances verified (2 skipped)`. - **Full scripts test suite green:** 1771 passed (49 files), including `railway-envs.test.ts`, `verify-railway-image-refs.test.ts`, and `emit-railway-envs-json.test.ts`. - **`emit --check`** confirms `railway-envs.generated.json` is in sync. - **tsc** clean (`tsc --noEmit -p showcase/scripts/tsconfig.json`). ## Test plan - [x] `npx tsx showcase/scripts/verify-railway-image-refs.ts` exits 0 against live Railway - [x] `vitest run` in `showcase/scripts` fully green - [x] `npx tsx showcase/scripts/emit-railway-envs-json.ts --check` passes - [x] `tsc --noEmit -p showcase/scripts/tsconfig.json` clean - [ ] CI green on this PR 🤖 Generated with [Claude Code](https://claude.com/claude-code)
2 parents 5dc38ce + fcf1a33 commit ca620d0

5 files changed

Lines changed: 88 additions & 14 deletions

File tree

showcase/scripts/__tests__/emit-railway-envs-json.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ describe("emit-railway-envs-json", () => {
2525
expect(json.projectId).toBe("6f8c6bff-a80d-4f8f-b78d-50b32bcf4479");
2626
expect(json.envIds.staging).toBe("8edfef02-ea09-4a20-8689-261f21cc2849");
2727
expect(json.envIds.prod).toBe("b14919f4-6417-429f-848d-c6ae2201e04f");
28-
expect(json.services.length).toBe(28);
28+
expect(json.services.length).toBe(29);
2929
const docs = json.services.find((s: { name: string }) => s.name === "docs");
3030
expect(docs.domains.staging).toBe("docs.staging.copilotkit.ai");
3131
expect(docs.domains.prod).toBe("docs.copilotkit.ai");

showcase/scripts/__tests__/verify-railway-image-refs.test.ts

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,13 @@ import type { ServiceEntry } from "../railway-envs";
2222
describe("ServiceEntry gateIgnore field", () => {
2323
it("is optional on the type and defaults to falsy when unset", () => {
2424
// Every real SSOT entry has gateIgnore unset (undefined / falsy),
25-
// EXCEPT the staging-only `showcase-harness-worker` pool-fleet worker,
26-
// which is deliberately gateIgnore:true (no prod instance, no public
27-
// domain — it does not fit the symmetric dual-env shape the gate
28-
// validates). See its SSOT entry in railway-envs.ts for the rationale.
29-
const GATE_IGNORED = new Set(["showcase-harness-worker"]);
25+
// EXCEPT two deliberately gateIgnore:true entries: the staging-only
26+
// `showcase-harness-worker` pool-fleet worker (no public domain, does not
27+
// fit the symmetric dual-env shape the gate validates) and the interim
28+
// `harness-legacy` fleet-migration bridge (runs a pinned out-of-band
29+
// digest, not the canonical :latest/@sha256 shape). See their SSOT
30+
// entries in railway-envs.ts for the rationale.
31+
const GATE_IGNORED = new Set(["showcase-harness-worker", "harness-legacy"]);
3032
for (const [name, entry] of Object.entries(SERVICES)) {
3133
const gi = (entry as ServiceEntry).gateIgnore;
3234
if (GATE_IGNORED.has(name)) {
@@ -232,15 +234,17 @@ describe("WS-C: all gate-managed services gateValidated, with correct overrides"
232234
["harness", "showcase-harness"],
233235
] as const;
234236

235-
it("has 28 services in the SSOT", () => {
236-
expect(Object.keys(SERVICES)).toHaveLength(28);
237+
it("has 29 services in the SSOT", () => {
238+
expect(Object.keys(SERVICES)).toHaveLength(29);
237239
});
238240

239241
it("marks every gate-managed service gateValidated (no Phase-2 holdouts)", () => {
240-
// The staging-only `showcase-harness-worker` is the sole intentional
241-
// gateValidated:false entry (it is gateIgnore:true — no prod instance,
242-
// no public domain). Every OTHER service must be gateValidated:true.
243-
const GATE_IGNORED = new Set(["showcase-harness-worker"]);
242+
// Two intentional gateValidated:false entries: the staging-only
243+
// `showcase-harness-worker` (gateIgnore:true — no public domain) and the
244+
// interim `harness-legacy` fleet-migration bridge (gateIgnore:true — runs
245+
// a pinned out-of-band digest). Every OTHER service must be
246+
// gateValidated:true.
247+
const GATE_IGNORED = new Set(["showcase-harness-worker", "harness-legacy"]);
244248
const unvalidated = Object.entries(SERVICES)
245249
.filter(
246250
([name, entry]) => !entry.gateValidated && !GATE_IGNORED.has(name),

showcase/scripts/railway-envs.generated.json

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,27 @@
115115
"driver": "harness"
116116
}
117117
},
118+
{
119+
"name": "harness-legacy",
120+
"serviceId": "11279eba-97eb-417e-82a5-7cb4254eb147",
121+
"prodInstanceId": "3d125700-a08d-4a7f-904b-c13f3f7cc0fc",
122+
"stagingInstanceId": "ed184024-fdfa-4b6f-bb51-37d6648e0beb",
123+
"ciBuilt": false,
124+
"gateValidated": false,
125+
"repoNameOverride": {
126+
"prod": "showcase-harness",
127+
"staging": "showcase-harness"
128+
},
129+
"domains": {
130+
"staging": "harness-staging-2ee4.up.railway.app",
131+
"prod": "showcase-harness-production.up.railway.app"
132+
},
133+
"probe": {
134+
"staging": false,
135+
"prod": false,
136+
"driver": "harness"
137+
}
138+
},
118139
{
119140
"name": "pocketbase",
120141
"serviceId": "ba11e854-d695-4738-9a45-2b0776788824",

showcase/scripts/railway-envs.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,9 +54,9 @@ describe("railway-envs SSOT", () => {
5454
expect(ENV_IDS.staging).toBe(STAGING_ENV_ID);
5555
});
5656

57-
it("contains exactly 28 services", () => {
57+
it("contains exactly 29 services", () => {
5858
const names = listServiceNames();
59-
expect(names.length).toBe(28);
59+
expect(names.length).toBe(29);
6060
});
6161

6262
it("contains the expected canonical services", () => {

showcase/scripts/railway-envs.ts

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -332,6 +332,55 @@ export const SERVICES: Record<
332332
// probe and the Railway-internal /health healthcheck.
333333
probe: { staging: false, prod: false, driver: "harness" },
334334
},
335+
"harness-legacy": {
336+
serviceId: "11279eba-97eb-417e-82a5-7cb4254eb147",
337+
// INTERIM service (fleet-migration bridge). This is the legacy all-probe
338+
// harness (HARNESS_ROLE unset) stood up to keep the non-d6 probe coverage
339+
// live while the pool-fleet migration proceeds. It runs a PINNED pre-fleet
340+
// `showcase-harness` image digest set out-of-band — it is NOT CI-built —
341+
// and will be torn down (removed from this SSOT and from Railway) at
342+
// migration end. Real serviceInstance IDs exist for BOTH envs on Railway
343+
// (resolved 2026-06-05 via GraphQL); we record both. gateIgnore keeps the
344+
// image-ref gate from validating either instance's (out-of-band) ref, and
345+
// ciBuilt:false keeps it out of the default CI_BUILT_SERVICES redeploy
346+
// scope. The build only failed because the Railway→SSOT untracked-services
347+
// check saw this service name with no SSOT entry; listing it here clears
348+
// that without subjecting its pinned digest to the gate's shape check.
349+
prodInstanceId: "3d125700-a08d-4a7f-904b-c13f3f7cc0fc",
350+
stagingInstanceId: "ed184024-fdfa-4b6f-bb51-37d6648e0beb",
351+
// Runs a pinned pre-fleet `showcase-harness` image digest, set
352+
// out-of-band rather than tracked by showcase_build.yml. ciBuilt:false
353+
// (no dedicated build slot — there is no `harness-legacy` build slot)
354+
// and a repoNameOverride to `showcase-harness` so the image-ref shape
355+
// resolves correctly if the gate ever validates it.
356+
ciBuilt: false,
357+
// gateIgnore: deliberately-untracked for the image-ref gate. This
358+
// interim service runs a pinned digest (not the canonical :latest /
359+
// @sha256 shape the gate enforces) and is short-lived. Listing it here
360+
// (with gateIgnore) is what clears the "untracked Railway service"
361+
// failure — findUntrackedServices treats any SSOT entry as known —
362+
// WITHOUT triggering a false "missing from prod" failure from
363+
// findMissingServices (which only checks gateValidated:true entries).
364+
// Mirrors showcase-harness-worker exactly.
365+
gateValidated: false,
366+
gateIgnore: true,
367+
repoNameOverride: {
368+
prod: "showcase-harness",
369+
staging: "showcase-harness",
370+
},
371+
// The schema requires both domains be set; point both at the
372+
// control-plane harness staging/prod hosts purely to satisfy the
373+
// no-scheme domain invariant. domainFor() is never called for this
374+
// service because its probe is disabled in both envs (below) and
375+
// verify-deploy skips probe:false services.
376+
domains: {
377+
staging: "harness-staging-2ee4.up.railway.app",
378+
prod: "showcase-harness-production.up.railway.app",
379+
},
380+
// probe disabled in BOTH envs: this interim service's coverage is
381+
// exercised out-of-band during the migration, not by verify-deploy.
382+
probe: { staging: false, prod: false, driver: "harness" },
383+
},
335384
pocketbase: {
336385
serviceId: "ba11e854-d695-4738-9a45-2b0776788824",
337386
prodInstanceId: "1ee376e2-13f2-4464-801e-d0aa0bf76532",

0 commit comments

Comments
 (0)