Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions .github/workflows/promotion.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,15 @@ on:
description: Destination org (requires a source org)
required: false
type: string
rollout:
description: >-
Gradual rollout orgs: default ramps; no-rollout publishes at once
(reverts); override pins over a split set outside gitops (needs
pipeline, from and to)
required: false
default: default
type: choice
options: [default, no-rollout, override]

permissions:
contents: write
Expand All @@ -40,6 +49,7 @@ jobs:
PIPELINE: ${{ inputs.pipeline }}
SOURCE_ORG: ${{ inputs.from }}
TARGET_ORG: ${{ inputs.to }}
ROLLOUT: ${{ inputs.rollout }}

steps:
- uses: actions/checkout@v4
Expand Down Expand Up @@ -84,6 +94,21 @@ jobs:
exit 1
}
args=(--apply)
case "${ROLLOUT:-default}" in
default) ;;
no-rollout) args+=(--no-rollout) ;;
override)
[[ "$supplied" == 3 ]] || {
echo "rollout: override needs pipeline, from and to."
exit 1
}
args+=(--rollout-override)
;;
*)
echo "Unknown rollout input: $ROLLOUT"
exit 1
;;
esac
if [[ "$supplied" == 3 ]]; then
args+=(--pipeline "$PIPELINE")
args+=(--from "$SOURCE_ORG" --to "$TARGET_ORG")
Expand Down
69 changes: 69 additions & 0 deletions .github/workflows/rollout.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
name: Advance gradual rollouts

# Moves each gradual rollout (orgs with rollout: in promotion.yml) one step
# once its interval has passed. The ramp's state lives in each assistant's
# traffic allocation, so this job commits nothing. See
# docs/guides/promotion.md#gradual-rollout-optional.
on:
schedule:
- cron: "*/15 * * * *"
workflow_dispatch:
inputs:
action:
description: advance, abort or status
required: false
default: advance
type: choice
options: [advance, abort, status]
org:
description: Only this org (required for abort)
required: false
type: string
assistant:
description: Only this assistant ID (e.g. support-agent)
required: false
type: string

permissions:
contents: read

# Its own group, never vapi-promotion: GitHub keeps one pending run per
# group, so a tick queued there would cancel a waiting promotion. An abort
# gets a group to itself, so it is never queued behind a tick.
concurrency:
group: ${{ inputs.action == 'abort' && format('vapi-rollout-abort-{0}', github.run_id) || 'vapi-rollout' }}
cancel-in-progress: false

jobs:
rollout:
# Scheduled ticks need the opt-in variable; a manual run (an abort, most
# of all) always works.
if: >-
github.event_name == 'workflow_dispatch' ||
vars.VAPI_ROLLOUT_ENABLED == 'true'
runs-on: ubuntu-latest
timeout-minutes: 15
env:
VAPI_PROMOTION_TOKENS: ${{ secrets.VAPI_PROMOTION_TOKENS }}
ACTION: ${{ inputs.action }}
ORG: ${{ inputs.org }}
ASSISTANT: ${{ inputs.assistant }}

steps:
- uses: actions/checkout@v4

- uses: actions/setup-node@v4
with:
node-version: 22
cache: npm

- run: npm ci

- name: Run the rollout
shell: bash
run: |
set -euo pipefail
args=("${ACTION:-advance}")
if [[ -n "${ORG:-}" ]]; then args+=(--org "$ORG"); fi
if [[ -n "${ASSISTANT:-}" ]]; then args+=(--assistant "$ASSISTANT"); fi
npm run rollout -- "${args[@]}"
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,7 @@ More detail: [How the engine works](docs/guides/how-it-works.md).
| `npm run sim -- <org>` | Run a simulation suite against deployed resources |
| `npm run check` | Run PR simulation checks against local files |
| `npm run promote` | Promote resources from one org to the next |
| `npm run rollout -- advance\|abort\|status` | Step, abort or list gradual rollouts |

`setup`, `apply`, `pull`, `push`, `cleanup` and `call` prompt interactively
when run without arguments. Full flags and examples:
Expand Down
1 change: 1 addition & 0 deletions docs/guides/commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ The other commands are direct only.
| `npm run call` | `npm run call -- <org> -a <assistant>` or `-s <squad>` | Talk to an assistant or squad from your terminal. |
| `npm run sim` | `npm run sim -- <org> --suite <name> --target <name>` | Run a simulation suite against deployed resources. Exits 0 passed, 1 failed, 3 incomplete. |
| `npm run check` | `npm run check -- <check>` or `--all`, `[--dry-run]` | Run [PR checks](pr-checks.md) against local files, nothing deployed. Exits 0 passed, 1 failed, 2 config or build error, 3 incomplete. |
| `npm run rollout` | `npm run rollout -- advance\|abort\|status [--org <org>] [--assistant <id>]` | Move [gradual rollouts](promotion.md#gradual-rollout-optional) a step, abort one (needs `--org`), or list them. The rollout workflow runs `advance` every 15 minutes. |
| `npm run promote` | `npm run promote -- --pipeline <p> --from <org> --to <org> [--apply] [--no-rollout \| --rollout-override]` | Plan, or apply, a [promotion](promotion.md) between orgs. Into an org with a [gradual rollout](promotion.md#gradual-rollout-optional), `--no-rollout` publishes at once and `--rollout-override` pins over a split set outside gitops. |
| `npm run build` | `npm run build` | Type-check the code and tests. |
| `npm test` | `npm test` | Run the test suite. |
Expand Down
5 changes: 5 additions & 0 deletions docs/guides/how-it-works.md
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,11 @@ a random UUID made once per command you run and shared by every process it
starts, so Vapi can group one run's requests. It identifies nothing about you
or your machine.

A [gradual rollout](promotion.md#gradual-rollout-optional)'s traffic-allocation
writes each name their own command (`rollout-pin`, `rollout-step`,
`rollout-complete`, `rollout-hold`, `rollout-abort`, `rollout-release`,
`rollout-restore`), so Vapi can count ramps by step, completion and abort.

Vapi uses these headers to count how the tool is used. Nothing else is sent beyond the
requests themselves; there is no separate telemetry.

Expand Down
47 changes: 44 additions & 3 deletions docs/guides/promotion.md
Original file line number Diff line number Diff line change
Expand Up @@ -185,9 +185,11 @@ The rollout workflow then moves the ramp one step at a time (see below).
Every write carries a `gitops-rollout …` note in the allocation's
description, which is the ramp's only state; the traffic tab shows it.

Not everything is split. A new assistant goes live when it's created; squads,
tools and structured outputs aren't versioned per call, so a change to them
reaches 100% of calls on apply. The plan output lists each of these.
Not everything is split. A new assistant goes live when it's created. Only
calls placed directly to an assistant follow its split; calls through a squad
don't. Tools, structured outputs and squads aren't versioned per call, so a
change to them reaches 100% of calls on apply. The plan output lists each of
these.

The promotion refuses, before the gate and with nothing written, when an
assistant has a split someone set by hand (a manual split wins) or is managed
Expand All @@ -200,6 +202,45 @@ for a revert: it also releases an assistant gitops was holding, so the
reverted version takes every call. An assistant whose files didn't change,
or a split set by hand, is left as it is.

### The rollout workflow

`.github/workflows/rollout.yml` runs `npm run rollout -- advance` every 15
minutes. For each assistant in a rollout org, it reads the newest traffic
allocation and, once `intervalMinutes` have passed since it was written,
moves to the next step. The last step is an explicit 100% on the new version;
the tick after that hands the assistant back to the default (every call to
the newest version). If someone published a newer version during the ramp,
that hand-back would send it every call un-ramped, so the ramp holds on the
ramped version instead and says so; promote again to ramp the newer one.

To turn the schedule on, set the repository variable
`VAPI_ROLLOUT_ENABLED=true`. It uses the same `VAPI_PROMOTION_TOKENS` secret
and commits nothing. Run it by hand from the Actions tab:

| `action` | What it does |
| --- | --- |
| `advance` | One step for every ramp whose interval has passed |
| `abort` | Puts the ramp back on its old version at 100% (needs `org`; `assistant` narrows it). Runs even with the schedule off, and never waits behind a tick |
| `status` | Lists ramps and anything waiting for a human, writes nothing |

An aborted assistant stays on its old version, and later publishes land at
0%, until the next promotion (which ramps again) or a `no-rollout` one.

The ramp stops, and `status` flags it, when the split changes under it (a
dashboard edit, say): it never overwrites someone else's change. A pin left
by a promotion that was killed mid-run is picked up after 6 hours: carried on
from step 1 if the run had published, released if it had replaced the
default, otherwise left for you. The 6 hours assume the promotion step's
330-minute limit in CI; a promotion run on your machine must finish within
them.

Keep in mind:

- GitHub runs scheduled workflows from the default branch only, may start
them minutes late, and disables them after 60 days without activity in a
public repository. Intervals are minimums.
- Each tick makes about two API reads per assistant in each rollout org.

## Rolling Back a Promotion

Treat a promotion rollback as a new, auditable Git change: revert the source
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
"check": "tsx src/check-cmd.ts",
"rollback": "tsx src/rollback-cmd.ts",
"promote": "tsx src/promote-cmd.ts",
"rollout": "tsx src/rollout-cmd.ts",
"build": "tsc --noEmit",
"test": "node --import tsx --import ./tests/no-vapi-api.ts --test tests/*.test.ts"
},
Expand Down
Loading
Loading