You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Commit fbbf64c
Browse filesBrowse the repository at this point in the historyBrowse files
feat(check): add the opt-in Vapi checks PR workflow and user docs (#64)
## Value
**V.A.L.U.E. tier:** project — PR 8 of 10 for inline simulation PR checks ([TEST-141](https://linear.app/vapi/issue/TEST-141/gitops-run-simulation-suites-against-pr-changes-inline-as-ci-checks)). This PR turns `npm run check` into a PR check and documents setup end to end.
- **Problem:** `npm run check` (PR 7) gives a verdict locally, but a reviewer needs it on the PR: a `Vapi Evals` status whose **Details** link opens the exact run (PAL-608's contract), run on every affected push, safe for forks and Dependabot.
- **Who it affects:** gitops users, single-org and CI-org alike, who get a PR check by adding one variable and one secret; and the maintainers of forks like Hoag's and Mudflap's, who hand-maintain shell workflows for this today (PAL-608).
- **What changes:**
- **`.github/workflows/vapi-checks.yml`** (opt-in):
- **Triggers:** `pull_request` (opened, synchronize, reopened, ready_for_review), plus `workflow_dispatch` with an optional `check` input. Never `pull_request_target`.
- **Opt-in:** it runs only when `vars.VAPI_CHECKS_ENABLED == 'true'`, and does nothing without a `vapi-checks.yml`, so on the upstream template it stays dormant.
- **Live vs dry run:**
- live only for same-repository, non-Dependabot PRs (checking both the actor and the PR author) and for dispatch;
- everything else gets a keyless dry run, and secrets are passed only to live runs.
- **Checkout:** the head SHA, with `fetch-depth: 0` so `--changed-since origin/<base>` diffs from the merge base, and `persist-credentials: false`.
- **Statuses:** `--all` posts the aggregate `Vapi Evals`; dispatching one named check never changes it.
- **Cancellation:** concurrency is keyed per PR with `cancel-in-progress`, and the run step `exec`s node, so GitHub's cancel reaches it and it cancels the superseded runs.
- **Time:** `--budget-minutes 22` under `timeout-minutes: 30`.
- **Permissions:** `contents: read` and `statuses: write`.
- **README "PR Checks":** the end-user guide, covering:
- test files with a judge example, and the check config;
- the dry run, plus the build-failure table (what fails and how to fix it);
- live runs, and turning on the workflow;
- what a PR shows, making `Vapi Evals` required, and fork/Dependabot unblocking;
- dedicated CI orgs;
- cost, and what stays real in the run org.
The project tree gains `vapi-checks.example.yml` and `check-cmd.ts`.
- **AGENTS.md:** the "Testing with Simulations" step 5 now points at `npm run sim` vs `npm run check`.
- **`docs/learnings/simulations.md` "Inline PR Checks":**
- the parity results and the two known differences (handoff names, tool order);
- mock behaviour, and hook-fired tools bypassing mocks;
- why servers are replaced rather than deleted;
- transfers;
- what still reaches real systems;
- the chat-mode limits;
- where run items keep tool results.
- **Indexes:** the learnings index row, and `improvements.md` #34 (RESOLVED). No `docs/changelog.md` edit.
## Evidence of value
- **Dormant on the template:** this PR adds the workflow, and `VapiAI/gitops` has no `VAPI_CHECKS_ENABLED` variable, so the `vapi-checks` job is **skipped** in this PR's own checks ([run 36941407785](https://github.com/VapiAI/gitops/actions/runs/36941407785): `completed / skipped`). That also shows GitHub parsed the workflow and evaluated its `if:`. Customers who haven't opted in see no change and no spend.
- **The command the workflow runs** (`--all`, `--changed-since`, live and dry run, statuses, job summary, JSON) is covered end to end by PR 7's tests against a stub of the simulations and GitHub APIs. It was also run live in the owner's test org: innocuous exit 0, degraded exit 1, no resources created, every tool result a mock.
- `npm test`: 474 passing (docs and workflow only; no code change).
## Testing plan
- The workflow YAML parses. `actionlint` isn't available in this environment, so it isn't linted. To check by hand: `if:` expressions, the `secrets` ternaries in step `env`, and the `exec` line.
- **Not yet tested — needs the owner,** because it needs a repository secret holding an API key:
1. Create a private dogfood repo from this branch, with `resources/<test-org>/` holding the parity squad from `tests/fixtures/check-parity/resources/parity/`, renamed, and that fixture's `vapi-checks.yml`.
2. Add the `VAPI_PRIVATE_API_KEY` secret and `VAPI_CHECKS_ENABLED=true`.
3. Open a degraded-prompt PR and an innocuous one. Expect red and green `Vapi Evals` statuses whose **Details** links land on the runs, plus the job summaries (screenshots to add here).
4. Open a PR from a fork. Expect a dry run and no status, because the token is read-only.
5. Push a `package.json` change as Dependabot would. Expect `Vapi Evals` = `error`.
6. Push twice quickly. Expect the first run canceled (`itemCounts.canceled > 0`).
Stacked on #63.
Refs TEST-141
🤖 Generated with [Claude Code](https://claude.com/claude-code)
5. Run against the deployed resources with `npm run sim`, or against the local files (nothing deployed) with `npm run check` — see "PR Checks" in the README. The PR workflow runs `npm run check` on every affected PR when `VAPI_CHECKS_ENABLED=true`
`tmp/check-payloads/`shows exactly what would be sent. The build fails,
521
+
naming the field, when it can't make a safe, faithful payload:
522
+
523
+
| Problem | Fix |
524
+
| --- | --- |
525
+
| A tool that can't be mocked (SMS, MCP, code, integrations), or an `apiRequest` with no `name` | Remove it, name the `apiRequest`, or set `toolMocks: off` with a dedicated CI org |
526
+
| A tool referenced by ID, or a `toolRefs` pin, with no file in `resources/<org>/tools/` | Pull the tool into gitops |
527
+
| A handoff leaving the squad (`dynamic`, another squad, a non-member), or an assistant target that hands off | Make the target a squad of those assistants |
528
+
| A legacy `assistantDestinations` entry naming an assistant by ID | Convert it to a handoff tool |
529
+
| Tools by ID inside `assistantOverrides`, `membersOverrides` or `targetOverrides` | Put them inline in `tools:append` |
530
+
| Tools outside `model.tools` / `model.toolIds` (`model.functions`, reasoner skills, a recording-consent decline tool) | Move them to `model.tools` |
531
+
| `model.knowledgeBaseId`, a custom knowledge base, or a knowledge base / `query` tool when the check runs in another org | Use a knowledge-base tool in the same org |
532
+
| Personality tools beyond `endCall`-style ones | Keep the personality free of side-effect tools |
533
+
| Two tools with the same type and name on one assistant | Rename one |
534
+
| Audio judges, scenario hooks, or no required text judge over chat | Add a text judge, or use `transport: voice` |
535
+
536
+
Two things to know:
537
+
538
+
- **Transfers never happen.** Every `transferCall` becomes a mocked
539
+
function, so a scenario that needs a real transfer fails rather than
540
+
falsely passing.
541
+
- **Handoff names.** The check warns when a prompt mentions an
542
+
auto-generated `handoff_to_…` name — those differ between inline and
543
+
deployed assistants. Give that handoff an explicit `function.name`.
544
+
545
+
### 4. Live run locally
546
+
547
+
```bash
548
+
npm run check -- core
549
+
```
550
+
551
+
Uses `.env.<org>`, prints the run link, and exits 0 passed, 1 failed, 2
552
+
config or build error, 3 incomplete. It uses simulation minutes.
553
+
554
+
### 5. Turn on the PR workflow
555
+
556
+
In GitHub → Settings → Secrets and variables → Actions:
557
+
558
+
- Secret `VAPI_PRIVATE_API_KEY` (single org), or `VAPI_CHECK_TOKENS` =
559
+
`{"my-org":"<private key>"}`(several orgs, or a CI org).
560
+
- Variable `VAPI_CHECKS_ENABLED=true`.
561
+
562
+
`.github/workflows/vapi-checks.yml`then runs every affected check on each
563
+
PR push. It asks for `statuses: write` only to post the direct links.
564
+
565
+
### 6. What a PR shows
566
+
567
+
- `Vapi Evals`, plus `Vapi Evals / <check> / <target>` per target. **Details**
568
+
opens the run in Vapi.
569
+
- A job summary: per-target result, failing judges with expected vs actual,
570
+
and any "unmocked tool called" notices. No PR comments.
571
+
- PRs that touch neither a check's org, its state, `vapi-checks.yml`,
572
+
`promotion.yml`, the engine (`src/**`, `package*.json`), nor the check's
573
+
own `paths` skip it, and `Vapi Evals` posts success.
574
+
- A newer push cancels the older run.
575
+
576
+
### 7. Make it required (after a burn-in)
577
+
578
+
Require the **commit status `Vapi Evals`** in branch protection — not the
579
+
`vapi-checks`job (fork dry runs succeed) and not the per-target statuses
580
+
(PRs that don't touch a check never get them).
581
+
582
+
- **Fork PRs** run a dry run without secrets and can't post statuses (the
583
+
token is read-only), so a required `Vapi Evals` blocks them.
584
+
- **Dependabot PRs** that change `package*.json` post `error`.
585
+
- **To unblock either**, a maintainer runs Actions → Vapi checks → Run
586
+
workflow on the PR's branch with `check` blank (push a fork's branch into
587
+
the repository first). A later PR event on the same commit resets the
588
+
status, so dispatch again after that. Running one named check by hand
589
+
never changes `Vapi Evals`.
590
+
591
+
### Dedicated CI org (optional; recommended with `toolMocks: off`)
592
+
593
+
1. `npm run setup -- my-ci-org --resources none`.
594
+
2. Create the credentials your agents need there, with the same names as
595
+
production.
596
+
3. `npm run pull -- my-ci-org --bootstrap --bindings-only`, then commit
597
+
`.vapi-state.my-ci-org.json`so the state knows those credentials. The PR
598
+
workflow refreshes bindings on every live run.
599
+
4. Add `runOrg: my-ci-org` (and `baseUrl` for EU) to the check, and
600
+
optionally `bindings:` (same shape as `promotion.yml`). Phone numbers are
601
+
omitted by default.
602
+
5. Put only the CI org's key in `VAPI_CHECK_TOKENS`.
603
+
604
+
### Cost and safety
605
+
606
+
- Every affected push starts paid runs; chat transport is the default.
607
+
- Tool calls get their scenario mock or an error, and every assistant and
608
+
function-tool server points at `https://vapi-gitops-ci.invalid`.
609
+
- Still real in the run org: custom LLM, voice and transcriber servers see
610
+
the conversation; org-wide and assistant monitors run; `observabilityPlan`
611
+
exports transcripts; prompts are stored with the run. A CI org avoids all
612
+
of these.
613
+
- `.ts`resource files execute during the check, with the workflow's secrets
614
+
on same-repository PRs.
615
+
616
+
---
617
+
459
618
## How to Use This Repo
460
619
461
620
1. **Run `npm run setup`** to configure your first org (or `npm run setup -- <org>` without a terminal)
|[voice-providers.md](voice-providers.md)| Per-provider voice block layout (Cartesia vs 11labs vs OpenAI/Azure/Rime/LMNT/Minimax/Neuphonic/SmallestAI) — saves 400s at push time |
0 commit comments