Docs
Search docs

Get a repository's GitHub relationship

GET/api/repos/{owner}/{repo}/github-relationship

getGithubRelationship

Which forge owns this repository's refs and provider-shaped data, the upstream it is tied to, its authority generation, and the health/admission posture of the direct-write path — plus the latest metadata backfill's issue counters. `404` only when the repository has **no** relationship row at all. A row this deployment is authoritative for (`neosource_managed`, `github_backup`) is reported here, deliberately: it is the relationship's history, and it is what explains why a once-imported repository cannot be re-linked to GitHub. The older `GET …/github-sync` answers a narrower question — "is there a running metadata sync?" — and 404s those rows. `authority_epoch` is the value a subsequent `POST` must echo back as `expected_authority_epoch`. `metadata_mode` says whether GitHub's issues are pulled (`off` = an attached repository that keeps its own), `authority_handoff` how a `neosource_managed` row left GitHub (`detached` or `cut_over`). `blockers` lists everything that currently stops GitHub sync, each with a stable `fix` code and, where the fix is made elsewhere, a `fix_url`; `attach` is the attach in flight, else the latest one.

Authentication optional — this operation also accepts anonymous requests. When authenticating, use a bearer token or a session cookie.

curl

curl -X GET 'https://neosource.dev/api/repos/OWNER/REPO/github-relationship' \
  -H 'Authorization: Bearer $NEOSOURCE_TOKEN'

fetch

fetch("https://neosource.dev/api/repos/OWNER/REPO/github-relationship", {
  method: "GET",
  headers: {
    Authorization: "Bearer $NEOSOURCE_TOKEN",
  },
});

Path parameters

ownerrequired

Repository owner or organization slug.

string

reporequired

Repository name.

string

Responses

200The repository's GitHub relationship

application/json

GithubRelationshipResponse

object

A repository's GitHub relationship (ADR 0115 Phase 4a) — the response body of both `GET` and `POST /api/repos/{owner}/{repo}/github-relationship`. Every field comes from a column `GithubRepoSyncRecord` already selects (or, for `github_is_authority` and `upstream_url`, is derived from one), plus the one `github_meta_jobs` lookup the legacy status route already makes: no new store read, and no field that is structurally always `None`. `issues_done` / `issues_total` are carried deliberately even though they are backfill progress rather than relationship state. With them this response **strictly supersedes** [`GithubSyncStatusResponse`], which is the precondition for Phase 4c being a deletion of the legacy pair rather than a redesign of this one. `git_last_error` keeps the pull's own error text (#1092 documents the `github_sync_would_discard_refs` refusal it carries). Reserved to later phases and deliberately ABSENT — each would be a field that is always its default until the phase that fills it lands, and a client cannot tell "not shipped" from "nothing to report": `paused_refs` (8), the LFS digest / checked-at / scanner version (5 — `write_health` already carries that verdict), `native_actions_mode` (7), `cutover_state` (9), and the App's permissions and health, which `GET …/github-installation` already serves — one statement per fact.

attach
one of
  • GithubAttachSummary

    object

    The latest attach, as the relationship read summarises it.

    created_atrequired

    integerint64

    Millis since the Unix epoch.

    finished_at

    integer | nullint64

    When the run reached a terminal state; `null` while in flight.

    last_error

    string | null

    originrequired
    GithubAttachOrigin
    run_idrequired

    string

  • null

authority_epochrequired

integerint64

Authority generation. The `expected_authority_epoch` a subsequent `POST` must echo back, and what invalidates a provider projection lease registered under an older one.

authority_handoff
one of
  • GithubAuthorityHandoff

    string

    How a `neosource_managed` relationship left GitHub's authority — the `github_repo_sync.authority_handoff` column. NULL on every other mode (a CHECK), and on `neosource_managed` rows that predate the column.

    "detached""cut_over"

  • null

blockersrequired

array

Everything that currently stops GitHub sync for this repository, in a stable order, each with the fix the panel renders. Empty means nothing at the repository level stops it.

items

GithubSyncBlockerRow

One thing that stops GitHub sync: the blocker itself — its `kind` plus whatever detail that kind carries (`outcome`, `health`, `refname`, `missing_events`) — the stable `fix` code the panel renders, and a link to where it is fixed when there is one.

all of
  • GithubSyncBlocker
  • object

    fixrequired
    GithubSyncFix
    fix_url

    string | null

    Where the fix is made: the GitHub App's install page for the App-side fixes, the GitHub repository's settings for an archived repository. `null` otherwise, and when this deployment has no App.

full_namerequired

string

GitHub `owner/name` at last observation.

git_consecutive_failures

integer | nullint32

Ticks that have failed in a row; `0` after any success. At `GITHUB_SYNC_MAX_CONSECUTIVE_FAILURES` (10) the row is **parked**: the poller's claim no longer returns it, `git_next_run_at` keeps its (past) timestamp, and only a re-arm — the relationship edge, the metadata-sync toggle, the import-complete hook — resets the budget. Below that it is lagging, not parked: the next attempt is at `git_next_run_at`, on the shared exponential backoff. `null` when the relationship carries no pull-side status.

git_last_error

string | null

Why the most recent tick failed, if it did; cleared by the next success. Never carries a credential. A message starting with `github_sync_would_discard_refs` is the first pull refusing to overwrite or delete local branches or tags no upstream branch or tag is proven to contain; it lists each ref and the remedy, and nothing moved.

git_last_pulled_at

integer | nullint64

Millis since the Unix epoch of the last tick that completed — an unchanged upstream counts, so this is "last checked", not "last changed". `null` until the first tick.

git_next_run_at

integer | nullint64

When the git pull-back next looks at the upstream, millis since the Unix epoch. `null` = **not armed**: nothing pulls this repository's refs from GitHub (the kill switch, and every row whose mode is neosource-authoritative). A timestamp in the past = due, waiting for a poller tick. Armed only on a GitHub-authoritative row — a `neosource_readonly_mirror`, which the pull follows, or a `github_managed` repository, which it observes — by the relationship edge into either mode, the metadata-sync toggle and the import-complete hook. A row that has exhausted its failure budget keeps its timestamp — see `git_consecutive_failures` for the parked state.

github_is_authorityrequired

boolean

Whether `mode` is one of the GitHub-authoritative pair. Derived from `mode` and sent anyway, so the SPA never restates the rule — the two would then be two statements of one fact with nothing comparing them.

issues_donerequired

integerint32

Issues upserted by the latest backfill run; `0` when none has run.

issues_totalrequired

integerint32

Upper bound for the backfill progress bar; `0` when unknown.

last_error

string | null

Most recent sync/backfill error, if any.

last_synced_at

integer | nullint64

Millis since the Unix epoch of the last successful poll/page.

metadata_moderequired

GithubMetadataMode

string

Whether GitHub's issues and pull requests are pulled into this repository — the `github_repo_sync.metadata_mode` column (`migrations/20260930170000_github_attach_runs.sql`). [`Self::Pulled`] is every relationship that predates the column and every read-only mirror (a CHECK). [`Self::Off`] is a repository attached to a GitHub repository while keeping its native issues (ADR 0136): its metadata is never claimed by the poller or the backfill.

"pulled""off"

moderequired

GithubRelationshipMode

string

Which forge owns an imported repository's refs and provider-shaped data. The ONLY relationship vocabulary, in the database (`github_repo_sync.mode`), in Rust, on the wire and in the SPA. The legacy `synced` / `mirror_readonly` pair it replaced was collapsed onto [`Self::NeosourceReadonlyMirror`] by `migrations/20260902120000_github_relationship_contract.sql`.

"github_managed""neosource_readonly_mirror""neosource_managed""github_backup"

provider_admission_openrequired

boolean

Whether provider projections may be admitted at all. `false` is the default and the parked state.

sync_staterequired

GithubSyncState

string

Lifecycle of a repo's metadata sync (`github_repo_sync.state`).

"backfilling""live""error"

upstream_urlrequired

string

The repository's page on github.com, so the SPA can link out without hard-coding the provider's host.

write_healthrequired

GithubWriteHealth

string

Whether a GitHub-authoritative repository is currently safe to write through — the `github_repo_sync.write_health` column. Closed vocabulary; the tokens are the CHECK list at `migrations/20260901120000_github_relationship_expand.sql`. The default for a fresh relationship is [`Self::LfsUnverified`], not `Healthy`: a repo whose LFS posture nobody has looked at yet is not known-good, and the fail-closed direction is the one that does not push objects at an upstream that will reject or mangle them.

"healthy""lfs_unverified""lfs_blocked""paused""error"

Standard errors

Bodies documented once for the whole API — see standard errors.

  • 400Bad Request — one of: invalid_input
  • 403Forbidden — one of: forbidden
  • 404Not Found — one of: not_found
  • 423Locked — one of: busy
  • 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

Schemas

Referenced above. Listed here rather than expanded inline, so the same definition is not repeated at every level.

GithubAttachOrigin

string

Who started an attach — `github_attach_runs.origin`.

"import""user"

GithubAttachRunState

string

Lifecycle of one attach attempt — `github_attach_runs.state`. ```text planning ─► planned ─(confirm)─► frozen ─► publishing ─► applying ─► applied ▲ │ │ │ └── needs_reconfirm ◄──(undo)───┴───────────┘ (+ failed, cancelled, expired) ``` The six nonterminal states are exactly the partial unique index `github_attach_runs_one_nonterminal_per_repo`, so a repository has at most one attach in flight.

"planning""planned""frozen""publishing""applying""applied""needs_reconfirm""failed""cancelled""expired"

GithubSyncBlocker

Everything that stops GitHub sync, enabling or ongoing: closed and typed, so the GitHub panel can name a fix for each. Each variant mirrors an existing core outcome rather than restating it: the binding ones are [`GithubAppBindingHealth`] values, `RulesetAudit` / `ProtectionEventsNotSubscribed` / `AdministrationReadMissing` are [`RulesetAuditOutcome`] refusals, `WritesRefused` is [`GithubWriteHealth::refuses_managed_writes`], the LFS ones are [`LfsVerificationResult`] verdicts. An empty or unrelated upstream is not a blocker (D11), nor is a tombstone (D7). Serialized internally tagged by `kind`, which is also [`Self::kind`].

one of
  • object

    The GitHub-sync offering is not enabled for this repository.

    kindrequired

    string

    "feature_not_offered"

  • object

    No App installation is bound to the repository.

    kindrequired

    string

    "no_app_binding"

  • object

    [`GithubAppBindingHealth::Unverified`], or `RulesetUnverified` with no refusing audit outcome recorded (awaiting a (re-)audit).

    kindrequired

    string

    "binding_unverified"

  • object

    [`GithubAppBindingHealth::Error`]: the binding probe failed.

    kindrequired

    string

    "binding_error"

  • object

    [`GithubAppBindingHealth::Revoked`].

    kindrequired

    string

    "binding_revoked"

  • object

    [`GithubAppBindingHealth::InsufficientScope`].

    kindrequired

    string

    "binding_insufficient_scope"

  • object

    The ruleset audit refused the binding for `outcome` ([`RulesetAuditOutcome::remedy`] is the fix). A struct variant because an internally tagged enum cannot carry a bare string newtype.

    kindrequired

    string

    "ruleset_audit"

    outcomerequired

    RulesetAuditOutcome

    string

    What the last ruleset audit of a binding found (`github_app_bindings.ruleset_audit_outcome`, migration `20260928120000_github_binding_ruleset_audit.sql`). [`Self::Clear`] is the one outcome that makes a binding `Healthy`; every other value records WHY it is `ruleset_unverified`. The audit fails closed: anything it cannot prove — a field GitHub did not report, a list it may not read, a rule whose ruleset it cannot see — is a refusal, never an assumed "the App cannot bypass" (`plans/github-managed-repositories-2026-09.md` Phase 2; evidence `docs/evidence/github-managed/provider-contract-2026-09-27-175844.md`).

    "clear""app_can_bypass""bypass_unknown""rulesets_unreadable""ruleset_hidden""too_many_rulesets""classic_protection_unreadable""app_in_classic_allowance""app_holds_administration_write""protection_events_not_subscribed"

  • object

    [`RulesetAuditOutcome::ProtectionEventsNotSubscribed`], with the webhook events the installation has not accepted (from the audit's detail; display only).

    kindrequired

    string

    "protection_events_not_subscribed"

    missing_eventsrequired

    array

    items

    string

  • object

    [`RulesetAuditOutcome::ClassicProtectionUnreadable`] on an installation without `administration: read`.

    kindrequired

    string

    "administration_read_missing"

  • object

    The write health refuses managed writes on its own ([`GithubWriteHealth::refuses_managed_writes`]).

    healthrequired

    GithubWriteHealth

    string

    Whether a GitHub-authoritative repository is currently safe to write through — the `github_repo_sync.write_health` column. Closed vocabulary; the tokens are the CHECK list at `migrations/20260901120000_github_relationship_expand.sql`. The default for a fresh relationship is [`Self::LfsUnverified`], not `Healthy`: a repo whose LFS posture nobody has looked at yet is not known-good, and the fail-closed direction is the one that does not push objects at an upstream that will reject or mangle them.

    "healthy""lfs_unverified""lfs_blocked""paused""error"

    kindrequired

    string

    "writes_refused"

  • object

    [`LfsVerificationResult::PointersDetected`] over the post-attach ref set, published refs included.

    kindrequired

    string

    "lfs_pointers_detected"

  • object

    [`LfsVerificationResult::Unverified`], or no verification at all.

    kindrequired

    string

    "lfs_unverified"

  • object

    The GitHub repository is archived.

    kindrequired

    string

    "github_archived"

  • object

    GitHub advertises `refname` inside the preserved namespace.

    kindrequired

    string

    "github_advertises_preserved_prefix"

    refnamerequired

    string

  • object

    This repository already holds `refname` inside the preserved namespace.

    kindrequired

    string

    "local_ref_under_preserved_prefix"

    refnamerequired

    string

  • object

    No preserved name within [`MAX_REFNAME_BYTES`] can be formed for `refname`.

    kindrequired

    string

    "preserved_name_too_long"

    refnamerequired

    string

  • object

    A GitHub ref intent is not terminal (an `ambiguous` one included).

    kindrequired

    string

    "unsettled_ref_intent"

  • object

    Another attach run for this repository is not settled.

    kindrequired

    string

    "unsettled_attach"

GithubSyncFix

string

The stable code the GitHub panel renders as the remedy for a [`GithubSyncBlocker`]. Closed, so the SPA's copy table is exhaustive; one code may answer several blockers ([`GithubSyncBlocker::fix`] is the map).

"contact_operator""install_github_app""wait_for_binding_audit""recheck_github_app""reinstall_github_app""grant_github_app_permissions""fix_github_rulesets""accept_github_app_events""resume_sync""check_write_health""remove_lfs_pointers""recheck_lfs""unarchive_github_repository""remove_preserved_refs_on_github""rename_local_ref""resolve_ref_intent""settle_attach"