sync: standalone CLI engine with SQLite + bulk GraphQL - #74
Merged
Conversation
There was a problem hiding this comment.
Pull request overview
This PR introduces a new standalone sync engine (packages/sync) that persists state to a local SQLite cache and fetches PRs/reviews in bulk via GraphQL, while also renaming workspace packages to drop the @github-dashboard/ prefix and updating scripts/workflows accordingly.
Changes:
- Added
packages/sync: CLI (ghd-sync), SQLite cache bootstrap/versioning, and GitHub provider fetchers (GraphQL for PRs/reviews; REST+ETag for notifications). - Renamed workspace package names (
server,web,desktop,sync) and updated root scripts + CI workflows to use shorterpnpm --filtertargets. - Updated lockfile and build config to support
better-sqlite3(includingpnpm.onlyBuiltDependencies).
Reviewed changes
Copilot reviewed 26 out of 27 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
| pnpm-lock.yaml | Adds deps for sync package (incl. better-sqlite3) and workspace name updates. |
| package.json | Updates pnpm --filter scripts and adds sync script + onlyBuiltDependencies entry for better-sqlite3. |
| packages/web/package.json | Renames workspace package to web. |
| packages/server/package.json | Renames workspace package to server. |
| packages/desktop/package.json | Renames workspace package to desktop and updates workspace dependency name. |
| packages/desktop/src/main.ts | Updates comment to reflect renamed desktop package. |
| .github/workflows/desktop-ci.yml | Updates pnpm filters to desktop. |
| .github/workflows/desktop-release.yml | Updates pnpm filters to desktop. |
| packages/sync/package.json | Adds new sync workspace package metadata, scripts, bin, and exports. |
| packages/sync/tsconfig.json | TypeScript config for the sync package. |
| packages/sync/vitest.config.ts | Vitest config for sync tests. |
| packages/sync/src/index.ts | Placeholder entrypoint for package exports. |
| packages/sync/src/cli.ts | Implements ghd-sync CLI commands: run/status/wipe. |
| packages/sync/src/sync.ts | Orchestrates reconciliation + per-instance fetches with a rate-limit floor guard and watch mode. |
| packages/sync/src/config.ts | Loads config.yml instances and derives API base URL + username. |
| packages/sync/src/cache/path.ts | Resolves SQLite cache path using XDG cache conventions. |
| packages/sync/src/cache/schema.ts | Declares schema version and DDL (WAL, tables, indexes, json_valid payload). |
| packages/sync/src/cache/open.ts | Opens SQLite DB, checks schema version, and wipes/recreates on mismatch. |
| packages/sync/src/cache/open.test.ts | Unit tests for cache open/wipe/version behavior. |
| packages/sync/src/cache/store.ts | DB store helpers for instances, PRs, notifications, and sync_state. |
| packages/sync/src/providers/github/client.ts | Shared Octokit client cache for GitHub API calls. |
| packages/sync/src/providers/github/queries.ts | GraphQL fragments/queries and response typings for PR search + review search. |
| packages/sync/src/providers/github/normalize.ts | Normalizes GraphQL PR nodes into the canonical client payload shape. |
| packages/sync/src/providers/github/fetchPrs.ts | Fetches authored PRs via bulk GraphQL and persists rows/payloads. |
| packages/sync/src/providers/github/fetchReviews.ts | Fetches review-requested PRs via bulk GraphQL incl. timeline heuristic and persists rows/payloads. |
| packages/sync/src/providers/github/fetchNotifications.ts | Fetches notifications via REST with ETag, filters redundancies, and persists rows/state. |
| packages/sync/src/providers/github/notificationUrl.ts | Converts notification API subject URLs into HTML URLs. |
Files not reviewed (1)
- pnpm-lock.yaml: Language not supported
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
AntonNiklasson
force-pushed
the
an/sync-package-scaffold
branch
from
June 10, 2026 07:59
6167956 to
6de1a7b
Compare
Workspace packages are private and only referenced via pnpm --filter and workspace:* links, so the scope adds noise without value. Rename to bare desktop/server/web and update all --filter invocations, the desktop workspace dep link, and the app.setName comment.
New `sync` workspace package: a standalone GitHub poller that caches PRs, reviews, and notifications to SQLite. Adds the manifest (octokit, better- sqlite3, yaml, zod; ghd-sync bin + dist exports), tsconfig, and vitest config. Registers better-sqlite3 as a built dependency and updates the lockfile. Source lands in the following commits.
The cache-file plumbing: resolveCachePath (XDG-aware default location), the schema DDL + CACHE_SCHEMA_VERSION, and openCache, which opens the better-sqlite3 handle, applies the schema, and wipes+recreates on a version mismatch. wipeCacheFile removes the file outright.
The Repository contract and its SQLite implementation: upserting instances, replacing PR/notification rows per instance+kind, reading back summaries, and tracking per-resource sync state (ETag/last-fetched). Built on the Cache handle from openCache.
loadInstances reads ~/.config/github-dashboard/config.yml, validates it with zod, and derives a stable instance id per host. resolveConfigPath and instanceIdFromDomain are exported for reuse.
The GitHub provider: a per-(id,token,baseUrl) Octokit client cache (so token rotation or baseUrl change gets a fresh client), GraphQL queries + normalization into PrRow/NotificationRow, and the three fetchers — authored PRs, review-requested PRs, and notifications. commentCount sums issue comments plus review-thread comments to match the server payload; notification paging stops early when the first page is short to spare the REST rate limit.
createSyncEngine drives a sync cycle across instances and kinds: reconcile configured instances, fetch PRs/reviews/notifications, persist via the repository. runOnce does a single pass; start runs it on an interval. A rate-limit floor skips fetches when the remaining budget is low so a cycle can't exhaust the GitHub quota. printSummary renders a cycle summary.
The ghd-sync entrypoint: once runs a single sync, loop runs continuously, status prints cached counts and per-resource sync state, wipe removes the cache file. Wires openCache + repository + engine together and adds a root-level `sync` script for convenience.
index.ts re-exports the cache, config, and engine APIs so consumers (the server, the CLI, future adopters) import only from the package root and the internal layout stays free to change. index.test.ts pins the exported names.
AntonNiklasson
force-pushed
the
an/sync-package-scaffold
branch
from
June 16, 2026 06:52
e91f815 to
18eb087
Compare
loadInstances() probed users.getAuthenticated() per instance on every call. The engine re-reads config each cycle (so reconcile sees added/removed instances), so in loop mode this spent one REST request per instance per cycle — unaccounted by the rate-limit floor — to re-resolve a username that effectively never changes. Memoize the probe by (baseUrl, token) for the process lifetime; a restart re-probes.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Part of #72.
Adds
packages/sync— a standalone sync engine for github-dashboard with both a library API and a CLI on top. The server, web, and desktop packages are untouched; phase B of #72 will replace the server'scache.ts+fetchers.tsby importing this package.Also drops the
@github-dashboard/prefix from all workspace package names (server,web,desktop,sync) sopnpm --filter ...invocations get shorter.Public API (what the server will use)
createSyncEngineaccepts an optionalloadInstancesfor tests.reconcileInstancesis exported as a standalone primitive. One entry point per concern:openCachefor the file,createSqliteRepositoryfor the storage contract,createSyncEnginefor orchestration.Engine internals
$XDG_CACHE_HOME/github-dashboard/cache.sqlite(default~/.cache/...). WAL mode, integerschema_versionin ametatable, wipe-and-recreate on mismatch in either direction. Pure regenerable state, so it lives under cache, not data.Databasehandle.createSqliteRepository(db)builds prepared statements once at factory time and closes over them (no re-preparing per call). Transactions for atomicreplacePrs/replaceNotificationslive inside.If-None-Match→ cheap 304s when nothing changed.json_validTEXT column, identical to what the existing web client renders today.config.ymlget inserted, removed ones cascade-delete fromprs/notifications/sync_state.CLI
loopcallsengine.start+ waits for SIGINT + callsengine.stop— stop interrupts sleep immediately and drains the in-flight cycle.Validation
Against a local config with two instances (github.com + a GHES instance):
rateLimit.costobserved: 4 on github.com per PR query, lower on the GHES instance. At a 25s-after-finished cadence (~144 cycles/h) that's ~12% of the 5000/h GraphQL bucket on github.com.pnpm sync loop --interval 2+ SIGINT cleanly stops the engine mid-sleep and drains in-flight.Known limitations
Team.slugrequiresread:orgscope, which most PATs don't have. The query drops Team identification and falls back to "any auto-actor event in the timeline" for team-attached PRs. Direct user-level auto-assignment is exact.Retry-Afterparsing or exponential backoff yet. Only the rate-limit floor guard.Test plan
pnpm typecheckclean across all 4 packagespnpm test— 14 (sync) + 145 (server) + 39 (web) = 198 passingpnpm lintcleanpnpm fmt:checkcleanpnpm sync once,pnpm sync once --kind <each>,pnpm sync once --instance <id>,pnpm sync loop --interval <n>with SIGINT,pnpm sync status,pnpm sync wipesrc/index.test.ts) imports only from./index.jsand exercises the full server-adoption flow