repos: backend — schema, /api/repos routes + GitHub sync provider

Migration 0034: two tables. repo_connections carries the workspace's
per-provider config (owner, base_url, label, last_synced_at,
last_sync_error) and points at an app_connections row for the PAT.
repos is the per-connection cache with (connection_id, external_id)
unique so upsert is idempotent across re-syncs. Cascading deletes clean
up cleanly on connection removal.

cm-secrets grows a FetchAuthorized op — GET with the stored PAT injected
as bearer, returns status + JSON body without ever exposing the
credential to cm-api. This is the least-privilege door for read-only
provider APIs (list repos), distinct from the InvokeHttp path that still
requires a single-use approval grant for outbound writes.

cm-api::routes::repos wires:
- POST /api/repos/connections (broker store_secret + insert both rows +
  initial sync + mark_synced)
- GET /api/repos/connections
- DELETE /api/repos/connections/:id
- POST /api/repos/connections/:id/sync
- GET /api/repos (500 cap, newest provider_updated first)
- GET /api/repos/:id (full detail incl. clone_url + html_url)

GitHub provider inline for v1 — paginated pull of /orgs/:owner/repos
(when owner set) or /user/repos (when absent), 100/page, capped at 20
pages (~2k repos) to keep first-sync latency bounded. Non-2xx surface
back to the caller as sync_error; parse failures are best-effort per
repo (skipped, logged, don't abort the batch).

Gitea + GitLab providers land in a follow-up — mostly URL swap +
response-shape adapter.
This commit is contained in:
Omar Sobh
2026-07-07 14:52:47 -07:00
parent 076f7724ca
commit 6d087bf537
21 changed files with 1532 additions and 5 deletions
@@ -0,0 +1,83 @@
{
"db_name": "PostgreSQL",
"query": "SELECT id, workspace_id, app_connection_id, provider, owner, base_url,\n label, last_synced_at, last_sync_error, created_at, updated_at\n FROM repo_connections\n WHERE id = $1 AND workspace_id = $2",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id",
"type_info": "Uuid"
},
{
"ordinal": 1,
"name": "workspace_id",
"type_info": "Uuid"
},
{
"ordinal": 2,
"name": "app_connection_id",
"type_info": "Uuid"
},
{
"ordinal": 3,
"name": "provider",
"type_info": "Text"
},
{
"ordinal": 4,
"name": "owner",
"type_info": "Text"
},
{
"ordinal": 5,
"name": "base_url",
"type_info": "Text"
},
{
"ordinal": 6,
"name": "label",
"type_info": "Text"
},
{
"ordinal": 7,
"name": "last_synced_at",
"type_info": "Timestamptz"
},
{
"ordinal": 8,
"name": "last_sync_error",
"type_info": "Text"
},
{
"ordinal": 9,
"name": "created_at",
"type_info": "Timestamptz"
},
{
"ordinal": 10,
"name": "updated_at",
"type_info": "Timestamptz"
}
],
"parameters": {
"Left": [
"Uuid",
"Uuid"
]
},
"nullable": [
false,
false,
false,
false,
true,
true,
false,
true,
true,
false,
false
]
},
"hash": "db179364174b07cb5b58d283d9fd1c218bcb43be8d952071e7cd6bc9fb9ad6ca"
}