feat(mcpd): CodeRepo + GitProvider — register a forge, discover and import repos
Two resources landing together, because a repo you have to hand-type is a repo nobody adds. CodeRepo is a git repository attached to a project. GitProvider is a configured forge instance you register once — URL and credential in one place — and then list and import from. Nothing above the driver layer knows what a Gitea is. src/mcpd/src/services/git/ holds the GitProviderDriver interface, the Gitea implementation and a registry that resolves by type, mirroring LlmAdapterRegistry.build. Adding GitHub or GitLab is one file plus one case. The instance URL always comes from the GitProvider row; there is no hardcoded host anywhere. Follows the mcpservertemplates convention: a declarative resource applied via apply -f, with instances keeping denormalized provenance that outlives the source. CodeRepo stores sourceProvider + sourceFullName alongside the FK, so "where did this repo come from" stays answerable after the forge is disconnected — the same reasoning as McpServer.templateName/templateVersion. The FK is SetNull, so disconnecting a forge never deletes the repos, or the tasks hanging off them, that came from it. Discovery uses /repos/search, deliberately NOT /user/repos. The latter needs the read:user scope, which repo-scoped automation tokens do not carry — the token in this repo's .env has write:repository and 403s on /user/repos. The driver also passes Gitea's 403 scope message through verbatim, so an under-scoped token says which scope it is missing instead of looking like an empty forge. Import is partial-success by contract: one bad name must not lose the other nineteen. Everything importable is imported; everything skipped comes back with a reason, and a re-import is distinguished from a name collision with an unrelated repo. Imported repos store no credential of their own and inherit the forge's, so rotating one token covers every repo from that forge. Credentials never leave as plaintext: reads return the reference only, and resolveCheckouts() — the single path that produces a token — exists for the session provisioner and is called by no route. References are verified at definition time, so a wrong key fails with "secret has no key 'X'" rather than as a clone that mysteriously cannot authenticate. Clone URLs must be https/ssh/scp-style and checkout paths must be relative with no ".." segment: both are inputs to the clone step, and neither should be able to point it at mcpd's own filesystem or escape the workspace. Tests: 18 CodeRepo + 22 GitProvider unit tests. Gitea driver additionally verified live against https://mysources.co.uk (Gitea 1.25.4, 10 repos discovered, query filter working). Local: build clean, workspace 2440 passed (+40), db 128 passed (same 4 pre-existing Secret failures), lint unchanged at 869. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01N4wNHWf7xSwnZCWpJcyv9p
This commit is contained in:
@@ -2797,9 +2797,9 @@
|
||||
"dependencies": [
|
||||
"119"
|
||||
],
|
||||
"status": "pending",
|
||||
"status": "done",
|
||||
"subtasks": [],
|
||||
"updatedAt": "2026-08-08T08:55:58.627Z"
|
||||
"updatedAt": "2026-08-08T15:42:40.442Z"
|
||||
},
|
||||
{
|
||||
"id": "133",
|
||||
@@ -2811,9 +2811,9 @@
|
||||
"dependencies": [
|
||||
"132"
|
||||
],
|
||||
"status": "pending",
|
||||
"status": "done",
|
||||
"subtasks": [],
|
||||
"updatedAt": "2026-08-08T08:55:58.627Z"
|
||||
"updatedAt": "2026-08-08T15:42:40.452Z"
|
||||
},
|
||||
{
|
||||
"id": "134",
|
||||
@@ -3247,13 +3247,27 @@
|
||||
"status": "pending",
|
||||
"subtasks": [],
|
||||
"updatedAt": "2026-08-08T08:55:58.627Z"
|
||||
},
|
||||
{
|
||||
"id": "163",
|
||||
"title": "GitProvider resource: register a forge, discover and import repos",
|
||||
"description": "Register a git forge instance (Gitea today) as a first-class resource, list its repositories live, and import selected ones into a project as CodeRepo rows.",
|
||||
"details": "Requested mid-epic: registering the instance once means repos are discovered and imported rather than hand-typed, and credentials live in one place.\n\n- Prisma `GitProvider` {name, type, url, credSecretId/Key, ownerId}; `CodeRepo` gains gitProviderId (SetNull) + denormalized sourceProvider/sourceFullName.\n- Driver abstraction in src/mcpd/src/services/git/ (types + gitea.driver + registry), mirroring LlmAdapterRegistry.build(type). Nothing above that layer knows what a Gitea is; the instance URL always comes from the row.\n- GET /api/v1/gitproviders/:id/repos discovers live (nothing stored) and marks rows the target project already has.\n- POST /api/v1/gitproviders/:id/import imports selected repos with partial success + per-repo skip reasons.\n- Discovery uses /repos/search, NOT /user/repos: the latter needs read:user scope which repo-scoped automation tokens do not carry.\n\nFollows the mcpservertemplates convention: declarative resource applied via apply -f, with instances keeping denormalized provenance that outlives the source (cf. McpServer.templateName).",
|
||||
"testStrategy": "22 unit tests in src/mcpd/tests/services/git-provider.test.ts covering driver field mapping, the token auth scheme, 403 scope-message passthrough, pagination, registry dispatch, discovery marking, and import partial-success. Driver verified live against https://mysources.co.uk (Gitea 1.25.4).",
|
||||
"priority": "high",
|
||||
"dependencies": [
|
||||
"132"
|
||||
],
|
||||
"status": "done",
|
||||
"subtasks": [],
|
||||
"updatedAt": "2026-08-08T15:42:14.594Z"
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
"version": "1.0.0",
|
||||
"lastModified": "2026-08-08T15:28:26.705Z",
|
||||
"taskCount": 162,
|
||||
"completedCount": 116,
|
||||
"lastModified": "2026-08-08T15:42:40.452Z",
|
||||
"taskCount": 163,
|
||||
"completedCount": 119,
|
||||
"tags": [
|
||||
"master"
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user