Files
openchamber/packages/web/server/lib/github/DOCUMENTATION.md
T
Serhii DziupinandSerhii Dziupin 86e6a2ae76 Remove verified dead declarations (#2714)
* chore: remove verified dead declarations

Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com>

* chore: narrow unused internal exports

Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com>

* chore: remove newly exposed dead helpers

Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com>

* chore: remove unused deep-link serializer

Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com>

* test: drop two tests that assert on copies of the code

mainLayoutMobileSidebarMount read MainLayout.tsx and SessionSidebar.tsx as
strings and asserted on source substrings down to exact indentation, so it
failed on formatting rather than behaviour. useProjectSessionSelection.test
reimplemented the hook's visitNodes logic inside the test file and asserted
against that copy, so it could not observe the hook at all.

Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com>

* test: repair sync suites that had rotted while unrunnable

No runner executed packages/ui, so these drifted from the source unnoticed:
two imported helpers that are no longer exported, one directory-store stub
predated the session field routeMessage reads, and the WebSocket fake missed
the mandatory url-token mint plus the close event the socket wrapper reads.

Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com>

* test: stop the web suite failing on timeouts and a hand-copied mock

The Git suites drive a real git binary, so the 5s default made a valid suite
fail differently per run. The gitApiHttp mock listed ~70 export names by hand
and fell behind the source; it now derives every stub from the real module,
which the added shared-UI aliases make resolvable.

Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com>

* test: run every suite from one command and in CI

packages/ui (232 files) and packages/vscode (22) had no test script at all, CI
ran neither, and 9 vscode files could never run because Node cannot resolve
their extensionless TypeScript imports. Three electron files sat outside every
script list, one of them importing vitest, which that package does not depend
on. A runner gives each file its own process, since these suites keep
module-level singletons and fail by load order when sharing one.

Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com>

* chore: delete a superseded repro harness and a completed plan

The issue-2638 harness needed lsof, overrode process.platform and spawned real
servers, and nothing referenced it; event-stream/rebind.test.js now covers the
same hub-pinned-to-the-old-port behaviour. The pairing v2 plan described relay
and the pairing UI as out of scope, both of which shipped.

Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com>

* docs: point at the theme tools and record the github barrel invariant

convert-vscode-theme and harmonize-theme were referenced nowhere, so the
theme-authoring reference now names them. The github barrel is loaded through
await import('./index.js') and destructured per route, which no static report
can see; documenting that is what stops the next cleanup from deleting it.

Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com>

* test: repair merge drift in bridge and route-registry mocks

upstream/main gained upsertProviderConfig on bridge-system-runtime and a
PATCH scheduled-task route after this branch forked. Their test doubles
were never updated to match:
- bridge-system-runtime.test.js: add upsertProviderConfig to the
  opencodeConfig mock so the import resolves.
- sse-routes.test.js: add app.patch to the route registry stub.

---------

Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com>
2026-08-13 15:30:54 +03:00

8.2 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 PRs by likely source owner plus exact head branch.
  • If that fails, it falls back to broader GitHub search for the branch name.
  • 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 -> stop regular 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.

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/.