Broker API¶
The broker is the GitHub App backend the action calls when auth-mode is
public-app. Four routes, all on the same origin. Anything else returns
404 not_found.
Base URL for the hosted broker:
https://api.diatreme.magmamoose.com
https://broker-diatreme.magmamoose.com serves the same application and is the
fallback the action tries when the primary is unreachable. See
Deployment for which infrastructure answers each
name.
Every response is JSON. Every failure carries a stable error string, and
/token verification failures also carry a coarse reason. Neither is
localised and neither changes shape between versions, so you can match on them.
POST /token¶
Exchange a GitHub Actions OIDC token for a short-lived GitHub App installation
token. This is the route auth-mode: public-app uses on every release.
Auth: the OIDC token in the body. The calling job needs id-token: write.
There is no API key.
Request¶
Content-Type: application/json
| Field | Type | Required | Effect |
|---|---|---|---|
oidcToken |
string | yes | The Actions OIDC JWT, minted for the oidc-audience the broker accepts. |
owner |
string | yes | Repository owner. Must match the token's repository claim. |
repo |
string | yes | Repository name. Must match the token's repository claim. |
ref |
string | no | Recorded for logging only. Never authorises anything. |
runId |
string | no | Recorded for logging only. |
sha |
string | no | Recorded for logging only. |
curl -sS -X POST https://api.diatreme.magmamoose.com/token \
-H 'Content-Type: application/json' \
--data '{"oidcToken":"<jwt>","owner":"MagmaMoose","repo":"diatreme"}'
Success¶
200 OK
{
"token": "ghs_xxxxxxxxxxxxxxxxxxxx",
"expires_at": "2026-08-20T13:00:00Z",
"repository": "MagmaMoose/diatreme"
}
The token is a GitHub App installation token scoped to that one repository. Its
permissions default to contents: write and pull_requests: write, overridable
per deployment with TOKEN_PERMISSIONS.
GitHub sets the lifetime, currently one hour, and the broker passes
expires_at through unchanged rather than computing it.
The action masks the token in the run log and writes it to the step outputs
token, expires-at and repository.
Failures¶
| Status | error |
When |
|---|---|---|
| 400 | invalid_json |
Body isn't parseable JSON. |
| 400 | invalid_request |
Body parsed but isn't a JSON object. |
| 400 | missing_required_fields |
One of oidcToken, owner, repo is absent or empty. |
| 400 | invalid_token_permissions |
The deployment's TOKEN_PERMISSIONS is set but parses to nothing usable. A broker misconfiguration, not a caller error. |
| 401 | invalid_oidc_token |
The token failed verification. Carries a reason. |
| 403 | repo_mismatch |
The token's repository claim isn't owner/repo. |
| 403 | repo_not_allowed |
The repository is outside the deployment's ALLOWED_REPOSITORIES. |
| 404 | app_not_installed |
The Diatreme App isn't installed on that repository. |
| 405 | method_not_allowed |
Anything other than POST. |
| 500 | github_installation_lookup_failed |
GitHub answered the installation lookup with something unusable. |
| 503 | oidc_key_fetch_failed |
The broker couldn't retrieve the issuer's key set. Retryable. Carries reason: jwks_unavailable. |
The split between 401 and 503 is deliberate and load-bearing. A 401 means the
broker reached GitHub's key set and your token failed against it. A 503 means
the broker never got a verdict. Retrying a 401 will not help. Retrying a 503
often will. Every reason value and its fix is in
Errors.
POST /sign¶
Create a GitHub-signed, App-attributed commit on a branch through
createCommitOnBranch. Diatreme uses it so version bumps and release commits
show as verified and attributed to the App rather than to a person.
Auth: Authorization: Bearer <PROCESS_TRIGGER_SECRET>. Returns
503 sign_disabled when that secret isn't configured, so a deployment that
doesn't set it simply has no signer.
Request¶
| Field | Type | Required | Effect |
|---|---|---|---|
repo |
string | yes | owner/name. |
branch |
string | yes | Branch to commit on. |
expected_head_oid |
string | yes | The commit SHA the branch must currently point at. GitHub rejects the write if it moved, which is what makes this safe to retry. |
message.headline |
string | yes | Commit subject. |
message.body |
string | no | Commit body. |
additions |
array | one of the two | [{ "path": "...", "contents": "<base64>" }]. |
deletions |
array | one of the two | [{ "path": "..." }]. |
user |
string | no | Accepted and ignored. Commits are App-attributed. |
At least one of additions or deletions must be non-empty.
Success¶
200 OK with { "ok": true, "commit": { ... } }, carrying GitHub's commit
object.
Failures¶
| Status | error |
When |
|---|---|---|
| 400 | invalid_json / invalid_request |
Malformed body. |
| 400 | missing_required_fields |
repo, branch, expected_head_oid or message.headline absent. |
| 400 | no_file_changes |
Both additions and deletions are empty. |
| 400 | invalid_repo |
repo isn't owner/name. |
| 401 | unauthorized |
Bearer missing or wrong. Compared in constant time. |
| 405 | method_not_allowed |
Anything other than POST. |
| 502 | installation_token_failed |
The broker couldn't mint a token for that repo. |
| 502 | sign_failed |
GitHub rejected the commit, commonly because expected_head_oid is stale. |
| 503 | sign_disabled |
PROCESS_TRIGGER_SECRET isn't set on this deployment. |
GET /releases¶
Latest release per repository, aggregated across the App's installations. It backs the private dashboard, not the action.
Auth: Authorization: Bearer <PROCESS_TRIGGER_SECRET>.
Success¶
200 OK
{
"generated_at": "2026-08-20T09:00:00.000Z",
"repos": [
{
"repo": "MagmaMoose/diatreme",
"latest": {
"tag": "v2.4.6",
"name": "v2.4.6",
"published_at": "2026-08-20T01:15:00Z",
"url": "https://github.com/MagmaMoose/diatreme/releases/tag/v2.4.6",
"draft": false,
"prerelease": false
}
}
],
"truncated": false,
"cached": true
}
latest is null for a repository with no releases. cached: true means the
response came from the KV cache rather than a fresh crawl. truncated: true
means the aggregate hit a traversal cap, so the list is incomplete. The caps are
in Limits. They are always reported, never
applied silently.
Failures¶
| Status | error |
When |
|---|---|---|
| 401 | unauthorized |
Bearer missing or wrong. |
| 405 | method_not_allowed |
Anything other than GET. |
| 503 | releases_disabled |
PROCESS_TRIGGER_SECRET isn't set. |
| 503 | app_unconfigured |
GITHUB_APP_ID or GITHUB_APP_PRIVATE_KEY isn't set. |
POST /webhook¶
Receiver for the Diatreme App's webhook deliveries. Only push is acted on: it
fast-forwards every open pull request targeting the branch that was pushed, via
GitHub's update-branch API. Every other event is acknowledged with
{ "ok": true, "ignored": "<event>" } so GitHub stops retrying it.
Auth: HMAC-SHA256 over the raw body in X-Hub-Signature-256, checked
against GITHUB_WEBHOOK_SECRET.
The behaviour is opt-in twice over. Without GITHUB_WEBHOOK_SECRET the route
returns 503 webhook_disabled. With the secret but without
AUTO_UPDATE_BRANCHES set to a true value, deliveries verify and then do
nothing.
| Status | error |
When |
|---|---|---|
| 400 | invalid_json / invalid_request |
Malformed body. |
| 401 | invalid_signature |
HMAC didn't match. |
| 405 | method_not_allowed |
Anything other than POST. |
| 503 | webhook_disabled |
GITHUB_WEBHOOK_SECRET isn't set. |
The hosted App doesn't need this configured
Auto-update is off on the hosted deployment. If you self-host and want it,
point the App's webhook at <your-broker>/webhook and set both secrets.
Related¶
- Configuration for every variable named above.
- Errors for what to do about each failure.
- Limits for caps, TTLs and lifetimes.