Skip to content

feat(runtime): formalize 4-value runtime channel taxonomy#337

Open
ctol3r wants to merge 1 commit into
mainfrom
wave/runtime-channels
Open

feat(runtime): formalize 4-value runtime channel taxonomy#337
ctol3r wants to merge 1 commit into
mainfrom
wave/runtime-channels

Conversation

@ctol3r

@ctol3r ctol3r commented May 12, 2026

Copy link
Copy Markdown
Owner

Summary

Closes the brief "formalize runtime channels — render visibly in /api/health, /status, /footer."

The ambiguity this fixes: Vercel's `VERCEL_ENV` is 3 values (`production`/`preview`/`development`) that conflate operator-preview (ephemeral per-PR deploy) with staging (long-running stable pre-prod). Operators can't tell which one they're looking at; audit findings can attach to the wrong environment.

The 4 channels

Channel When
`local_dev` `pnpm dev` on a developer machine
`operator_preview` Vercel per-PR preview deploy (ephemeral)
`staging` Long-running stable staging environment
`production` Live customer-facing deploy

Resolution rules (deterministic)

  1. Explicit `VITALCV_RUNTIME_CHANNEL` env override wins.
  2. `VERCEL_ENV=production` → `production`.
  3. `VERCEL_ENV=preview` AND `VERCEL_URL` matches staging pattern → `staging`. Default pattern matches `staging` or `stage` substring; overridable via `VITALCV_STAGING_URL_PATTERN`.
  4. `VERCEL_ENV=preview` otherwise → `operator_preview`.
  5. `VERCEL_ENV=development` → `local_dev`.
  6. `NODE_ENV=production` fallback → `production` (self-hosted).
  7. Final fallback → `local_dev`.

Files

  • New `apps/web/lib/runtime/channel.ts` — typed union + resolver + label map.
  • Modified `apps/web/app/api/health/route.ts` — adds `runtime_channel` to the response body.
  • New `apps/web/components/runtime/RuntimeChannelFooter.tsx` — inline-styled server component. Renders null in production by default; `alwaysRender` prop forces the chip (for audit pages).
  • New `apps/web/tests/runtime-channel.test.tsx` — 46-test lockdown.

Truth-contract invariants (46-test lockdown)

  • Closed union of exactly 4 channels in canonical order.
  • Every channel has a label; map is frozen; no bare "Verified".
  • `narrowRuntimeChannel` returns null on every non-matching input.
  • Resolver respects priority order in all 7 rules above.
  • Custom `VITALCV_STAGING_URL_PATTERN` overrides default.
  • Resolver is deterministic — same env always returns same channel.
  • `/api/health` surfaces `runtime_channel` field in response body.
  • `/api/health` resolves all 4 channels correctly under matching env.
  • Wave-136 `config.clerk` shape preserved.
  • Footer renders null in production by default; `alwaysRender` forces chip.
  • Every chip has distinct `data-runtime-channel` + visible label + `data-testid`.
  • No vibrant red/green hex (monochrome doctrine).
  • Banned-strings scan: clean (3 files).

What this PR does NOT do

Validation

  • Targeted tests: 46/46.
  • Full web build: 13/13.
  • Truth-contract scan: CLEAN.
  • Diff scope: 3 new files + 1 modified.
  • Standalone: branches off `origin/main`, no stacking.

Closes the brief "formalize runtime channels — local_dev,
operator_preview, staging, production. Render visibly in /api/health,
/status, /footer."

The ambiguity this fixes: Vercel's VERCEL_ENV is a 3-value union
(production / preview / development) that conflates two distinct
operational contexts:
  - "operator preview" — ephemeral per-PR Vercel deploy
  - "staging"          — long-running stable pre-prod deploy
Both map to VERCEL_ENV=preview today. Operators can't tell which
they're looking at, which means audit findings can attach to the
wrong environment.

apps/web/lib/runtime/channel.ts:
  - Closed RUNTIME_CHANNELS union: local_dev | operator_preview |
    staging | production.
  - Frozen label map (compound forms; never bare "Verified").
  - resolveRuntimeChannel(env?) — deterministic resolver. Priority:
    1. Explicit VITALCV_RUNTIME_CHANNEL override (wins).
    2. VERCEL_ENV=production → production.
    3. VERCEL_ENV=preview + VERCEL_URL matches staging pattern →
       staging. Default pattern matches 'staging' or 'stage'
       substring; overridable via VITALCV_STAGING_URL_PATTERN env.
    4. VERCEL_ENV=preview otherwise → operator_preview.
    5. VERCEL_ENV=development → local_dev.
    6. NODE_ENV=production fallback → production (self-hosted).
    7. Final fallback → local_dev.
  - narrowRuntimeChannel(unknown) — returns null on unknown (no
    silent default).
  - isProductionChannel + isPreReleaseChannel — taxonomy helpers.

apps/web/app/api/health/route.ts — modified:
  - Adds runtime_channel field to the response body.
  - Preserves Wave-136 backward-compat config.clerk shape.

apps/web/components/runtime/RuntimeChannelFooter.tsx:
  - Server component (reads process.env via resolveRuntimeChannel).
  - Inline monochrome styles (no design-token dep — ships
    standalone before #323 lands).
  - Renders null in production by default (customer-facing pages
    don't carry a channel banner).
  - alwaysRender prop forces production chip (use on /status,
    /truth-boundary, etc. where the channel attestation is part
    of the audit surface).
  - data-runtime-channel attribute matches channel id for
    programmatic inspection.

46-test lockdown:
  - Union has exactly 4 channels in canonical order.
  - Every channel has a label; map is frozen; no bare "Verified".
  - narrowRuntimeChannel returns null on every non-matching input.
  - Resolver respects priority: override > VERCEL_ENV=production >
    preview-staging-pattern > preview-default > VERCEL_ENV=dev >
    NODE_ENV=production fallback > local_dev fallback.
  - Custom VITALCV_STAGING_URL_PATTERN works.
  - Resolver is deterministic: same env → same result every call.
  - /api/health surfaces runtime_channel in the response body and
    resolves correctly for production / preview / staging /
    local_dev.
  - /api/health preserves Wave-136 config.clerk shape.
  - Footer renders null in production by default; alwaysRender
    forces the chip.
  - Every non-production channel renders distinct data-runtime-channel
    attribute + visible label.
  - Every chip carries data-testid="runtime-channel-footer".
  - No vibrant red/green hex anywhere (monochrome doctrine).
  - Banned-strings scan: clean (3 files).

What this PR does NOT do:
  - Does NOT mount RuntimeChannelFooter into any specific page.
    The component is ready; per-page wiring is a small follow-up
    (one import + one render in app/layout.tsx).
  - Does NOT modify /status page. That's an existing page;
    if you want the chip mounted there too, it's a one-line
    import on top of this PR's primitive.
  - Does NOT remove the existing runtime_mode field (from
    open PR #334). The two fields coexist:
      runtime_mode    — Vercel's 3-value
      runtime_channel — VitalCV's 4-value
    runtime_channel is the disambiguated taxonomy; runtime_mode
    is preserved for backward-compat with monitoring that reads
    Vercel's native value.

Standalone — branches off origin/main, no stacking.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@vercel

vercel Bot commented May 12, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
vcv-web Ready Ready Preview, Comment May 12, 2026 3:10am
vitalcv Ready Ready Preview, Comment May 12, 2026 3:10am

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: c478b167f0

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +90 to +92
const pattern = typeof env.VITALCV_STAGING_URL_PATTERN === 'string'
? new RegExp(env.VITALCV_STAGING_URL_PATTERN, 'i')
: DEFAULT_STAGING_PATTERN;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Guard custom staging regex parsing

Constructing new RegExp(env.VITALCV_STAGING_URL_PATTERN, 'i') without error handling can crash channel resolution when the env var contains an invalid pattern (for example a typo like '['), which turns /api/health into a 500 in VERCEL_ENV=preview and would also break any page that renders RuntimeChannelFooter. This regression is user-triggerable through configuration and should fail closed (e.g., catch SyntaxError and fall back to DEFAULT_STAGING_PATTERN or operator_preview).

Useful? React with 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants