# GitHub
URL: https://synquic.tech/docs/api/github.md

Connect GitHub accounts, repositories, and App sources.

import { TypeTable } from 'fumadocs-ui/components/type-table';

The GitHub API connects an Synq instance to GitHub — via the Synq GitHub App, an OAuth account,
or the local `gh` CLI — and then lets you browse the accounts, repositories, branches and files you can
deploy from. It also manages the push webhooks that drive [auto-deploy](/docs/guides/deploy-from-github).
In the dashboard this is the **GitHub** connection card in Settings and the repo picker in the deploy wizard.

<Callout title="Both cloud and self-hosted" type="info">
This module is mounted on every instance. The local identity routes and every `/api/github/sources` route
are self-hosted only and return 404 on a hosted cloud instance (switched off in Synq). Source management additionally requires the active
workspace's `owner` role.
</Callout>

## Connection state

`GET /api/github/home` is the dashboard's single entry point: it returns `{ state, accounts, repos }` in one
round trip, filtered to the accounts and repos your member role is allowed to see.
`GET /api/github/status` is narrower — the Settings card's data source — returning `{ state, accounts,
installUrl, cloudUnreachable, customSourcesConfigured }`. The last field tells clients that App lifecycle
controls belong to the workspace source manager rather than the legacy Synq App connection.

<Tabs items={['SDK', 'REST API']} groupId="api-transport" persist>
<Tab value="SDK">

```ts
const result = await ship.github.getHome();
console.log(result);
```

</Tab>
<Tab value="REST API">

```bash
curl "$SYNQ_URL/api/github/home" \
  -H "Authorization: Bearer $SYNQ_TOKEN"
```

</Tab>
</Tabs>

## Connect

Starts or advances the connection flow. The response is intentionally mode-agnostic — the frontend just
reacts to the `flow` field:

- `{ connected: true }` — already connected, nothing to do.
- `{ connected: false, flow: "redirect", url }` — open `url` (OAuth handoff or GitHub App install).
- `{ connected: false, flow: "device_code", userCode, verificationUri, ... }` — self-hosted device login.
- `{ connected: false, flow: "terminal", command, message }` — run `command` (e.g. `gh auth login`) yourself.

The optional body field `source` (`"oauth"` | `"cli"`) forces a specific path when the dashboard shows both
the "Connect Synq App" and "Use gh CLI" buttons; omit it to let the server pick based on the instance's
auth mode.

<Tabs items={['SDK', 'REST API']} groupId="api-transport" persist>
<Tab value="SDK">

```ts
const result = await ship.github.connect({
  source: "oauth",
});
console.log(result);
```

</Tab>
<Tab value="REST API">

```bash
curl -X POST "$SYNQ_URL/api/github/connect" \
  -H "Authorization: Bearer $SYNQ_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"source":"oauth"}'
```

</Tab>
</Tabs>

## Disconnect

```
POST /api/github/disconnect
```

Body (or `?source=`): `source` is `"oauth"`, `"cli"`, or `"all"` (default `"all"`). This drops Synq's
stored credentials for that source; it does **not** uninstall the GitHub App — that happens only when you
remove it from github.com (Synq reacts to the uninstall webhook).

This endpoint does not delete a workspace-owned source. Use `DELETE /api/github/sources/:id` for that
explicit owner-only operation.

## Workspace-owned GitHub App sources

These endpoints are available only on self-hosted instances. All reads are sanitized: private keys, client
secrets, webhook secrets, and their encrypted envelope are never returned.

### List sources

Returns `{ data, configuration }`. `configuration` contains `publicReady`, `publicUrl`, `setupUrl`, and
`webhookUrl`. Each source in `data` contains public App metadata, health, default status, and its claimed
installations.

### One-click manifest

Start the GitHub.com flow with:

The response contains `{ url, manifest }`. Submit `manifest` as a JSON string in a form field named
`manifest` to `url`; the dashboard does this automatically. GitHub redirects to the configured setup page
with `code` and `state`, which must be sent to:

```text
POST /api/github/sources/manifest/convert
{ "code": "…", "state": "…" }
```

Conversion consumes the user/workspace-bound state exactly once, encrypts GitHub's generated credentials,
and returns `{ data, installUrl }`.

### Manual registration and GitHub Enterprise Server

```text
POST /api/github/sources/manual
```

<TypeTable
  type={{
    name: { type: 'string', description: 'Workspace-facing source label.', required: true },
    appId: { type: 'integer', description: 'Numeric GitHub App ID.', required: true },
    clientId: { type: 'string', description: 'Optional App client ID.' },
    clientSecret: { type: 'string', description: 'Optional App client secret; stored encrypted.' },
    privateKeyPem: { type: 'string', description: 'Complete App private key PEM; stored encrypted.', required: true },
    webhookSecret: { type: 'string', description: 'Matching App webhook secret, at least 16 characters; stored encrypted.', required: true },
    webBaseUrl: { type: 'string', description: 'GitHub web origin. Defaults to https://github.com.' },
    apiBaseUrl: { type: 'string', description: 'REST API base. Defaults to https://api.github.com; GHE commonly uses https://host/api/v3.' },
    isDefault: { type: 'boolean', description: 'Prefer this App when more than one source covers an owner.' },
  }}
/>

The API signs an App JWT and verifies `/app` before saving the source. It returns `{ data, installUrl }`.

`PATCH /api/github/sources/:id` accepts the same identity and endpoint fields, all optional. Omitted secret
fields retain their existing encrypted values. App ID or API URL cannot change while the source has claimed
installations. `POST .../verify` refreshes public App metadata and marks bad credentials as needing attention.

Making a source default atomically rebinds existing projects for every GitHub owner installed on that source.
Deleting a source promotes another active source when possible and rebinds affected projects; projects with no
replacement lose their installation binding and auto-deploy is disabled. Deleting locally does not uninstall
the GitHub App itself.

## Create a repository

<Tabs items={['SDK', 'REST API']} groupId="api-transport" persist>
<Tab value="SDK">

```ts
const result = await ship.github.createRepo({
  name: "my-app",
  private: true,
  owner: "acme",
});
console.log(result);
```

</Tab>
<Tab value="REST API">

```bash
curl -X POST "$SYNQ_URL/api/github/repos" \
  -H "Authorization: Bearer $SYNQ_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"name":"my-app","private":true,"owner":"acme"}'
```

</Tab>
</Tabs>

<TypeTable
  type={{
    name: { type: 'string', description: 'Repository name. Letters, digits, dot, underscore and hyphen only.', required: true },
    description: { type: 'string', description: 'Optional repository description.' },
    private: { type: 'boolean', description: 'Create the repo as private.', default: 'false' },
    owner: { type: 'string', description: 'Org / account to create under. Defaults to the connected user account.' },
  }}
/>

Returns `201` with `{ data }` (the created repository).

## Browse repos, branches and files

List repos for the connected account (`?owner=` narrows to one account), or for a named org via
`GET /api/github/orgs/:org/repos`. Both return `{ data: [...] }` filtered to repos your role may see.

File browsing reads directly from GitHub:

- `GET /api/github/repos/:owner/:repo/files` — directory listing. Query: `path` (default repo root) and
  `branch`.
- `GET /api/github/repos/:owner/:repo/file` — one file's contents. Query: `file` (default `package.json`)
  and `branch`. `.json` files are parsed and returned as JSON.

<Tabs items={['SDK', 'REST API']} groupId="api-transport" persist>
<Tab value="SDK">

```ts
const result = await ship.github.getFile({
  owner: "acme",
  repo: "my-app",
  file: "package.json",
  branch: "main",
});
console.log(result);
```

</Tab>
<Tab value="REST API">

```bash
curl "$SYNQ_URL/api/github/repos/acme/my-app/file?file=package.json&branch=main" \
  -H "Authorization: Bearer $SYNQ_TOKEN"
```

</Tab>
</Tabs>

## Webhooks

`POST /api/github/repos/:owner/:repo/webhooks` registers the Synq push webhook on the repo (or reuses an
existing one) so pushes trigger auto-deploy. It takes **no request body** — the target repo comes from the
path. `GET` lists the repo's hooks. To remove one, `DELETE` with the hook id:

```
DELETE /api/github/repos/:owner/:repo/webhooks
```

<TypeTable
  type={{
    hookId: { type: 'number', description: 'The GitHub webhook id to delete (from the list endpoint).', required: true },
  }}
/>

## Clone token

`GET /api/github/repos/:owner/:repo/clone-token` mints a short-lived (under an hour) GitHub App installation
token and returns `{ token, cloneUrl, command }` — a ready-to-run `git clone`. This is the same credential
the build pipeline clones with, and is available only when the Synq GitHub App is installed for the owner.

## Errors

<Callout title="400 — Not connected to GitHub" type="warn">
Returned by the repo-listing routes when no usable GitHub source (App installation, OAuth account, or `gh`
CLI) is configured. Connect first with `POST /api/github/connect`, then retry.
</Callout>

<Callout title="409 — no installation token" type="warn">
`clone-token` needs a GitHub App installation for the owner. In `gh` CLI or PAT modes there is no
installation token, so this route 409s. Install the Synq GitHub App on that account to use clone tokens.
</Callout>

<Callout title="503 — cloud_unreachable" type="error">
Hosted-cloud only, switched off in Synq. On an instance connected to a hosted cloud, GitHub OAuth and
App-install URLs are resolved through that cloud; if it is unreachable, `connect` returns 503 rather than a
dead install link. A Synq install never connects to a hosted cloud, so it doesn't see this error.
</Callout>

An instance configured with its [own self-hosted GitHub App](/docs/guides/self-hosted-github-app) does not
use this cloud proxy; `GITHUB_AUTH_MODE=auto` selects the complete local App configuration first.

<Callout title="404 on a repo route" type="error">
The `:owner/:repo` isn't visible to the active GitHub source (private, not granted to your member role, or
does not exist). Confirm the repo appears in `GET /api/github/home` first.
</Callout>

## Operations

{/* api-operations:start */}

### Connect

| Operation | SDK | REST API |
| --- | --- | --- |
| Read GitHub connection status. | `github.getStatus(input?)` | `GET /api/github/status`<br />`github:read` |
| Read the GitHub overview for the current identity. | `github.getHome()` | `GET /api/github/home`<br />`github:read` |
| Start a supported GitHub connection flow. | `github.connect(input?)` | `POST /api/github/connect`<br />`github:write` |
| Claim an authorized GitHub App installation. | `github.claimInstallation(input)` | `POST /api/github/installations/claim`<br />`github:write` · Self-hosted |
| Read local Git identity availability. | `github.getLocalStatus()` | `GET /api/github/local-status`<br />`github:read` · Self-hosted |
| Poll an in-progress connection flow. | `github.pollConnect()` | `GET /api/github/connect/poll`<br />`github:read` · Self-hosted |
| Configure the installation’s GitHub token. | `github.setInstanceToken(input)` | `POST /api/github/instance-token`<br />`github:admin` · Self-hosted |
| Disconnect a GitHub identity or source association. | `github.disconnect(input?)` | `POST /api/github/disconnect`<br />`github:admin` |

### Repositories and source

| Operation | SDK | REST API |
| --- | --- | --- |
| List accessible repositories. | `github.listRepos(input?)` | `GET /api/github/repos`<br />`github:list` |
| List repositories in a GitHub organization. | `github.listOrgRepos(input)` | `GET /api/github/orgs/:org/repos`<br />`github:list` |
| Read repository metadata. | `github.getRepo(input)` | `GET /api/github/repos/:owner/:repo`<br />`github:read` |
| Create a repository through the configured GitHub identity. | `github.createRepo(input)` | `POST /api/github/repos`<br />`github:write` |
| Delete an authorized repository. | `github.deleteRepo(input)` | `DELETE /api/github/repos/:owner/:repo`<br />`github:admin` |
| List repository branches. | `github.listBranches(input)` | `GET /api/github/repos/:owner/:repo/branches`<br />`github:list` |
| Obtain an authorized repository clone credential. | `github.getCloneToken(input)` | `GET /api/github/repos/:owner/:repo/clone-token`<br />`github:read` |
| Detect build configuration in a repository. | `github.detectStack(input)` | `GET /api/github/repos/:owner/:repo/detect`<br />`github:read` |
| List repository files at a path. | `github.listFiles(input)` | `GET /api/github/repos/:owner/:repo/files`<br />`github:list` |
| Read a repository tree. | `github.listTree(input)` | `GET /api/github/repos/:owner/:repo/tree`<br />`github:list` |
| Read repository file content. | `github.getFile(input)` | `GET /api/github/repos/:owner/:repo/file`<br />`github:read` |

### Webhooks

| Operation | SDK | REST API |
| --- | --- | --- |
| List repository webhooks. | `github.listWebhooks(input)` | `GET /api/github/repos/:owner/:repo/webhooks`<br />`github:list` |
| Register a repository webhook for deployment events. | `github.registerWebhook(input)` | `POST /api/github/repos/:owner/:repo/webhooks`<br />`github:write` |
| Delete a repository webhook. | `github.deleteWebhook(input)` | `DELETE /api/github/repos/:owner/:repo/webhooks`<br />`github:admin` |

### GitHub App sources

| Operation | SDK | REST API |
| --- | --- | --- |
| List configured GitHub App sources. | `github.listSources()` | `GET /api/github/sources`<br />`github:admin` · Self-hosted |
| Start GitHub App manifest registration. | `github.beginManifest(input)` | `POST /api/github/sources/manifest`<br />`github:admin` · Self-hosted |
| Convert a completed GitHub App manifest. | `github.convertManifest(input)` | `POST /api/github/sources/manifest/convert`<br />`github:admin` · Self-hosted |
| Register an existing GitHub App source. | `github.createManualSource(input)` | `POST /api/github/sources/manual`<br />`github:admin` · Self-hosted |
| Change a GitHub App source’s configuration. | `github.updateSource(id, input)` | `PATCH /api/github/sources/:id`<br />`github:admin` · Self-hosted |
| Verify source credentials with GitHub. | `github.verifySource(id)` | `POST /api/github/sources/:id/verify`<br />`github:admin` · Self-hosted |
| Select the default GitHub App source. | `github.setDefaultSource(id)` | `POST /api/github/sources/:id/default`<br />`github:admin` · Self-hosted |
| Create an installation URL for a GitHub App source. | `github.createInstallUrl(id)` | `POST /api/github/sources/:id/install`<br />`github:admin` · Self-hosted |
| Remove a GitHub App source. | `github.deleteSource(id)` | `DELETE /api/github/sources/:id`<br />`github:admin` · Self-hosted |

### HTTP endpoints

| Operation | SDK | REST API |
| --- | --- | --- |
| GitHub OAuth callback — browser navigates here, no session yet. | HTTP only | `GET /api/github/connect/redirect`<br />`Handler authentication` |

{/* api-operations:end */}
