Commit Graph

12110 Commits

Author SHA1 Message Date
Jordan Ritter 5f21584b3a test(showcase): D6 langgraph-python fixtures — drive to 180/5/2
Refresh d6/langgraph-python fixtures (beautiful-chat, chat-slots,
frontend-tools-async, gen-ui-{agent,custom,declarative,interrupt,open},
headless-complete, hitl-in-app, interrupt-headless, shared-state-streaming,
tool-rendering, tool-rendering-reasoning-chain, _from-feature-parity) plus
d4/langgraph-python/chat.json. Combined with the conveyance shim landed
in this branch, LGP is fixture-complete at 180 pass / 5 fail / 2 skip.

The residual 5 fails trace to react-core/v2's custom-wildcard-renderer
bug surfaced through gen-ui-interrupt (shared useInterrupt hook misbehaves
on the second interrupt; cross-integration D6 blocker, not a fixture or
conveyance defect). The 2 skips are the by-design mcp-apps iframe gap.
2026-05-29 16:15:57 -07:00
Jordan Ritter 671cc6ae1d feat(showcase/harness): support concurrent --isolate runs
- scripts/cli/_common.sh apply_isolation rewrites compose-file relative
  paths to absolute (build/context/dockerfile/volumes/env_file), enforces
  the docker compose [a-z0-9_-] project-name rule (normalize-with-warn or
  hard fail), and exports SHOWCASE_COMPOSE_FILE / SHOWCASE_INFRA_PORT_OFFSET
  plus offset host URLs (AIMOCK_URL_LOCAL / DASHBOARD_URL_LOCAL /
  POCKETBASE_URL_LOCAL) so the TS harness CLI talks to THIS project's
  aimock instead of the default :4010
- harness/src/cli/{aimock-rebuild,config,doctor,lifecycle}.ts honor the
  new env vars; lifecycle picks up the offset infra port for health
  probes so concurrent stacks no longer report each other's services as
  healthy
- Commit a generated harness/package-lock.json (new file) so npm ci
  resolves deterministically
2026-05-29 16:15:47 -07:00
Jordan Ritter af2036996a feat(showcase/built-in-agent): D6 conveyance — header-forwarding shim + factory wiring
- New src/lib/header-forwarding.ts mirrors the cross-integration shim
  pattern: reads inbound x-* headers and forwards them onto outbound
  LLM calls so aimock fixture matching sees x-aimock-context
- Wire copilotkit route handler and tanstack factory through the shim
- Frontend-tools and a2ui-fixed-schema demo adjustments paired with the
  conveyance work
- Bump package.json / package-lock.json against the updated tree
2026-05-29 16:15:35 -07:00
Jordan Ritter 6d5afd458a feat(showcase/langgraph-typescript): D6 conveyance — propagate request headers into ChatOpenAI
- Add openai-headers.ts: a per-request helper that pulls x-aimock-context
  off the inbound LangGraph request and threads it into the ChatOpenAI
  client construction so aimock fixture matching sees the inflight test
  context
- Wire every agent module (a2ui-*, agentic chat surfaces, byoc-*,
  frontend-tools-*, gen-ui-*, headless-complete, hitl-*, interrupt-agent,
  mcp-apps, multimodal, open-gen-ui*, readonly-state, reasoning-agent,
  shared-state-*, subagents, tool-rendering*) through the helper
- Bump package.json/package-lock.json against the updated tree
2026-05-29 16:15:26 -07:00
Jordan Ritter ceff27031c feat(showcase/langgraph-python): D6 conveyance shim + copilotkit 0.1.93 bump
- Add _header_forwarding_middleware.py paired with reasoning / subagent /
  tool-rendering-reasoning-chain agent edits so D6 fixture-matching sees
  the inflight x-aimock-context on outbound LLM calls
- Bump copilotkit 0.1.92 → 0.1.93 in requirements.txt; regenerate
  package-lock.json against the post-npm-ci tree (sibling of 03bed3b76,
  which switched this integration to npm ci)
- Drop the stale pnpm-lock.yaml left behind by the npm ci migration —
  Dockerfile uses 'npm ci --legacy-peer-deps' and every other showcase
  integration committed only package-lock.json after 03bed3b76. Removing
  the orphan prevents npm/pnpm tooling drift from re-resolving against it.
2026-05-29 16:15:12 -07:00
Jordan Ritter 0ca2bd40aa feat(showcase): D6 conveyance — forward x-aimock-context headers to LLM clients
Add a per-integration header-forwarding shim so inbound x-* request headers
ride along to outbound LLM HTTP calls. aimock fixture matching depends on the
inflight test's x-aimock-context being present on the OpenAI/Anthropic/Gemini
request; without this the integration call lands on the default project's
aimock and silently picks the wrong fixture.

Shape per integration:
- New _header_forwarding.{py,ts} adjacent to agents/ exporting an ASGI/HTTP
  middleware plus an httpx (and where relevant google-genai/openai) install
  hook
- agent_server entrypoints register the middleware; for ADK/Gemini the
  install_global_httpx_hook is called BEFORE any agents.* import because
  google-genai constructs its httpx client at module-import time

Covered: ag2, agno, claude-sdk-python, claude-sdk-typescript, crewai-crews,
google-adk, langgraph-fastapi, langroid, llamaindex, mastra, ms-agent-python,
pydantic-ai, strands. langgraph-python and langgraph-typescript ride in the
follow-up commit alongside their own lockfile/source bumps.
2026-05-29 16:15:00 -07:00
Mark 7c637d3c3b docs(cookbook): Cookbook section + Daytona recipe + skill (OSS-222) (#5087)
## Summary
- New top-level **Cookbook** docs section with a small landing page,
added both as a top-level nav tab (next to Learn) and as a sidebar
section under Documentation. First recipe wires CopilotKit's Built-in
Agent up with a `runCode` server tool that executes Python, TypeScript,
or JavaScript in an isolated [Daytona](https://www.daytona.io) sandbox
and returns stdout to chat.
- New **`copilotkit-daytona` Agent Skill**
(`skills/copilotkit-daytona/`) — `SKILL.md` + `assets/` + `references/`
+ `eval.yaml` + a workspace fixture, mirroring
`skills/copilotkit-setup`. The skill's reference doc deliberately points
at Daytona's maintained sources (`llms.txt`, the dashboard limits page,
the official `daytona` Agent Skill, and the Daytona MCP server) rather
than duplicating a hand-copied SDK surface that would rot.
- Fixes a stale `learn/meta.json` link that pointed at
`../direct-to-llm/cookbook/state-machine` (target no longer exists).

Closes OSS-222.

## Test plan
- [x] `next build` compiles `/cookbook` and `/cookbook/daytona` as
static routes; OG images emit for both.
- [x] Broken-link checker shows no new broken links relative to `main`.
- [x] Cookbook appears as a top-level nav tab next to Learn, and as a
section in the Documentation sidebar.
- [x] Recipe code validated end-to-end on a deployed Railway app against
published `@copilotkit/runtime@1.58.0`: agent invokes `runCode`, a real
Daytona sandbox executes both Python and JavaScript snippets, output
round-trips through the chat (unique-token proof).

## Notes (out of scope, separate follow-ups)
- The Built-in Agent **quickstart** doc shows a v1 `<CopilotKit>`
provider mixed with v2 `<CopilotSidebar>`; the v2 demos use the all-v2
stack (`CopilotKitProvider` + `useSingleEndpoint` +
`createCopilotEndpointSingleRoute`). Worth a doc reconciliation in a
separate PR.
- The Vercel AI SDK emits *"System messages in the prompt or messages
fields…"* when the agent runs — it originates inside `BuiltInAgent`'s
call construction (passes system content via `messages` rather than the
SDK's `system` option). Benign, but a CopilotKit runtime improvement
worth filing separately.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
2026-05-29 15:32:31 -07:00
Mark 447e9d8156 Merge branch 'main' into docs/222-daytona-cookbook 2026-05-29 15:26:38 -07:00
Jordan Ritter e3e777a921 fix(showcase): bind github.ref to env in validate notify to satisfy zizmor
The new no-webhook log step interpolated github.ref directly into a shell
echo, which zizmor flags as template-injection (HIGH). Bind it to a step
env var REF and reference $REF in the script instead. Message text is
unchanged; no Co-Authored-By.
2026-05-29 15:11:52 -07:00
Jordan Ritter 7c952d27fb test(showcase): fix harness probe tests for the D4/D6 fixture reorg
Repoint the aimock fixture-coverage probe to the per-integration d4/d6/shared layout; align
d5-multimodal assertions to the current auto-send button sentinels.
2026-05-29 15:06:59 -07:00
Jordan Ritter 5748b7c27a feat(showcase): serialize promote concurrency and add red-path workflow alerts
Collapse the promote workflow to an input-agnostic concurrency group so promotes can't race the
same Railway service; add #oss-alerts failure notifications to the build and validate workflows
(build via extended needs, validate via a new workflow-level notify job).
2026-05-29 15:06:54 -07:00
Jordan Ritter 90773f73e7 refactor(showcase): use shared railway token resolver and SSOT IDs in deploy-to-railway
Replace the divergent getToken with resolveRailwayToken; source project/env IDs from the
railway-envs SSOT; read the GHCR username from env (trimmed, fail-loud) instead of a hardcoded
handle.
2026-05-29 15:06:49 -07:00
Jordan Ritter 722e411249 refactor(showcase): brand ProbeTarget.host as a validated Host type
Introduce a branded Host produced by asHost (rejects scheme/path/empty/whitespace/userinfo/query/
fragment); brand at the resolveProbeTargets ingress; make ProbeTarget fields readonly; add asHost
+ override-seam tests.
2026-05-29 15:06:45 -07:00
Jordan Ritter 253a49603e fix(showcase): harden bin/railway rollback against shell injection
Replace RollbackCommitCommand backtick subshells with IO.popen arg-arrays; validate --sha (hex)
and --env (known set) before any git call; gate both git ls-tree and git show on $?.exitstatus
before parsing; add injection + git-show-failure + empty-snapshot spec. Also encapsulate promote
snapshots behind fleet_*/target_* accessors so fleet-vs-target selection is name-enforced.
2026-05-29 15:06:40 -07:00
Ben Taylor 3dc9bea09e chore(examples): update LangGraph threads CopilotKit pins (#5105)
## Summary
- update the LangGraph Python Threads template CopilotKit packages from
1.57.0 to 1.59.1
- align @ag-ui/client to 0.0.53 so @ag-ui/langgraph and
@copilotkit/runtime share the same agent base types
- regenerate the standalone template package-lock.json

## Verification
- npm run build (from examples/integrations/langgraph-python-threads)
- lefthook pre-commit: check-binaries, sync-lockfile,
test-and-check-packages
2026-05-29 16:17:03 -05:00
Martha Schumann 8092f8ba7f chore(examples): update langgraph threads copilotkit pins 2026-05-29 13:07:47 -07:00
Jordan Ritter 482c2a0ae3 style(sdk-python): apply ruff format to test_agui_agent.py (#5104)
Fixes the red `static / quality` format gate on main caused by an
unformatted Python test file (pre-existing, unrelated to the showcase
pipeline work). Applied ruff 0.15.13 format to
`sdk-python/tests/test_agui_agent.py` only.
2026-05-29 13:05:44 -07:00
Mark Fogle 38ce421f85 feat(examples): trim demo chrome to fit embedded iframe viewport
The cookbook's 'Try it live' iframe is 600px tall, but the demo page
included its own h1/subtitle + ~32px main padding + the chat section had
minHeight:520, which pushed total content past 600px and forced a
scrollbar inside the iframe.

The shell-docs cookbook page already provides a header and surrounding
chrome; the demo's own h1 ('CopilotKit x Daytona') and subtitle are
redundant in the embedded context. Drop them and let the chat fill the
iframe viewport exactly:

- Remove the <header> (h1 + subtitle).
- Set <main> to height:100vh, padding:0, margin:0.
- Chat <section> uses flex:1 + minHeight:0 (no fixed minHeight) so it
  grows/shrinks to fill the available iframe height.

Side effect: the standalone Railway URL now also shows just the chat
(no h1/subtitle). Fine — the URL is a demo embedding target rather
than a polished standalone landing.

OSS-222
2026-05-29 19:56:30 +00:00
Jordan Ritter ea5ea897c1 style(sdk-python): apply ruff format to test_agui_agent.py
main's static/quality format gate was red because this file was not
ruff-formatted (introduced by an earlier unrelated python commit).
Apply ruff 0.15.13 format to restore the gate.
2026-05-29 12:54:47 -07:00
Jordan Ritter d4531851ae feat(showcase): multi-service deploy pipeline + runtime config (#5093)
## Summary

- Establishes a two-stage push-to-main deploy pipeline for the showcase
fleet: build GHCR images and redeploy STAGING (which floats `:latest`);
PROD is digest-pinned (`@sha256:`) and promote-only via a gated
workflow.
- Introduces a single source of truth (SSOT) for Railway env/service
IDs, image refs, per-env domains, and probe configs, consumed by Ruby
tooling, TypeScript pipeline scripts, and CI workflows.
- Adds a Ruby `bin/railway` promote command with per-service targeting,
`--digest` pinning, and fleet-scoped preflight gates (P1 GHCR digest
existence, P2 staging-latest SUCCESS, P3 staging-green live re-probe, P5
mutation correctness, P6 parity matrix).
- Migrates the four Next.js shells (shell, shell-dashboard, shell-docs,
shell-dojo) to runtime URL config (\"Option B\"): server-side reads of
`NEXT_PUBLIC_*` at request time + `window.__SHOWCASE_CONFIG__`
injection, with an oxlint rule guarding against build-time public-env
reads.
- Hardens the image-ref gate (per-env: prod `@sha256`, staging
`:latest`), adds untracked-service hard-fail and `gateIgnore` opt-out,
and rolls five additional services into the gate.
- Adds verify-deploy probe drivers, per-env verify matrix resolution,
per-slot build-result artifacts, and end-to-end fail-loud boundaries
across the pipeline scripts.

## Key changes

### Deploy model and workflows
- `showcase_build.yml`, `showcase_build_check.yml`,
`showcase_deploy.yml`, `showcase_promote.yml` updated/added for the new
two-stage flow.
- Push-to-main redeploy retargeted from prod to staging;
redeploy-summary upload is now mandatory and bridged into the build
workflow artifact.
- New prod promote workflow (`showcase_promote.yml`) gates on the Ruby
preflight suite.

### Railway envs SSOT
- `showcase/scripts/railway-envs.ts` + generated
`railway-envs.generated.json` as the canonical source of env/service
IDs, per-env domains, probe config, dispatch-name uniqueness invariant,
and `gateIgnore` per service.
- CI `--check` mode verifies the generated artifact is in sync with the
TS source.
- Ruby `bin/railway` reads `EXPECTED_DOMAINS` from the TS SSOT artifact.
- Snapshot schema v2 adds
`healthcheck`/`region`/`replicas`/`restartPolicy` for the parity matrix.

### TS pipeline scripts (`showcase/scripts/`)
- `redeploy-env.ts` for explicit per-env Railway redeploys with
per-service JSON summary emission.
- `resolve-verify-matrix.ts` for SSOT-driven verify matrix resolution
(skips verify when redeploy success-set is empty; pinned by a contract
test).
- `verify-deploy.ts` parameterized per-env probe + baseline driver
implementations.
- `verify-railway-image-refs.ts` per-env gate (prod `@sha256`, staging
`:latest`), untracked-service hard-fail, malformed-ref negatives,
per-service shape tests.
- `aggregate-build-results.ts` per-slot build-result artifacts,
fail-loud on missing slot file, GHA heredoc output.
- `emit-railway-envs-json.ts` SSOT artifact emitter with hermetic,
fail-loud read errors.
- Shared `scripts/lib/`: unified Railway GraphQL endpoint
(backboard.railway.app), shared token resolver preferring
`user.accessToken`, whitespace/type-safe narrowing, sanitized GraphQL
error bodies.

### Ruby promote (`showcase/bin/railway` + `lib/`)
- `PromoteCommand` refactored for testable preflight; single-service
targeting via `--digest`.
- `Railway::Auth.ghcr_token` + `GHCR.manifest_exists` (P1 GHCR digest
gate).
- P2 staging-latest-deployment SUCCESS check.
- P3 require-staging-green live re-probe via `verify-deploy --env
staging`.
- P5 mutation-correctness verification (boolean + re-query retry).
- P6 parity matrix (healthcheck/region/replicas/restartPolicy).
- Promote resolves staging tag to GHCR digest once, pins prod, verifies
redeploy, fails loud on partial promote.

### Runtime URL config across Next shells (\"Option B\")
- `shell`, `shell-dashboard`, `shell-docs`, `shell-dojo`: new
`runtime-config.ts` (server) + `runtime-config.client.ts` (client)
modules; `__SHOWCASE_CONFIG__` injected via root layout; middleware and
URL consumers migrated.
- Dropped build-time `NEXT_PUBLIC_*` build-args from CI and Dockerfiles;
re-declared `COMMIT_SHA`/`BRANCH` ARG in runner stages where needed.
- `shell-docs` sitemap/robots made dynamic; analytics + middleware
routed through runtime config; `signup-link` and CTA components migrated
with SSR tests.
- New oxlint rule rejects `NEXT_PUBLIC_*` reads from shell source.
- Playwright env-routing spec per shell per env; no-rebuild env-switch
integration test.

### Test hardening
- vitest coverage for runtime-config server/client, SSR safety, env-name
tolerance.
- Contract tests pin verify-matrix SSOT shape and CLI behavior.
- Per-service image-ref gate shape tests; malformed-ref negatives.

## Test plan

- [ ] CI: \`build-checks\` green on this branch
- [ ] CI: \`validate-showcase\` green (SSOT \`--check\` passes)
- [ ] CI: Python tests green
- [ ] CI: oxlint passes (no NEXT_PUBLIC_* reads in shells)
- [ ] Local: \`bundle exec rspec\` (Ruby promote suite) green
- [ ] Local: \`pnpm -C showcase/scripts test\` (vitest) green
- [ ] Local: \`pnpm -C showcase/shell test\` and sibling shells green
(runtime-config + SSR)
- [ ] Local: \`docker buildx build\` succeeds for each shell with the
trimmed build-args
- [ ] Local: Playwright env-routing spec passes against a running shell
- [ ] Manual: dry-run \`bin/railway promote --digest <sha256>\` against
a staging service; P1–P6 gates report as expected
2026-05-29 12:42:00 -07:00
Mark Fogle 8dca50f422 chore(examples): rename next.config.mjs to .ts + allowlist
CI's check-config-allowlist failed on the new daytona-runcode showcase.
Rather than just adding an .mjs row, convert to .ts to match the
dominant pattern across examples/showcases/* (the .mjs predates the
Next 14 TS-config support and is only kept in a handful of older
showcases — banking, enterprise-brex, orca, presentation). New
showcases should follow the .ts convention.

- Replace examples/showcases/daytona-runcode/next.config.mjs with an
  equivalent next.config.ts (empty config, same behavior).
- Add the new path to .github/config-allowlist.txt in alphabetical
  position (between chatkit-studio/.../world and deep-agents-finance-erp).
- Verified locally: bash .github/scripts/check-config-allowlist.sh passes.

OSS-222
2026-05-29 19:38:46 +00:00
Mark Fogle a0183e02c9 feat(examples): drop blue gradient + literal-x header per review
Addresses PR #5087 review comment from @tylerslaton:
> Remove the blue padding around the chat, change the header to
> 'CopilotKit x Daytona'

- Drop the linear-gradient(160deg,#f8fafc,#eef2ff) background from
  <main>. The chat's own white card now stands on the default page
  background — no surrounding tint.
- Change the h1 from 'CopilotKit × Daytona' (× = U+00D7
  multiplication sign) to literal 'CopilotKit x Daytona' (ASCII x), per
  Tyler's literal request.

Mirrored byte-for-byte from the Railway demo source.

OSS-222
2026-05-29 19:32:22 +00:00
Mark Fogle aee804e6a3 docs(cookbook): scope cookbook sidebar to its own route
Addresses PR #5087 review comment from @tylerslaton:
> When we open the cookbook section, the sidebar should update to include
> only the recipes. Similar to how Reference works today.

Mirror the dedicated-route approach Reference uses (app/reference/page.tsx
+ app/reference/[...slug]/page.tsx), reusing the existing MDX flow via
DocsPageView's pre-built navTree prop:

- app/cookbook/page.tsx — landing route. Builds a navTree scoped to the
  cookbook subdir (buildNavTree(CONTENT_DIR/cookbook, 'cookbook')) and
  passes it to DocsPageView so the sidebar shows only cookbook entries.
- app/cookbook/[...slug]/page.tsx — catch-all for /cookbook/<recipe>,
  using the same scoped navTree.
- Remove the '---Cookbook---' divider and '...cookbook' spread from
  showcase/shell-docs/src/content/docs/meta.json so cookbook no longer
  appears in the Documentation sidebar (only via the navbar tab).

Verified locally:
- /cookbook and /cookbook/daytona serve 200 with sidebar scoped to
  Overview + Daytona only (active highlight tracks the current page).
- / has exactly one /cookbook anchor — the navbar tab — and no cookbook
  entries in the Documentation sidebar.
- /built-in-agent sidebar has zero /cookbook entries.

OSS-222
2026-05-29 19:00:10 +00:00
Jordan Ritter 10509d0de5 fix(showcase): align ruby/setup-ruby pin in promote workflow to satisfy zizmor
zizmor ref-version-mismatch flagged the @a4effe49 (#v1) pin; align to the repo's existing
afeafc3d / v1.310.0 ruby/setup-ruby pin used in showcase_lint_prod.yml so both workflows
share one zizmor-clean version.
2026-05-29 11:51:37 -07:00
Jordan Ritter 8714ab569b chore(showcase): apply oxfmt formatting across showcase scripts and shells
oxfmt --write normalized formatting on showcase scripts, the four shells, and the
new oxlint rule; required for the repo-root oxfmt --check CI gate.
2026-05-29 11:45:16 -07:00
Jordan Ritter fe92329983 fix(showcase): propagate showcase-ms-agent-harness-dotnet to probe parity configs
the integration was added to the railway-services SSOT and smoke.yml nameExcludes; this
adds it to the sibling probe configs the probe-config-parity invariant requires to stay
in lockstep.
2026-05-29 11:45:16 -07:00
Jordan Ritter 5ec48a3ec2 test(showcase): pin verify-matrix SSOT contract to resolve-verify-matrix.ts
deploy.yml moved SSOT consumption into resolve-verify-matrix.ts; the test now asserts
deploy.yml invokes that script AND the script reads railway-envs.generated.json + filters
probe.staging===true, rather than scanning the YAML for literals that moved one hop down.
2026-05-29 11:45:16 -07:00
Jordan Ritter 39eac01f10 chore(showcase): drop stale URL/analytics build-args from PR-check workflow
shell-dashboard/shell-docs read those URLs at runtime (Option-B runtime-config); their
Dockerfiles no longer declare the ARGs. Matrix aligned with showcase_build.yml; dead
downstream handling removed; sha/branch retained.
2026-05-29 11:45:16 -07:00
Jordan Ritter 9340724370 fix(showcase): re-declare COMMIT_SHA/BRANCH ARG in shell and shell-dojo runner stages
runner-stage ENV NEXT_PUBLIC_COMMIT_SHA/BRANCH expanded empty because Docker ARGs are
per-stage; re-declaring them in the runner stage (mirroring shell-docs) restores build-arg
values at runtime. Verified via local buildx.
2026-05-29 11:45:15 -07:00
Jordan Ritter f48b2f0e4d fix(showcase): promote single-service targeting with --digest and fleet-scoped preflight
CLI accepts optional positional SERVICE + --digest REF (the showcase_promote.yml per-service
loop contract); validates against the SSOT; narrows the promotion target while fleet-scoped
preflight (service-set parity, expected prod domains) keeps reading full snapshots so a
healthy fleet isn't spuriously refused; adds red-green specs.
2026-05-29 11:45:15 -07:00
Jordan Ritter 09b9f8910b chore(showcase): pre-push cleanup -- comment rot, log levels, env coalesce, test hardening
Non-functional cleanup pass on the showcase deploy-pipeline integration
branch. All changes are scoped to comment rot, log severity for already-
demoted runtime-config fields, length-aware env-name coalescing (a
deliberately-empty primary no longer masks a populated alternate), and
test-quality tightening. No production behavior change beyond the
specific items below.

Changes by area:

- shell/shell-dashboard/shell-docs runtime-config.ts: factor the
  `process.env[primary] ?? process.env[alt]` chain into a shared
  length-aware `readEnvPair` helper. The prior `??` form treated
  `PRIMARY=""` as set, masking a populated alternate; the helper now
  treats empty-string as unset and falls through to the alternate.
- shell-docs runtime-config.ts: demote the two recoverable URL fields
  (`intelligenceSignupUrl`, `posthogHost`) from console.info to
  console.warn. The `FATAL-CONFIG:` Sentry-alert prefix is preserved
  only on the true sentinels; the demoted fields now clear prod log-
  aggregation thresholds without raising ops alerts.
- All three shells' runtime-config.ts: prefix log lines with the shell
  name (e.g. `[shell-docs runtime-config]`) so the shared log stream
  identifies which shell emitted the line.
- shell-docs runtime-config-serialize.ts: rewrite the U+2028 / U+2029
  RegExp arguments using six-character ASCII backslash-u escape
  sequences (was: literal codepoints in the string arg). The literal
  codepoints are line terminators that a formatter or editor could
  silently strip, breaking the security-critical XSS escape. The
  ASCII form is robust to any such pass.
- shell-docs use-google-analytics.test.ts: de-tautologize the hook-
  order test. It now asserts `usePathname(` and `useEffect(` both
  exist in the source, so deleting all hooks would fail the test
  rather than trivially satisfying the early-return path.
- shell-dashboard baseline-types.test.ts: update the partner-count
  expectation from 25 to 26 -- the 26th entry (Cloudflare) is a
  legitimate integration that landed independently; the test was
  stale and had nothing to do with this branch.
- scripts/resolve-verify-matrix.ts: drop the `FIX 7 --` plan-
  internal prefix from a comment; keep the explanation.
- shell-docs/.env.example: correct the `NEXT_PUBLIC_SHELL_URL`
  fallback claim (sentinel, not canonical prod host) and document
  the remaining 7 consumed env vars with their FATAL/warn/silent
  semantics so the example matches runtime-config.ts.

Skipped:
- C-SENTINEL-DEDUP (`http://ops.invalid` shared constant across
  shell-dashboard's next.config.ts and runtime-config.ts): both
  files are at different module levels (root vs src/lib) and the
  string appears once in each; extracting to a shared module would
  widen the diff into a refactor for marginal benefit. Skipped per
  the spec's "if it widens diff awkwardly, skip" guidance.
- C-SSRTEST: already exhaustively covered. Each of the three shells
  has an SSR placeholder test that exercises every URL field via
  `new URL()` parseability and (for shell-docs) the analytics-key
  empty-string semantics. Treated as a no-op.

Validation: shell + shell-dashboard + shell-docs runtime-config /
serialize / GA tests green; bin/showcase Ruby suite green (87 runs);
showcase/scripts resolve-verify-matrix + aggregate-build-results +
lint-rule-no-public-env green (79 runs).
2026-05-29 11:45:15 -07:00
Jordan Ritter 28f33ecc8a fix(showcase): stop SSR 500 + hook-order regressions in shell runtime-config; tolerate env-name variants
Six fixes addressing CR findings on the Option-B runtime URL-injection migration:

1. SSR_PLACEHOLDER must be parseable URL sentinels — `new URL("")` throws on
   SSR causing 500s for any consumer that constructs URLs from runtime-config
   fields. Use `.invalid`-TLD sentinels (RFC 2606) for URL fields; analytics
   keys stay empty string. Add `suppressHydrationWarning` on consumers that
   render the placeholder server-side and the real value post-hydration
   (integration-grid, page-actions popover).

2. Hook-order: move `usePathname()`/`useEffect` ABOVE the early-return in
   use-google-analytics. Gate the effect bodies on `GA_ID` instead so React
   sees a stable hook order across renders.

3. `readUrl`/`readKey` accept either bare or `NEXT_PUBLIC_*`-prefixed env
   names via a fallback chain — covers both server-only and inlined-public
   variable conventions without forcing a rename across deploy targets.

4. Extract `serializeRuntimeConfig` to `lib/runtime-config-serialize.ts` so
   the OWASP-escape behavior (XSS via </script>, U+2028/U+2029 line-terminator
   injection) can be unit-tested without importing the layout into vitest.

5. Reclassify `intelligenceSignupUrl`/`posthogHost` from FATAL-CONFIG to
   info-level in shell-docs — these are optional integrations, not hard
   wiring failures, so absence should not poison the error stream.

6. Comment-rot cleanup: drop "Option B", B12, "the bug we are fixing", fix
   "four substrings"→"three substrings" miscounts, and refresh shell-docs
   .env.example to describe the runtime-injection contract instead of a
   stale next.config throw claim.

V1: shell + shell-docs `next build` succeeds (no Edge-runtime crash on
`unstable_noStore`).
V2: `OPS_BASE_URL=` shell-dashboard `next build` no longer throws —
`next.config.ts` is now a phase-aware function that emits a sentinel
destination at build time and throws only at start (PHASE_PRODUCTION_BUILD
from next/constants).

Tests: shell-docs 72/72, shell 12/12, shell-dashboard runtime-config 16/16
(pre-existing baseline-partner-count failure unchanged).
2026-05-29 11:45:15 -07:00
Jordan Ritter 41fae67618 fix(showcase): tighten verify-matrix drift guard + fail-loud boundaries; fix stale comment + flaky test
Closing hardening pass on the showcase deploy-gate's verify-matrix
resolver. The 7-agent review confirmed the gate is correct; this
commit fixes the residual rough edges.

- showcase_deploy.yml: correct the false §3 ok-non-empty comment.
  The empty-intersection case can coexist with redeploy_red=false
  (every redeploy succeeded, just none probe-eligible) — that's a
  correctly-green run, not a red one.
- showcase_deploy.yml: tighten the summary.json shape guard to catch
  PARTIAL drift (TOTAL>0 && WITH_STATUS<TOTAL). The previous all-or-
  nothing TOTAL>0 && WITH_STATUS==0 check silently dropped drifted
  rows on a mixed summary. Validated locally on mixed/normal/empty/
  total-drift jq samples.
- resolve-verify-matrix.ts: add asSupportedEventName narrowing helper
  + use it in the CLI. Replaces the unchecked `as` cast — type system
  and runtime now tell one story. Resolver's internal eventName
  guard becomes defense-in-depth for direct (test) callers.
- resolve-verify-matrix.ts: make the workflow_run boundary total —
  summaryPresent MUST be exactly "true"/"false". Any other value
  (including "" from a step-id-rename wiring break) throws now
  instead of silently emitting has_services=false.
- resolve-verify-matrix.ts: drop the try/catch around
  fileURLToPath(import.meta.url) in `invokedDirectly`. The catch
  used to swallow ESM-interop failures and silently no-op the CLI
  (exit 0, no GITHUB_OUTPUT write → verify skipped = false-green).
- resolve-verify-matrix.ts: reword parseSsotServices JSDoc to
  distinguish schema-drift from truncation (the two are different
  failure modes, not one conflated story).
- showcase_build.yml: comment addendum on the redeploy-summary
  upload — swapping the guard to `if: always()` would red the
  legitimate services=='' path (no summary written), trading the
  already-closed false-green for a false-red on every non-buildable
  push.
- resolve-verify-matrix.cli.test.ts: switch to spawnSync so stderr
  is captured on both zero and non-zero exit (execFileSync only
  exposes stderr on throw). Hard-code two stable probe-eligible
  names ("aimock", "harness") for the sorted-CSV test rather than
  picking probe[0]/probe[1] off the live SSOT — the prior test was
  tautological (already-sorted in, sorted out) and would silently
  pass if the resolver did nothing.
- resolve-verify-matrix.cli.test.ts: add CLI coverage for the
  dropped-token ::warning:: path (FIX 3 — the entire drift-detection
  contract had zero CLI coverage), the unexpected-EVENT_NAME error
  (FIX 5), and the workflow_run-summary_present total boundary
  (FIX 7, both "" and "True" inputs).
- resolve-verify-matrix.test.ts: add unit coverage for the new
  workflow_run summaryPresent boundary (empty + "True" + the
  workflow_dispatch ignores-summaryPresent regression).

Red-green: 6 tests RED before code changes (FIX 3 warning, FIX 5
unknown EVENT_NAME, FIX 7 unit + CLI ×2 for "" and "True"); 79
tests GREEN after.

Validation: 4 vitest files / 79 tests passing; 87/87 ruby specs
passing; actionlint findings unchanged vs integration baseline
(8 → 8, identical diff); yaml.safe_load OK on both workflows.
2026-05-29 11:45:15 -07:00
Jordan Ritter aafafa53bd fix(showcase): validate verify-matrix boundaries (SSOT + summary shape), fail loud, test CLI contract
A 7-agent review of the verify-matrix resolver and its surrounding workflow plumbing found three
boundary surfaces that could silently produce a GREEN deploy on a broken release, plus an
untested CLI contract that CI compares against the literal strings 'true' / 'false'.

FIX 1 — Validate the SSOT shape in loadSsotServices(). The prior `JSON.parse(...) as
{services: SsotService[]}` was an unchecked cast: a truncated/drifted SSOT (emitter crashed
mid-write, or schema renamed) parses fine but silently shrinks/empties the probe-eligible set
→ some redeployed services go unverified, or verify is skipped on a real redeploy. Extract a
pure exported parseSsotServices(raw, path) that requires the shape we depend on (non-empty
services array; each entry has a non-empty string name, an optional string|null dispatchName,
and a probe object with a boolean staging). Throw `::error::SSOT <path> malformed: <detail>`
on any violation. Also re-check existsSync(SSOT_JSON) after the regenerate-if-missing
execFileSync — a regen that exits 0 without writing must not proceed to a useless JSON.parse
crash. Drop the defensive `probe?.staging` once shape is guaranteed.

FIX 2 — Validate summary.json shape in the redeploy-gate bash. The bullseye false-green
surface: if redeploy-env.ts's schema ever drifts (e.g. `status` → `state`, `ok` → `success`),
every `jq select(.status==...)` yields empty → redeploy_red=false AND ok_services="" →
resolver skips verify → GREEN CI on a real unverified redeploy. Add a TOTAL vs WITH_STATUS
shape guard right after loading the summary: if TOTAL > 0 && WITH_STATUS == 0, emit
::error::summary.json has $TOTAL entries but none with status ok|error (schema drift?) and
exit 1. The legitimate empty-array path (TOTAL=0) is preserved.

FIX 3 — Fail loud on unknown eventName in resolveVerifyMatrix. The prior code fell through to
the workflow_run intersection branch for ANY unrecognized eventName (typo, unexpected
trigger), silently emitting has_services=false → indistinguishable from a legit "summary
absent" skip. Add an explicit guard so only workflow_run / workflow_dispatch are accepted;
anything else throws ::error::resolve-verify-matrix: unexpected eventName '<value>'. Tighten
the eventName parameter type to the literal union.

FIX 4 — Trim ok tokens + warn on dropped tokens in okCsvToCanonicalNames. Split, then
.map(t => t.trim()).filter(Boolean) so "a, b" (spaces) matches. Collect tokens that match NO
SSOT service (by name or dispatchName) and have the CLI wrapper emit ::warning::ok_services
tokens dropped (no SSOT match): <list> on stderr when non-empty — surfaces SSOT/build drift.
The pure function stays IO-free; logging lives in the wrapper.

FIX 5 — CLI wrapper integration test. New resolve-verify-matrix.cli.test.ts spawns
`npx tsx showcase/scripts/resolve-verify-matrix.ts` with a temp $GITHUB_OUTPUT file across
four scenarios and asserts the temp file contents EXACTLY (the workflow YAML compares
has_services against the literal strings 'true'/'false', so the byte-for-byte format is part
of the contract). Uses the real railway-envs.generated.json so the loader exercise is real.

FIX 6 — Cleanup. Remove the dead `env: DISPATCH_SERVICE: ...` block on the redeploy-gate
step (the next step redeclares it — leftover from the extraction). Soften the §3
decision-table all-errors bullet to match resolve-verify-matrix.ts's careful wording, and
append that when the success-set is empty (or the intersection collapses to empty), verify
is skipped and the gate reds independently. Append to showcase_build.yml's "Upload redeploy
summary" path-(A) comment that `if-no-files-found: error` still reds path (A) even if a
future change adds `if: always()`.

Tests: red→green for FIX 1/3/4/5 verified locally. Resolve-verify-matrix vitest count:
12 → 28. Full requested suite (resolve-verify-matrix + cli + aggregate-build-results +
lint-rule-no-public-env): 72 passed. showcase/bin ruby specs: 87 runs / 0 failures / 0
errors / 0 skips. actionlint baseline preserved (8 findings, identical to integration tip).
2026-05-29 11:45:15 -07:00
Jordan Ritter c579ad753a fix(showcase): extract+test verify-matrix resolver; skip verify when redeploy success-set empty
Extract the inline bash+jq decision logic from showcase_deploy.yml's
resolve-matrix job into showcase/scripts/resolve-verify-matrix.ts, a
pure function with a vitest suite. The bash had produced two confirmed
bugs across prior CR rounds, so making it testable is the lasting fix.

Issue A (the bug this PR fixes): when summary_present=true but
ok_services is empty (every service errored on redeploy), the old bash
skipped the intersection and fell through to the full probe-eligible
fleet, gratuitously probing every service against stale :latest. The
resolver now returns has_services=false in that case — enforce-redeploy
-gate independently reds the workflow on redeploy_red=true, so this
case is already loud; there is nothing left to verify.

Parity preserved for unchanged cases:
  - workflow_dispatch + 'all'/empty   → full probe-eligible set
  - workflow_dispatch + specific svc  → that one (unknown → error exit)
  - workflow_run + summary_present=false → has_services=false
  - workflow_run + present + ok non-empty → intersection with probe-
    eligible (SSOT key OR dispatchName aliases both resolve)

Also clarified the Upload-redeploy-summary comment in showcase_build.yml
to document both red paths (hard crash → redeploy step exits non-zero;
exit-0-but-no-file → if-no-files-found:error reds the step) so no
false-green path is possible.

Tests: 12-case vitest suite covers each decision-table row plus the
Issue A fix (written red-first; failed against a naive full-fleet
fallback, passed once the early return was added). CLI parity verified
against the real generated SSOT for the three representative env-var
combinations (workflow_run + present + ok=[a,c]; workflow_run + present
+ ok empty; workflow_dispatch + 'all').
2026-05-29 11:45:14 -07:00
Jordan Ritter 22895c104e fix(showcase): skip verify on no-redeploy run + make redeploy-summary upload mandatory
Three correctness holes uncovered by confirmation review of the earlier
deploy-gate fix:

(1) showcase_deploy.yml — Build verify matrix step: when workflow_run fires
with summary_present=false (legitimate "build redeployed nothing", e.g.
docs/script-only push under showcase/**), the gate correctly no-oped but
the matrix fell through the empty-OK_FROM_REDEPLOY branch and resolved to
the FULL probe-eligible set. Verify then ran against the whole staging
fleet for a push that deployed nothing — gratuitous, and false-reds the
deploy workflow if any unrelated staging service happens to be unhealthy
at probe time. Thread github.event_name + summary_present into the step
via env and add an explicit (workflow_run && summary_present==false)
guard that sets services_csv="" / has_services=false. workflow_dispatch
fall-through (full fleet / chosen service) preserved. workflow_run +
summary present + all-errors path unchanged: enforce-redeploy-gate still
trips RED on redeploy_red=true.

(2) showcase_build.yml — Upload redeploy summary step: was gated on
services != '' && hashFiles('.redeploy/summary.json') != ''. If
redeploy-env.ts crashes before writing summary.json (the script is
documented "always exits 0", but a crash/OOM/unhandled-rejection can
skip the write), services != '' but hashFiles == '' silently skipped
the upload. The deploy side then saw "artifact absent", treated it as
"nothing redeployed", skipped the gate, and produced a FALSE GREEN
despite a real redeploy failure. Drop the hashFiles clause so the upload
is mandatory whenever a redeploy was attempted; if-no-files-found:error
(already set) then fails the step → fails the redeploy-staging job →
fails the build workflow → showcase_deploy.yml's resolve-matrix.if
(workflow_run.conclusion == 'success') blocks the deploy run from
starting at all. Loud failure on the build side. The legitimate
services == '' (matrix ∩ success-set empty) path is preserved by the
services != '' guard.

(3) showcase_deploy.yml — check-redeploy-summary github-script: was a
single per_page:100 list call. While the current run uploads ~28
artifacts (well within 100), a future expansion past 100 could push
redeploy-summary off the first page and produce a false "absent" → gate
skipped → false-green. Switch to github.paginate.iterator with the
endpoint's name="redeploy-summary" filter for an exact-match,
pagination-safe lookup. No try/catch is added: github-script propagates
unhandled rejections by default, so a 5xx/permission error fails the
step → resolve-matrix.result == 'failure' → enforce-redeploy-gate trips
RED. Silent default-to-false on API error would open the gate on a
broken pipeline, which is what we explicitly do NOT want.

Validation: actionlint shows 8 findings on both files, identical to the
integration baseline (zero new findings). python3 yaml.safe_load OK on
both. Regression suites green: showcase/scripts vitest 44/44
(aggregate-build-results + lint-rule-no-public-env);
showcase/bin/spec/all_tests.rb 87 runs / 251 assertions / 0 failures.
2026-05-29 11:45:14 -07:00
Jordan Ritter 7284ed3d84 fix(showcase): guard redeploy-summary download against legit no-redeploy + harden env lint rule
D1 — showcase_deploy.yml false-red fix
======================================

The build workflow legitimately uploads no `redeploy-summary` artifact when it ran
(push touched `showcase/**` so `paths:` matched) but `detect-changes` found no
buildable service, so `redeploy-staging` was skipped. The build still concludes
`success`, so `showcase_deploy.yml` fires on `workflow_run` and `resolve-matrix`
runs. `actions/download-artifact@v4` with `name:` HARD-FAILS on a missing
artifact, so the unguarded download was failing the job, and a downstream guard
that trips `enforce-redeploy-gate` on `resolve-matrix.result == 'failure'` was
flipping the workflow RED — a false-red on a routine showcase-docs/script change.

Add an artifact-existence pre-check using `actions/github-script` (pinned by SHA,
matching the existing repo convention) that lists the artifacts for
`workflow_run.id` via `actions: read` (already granted to `resolve-matrix`) and
sets `summary_present=true|false`. Gate the existing download step on
`summary_present == 'true'`. Keep NO `continue-on-error`, so the C1 property
holds: when the artifact exists but the download genuinely fails, the job still
fails loud and `enforce-redeploy-gate` correctly reds the workflow. When the
artifact is legitimately absent, the bash gate's existing `[ ! -f "$SUMMARY" ]`
branch no-ops (`redeploy_red=false`, `ok_services=""`) — nothing was
redeployed, so there is nothing to gate.

Updated the step comment block to enumerate the three distinct cases now
handled: workflow_dispatch (no download); workflow_run + artifact absent
(graceful skip); workflow_run + artifact present (download with fail-loud).

L1-L5 — env lint rule hardening
===============================

- L1: route the destructuring (VariableDeclarator/ObjectPattern) branch through
  the shared `staticKeyName()` helper so the computed-string-key form
  `const { ["NEXT_PUBLIC_X"]: y } = process.env` and the no-expression
  template-literal form `const { [\`NEXT_PUBLIC_X\`]: y } = process.env` are
  caught with the same parity as the bracket-member read.
- L2: unwrap a wrapping `ChainExpression` at the top of `isProcessEnv()` so
  `process.env?.X` is matched robustly across parser flavors; corrected the
  helper's doc comment to describe the actual semantics.
- L3: export `BANNED_KEYS` from the rule module and have the table-driven test
  dynamically import the rule's own Set instead of hand-mirroring it — the
  test set now cannot drift from the rule.
- L4: added override-scoping fixtures for `showcase/shell/src/**` and
  `showcase/shell-dojo/src/**`; the `.oxlintrc.json` override list already
  includes these, but the test now exercises them so an accidental drop is
  caught.
- L5: expanded the file-header "Out of scope" doc list to include bulk-iteration
  reads (`Object.keys/values/entries(process.env)`, for-in, spread
  `{...process.env}`), rest-pattern destructuring, compound-assignment LHS, and
  update operators. Documentation-only — the deliberate non-coverage is now
  auditable.

Validation
==========

- RED→GREEN confirmed for L1 (two new destructuring computed-key tests) and L3
  (dynamic `await import(...)` of BANNED_KEYS failed pre-fix with
  "Rule module did not export a non-empty BANNED_KEYS Set", green after export).
- vitest: 38 passed (was 34 baseline + 4 new); aggregate-build-results 6 passed.
- Ruby promote suite: 87 runs, 251 assertions, 0 failures (unchanged).
- python3 yaml.safe_load: showcase_deploy.yml + showcase_build.yml +
  showcase_promote.yml all parse OK.
- actionlint: zero NEW findings on the changed file. The pre-existing
  showcase_build.yml SC2086/SC2129/runner-label findings are identical on the
  integration baseline (unchanged by this commit).
2026-05-29 11:45:14 -07:00
Jordan Ritter a6239cde11 fix(showcase): close deploy-gate false-greens and broaden public-env lint rule
Seven-agent CR surfaced correctness defects in the build/deploy/promote
pipeline and in the no-public-env-shell-read oxlint rule. This commit
closes the false-green paths and broadens lint coverage.

Workflow fixes:
- showcase_deploy.yml: drop `continue-on-error: true` on the redeploy-summary
  artifact download. The dispatch path is already guarded by the `if:
  workflow_run` clause, so the bash "no summary" branch handles legitimate
  manual dispatches. A genuine workflow_run download failure must now fail
  loud instead of silently widening verify to the full service set against
  stale `:latest`.
- showcase_build.yml: redeploy-staging now intersects the build matrix with
  the aggregator success set (`needs.aggregate-build-results.outputs.results`,
  status == "success") before producing the redeploy CSV. Failed/skipped
  slots no longer get redeployed (which would just re-pull stale `:latest`
  and look healthy).
- showcase_build.yml: `notify-all-builds-failed` now additionally requires
  `needs.build.result == 'failure'` so it doesn't Slack-spam when the build
  job was SKIPPED (verify-image-refs upstream failure).
- showcase_build.yml: `notify` now lists [build, aggregate-build-results,
  redeploy-staging] in `needs:` so aggregator/redeploy failures still emit
  a Slack signal. `if: failure()` still skips when none of the needs failed.
- showcase_build.yml: `set -euo pipefail` on the Prepare build args step
  so a transient $GITHUB_OUTPUT write failure can't ship images without
  COMMIT_SHA/BRANCH baked in.
- showcase_deploy.yml: `enforce-redeploy-gate` now also trips on a
  resolve-matrix failure (`needs.resolve-matrix.result == 'failure'`) so
  an upstream crash that leaves `redeploy_red` empty can't bypass the gate.
- Doc-comment accuracy: drop stale `(PR #5093)` reference; correct the
  env-IDs source-of-truth comment; document the optional `skip_build` field
  in ALL_SERVICES; clarify that health_path is informational and verify
  uses per-service drivers; add the missing `resolve-targets` step 0 to the
  promote workflow's "Order:" header.

Aggregator fix (RED-GREEN):
- aggregate-build-results.ts: throw on zero slot dirs. The job is gated
  upstream on has_changes == 'true', so zero slot dirs is a broken artifact
  download, not a legitimate empty build set. Silently emitting
  any_success=false + results=[] is indistinguishable from "all builds
  failed" and lets the deploy workflow fall back to probing the full
  service set against stale `:latest`. Refuse the ambiguity.
- aggregate-build-results.test.ts: existing empty-INPUT_DIR test was
  updated to assert the throw (was: return []).

Oxlint rule (RED-GREEN):
- no-public-env-shell-read.mjs: handle destructuring reads
  (const { NEXT_PUBLIC_X } = process.env and aliased form), template-literal
  computed keys (process.env[\`NEXT_PUBLIC_X\`]), and explicitly skip
  assignment-LHS / `delete` targets (writes are not reads). Optional
  chaining already worked through the existing MemberExpression path.
  Aliasing (`const e = process.env; e.X`) is intentionally documented as
  out of scope (needs scope tracking). Description sharpened to say the
  rule guards a specific banned-key set, not all NEXT_PUBLIC_* reads.
- .oxlintrc.json: tighten the off-override glob from
  `showcase/**/*runtime-config*` to
  `showcase/**/lib/runtime-config*.{ts,tsx}` so it only silences the
  intended implementation files, not arbitrary paths containing that
  substring.
- lint-rule-no-public-env.test.ts: rewritten as table-driven coverage of
  every BANNED_KEYS entry (dotted + bracket-string forms), every ALLOWED
  key (asserting non-firing), all new variants from the rule expansion,
  the assignment/delete non-fire cases, and override scoping
  (runtime-config exempt; packages exempt; shell-tree non-runtime-config
  flagged).

Validation:
- actionlint on all three workflows: 8 pre-existing findings (depot label,
  pre-existing SC2086 infos in untouched steps); my edits add zero.
- python3 yaml.safe_load: all three workflows OK.
- vitest aggregate-build-results.test.ts: 6/6 pass (incl. new throw test).
- vitest lint-rule-no-public-env.test.ts: 34/34 pass.
- vitest full showcase/scripts suite: 1654/1654 pass across 46 files.
- ruby showcase/bin/spec/all_tests.rb: 87 runs, 0 failures.
- Intersection jq proof (matrix a,b,c × success a,c) → "a,c"; all-failed
  → ""; skipped status excluded.
2026-05-29 11:45:14 -07:00
Jordan Ritter 3f5eba4999 fix(showcase): harden promote P2 race-check, resolve-once map, and pin verification
Nine correctness fixes to bin/railway PromoteCommand, each red-green tested.

- P2 in-flight race-check now compares deployed digest against the digest
  captured in @promote_refs (P1-resolved), not svc["digest"] which is nil
  for tag-form staging — the check was dead code. Also: parse JSON-string
  Deployment.meta; sort fetch_latest_staging_deployments by createdAt desc.
- @promote_refs is RESET (not memoized) at the top of check_p1_ghcr_digests,
  so a reused command instance cannot carry stale A-era refs into a B-era
  promote. execute_promotion hard-guards against a nil @promote_refs.
- execute_promotion pre-validates that every prod-matched service has a
  digest-shaped @promote_refs entry BEFORE pinning anything, eliminating
  the partial-promotion-on-missing-ref hazard.
- execute_promotion rescue broadens to MutationError + GraphQL::Error +
  StandardError so a transient mid-loop failure still surfaces the
  PARTIAL-PROMOTION recovery report; dedup the duplicate warn line and
  note that source.image may already be partially advanced on Railway.
- check_p1_ghcr_digests emits REFUSE: P1 ... "no image" for an imageless
  staging service (instead of a silent skip that surfaced later as a
  misleading "internal error").
- check_p1_ghcr_digests per-service rescue broadens to StandardError so a
  non-GHCR error (e.g. ArgumentError, network) does not bypass the rescue
  and crash the loop, discarding earlier services' findings.
- pin_and_verify raises ArgumentError immediately if called with a
  tag-form image (instead of 30s of futile retries + misleading error).
- pin_and_verify timestamp gate is non-vacuous: a non-nil observed
  updatedAt is ALWAYS required, even when pre_update_ts is nil
  (which previously collapsed the gate to digest-equality alone).
- run_staging_probe rescues Errno::ENOENT / StandardError around the
  IO.popen launch so a missing npx produces a clean ok:false summary
  instead of a raw stack trace bubbling out of P3.

Spec hygiene: drop the unused FakeGQL class in test_promote_execute.rb
(it referenced an uninitialized @after_image); give the unresolvable-tag
fixture a placeholder digest so it never builds a malformed "...@" ref;
test_promote_p2.rb tests now capture both streams and assert against the
combined output, matching the convention used elsewhere in the suite.
2026-05-29 11:45:14 -07:00
Jordan Ritter 108a0a79d7 fix(showcase): resolve promote digest once + verify redeploy + loud partial-promote
PromoteCommand had a TOCTOU window: resolved_prod_image(svc) was called
twice for every staging service — once in check_p1_ghcr_digests (where
the resolved digest was manifest_exists-verified), and again in
execute_promotion (whose result is what actually got pinned). Because
staging is a mutable :latest tag, a concurrent push between P1 and
execute could make the two resolutions return different digests, and
prod would be pinned to a digest P1 never verified. It also doubled
the GHCR round-trip per service.

Resolve+verify each staging service's digest exactly once during P1,
store the result on @promote_refs (service_name => digest-pinned ref),
and reuse that exact ref in execute_promotion. If a service has no
entry (P1 didn't run or didn't pass), refuse rather than silently fall
back to a tag.

Also:

- check_p1_ghcr_digests had a method-level rescue Railway::GHCR::Error
  that replaced the entire findings array with one entry — so a GHCR
  error on service N discarded findings already accumulated for
  services 1..N-1. Move the rescue inside the per-service iteration
  so each error becomes its own REFUSE finding and the loop continues.

- self.pin_and_verify asserted serviceInstanceUpdate == true but
  discarded the serviceInstanceRedeploy result. A failed redeploy
  could pass verification because the update mutation had already
  advanced source.image+updatedAt. Require truthy redeploy result;
  raise MutationError otherwise, symmetric with the update check.

- check_p2_staging_deployments already guarded meta.is_a?(Hash) so it
  doesn't crash on a String meta, but the silent skip of the in-flight
  race-check was invisible. Add a WARN finding so the skip is visible.
  SUCCESS status remains the real gate (still REFUSE).

- execute_promotion now tracks already-pinned services and, on a
  mid-loop MutationError, emits a loud PARTIAL PROMOTION report
  naming both the already-pinned services and the failing one with
  a pointer at bin/railway rollback-commit. Auto-rollback is left as
  a follow-up — the goal here is just to make the mixed-state loud
  and actionable rather than a quiet exit 1.
2026-05-29 11:45:14 -07:00
Jordan Ritter c03270135e fix(showcase): promote resolves staging tag to GHCR digest before pinning prod
The showcase deploy model is STAGING = mutable :latest tag, PROD = immutable
@sha256: digest (P6 enforces both shapes). SnapshotCommand#build_snapshot
stored the raw serviceInstance.source.image, so for staging svc["image"] was
the :latest TAG. execute_promotion was pinning THAT mutable tag to prod via
serviceInstanceUpdate, defeating the immutable-prod invariant before
pin_and_verify raised on the nil expected_digest.

Fix: add PromoteCommand#resolved_prod_image — returns the staging svc as
@sha256:-pinned (pass-through if already pinned; resolves the tag via the
shared GHCR client otherwise; returns nil if the tag cannot be resolved).
execute_promotion now refuses (P0) rather than pin a mutable tag, and
check_p1_ghcr_digests verifies the resolved digest (it previously SKIPPED
tag-form images entirely, so :latest was never P1-checked).

Also:
- P2 race-check guards latest["meta"] when Railway returns a JSON String
  (deserialized as Ruby String, not Hash) — .dig used to crash with
  NoMethodError. SUCCESS status remains the real gate.
- Remove dead --include-startcommand flag (never read; doubly inert because
  P6 REFUSEs on any startCommand divergence).
- Spec hygiene: P3 skip-test raises if probe runs under --no-require-staging-
  green; P6 warn-proceed stubs execute_promotion to isolate the gate and
  asserts rc==0; test_ghcr_token teardown unconditionally deletes
  GITHUB_TOKEN/GHCR_TOKEN/RAILWAY_TOKEN before restoring priors.

70 runs, 204 assertions, 0 failures (up from 66/188 baseline).
2026-05-29 11:45:13 -07:00
Jordan Ritter ccc5014ccf fix(showcase): verify-deploy arg-parse symmetry + non-bare releaseBody catch 2026-05-29 11:45:13 -07:00
Jordan Ritter 48bb0a5d21 fix(showcase): make verify-deploy fail loud on zero targets + non-ENOENT token errors 2026-05-29 11:45:13 -07:00
Jordan Ritter 690ab1d675 fix(showcase): trim RAILWAY_TOKEN env lane to honor no-whitespace-in-header invariant
A RAILWAY_TOKEN secret with trailing whitespace/newline (common from op
read, heredoc, shell export) was returned verbatim and produced invalid
Authorization: Bearer headers and silent Railway 401s. Trim the env-var
lane and treat whitespace-only as UNSET so the config-file fallback runs.
Also adds a missing should-have-thrown guard in the NO_HOME test and
removes a stale gateIgnore clause from findUntrackedServices docstring.
2026-05-29 11:45:13 -07:00
Jordan Ritter e1a036e86c fix(showcase): aggregate-build-results fail-loud on missing slot file + GHA heredoc output + tests 2026-05-29 11:45:13 -07:00
Jordan Ritter 1883d8c347 fix(showcase): fail loud on non-ENOENT emit read errors + make emit test hermetic
Distinguish ENOENT (treat as drift) from other read errors in
emit-railway-envs-json.ts --check; non-ENOENT errors now exit 2 with the
real error on stderr instead of being silently coerced into a misleading
'stale' message or an overwrite on a false drift signal.

Add a --out=<path> override so tests can write to a temp directory and
never mutate the tracked railway-envs.generated.json artifact. Rewrite
the emit-railway-envs-json test to use mkdtempSync + --out, switch the
staleness assertion to spawnSync so it asserts on exit code 1 plus the
stale-diagnostic substring, and add coverage for the new EISDIR
fail-loud path. After this change git status is clean post-test.
2026-05-29 11:45:13 -07:00
Jordan Ritter b64058a270 fix(showcase): domainFor scheme guard checks :// not startsWith http 2026-05-29 11:45:12 -07:00
Jordan Ritter db84d82d42 fix(showcase): unify railway token resolver + sanitize GraphQL error bodies + null-check project 2026-05-29 11:45:12 -07:00
Jordan Ritter e5d2d47a04 fix(showcase): canonicalize build-result service names (trim) + reject array payloads 2026-05-29 11:45:12 -07:00
Jordan Ritter 781bc92e90 fix(showcase): return trimmed railway token to avoid invalid Authorization header
A token read from ~/.railway/config.json with surrounding whitespace or a
trailing newline passed nonEmpty() but was returned verbatim, so an
'Authorization: Bearer <token>' header could carry CR/LF or stray spaces
(Node HTTP rejects invalid header chars; Railway 401s otherwise). Trim
each return path so the canonical token is always emitted; whitespace-only
values still fall through. Also clarify the JSDoc that this resolver does
not consult process.env.RAILWAY_TOKEN.
2026-05-29 11:45:12 -07:00