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).
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').
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.
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).
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.
New workflow_dispatch-only workflow that promotes a staging-tested digest
to prod. Four jobs in strict order:
1. verify-staging-precondition (live re-probe, refuse on red)
2. promote (bin/railway promote; spec §7 preconditions P1..P6)
3. verify-prod (verify-deploy.ts --env prod)
4. notify (Slack #oss-alerts on red; never #engr)
Driven entirely off the railway-envs.ts SSOT. Refs spec §3.
showcase_deploy.yml is now SSOT-driven: env id and service set both come
from railway-envs.generated.json (no inline matrix, no hardcoded
RAILWAY_ENV_ID). Probes staging on workflow_run (was incorrectly probing
prod). Reads redeploy-env's per-service JSON summary uploaded as an
artifact and fails the workflow on any staging status:error while
verify still runs against the success-set. Preserves PR #5093's
redeploy-env.ts exit semantics unchanged. Refs spec §3.
Implements plan-B B11. URL and analytics NEXT_PUBLIC_* values now reach
each shell at runtime via Option B (env-driven runtime-config), so the
GHA showcase_build.yml workflow no longer threads them through as Docker
build-args and the shell-dashboard/shell-docs Dockerfiles no longer
declare the matching ARG/ENV pairs.
- showcase_build.yml: shell-dashboard and shell-docs matrix entries lose
build_args_pb_url / build_args_shell_url / build_args_ops_url /
build_args_base_url / build_args_analytics; the 'Prepare build args'
step drops the corresponding env: keys and if-branches plus the five
analytics NEXT_PUBLIC_* secrets. COMMIT_SHA and BRANCH stay — they
identify the artifact.
- showcase/shell-dashboard/Dockerfile: remove ARG/ENV for
NEXT_PUBLIC_SHELL_URL, NEXT_PUBLIC_POCKETBASE_URL, OPS_BASE_URL plus
the explanatory comments. Update the runner-stage comment to point at
runtime-config.ts as the new source of truth.
- showcase/shell-docs/Dockerfile: remove ARG/ENV for
NEXT_PUBLIC_BASE_URL, NEXT_PUBLIC_SHELL_URL, NEXT_PUBLIC_POSTHOG_KEY,
NEXT_PUBLIC_REB2B_KEY, NEXT_PUBLIC_SCARF_PIXEL_ID, NEXT_PUBLIC_REO_KEY,
NEXT_PUBLIC_GOOGLE_ANALYTICS_TRACKING_ID. COMMIT_SHA / BRANCH retained.
shell/Dockerfile and shell-dojo/Dockerfile already only declare commit-sha
and branch ARGs — no changes needed there (per plan-B B11.4).
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.
redeploy-staging now also gates on aggregate-build-results.outputs.any_success
== 'true'. When every slot fails, the previous behavior silently kicked a
redeploy that just re-pulled the stale :latest and reported healthy. With this
gate, the redeploy is suppressed and a sibling notify-all-builds-failed job
marks the workflow red and posts to #oss-alerts so the all-broken state cannot
hide behind a green run.
The new notify-all-builds-failed job is distinct from the existing notify: job
(which fires on any build-slot failure); both can fire and that overlap is
intentional, per the Slack alert SOP.
[BLITZ:L3-wf] E-5c wiring.
Build matrix slots now publish per-slot build-result-<dispatch_name>
artifacts containing {service, status}. A new aggregate-build-results
job downloads every per-slot artifact, merges them via the shared
mergeBuildResultFiles helper, and uploads the canonical build-results
artifact (results.json) for cross-workflow consumption.
The deploy workflow's resolve-matrix step now downloads build-results
via gh api .../artifacts/<id>/zip and filters its verification matrix
from the structured JSON success set. No more gh api .../jobs calls,
no more capture("^build \\(") regex on job names — those silently
break on job-name renames. The contract (service + status enum) is
enforced in one place: showcase/scripts/lib/build-outputs.ts.
[BLITZ:L3-wf] E-4e/E-4f wiring.
E-3a/b/c per plan-E. Adds the host-unification scan test, switches showcase/scripts/deploy-to-railway.ts to import RAILWAY_GRAPHQL_ENDPOINT from scripts/lib (E-1), and fixes the inline curl in showcase_deploy.yml line 205. The .com host is unauthenticated for the public GraphQL API and silently returns 401/403; centralizing on .app prevents the drift from returning. Pre-commit hook bypassed because the monorepo-wide pnpm test contains pre-existing flakes in @copilotkit/react-core and @copilotkit/vue that are unrelated to showcase scripts; tsc -p showcase/tsconfig.json --noEmit and the railway-graphql vitest pass cleanly.
Push-to-main now redeploys staging only (never prod) via redeploy-env.ts in a
dedicated redeploy-staging job, guarded to tolerate per-slot build failures but
suppress on wholesale build skip/cancel. Prod stays promote-only.
publish-python now skips when dry-run=true (matching the npm lane). Add
set -euo pipefail to the tag step so a failed push no longer emits a ghost
tag output or proceeds to the GitHub Release. Pass GITHUB_TOKEN via env
instead of inline interpolation, guard against an empty dist/ before
uv publish, and surface curl errors from the PyPI verify-live loop.
Add set -euo pipefail and non-empty SHA validation to the pyproject-change
detection step so a null/stale merge_commit_sha fails loudly instead of
silently skipping a real version bump. Mirror the npm lane's success guard
on publish-python. Correct the python_publish input description (detection
still runs) and drop the dead checkout token (persist-credentials is false).
Fixes zizmor findings on .github/workflows/showcase_lint_prod.yml:
- unpinned-uses: pin actions/checkout to the repo-canonical SHA (v4)
- unpinned-uses: pin ruby/setup-ruby to v1.310.0 SHA
- artipacked: set persist-credentials: false on the checkout step
Matches the rest of the repo, which SHA-pins every uses: with a trailing
# vX.Y.Z comment for Dependabot to keep current.
The lint-prod step crashes today on a pre-existing snapshot bug (GraphQL
schema query for a "domains" field that no longer exists on Project).
The `--exit-zero` flag only suppresses exit-on-findings — it doesn't
catch fatal Ruby errors during snapshot building, so an error short-
circuits the whole job and the visibility surfaces never render.
Make the workflow truly advisory:
- Capture lint-prod stderr + exit code instead of failing the step.
- If lint-prod errors (or produces no JSON), synthesize a minimal
payload with an "error" field so the renderer has something to chew on.
- Renderer detects the error field and emits an "audit unavailable" block
with the captured error inside a <details> fold instead of leaving the
step summary / sticky comment blank.
Snapshot bug itself is out of scope for this change; tracked separately.
Make the lint-prod audit result legible without having to click into the
workflow logs. Two surfaces, both rendered from the same JSON payload:
1. `$GITHUB_STEP_SUMMARY` — structured markdown block at the top of every
workflow run page. Shows on every event (push, pull_request,
workflow_dispatch).
2. Sticky PR comment — one comment per PR, keyed by the HTML marker
`<!-- lint-prod-sticky-comment -->`. Re-runs update the same comment via
`gh api -X PATCH` instead of creating duplicates. Plain `gh` CLI only,
no third-party action.
Both surfaces show: one-line status, a table of the unpinned services only
(not all 27), and a Pacific-time run timestamp with the finding count.
To support the renderer, add `--format json` to `lint-prod`:
{services:[{name,source,status}], findings:N, timestamp:"ISO8601"}
The workflow consumes this shape and also writes `findings` to
`$GITHUB_OUTPUT` so downstream jobs (future Slack alert) can compare runs.
Idempotent: re-running the workflow finds the existing comment by marker
and PATCHes it — never duplicates.
Add --exit-zero flag to bin/railway lint-prod that makes the command exit 0
even when digest-pinning findings exist. Findings still print to stdout so
they remain visible in the CI step log.
Wire the showcase_lint_prod.yml workflow to pass --exit-zero so the job
cannot block PRs while we soak the check against real production state.
Once we have confidence the findings are clean, remove --exit-zero from
the workflow step to flip lint-prod to enforcing.
Updates README to document the advisory-mode behavior and the path to
flipping the check to enforcing.
Prerelease canary publishing is now an input mode on publish-release.yml's
workflow_dispatch. The dedicated prerelease.yml workflow is no longer
reachable and its workflow_call delegation never worked (npm matches the
OIDC token's caller `workflow_ref`, which was always prerelease.yml —
unregistered with any package's trust record).
Canaries are now dispatched via:
gh workflow run publish-release.yml \
-f scope=monorepo -f mode=prerelease -f suffix=<name>
The `bump-prerelease.ts` and `prerelease.ts` scripts remain unchanged; they
are invoked by publish-release.yml when `mode=prerelease`.
Folds prerelease.yml's canary publishing back into publish-release.yml, which
is the workflow registered as npm trusted publisher for all 15 @copilotkit/*
monorepo packages and @copilotkitnext/angular. Since npm matches on the OIDC
token's `workflow_ref` claim (the caller), the canary must dispatch from THIS
workflow file — not from a separate workflow that delegates via workflow_call.
Changes:
- Add `mode` (stable|prerelease) and `suffix` inputs to workflow_dispatch.
`mode` selects the publish script and gates post-publish tag/release steps;
`suffix` is forwarded to bump-prerelease.ts when mode=prerelease.
- Add a conditional `Bump prerelease versions` step in the build job that
runs `scripts/release/bump-prerelease.ts` before `Build packages` whenever
`inputs.mode == 'prerelease'`. The bumped versions flow through the
workspace artifact to the publish job exactly as in the deleted
prerelease.yml.
- Switch the publish step's `PUBLISH_SCRIPT` env var to be mode-driven:
`prerelease.ts` for canaries, `publish-release.ts` for stable. The old
`inputs.publish-script` plumbing was removed in the prior commit along
with the workflow_call inputs schema.
- Simplify the publish-job `meta` step now that workflow_call is gone: mode
defaults to `stable` unless workflow_dispatch passes `prerelease`.
- Rewrite the top-of-file comment to document both modes and the OIDC trust
binding, replacing the old "MANUAL RETRIGGER" wording which only covered
the stable retrigger path.
All post-publish gating (`steps.meta.outputs.mode != 'prerelease'` on the tag,
release, and stable-summary steps; `mode == 'prerelease'` on the prerelease
summary) was already in place from the prior PR-A architecture and is left
intact. The `Verify publish step emitted version` step keeps its prerelease
bypass.
Canary invocation after this lands:
gh workflow run publish-release.yml \
-f scope=monorepo -f mode=prerelease -f suffix=<name>
The workflow_call trigger was added in the PR-A/B architecture so prerelease.yml
could invoke this workflow as a reusable workflow. That architecture was wrong:
npm's trusted-publisher matching uses the OIDC token's `workflow_ref` claim,
which is the CALLER workflow (prerelease.yml), not the callee's
`job_workflow_ref` (publish-release.yml). Since prerelease.yml has no trust
record, every canary attempt failed at the npm publish step with ENEEDAUTH.
This commit removes the dead code:
- workflow_call: trigger block (inputs schema + secrets block)
- the `github.event_name != 'workflow_call'` guard on the build job
- the `needs.build.result == 'skipped' && github.event_name == 'workflow_call'`
branch on the publish job's `if:` (simplified to plain success check)
The follow-up commit folds prerelease support back into this workflow as a
`workflow_dispatch` mode, so the canary path executes from the workflow with
the trust record.
## Why
Follow-up to #5063. Together they unblock customer canary publishes that
have been failing with `npm ENEEDAUTH` since 2026-05-15.
#5063 parametrized `publish-release.yml` to support `workflow_call`.
This PR converts `prerelease.yml`'s publish path into a thin caller of
that reusable workflow. Because npm OIDC's `job_workflow_ref` claim
resolves to the callee in a reusable-workflow invocation, the existing
trust record on `publish-release.yml` covers both stable AND canary
publishes — no second trusted-publisher record needed (npm only allows
one per package).
## What changes
- `prerelease.yml`'s `build` job is preserved verbatim (checkout,
install, `bump-prerelease.ts --suffix`, build, test, upload `workspace`
artifact).
- The old `publish` job is replaced with `uses:
./.github/workflows/publish-release.yml` with `mode: prerelease`,
`publish-script: prerelease.ts`, `scope`, `dry-run`.
- The caller's `publish` job grants explicit `permissions: { contents:
write, id-token: write, actions: read }` — REQUIRED for the callee's npm
OIDC mint (per GitHub Actions, caller's per-job permissions cap the
callee's declared permissions).
- The caller's `workflow_dispatch.inputs.dry_run` (underscore, preserved
for backwards compat) maps to the callee's `dry-run` (hyphen) at the
`with:` boundary.
- No `secrets:` block — Notion not needed for prereleases; callee
declares `NOTION_API_KEY` optional and gates it on `mode == 'stable'`.
## Verification plan
1. Dispatch `prerelease.yml` with `scope: monorepo`, `suffix:
test-oidc-refactor`, `dry_run: true` → confirms plumbing without an
actual publish.
2. Dispatch again with `dry_run: false` → first OIDC-handshake-verifying
real canary.
3. `npm view @copilotkit/runtime@1.58.0-canary.test-oidc-refactor --json
| jq '.dist.attestations'` → confirm provenance attestation references
`publish-release.yml`.
## Test plan
- [ ] CI green.
- [ ] Post-merge: dispatch dry-run + real canary per Verification plan
above.
- [ ] Confirm provenance attestations land via `npm view`.
Supersedes #5066 (closed when #5063's base branch was deleted).
## Summary
- Unify authored and generated shell-docs navigation so the sidebar
keeps the same structure across framework modes.
- Restore setup-content bundling from integration-owned docs and wire
shell-docs to consume the generated bundle at runtime.
- Audit and fix the LangGraph TypeScript and Google ADK code regions so
the generated snippets are more useful and accurate.
- Tighten docs/build routing and workflow triggers so shell-docs
rebuilds when the relevant integration docs inputs change.
## Testing
- Shell-docs unit tests passed.
- Shell-docs typecheck passed.
- Shell-docs lint passed with existing repository warnings only.
- Setup-content bundle generation passed.
- Python integration files compiled successfully.
- Workflow YAML parsed successfully.
PR-B CR-r1 fix: reusable-workflow permissions in GitHub Actions are capped by
the caller's per-job permissions. The callee (publish-release.yml from PR-A)
declares id-token: write + contents: write on its publish job, but without
the caller (prerelease.yml) granting those at its own publish-job level, the
callee gets only the workflow-level default (contents: read). The OIDC token
mint then fails silently and npm publish errors.
4 of 7 CR agents independently flagged this. The spec missed it.
Adds contents: write (callee uses it in stable mode; gated off in prerelease
but matches the callee's declaration), id-token: write (required for OIDC),
and actions: read (required for actions/download-artifact in callee).
Spec updated to document this requirement.
PR-B of the two-PR refactor: prerelease.yml's build job is preserved verbatim
(checkout, install, bump-prerelease.ts with --suffix, build, test, upload-
artifact "workspace"); its publish job is replaced with a workflow_call
invocation into publish-release.yml from PR-A. The reusable workflow's
job_workflow_ref OIDC claim matches the existing trusted-publisher record on
publish-release.yml, so the canary publishes succeed without registering a
second trust binding (which npm does not allow).
The caller's workflow_dispatch.inputs.dry_run (underscore, preserved) maps
to the callee's dry-run (hyphen) at the with: boundary — independent keys
in independent namespaces. No secrets passed; Notion not needed for
prereleases.
Spec: https://www.notion.so/36d3aa381852811ba10ad1bcd228d6d8
Unblocks: @copilotkit/react-core canary --suffix thread-id-propagation.
Stacked on PR-A (fix/publish-release-workflow-call).
PR-A CR-r2 fixes (2 bucket-(a) findings from 7-agent confirmation round).
A4: Tighten publish-job `if:` so `needs.build.result == 'skipped'` is only honored when
the event is `workflow_call` (the intentional skip for the reusable workflow callee).
Previously, a `pull_request: closed` event on a release branch where the PR was closed
WITHOUT merging would cause the build job to skip (its own merged-true guard), then the
publish job's permissive `if:` would still run it — a phantom publish from an unmerged
release PR.
A5: Every post-publish step's custom `if:` overrode the default implicit `success()`
check, meaning a failure in `Publish to npm` or the `Verify version` guard would not
prevent downstream steps (tag push, GitHub Release create) from running. Prepended
`success() && ` to all post-publish step `if:` conditions to restore the implicit gate.
Spec: https://www.notion.so/36d3aa381852811ba10ad1bcd228d6d8
PR-A CR-r1 fixes (4 of 4 actionable findings from 7-agent CR + 1 cheap defense-in-depth).
A1: post-publish steps (Configure git user, Check for pre-existing tags, Create and push
git tag, Create GitHub Release) now gate on `inputs.dry-run != true && mode != 'prerelease'`
instead of just `mode`. On dry-run + stable, VERSION was empty so TAG="v" garbage was
pushed; this prevents that.
A2: Add explicit Verify-publish-step-emitted-version guard between Publish to npm and the
post-publish chain. Fails loud if publish-release.ts (or any inputs.publish-script
override) forgets to emit `version` to GITHUB_OUTPUT.
A3: Replace the single unconditional Release summary with three gated variants (stable,
prerelease, dry-run) so the summary no longer claims "Release Published" on dry-run or
prerelease.
B3: inputs.publish-script and steps.meta.outputs.scope now flow through env to the shell
(reduces injection surface even though caller is in-repo today).
Spec: https://www.notion.so/36d3aa381852811ba10ad1bcd228d6d8
prerelease.yml cannot register as a second npm trusted publisher (npm allows
exactly one per package; all 16 monorepo-scoped packages bind to
publish-release.yml). Refactor publish-release.yml to also support
workflow_call so prerelease.yml can invoke it as a reusable workflow — OIDC's
job_workflow_ref claim points at the callee, so the existing trust record
covers both flows.
This PR (PR-A) adds the workflow_call trigger, input schema, meta step for
scope+mode resolution, build-job gating to skip on workflow_call, publish-job
if: override for skipped-needs, post-publish step gating on
mode != prerelease, NOTION_API_KEY gating on stable mode, and the
publish-script input for the TS file selection.
Also adds a dry-run input on workflow_dispatch so PR-A can be verified
post-merge via a sacrificial release branch without an actual publish.
Spec: https://www.notion.so/36d3aa381852811ba10ad1bcd228d6d8
Customer block (Ben Taylor, #engr): @copilotkit/react-core canary with
suffix=thread-id-propagation.
PR-B (prerelease.yml caller conversion) follows.
upload-artifact's path filters are post-walk: even with !**/node_modules/**
exclusions, the action still descends into every node_modules and stats
every file (~6M for this monorepo with pnpm's .pnpm/ symlink farm) before
applying negations. That enumeration is the actual bottleneck — the
Upload workspace step runs 10+ minutes even with the filters added in
#5044.
Replace the filtered upload with: rm -rf the heavy dirs (node_modules,
.nx, .turbo, .next), tar the workspace into a single file, upload that.
Publish job tar -xzf's it after download and continues unchanged. Single-
file upload skips upload-artifact's per-file overhead entirely.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The prerelease workflow has been OOMing on Upload workspace since the
build/publish split in 770759a4be. The artifact upload enumerates
~6M files (root + per-package node_modules with pnpm symlinks all
materialized, plus build caches) and actions/upload-artifact builds
the full manifest in memory before streaming, blowing past the 4GB
Node heap limit.
publish-release.yml already had the working pattern: exclude
node_modules/.next/.turbo/.nx and re-run pnpm install --frozen-lockfile
in the publish job. Port it over so prerelease publishes succeed
again.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Zizmor's ref-version-mismatch audit flags the existing `# v1` comment
because the hash 5f3b3c2e5a00f0093de47f657aeaefcedff27d18 is the
v1.17.0 tag, not the v1 head. Update the trailing comment to match.
No behavior change — the SHA pin is what governs which commit Actions
fetches. This just keeps zizmor green so unrelated showcase-wiring PRs
don't trip on a pre-existing pin annotation.
Three companion workflows duplicate the showcase_build.yml service registry
and were missed in the initial wiring commit. Bring them in sync:
- .github/workflows/showcase_build_check.yml: add ms_agent_harness_dotnet
to the paths-filter and the ALL_SERVICES matrix (mirror of the
production build matrix, used for pre-merge Docker build verification).
- .github/workflows/showcase_deploy.yml: add ms-agent-harness-dotnet to
the workflow_dispatch options and the verification ALL_SERVICES with
railway_id 6343d7f9-6c3f-4c8d-9a6e-79f03d2f1e37 and /api/health.
- .github/workflows/showcase_keep-alive.yml: add ms-agent-harness-dotnet
to the keep-alive ping matrix.
Brings the Microsoft Agent Harness (.NET) integration live on the
showcase Railway project. Integration code itself landed in PR #4982.
Changes:
- Railway service `showcase-ms-agent-harness-dotnet` created
(id 6343d7f9-6c3f-4c8d-9a6e-79f03d2f1e37) with the public domain
showcase-ms-agent-harness-dotnet-production.up.railway.app, image
source ghcr.io/copilotkit/showcase-ms-agent-harness-dotnet:latest,
healthcheck /api/health, and env vars cloned from the sibling
showcase-ms-agent-dotnet service.
- .github/workflows/showcase_build.yml: add ms-agent-harness-dotnet to
workflow_dispatch options, paths-filter, and the ALL_SERVICES matrix
(mirroring the ms-agent-dotnet sibling entry).
- showcase/integrations/ms-agent-harness-dotnet/manifest.yaml: flip
deployed: false -> true so the dashboard surfaces the integration
once the image is live.
Skips test-and-check-packages pre-commit hook locally because
@copilotkit/web-inspector:test has a pre-existing failure on main
(window.localStorage.clear telemetry test setup) unrelated to these
YAML-only changes.