Home

madison / open-git

public
Code Branches Pull requestsIssuesInsights Campfire Docs
main
Home Code PRsIssues

Docs

Markdown docs from docs.

Pages
Dashboard list "table" patternGitHub two-way syncOrganizations and Repository RBACUI Component Audit
GitHub two-way sync

GitHub two-way sync

Links an open-git repository to a GitHub repository and keeps refs, issues, pull requests, comments, and reviews in sync in both directions.

Moving parts

Piece Location
Pure sync logic (API client, mappers, echo detection) packages/github-sync
Webhook ingress apps/web/app/api/github/webhooks/route.ts
Job queue + data access apps/web/db/github-sync.ts
Sync handlers apps/web/lib/github/sync/
Worker daemon apps/github-sync-worker

Webhooks are verified and persisted synchronously, then processed asynchronously off the github_sync_jobs queue. Nothing in the request path talks to the GitHub API.

Creating the GitHub App

  1. Go to Settings → Developer settings → GitHub Apps → New GitHub App (or the org equivalent).
  2. Homepage URL: your app URL. Webhook URL: https://<app-url>/api/github/webhooks.
  3. Generate a Webhook secret and keep it.
  4. Repository permissions:
    • Contents: Read and write
    • Issues: Read and write
    • Pull requests: Read and write
    • Metadata: Read-only
  5. No organization or account permissions are needed. GitHub actors are read from webhook and API payloads, not from org membership lookups.
  6. Leave Request user authorization (OAuth) during installation unchecked. Per-user tokens come from the separate OAuth app below, not from this App.
  7. Subscribe to events: Push, Create, Delete, Issues, Issue comment, Pull request, Pull request review, Pull request review comment, Repository, Installation, Installation repositories.
  8. Generate a private key and download the .pem.

Then set:

GITHUB_APP_ID=123456
GITHUB_APP_SLUG=your-app-slug
GITHUB_APP_WEBHOOK_SECRET=...
GITHUB_APP_PRIVATE_KEY="-----BEGIN RSA PRIVATE KEY-----\n...\n-----END RSA PRIVATE KEY-----"

The private key may be a literal multi-line PEM or a single line with \n escapes; both are accepted.

Sync stays fully disabled until all four are present, so leaving them unset is a supported configuration.

Local development

GitHub cannot reach localhost, so tunnel the webhook endpoint:

# smee
npx smee-client --url https://smee.io/<channel> --path /api/github/webhooks --port 3001

# or cloudflared
cloudflared tunnel --url http://localhost:3001

Point the App's webhook URL at the tunnel. Run the worker alongside the web app:

pnpm --filter github-sync-worker dev

Identity and attribution

Writes to GitHub prefer the acting user's own linked GitHub OAuth token, so their activity appears under their real account. When the actor has not linked GitHub, the App bot writes instead and appends an attribution footer naming the original author. The footer is stripped on the way back in, so it never accumulates and never registers as a remote edit.

Those per-user tokens come from the Better Auth GitHub provider, which is a separate OAuth app from the GitHub App above and needs the repo scope:

GITHUB_CLIENT_ID=
GITHUB_CLIENT_SECRET=

Without these, nobody can link a GitHub account, so every write falls to the bot. That looks like working sync with the wrong author on every comment, which is why it is worth confirming before concluding the actor path is broken.

A linked account is not proof of write access on GitHub. If the contributor's own token is refused, the write is retried as the bot and recorded in github_actor_write_denials, surfaced under Settings → GitHub sync.

GitHub users with no open-git account are represented by shadow profiles: user_profiles rows with is_shadow = true and no username, rendered from external_login. They satisfy the non-null author foreign keys and are upgraded automatically if that person later links their GitHub account.

Numbering

Issues and pull requests share one number space per repository on both platforms. For a linked repo GitHub is the authority: creating an issue or PR in open-git calls the GitHub API inline and adopts the number it returns. If GitHub is unreachable the create fails rather than allocating a number that would later conflict.

Divergence

Only fast-forward ref updates sync automatically. If both sides move the same ref away from the last agreed sha, or an update is not a fast-forward, the link is marked diverged and ref sync pauses for that repository. Resolve it from Settings → GitHub sync by choosing which side wins; nothing is force-pushed automatically.