Change a repository's GitHub relationship
POST/api/repos/{owner}/{repo}/github-relationship
updateGithubRelationship
Moves the repository's relationship under a **compare-and-swap**: `expected_mode` and `expected_authority_epoch` must both match the stored row, or the request is refused with `409 relationship_conflict` and nothing is written. `expected_mode: null` with `expected_authority_epoch: 0` is how a caller says "this repository has no relationship yet" — which is a different precondition from a row that happens to sit at epoch 0. The demotion re-checks the precondition inside its own write, so a demotion racing another demotion is refused rather than lost. The arming edges check it against the row the request read — which is the whole guarantee this build makes, and enough of one, because `neosource_readonly_mirror` is the only relationship any writer in this build can establish, so an arm has nothing to race. Requires repository **admin**, and — for the arming edges only — a linked GitHub account with a stored access token, since that is the credential the metadata backfill and the recurring polls run on. The demotion never requires one: it is the kill switch, and a repository whose importer lost their GitHub connection is exactly the case that needs it. Omitting `expected_authority_epoch` is refused by body validation rather than read as an implicit `0`: a defaulted precondition is a compare-and-swap that silently is not one. A negative value is `400 invalid_input`. Three edges are always admitted, all of them landing on `neosource_readonly_mirror`: arming the relationship on a repository that has none, an idempotent re-arm (which does **not** move the epoch, leaves the existing importer in place, and doubles as the recovery surface for a sync parked in `error`), and the demotion of a `github_managed` repository — ADR 0115's "stop accepting writes immediately" kill switch, an edge that bumps `authority_epoch` and so invalidates any provider lease registered under the old one. A deployment that **offers** the `github_managed` relationship admits two more: the promotion of an armed `neosource_readonly_mirror` (epoch +1, and the edge that opens provider admission) and an idempotent re-assert of a repository already in it (which does **not** move the epoch, so an outstanding provider lease survives it). Both additionally require the repository to carry a GitHub App installation whose grant is neither revoked nor too narrow, and a `write_health` that is not `lfs_blocked` — a repository that fails one of those is refused with `409 relationship_conflict` and a message naming the state to fix. A deployment that does not offer the relationship refuses both edges with `409 relationship_unavailable`. Everything else is `409 relationship_unavailable`, which is **never** retryable: `neosource_managed` and `github_backup` belong to later phases, a repository with no relationship row cannot go straight to `github_managed` (arm it first), and a repository this deployment is already authoritative for never goes back to GitHub. No `Idempotency-Key`: the compare-and-swap is the idempotency, since a replay carries a now-stale epoch.
Requires authentication using a bearer token or a session cookie — see tokens and scopes.
curl
curl -X POST 'https://neosource.dev/api/repos/OWNER/REPO/github-relationship' \
-H 'Authorization: Bearer $NEOSOURCE_TOKEN' \
-H 'Content-Type: application/json' \
-d '{"expected_authority_epoch":0,"mode":"string"}'fetch
fetch("https://neosource.dev/api/repos/OWNER/REPO/github-relationship", {
method: "POST",
headers: {
Authorization: "Bearer $NEOSOURCE_TOKEN",
"Content-Type": "application/json",
},
body: JSON.stringify({"expected_authority_epoch":0,"mode":"string"}),
});Path parameters
ownerrequiredRepository owner or organization slug.
reporequiredRepository name.
Request bodyrequired
application/json
Responses
200The repository's relationship, after the change
application/json
Standard errors
Bodies documented once for the whole API — see standard errors.
- 400Bad Request — one of: invalid_input
- 401Authentication required
- 403Forbidden — one of: forbidden
- 404Not Found — one of: not_found
- 409Conflict — one of: conflict, relationship_conflict, relationship_unavailable
- 429Rate limited — retry after the `Retry-After` header
- 500Internal server error
- 503Service temporarily unavailable / at capacity — retry after the `Retry-After` header
- 504Gateway timeout — the request exceeded the server's handling budget