Skip to content

Commit f2c1553

Browse files
ci: validate every org's resources on every pull request
Nothing ran `npm run validate` before merge. A config that `apply` refuses (a name over 40 characters, a per-provider voice schema error) could merge green, and deploys and promotion out of main then stopped until a fix landed. Plain `push` only warns, and can fail partway with an API 400. The validator's other rules (structured-output lockstep, duplicated prompts, the maxTokens floor) are warnings and don't fail it. - ci.yml gets a Validate resources job: validate for every folder under resources/, reporting every failing org rather than stopping at the first. validate makes no network call; the engine's config only needs a key to be set, so the step sets a placeholder key and an unroutable base URL, so nothing can be sent. The job has no secrets and installs without scripts, so forks get it too. No engine change. - The failure message, troubleshooting guide and AGENTS.md give a placeholder-key command to reproduce it, so nobody fetches a real key for an offline check. - The docs say what requiring it costs (every org gates every PR), the merge-queue trigger it needs, and that CI validates .ts resources without .env.<org>. - The starter example's one-sided structured-output link is fixed, so it validates without warnings. - tests/ci-validate-workflow.test.ts runs the step itself against fixture orgs: no orgs, all valid (and warning-free), one invalid org among valid ones, an invalid folder name, and no secrets, permissions, privileged trigger or persisted credentials. - README, AGENTS.md (change loop), the workflows, PR checks and troubleshooting guides, and improvements.md #37 describe it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent 1aa43fe commit f2c1553

10 files changed

Lines changed: 349 additions & 3 deletions

File tree

‎.github/workflows/ci.yml‎

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,3 +64,61 @@ jobs:
6464
6565
- name: Lint workflows
6666
run: ./actionlint -color
67+
68+
validate:
69+
name: Validate resources
70+
runs-on: ubuntu-latest
71+
timeout-minutes: 10
72+
73+
steps:
74+
- uses: actions/checkout@v4
75+
with:
76+
persist-credentials: false
77+
78+
- uses: actions/setup-node@v4
79+
with:
80+
node-version: 22
81+
cache: npm
82+
83+
# Install scripts only build the optional audio deps for `npm run call`.
84+
- run: npm ci --ignore-scripts
85+
86+
# The same validator `apply` runs before every deploy, here before merge:
87+
# a config that `apply` would refuse never reaches main, where it would
88+
# block deploys and promotion until someone noticed. Every org is
89+
# validated, including resources no PR check targets.
90+
#
91+
# `validate` makes no network call, but loading the engine's config
92+
# needs a key to be set. The placeholder below is never sent anywhere
93+
# (the base URL is unroutable, so an accidental API call fails here),
94+
# and no secret is available to this job, so it runs the same on forks.
95+
# `.ts` resources run without `.env.<org>` here, unlike under `apply`.
96+
- name: Validate every org
97+
shell: bash
98+
env:
99+
VAPI_PRIVATE_API_KEY: validate-only-never-sent
100+
VAPI_BASE_URL: http://127.0.0.1:9
101+
run: |
102+
set -euo pipefail
103+
shopt -s nullglob
104+
orgs=()
105+
for dir in resources/*/; do
106+
orgs+=("$(basename "$dir")")
107+
done
108+
if (( ${#orgs[@]} == 0 )); then
109+
echo "No org folders under resources/; nothing to validate."
110+
exit 0
111+
fi
112+
failed=()
113+
for org in "${orgs[@]}"; do
114+
echo "::group::Validate ${org}"
115+
if ! node --import tsx src/validate-cmd.ts "$org"; then
116+
failed+=("$org")
117+
fi
118+
echo "::endgroup::"
119+
done
120+
if (( ${#failed[@]} > 0 )); then
121+
echo "::error::Validation failed for: ${failed[*]}. Each org's findings are in its log group above. To reproduce locally without that org's key: VAPI_PRIVATE_API_KEY=validate-only npm run validate -- <org>"
122+
exit 1
123+
fi
124+
echo "Validated ${#orgs[@]} org(s): ${orgs[*]}"

‎AGENTS.md‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,12 @@ precisely.
130130
2. **Edit the files** under `resources/<org>/`. Settings and examples:
131131
[resource reference](docs/guides/resource-reference.md); tested files to
132132
copy from: [`examples/starter/`](examples/starter/README.md).
133-
3. **Validate:** `npm run validate -- <org>` (offline).
133+
3. **Validate:** `npm run validate -- <org>` (offline). CI's **Validate
134+
resources** check runs it for every org on every PR; if that check fails,
135+
run it locally for the org it names and fix the errors. Without that org's
136+
`.env.<org>`, run `VAPI_PRIVATE_API_KEY=validate-only npm run validate -- <org>`;
137+
never ask for a real key just to validate. Don't weaken the check or the
138+
workflow to get past it.
134139
4. **Build PR checks offline** if `vapi-checks.yml` exists:
135140
`npm run check -- --all --dry-run`. Fix anything it reports.
136141
5. **Deploy only with a yes** (safety rule 1): `npm run apply -- <org>`, or

‎README.md‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,9 @@ npm run apply -- my-org # pull the latest, merge, push
144144
```
145145

146146
Commit the changed files and `.vapi-state.my-org.json` so your team shares the
147-
same name → UUID mappings.
147+
same name → UUID mappings. Every pull request runs the same validator for
148+
every org in CI (the **Validate resources** check), so a config `apply` would
149+
refuse fails before it merges.
148150

149151
### 5. Test it
150152

‎docs/guides/file-formats.md‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,8 @@ destinations:
8989
# examples/starter/resources/starter/structuredOutputs/call-summary.yml
9090
name: call-summary
9191
type: ai
92+
assistant_ids:
93+
- receptionist
9294
description: Summarizes the call for the front-desk log.
9395
schema:
9496
type: object
@@ -180,4 +182,6 @@ simulationIds:
180182

181183
Any resource can also be a `.ts` file whose default export is the resource
182184
object, useful for generating config. It is executed when loaded, so treat
183-
`.ts` resources like code in review.
185+
`.ts` resources like code in review. CI validates `.ts` resources without your
186+
`.env.<org>`, so build them from files in the repository, not from
187+
`process.env`.

‎docs/guides/pr-checks.md‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,9 @@ PR push. It asks for `statuses: write` only to post the direct links.
114114
`promotion.yml`, the engine (`src/**`, `package*.json`), nor the check's
115115
own `paths` skip it, and `Vapi Evals` posts success.
116116
- A newer push cancels the older run.
117+
- Separately, the **Validate resources** check (in `ci.yml`) runs
118+
`npm run validate` on every org, including resources no check targets.
119+
It's offline and runs whether or not PR checks are turned on.
117120

118121
## 7. Make it required (after a burn-in)
119122

‎docs/guides/troubleshooting.md‎

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,3 +81,30 @@ falling through to a full deploy. Pass either:
8181
- a resource type — `npm run push -- my-org assistants`, or
8282
- a path — `npm run push -- my-org assistants/foo.yml` (short form)
8383
or `npm run push -- my-org resources/my-org/assistants/foo.yml` (long form).
84+
85+
## "Validate resources" fails in CI
86+
87+
The check runs `npm run validate` for every org under `resources/`. The
88+
job log names each failing org, and that org's log group lists every
89+
finding. To reproduce locally:
90+
91+
```bash
92+
VAPI_PRIVATE_API_KEY=validate-only npm run validate -- <org>
93+
```
94+
95+
`validate` never calls the API, but the engine won't start without a key, so
96+
a placeholder is enough. You don't need that org's real key or its
97+
`.env.<org>`.
98+
99+
Each error names the resource (`assistants/<id>`), field and rule. Plain `push` only warns about
100+
these errors, so a repository that has been deploying with `push` can carry
101+
some from before the check existed; they show up on the next pull request,
102+
whatever it changes. Fix them in that PR or a separate one first. `apply`
103+
refuses to deploy until they're fixed anyway.
104+
105+
A `Failed to import TypeScript resource … is not set` error from this check
106+
means a `.ts` resource reads a variable from `.env.<org>`, which CI doesn't
107+
have. Build `.ts` resources from files in the repository instead.
108+
109+
A folder under `resources/` that isn't a valid org name (lowercase letters,
110+
digits and hyphens) fails too. Rename it, or move it out of `resources/`.

‎docs/guides/workflows.md‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,21 @@ npm run validate -- <org>
1717
npm run apply -- <org>
1818
```
1919

20+
CI runs the same validator on every pull request, for every org under
21+
`resources/` (the **Validate resources** check in `.github/workflows/ci.yml`).
22+
It needs no secrets, so it runs on forks too. Make it a required check in
23+
branch protection, so a config that `apply` would refuse can't reach
24+
`main`, where it would block deploys and promotion.
25+
26+
Two things to know before you require it:
27+
28+
- It validates every org on every pull request, so one org with errors
29+
blocks every pull request in the repository, including those from teams
30+
that never touch that org.
31+
- If you merge through a GitHub merge queue, first add `merge_group:` to
32+
`ci.yml`'s `on:` triggers, or the required check never reports and the
33+
queue stalls.
34+
2035
To deploy only some resources, pass resource types or file paths. `apply`
2136
and `push` accept the same scoping:
2237

‎examples/starter/resources/starter/structuredOutputs/call-summary.yml‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
name: call-summary
22
type: ai
3+
assistant_ids:
4+
- receptionist
35
description: Summarizes the call for the front-desk log.
46
schema:
57
type: object

‎improvements.md‎

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ you which stack PR closes the row.**
8888
| 34 | No pre-merge simulation signal; simulations only tested what was deployed | A PR that breaks an agent merges green | #33 | RESOLVED 2026-10-01 |
8989
| 35 | A failed promotion pushed nothing, not even state | git lost track of resources already on the platform | None | RESOLVED 2026-10-01 |
9090
| 36 | `cleanup` deletes resources excluded by `.vapi-ignore` | A destructive cleanup can delete resources another team owns | None | RESOLVED 2026-10-03 |
91+
| 37 | Resource validation ran only at deploy time, after merge | A config `apply` refuses could merge and block deploys and promotion | #32 | RESOLVED 2026-10-03 (#76) |
9192

9293
**Active backlog after cleanup:** `#2`, `#6`, `#8`, `#12`, `#20`, `#24–#26`, `#31`, and the open remainder of `#27` (wiring the listing-completeness verdict into push/delete/audit, and moving `cleanup.ts` onto the shared pager). Resolved entries stay in this file as historical incident notes per the maintenance directive; stale superseded backlog rows are not duplicated.
9394

@@ -1933,6 +1934,58 @@ orphan; after it, only the orphan.
19331934

19341935
---
19351936

1937+
## 37. Resource validation ran only at deploy time, after merge
1938+
1939+
**[RESOLVED 2026-10-03] (#76)**
1940+
1941+
**Discovered:** 2026-10-03, while reviewing which static checks run before
1942+
the PR check's simulations.
1943+
1944+
### Problem
1945+
1946+
`npm run validate` fails on shapes the API rejects mid-push (a name over 40
1947+
characters, per-provider voice schema) and warns on silent inconsistencies
1948+
(structured-output lockstep, duplicated prompts, the `maxTokens` floor), but
1949+
nothing ran it before merge. A config that `apply` refuses could land on
1950+
`main`, and was found only when someone deployed or promoted it.
1951+
1952+
### Current behavior (Verified)
1953+
1954+
- `src/apply.ts` runs `validate` before every deploy and stops on errors.
1955+
Promotion deploys through `apply`, so it stops too, but only after the
1956+
change merged.
1957+
- `src/push.ts` runs the same validators but only warns unless `--strict`.
1958+
- `ci.yml` ran the build and tests only. `tests/examples.test.ts`
1959+
validates `examples/`, not `resources/<org>/`.
1960+
- The PR check's payload build (`npm run check -- --dry-run`) covers only
1961+
the resources its targets reach, and only in repos that turned PR checks on.
1962+
1963+
### Risk
1964+
1965+
A broken config merges green. Deploys and promotion out of `main` then stop
1966+
until a fix PR lands, or, with plain `push`, the push continues and fails
1967+
partway with an API 400.
1968+
1969+
### Current mitigation
1970+
1971+
None needed once the fix below lands.
1972+
1973+
### Possible fix (landed)
1974+
1975+
A **Validate resources** job in `.github/workflows/ci.yml` runs `validate`
1976+
for every folder under `resources/` on every pull request, reporting every
1977+
failing org rather than stopping at the first. `validate` makes no network
1978+
call, but loading the engine's config requires a key, so the step sets a
1979+
placeholder that is never sent; the job has no secrets, so it runs on forks.
1980+
No engine change. `tests/ci-validate-workflow.test.ts` runs the step itself
1981+
against fixture orgs.
1982+
1983+
### Status
1984+
1985+
**RESOLVED 2026-10-03.**
1986+
1987+
---
1988+
19361989
## Out of scope (intentionally not improvements)
19371990

19381991
- **State file is identity-only and not git-ignored.** It's intentionally

0 commit comments

Comments
 (0)