For Divvent’s own team. It is generated from the operations document in our repository, which is the one to edit. If you use Divvent, the help centre is for you.
6d. The dispatch App: a deploy that starts in seconds — BY HAND
A customer's Deploy writes a request and puts a message on the deploys queue. Its consumer, the dispatcher Worker, mints a GitHub App installation token and sends one workflow_dispatch of deploys.yml, so a runner starts in seconds instead of whenever GitHub's schedule next fires (DESIGN.md §10, decided 2026-09-23). Until the three secrets below exist it answers not configured, and the schedule claims every request exactly as before; the deploy log says unset DIVVENT_DISPATCH_APP_ID, … on every run so that state is never silent.
By hand because an App is created in GitHub's settings, and its key is shown once. **A second App, not divvent-agent**: the agent App's token is a platform login at the console, and the whole argument for putting a key on a Worker is that this one can start the deploy workflow and nothing else.
Never grant this App anything but Actions. Two Apps are installed on this repository, and an installation token cannot say which App minted it — so the console's agent exchange (§6b) tells them apart by what they can do: it admits an installation as the agent only if its token can read the repository's contents, which divvent-agent can (Contents: read) and this App must never. Tick a second permission on this App's settings page and its key could sign in at the staging console as the agent. The dispatcher checks this on every batch with its own App's unscoped token and refuses to dispatch if the App holds anything but Actions or can read a file; the console's Logs page then shows a dispatcher record, DispatchOverPrivileged, and deploys wait for the schedule until the permission is removed. docs/SECURITY.md §13 is the boundary.
**Keep divvent-hq/divvent private.** The fence is "can this token read a file", and in a public repository every token can — so the console would admit the dispatch App, or any App installed on the repository, as the agent. If it is ever made public the dispatcher refuses every batch and the Logs page shows fence public; the fix is the repository's visibility, not the App.
- Create the App. GitHub →
divvent-hq→ Settings → Developer settings → GitHub Apps → New GitHub App.- Name:
divvent-dispatch. Homepage URL:https://divvent.co.uk. - Webhook: untick Active. It receives nothing.
- Repository permissions: Actions → Read and write, and nothing else — above all not Contents, which is what makes an App the agent at the console. GitHub adds Metadata → Read-only itself and will not let it be removed. No organization permissions, no account permissions, no events.
- Where can this GitHub App be installed: Only on this account.
- Name:
- Take its id and a key. On the App's General page, copy the App ID. Under Private keys, Generate a private key: a
.pemdownloads, and it is the only copy. - **Install it on
divvent-hq/divventonly.** The App's page → Install App →divvent-hq→ Only select repositories →divvent. The installation id is the number at the end of the page you land on,github.com/organizations/divvent-hq/settings/installations/<id>. It is not the App ID; swapping the two is a 404 the Worker names. - **Put the three in the
stagingandproductionenvironments.** From the directory holding the.pem, withghsigned in todivvent-hq:gh secret set DIVVENT_DISPATCH_APP_ID --env staging gh secret set DIVVENT_DISPATCH_INSTALLATION_ID --env staging Get-Content -Raw .\divvent-dispatch.*.private-key.pem | gh secret set DIVVENT_DISPATCH_PRIVATE_KEY --env staging gh secret set DIVVENT_DISPATCH_APP_ID --env production gh secret set DIVVENT_DISPATCH_INSTALLATION_ID --env production Get-Content -Raw .\divvent-dispatch.*.private-key.pem | gh secret set DIVVENT_DISPATCH_PRIVATE_KEY --env productionThe first two prompt for the value. The key goes in on stdin, whole, both marker lines included, and never through a terminal's scrollback or a command's arguments — no
divventcommand takes any of the three. Then delete the.pem; GitHub keeps the public half, and a new key is one click.Production holds them and does not use them yet.
production.ymlpromotes bydeploy --prebuilt, which does not route a Worker's own secrets, and production declares noDIVVENT_REPOSITORY— so its dispatcher answersnot configuredwhatever is set, which is right whiledeploys.ymlruns--env stagingonly. They are there so production needs no second visit the day it gets a deploy pass of its own. - **Let the next push to
mainput them on the Worker.**staging.yml's Deploy the control plane step is the only step that carries them, and the deployer puts them ondivvent-staging-dispatcherand on no other Worker. Theunsetline disappears from that step's log. Two of the three set is refused before anything is created, naming the missing one. - Watch one work. Press Deploy in the portal, or ask what is waiting:
divvent deploys --env staging --runner by-hand --listA
deploysrun with the event workflow_dispatch should appear under Actions within seconds. If it does not, the console's Logs page has adispatcherrecord.DispatchOverPrivileged(fence permissionsorfence contents) is the App holding more than Actions: remove it on the App's settings page.GitHubRefusedcarries the step and GitHub's status —mint 401is the App ID or the key,mint 404the installation id,mint 422the App is missing Actions write or is not installed ondivvent,dispatch 404the workflow. The Worker's own log has the sentence. The schedule claims the request either way. - Register it. Console → Credentials:
DIVVENT_DISPATCH_PRIVATE_KEY, kindgithub, ownerdivvent-hq, rotation in 90 days. Rotating is generating a new key on the App's page,gh secret setagain, and running the environment's deploy:control-plane deploywrites every secret it is given on every run, so the new key is ondivvent-staging-dispatcherwhen it finishes (it used to put a secret only where the Worker had none, and this step said to paste the key into the dashboard by hand; that is no longer needed, and a key pasted by hand is replaced by the environment's at the next deploy). Then delete the old key on the App's page; generating one does not revoke the other.
- **Restrict the environments to
main(andv*).** Settings → Environments →staging→ Deployment branches and tags → Selected branches and tags:main, and tags matchingv*for the release's recovery drill (§9.4).productionis alreadyv*only (§9.3). This is the half of the fence the workflows cannot hold: the App'sactions: writecan dispatch any workflow on any branch that exists, and a branch's copy of a workflow need not carry thedivvent-dispatch[bot]guard thatmain's do. With the rule, such a run gets none of the environment's secrets.
**What actions: write reaches.** Not only deploys.yml: the App can dispatch or re-run any workflow in the repository, cancel runs and delete their logs and artifacts. Every workflow but deploys.yml therefore skips any job whose github.triggering_actor is divvent-dispatch[bot] — a run the App started or re-ran shows every job skipped — and workflow-stages.test.ts fails a workflow that loses the guard. Rename the App and that login changes: change DISPATCH_APP_SLUG in packages/control/src/github-app.ts and every guard in the same commit, which the test will insist on.
To turn it off, uninstall the App: every mint is then a 404, the Worker records it once per batch, and the schedule is the floor again.