Files
openchamber/packages/web/server/lib/github/DOCUMENTATION.md
T
Bohdan Triapitsyn 268f9ea9f2 fix(github): keep merged PRs as branch history instead of hiding them
Branch status resolves an open PR across the whole fork network first, so a
merged fork PR can never hide an open upstream PR for the same head. Only when
no target has an open PR does the branch's newest closed/merged PR come back,
as history.

The panel shows that history as a compact note and offers creating the next PR
below it, instead of either sticking on a terminal PR or going blank after a
merge. Terminal associations stay persisted for reload continuity but are never
treated as authority: they revalidate on the discovery cadence and on focus.

History is looked up only for the branch's own remote and name, and remembered
per repo+branch, so the extra lookup cannot exhaust the route's resolve budget.
The checks summary and merge-permission lookup are skipped for a closed or
merged PR, where neither is actionable.
2026-08-15 17:56:55 +03:00

9.7 KiB

GitHub Module Documentation

Purpose

  • This module owns GitHub auth, Octokit access, repo resolution, and Pull Request status resolution for OpenChamber.
  • From user perspective, this is the layer that lets the app know which PR belongs to a local branch and keeps that UI feeling current.

Entrypoints and structure

  • packages/web/server/lib/github/index.js: public server entrypoint. routes.js loads it lazily with await import('./index.js') and destructures the handler it needs, so a re-export removed from here breaks a route at request time rather than at build time. Static "unused export" reports do not see these consumers.
  • packages/web/server/lib/github/routes.js: Express route registration for /api/github/* endpoints.
  • packages/web/server/lib/github/auth.js: auth storage, multi-account support, client id, scope config.
  • packages/web/server/lib/github/device-flow.js: OAuth device flow.
  • packages/web/server/lib/github/octokit.js: Octokit factory for the current auth.
  • packages/web/server/lib/github/repo/index.js: remote URL parsing and directory-to-repo resolution.
  • packages/web/server/lib/github/pr-status.js: PR lookup across remotes, forks, and upstreams.
  • packages/web/server/index.js: API route layer that calls this module.
  • packages/web/src/api/github.ts: web client wrapper for GitHub endpoints.

Public exports

Auth

  • getGitHubAuth(): current auth entry.
  • getGitHubAuthAccounts(): all configured accounts.
  • setGitHubAuth({ accessToken, scope, tokenType, user, accountId }): save or update account.
  • activateGitHubAuth(accountId): switch active account.
  • clearGitHubAuth(): clear current account.
  • getGitHubClientId(): resolve client id.
  • getGitHubScopes(): resolve scopes.
  • GITHUB_AUTH_FILE: auth file path.

Device flow

  • startDeviceFlow({ clientId, scope }): request device code.
  • exchangeDeviceCode({ clientId, deviceCode }): poll for access token.

Octokit

  • getOctokitOrNull(): current Octokit or null.

Repo

  • parseGitHubRemoteUrl(raw): parse SSH or HTTPS remote URL into { owner, repo, url }.
  • resolveGitHubRepoFromDirectory(directory, remoteName): resolve GitHub repo from a local git remote.

Auth storage and config

  • Auth storage: ~/.config/openchamber/github-auth.json
  • Writes are atomic and file mode is 0o600.
  • Client ID resolution order: OPENCHAMBER_GITHUB_CLIENT_ID -> settings.json -> default.
  • Scope resolution order: OPENCHAMBER_GITHUB_SCOPES -> settings.json -> default.
  • Account id resolution order: explicit accountId -> user login -> user id -> token prefix.

PR integration overview

  • The UI asks github.prStatus(directory, branch, remote?) from packages/web/src/api/github.ts.
  • That hits GET /api/github/pr/status in packages/web/server/index.js.
  • The route calls resolveGitHubPrStatus(...) in packages/web/server/lib/github/pr-status.js.
  • The resolver finds the most likely repo and PR for a local branch.
  • The route then enriches that result with checks, mergeability, and permission-related fields.
  • The client caches and shares the result between sidebar and Git view.

Consumers of PR data

  • packages/ui/src/components/session/SessionSidebar.tsx reads all PR entries and maps them to directory::branch.
  • packages/ui/src/components/session/sidebar/SessionGroupSection.tsx renders the compact badge, PR number, title, checks summary, and GitHub link.
  • packages/ui/src/components/views/git/PullRequestSection.tsx uses the same shared entry for the full PR workflow.
  • packages/ui/src/components/ui/MemoryDebugPanel.tsx reads request counters for debugging.

How PR resolution works

  • It reads local git status and remotes first.
  • It ranks remotes in this order: explicit remote, tracking remote, origin, upstream, then the rest.
  • It resolves those remotes into GitHub repos.
  • It expands each repo through parent and source so PRs in upstream repos can still be found.
  • It skips PR lookup when the current branch matches that repo's default branch.
  • It first searches for open PRs by likely source owner plus exact head branch.
  • If that fails, it falls back to broader GitHub search for open PRs on the branch name.
  • An open PR from any candidate repo always wins over a closed/merged one, so a merged fork PR can never hide an open upstream PR for the same head.
  • Only when no target has an open PR does it return the branch's newest closed/merged PR, as history.
  • History is looked up only for the ranked-first remote and the branch's own name — the repo it actually pushes to. Live status is worth searching the whole fork network for; history is not, and asking every target for it multiplies serial GitHub calls until the route hits its 12s resolve timeout and returns no status at all.
  • The history answer is remembered per repo+branch so discovery polls do not re-query it: a found closed/merged record for 6h, and "no history yet" for 10m. A found record only changes if a second PR appears on the same head, and while that one is open the open-PR path wins without ever reading this cache.
  • Creating, merging, or closing a PR invalidates both the shared repo pull list and that remembered history.
  • The route skips the checks summary and the merge-permission lookup for a closed/merged PR: neither is actionable, and both cost extra GitHub calls.
  • 403 and 404 during repo lookups are treated as expected gaps, not hard errors.

Shared client state model

  • Client key is effectively directory::branch.
  • One entry stores last known status, loading state, error, timestamps, watcher count, identity, and resolved remote.
  • Requests are deduplicated by branch signature, not by component instance.
  • This keeps sidebar and Git view aligned and avoids duplicated fetches.

Persistence

  • PR state is persisted in local storage under openchamber.github-pr-status.
  • Persisted fields include status, timestamps, identity, and resolved remote.
  • Runtime-only details are not persisted.
  • Persisted entries expire after 12 hours.
  • On reload, users get last known state first, then background refresh resumes.

Polling and refresh model

  • There are two layers: entry-level polling in useGitHubPrStatusStore and repo scanning in useGitHubPrBackgroundTracking.
  • Entry-level polling decides when a known branch should revalidate PR state.
  • Background tracking decides which directories and branches should even be watched.

Entry-level polling rules

  • Start watching -> immediate refresh.
  • If no PR is found yet -> retry after 2s and 5s.
  • Still no PR -> discovery refresh every 5m.
  • Open PR with pending checks -> refresh about every 1m.
  • Open PR with non-pending checks -> refresh about every 5m.
  • Open PR without a stable checks signal -> refresh about every 2m.
  • Closed or merged PR -> discovery refresh every 5m (do not permanently stop polling).
  • Hidden tab -> skip polling.
  • Non-forced refreshes use a 90s TTL.
  • Failed non-forced attempts also observe the 90s TTL so transient server or rate-limit failures cannot retry on every sidebar update. Forced user/action refreshes bypass this guard.

Persistence notes for terminal PRs

  • Closed/merged branch associations are persisted like open ones, so a reload still shows that the branch's PR was merged.
  • Hydrate resets lastDiscoveryPollAt for them, so restored history revalidates on the first watcher tick instead of waiting out a discovery interval.

Background tracking rules

  • Track up to 50 likely directories.
  • Sources are current directory, projects, worktrees, active sessions, and archived sessions.
  • Active directory branch TTL is 15s.
  • Background directory branch TTL is 2m.
  • Background scan wakes every 15s, but only fetches directories whose TTL expired.
  • Each scan reads branch, tracking, ahead, and behind from git status.
  • If any of those branch signals change, that branch's PR status refreshes immediately.
  • After that, one more delayed refresh runs after 5s to catch GitHub eventual consistency.

UI refresh triggers

  • App or tab becomes visible.
  • Window regains focus.
  • Current branch changes.
  • Tracking branch changes.
  • Ahead or behind changes.
  • User selects a different remote in Git view.
  • GitHub auth state changes.

Action-based refreshes in Git view

  • After Create PR -> refresh now, then after 2s and 5s.
  • After Merge PR -> refresh now, then after 2s and 5s.
  • After Mark ready for review -> refresh now, then after 2s and 5s.
  • After Update PR -> refresh now, then after 2s and 5s.

Sidebar behavior

  • Sidebar shows only compact PR state.
  • Aggregation is by directory::branch, so multiple sessions on one branch share one signal.
  • If multiple entries exist, sidebar keeps the strongest visible PR state.
  • Visual state is based on PR health, not merge permissions.

Git view behavior

  • Git view watches one branch directly.
  • It supports create, edit, mark ready, and merge.
  • It can probe alternate remotes so fork-heavy setups still find the right PR.
  • It uses the same shared store as the sidebar.

Failure handling

  • If GitHub is disconnected, API returns connected: false.
  • If a repo is private or inaccessible, resolver calls may quietly return no PR.
  • Sidebar stays quiet on missing or inaccessible PR state.
  • Git view is where explicit PR-level problems should be shown.

Notes for contributors

  • Keep the UI calm. Do not add noisy diagnostics to the sidebar.
  • Prefer shared state over per-component fetches.
  • Prefer event-shaped refreshes over blind frequent polling.
  • Prefer correctness for fork and multi-remote setups over assuming origin is enough.
  • Device flow handles GitHub authorization_pending at caller level.
  • Repo parser supports git@github.com:, ssh://git@github.com/, and https://github.com/.