# Git providers and repositories Two resources: - **`gitprovider`** — a connection to a git forge (Gitea today). One per instance/account. Owns the credential. - **`repo`** (CodeRepo) — a repository attached to a project. Either imported from a provider or added by URL. Registering a provider is what makes discovery, import and **merge-on-accept** possible. A repo added by bare URL can be worked in, but nothing can land its pull requests, because there is no API to land them through. ## Registering a forge The token lives in a Secret; the provider references it by name and key. ```sh mcpctl create secret gitea-token --data TOKEN= mcpctl create gitprovider mysources \ --url https://mysources.co.uk \ --secret gitea-token --key TOKEN ``` Registration **probes the credential** — a wrong URL or an under-scoped token fails here with a clear message, rather than later at discovery where it looks like "the forge has no repos". Use `--skip-probe` to wire config before the infrastructure exists. The token needs repository scope. Discovery deliberately calls `/repos/search` rather than `/user/repos`, because the latter requires `read:user`, which the repo-scoped tokens people actually create for automation do not carry. ## Discover and import ```sh mcpctl repos discover mysources # everything visible mcpctl repos discover mysources -q mcp --limit 20 # filter mcpctl repos discover mysources --project mcpctl # mark what's already imported mcpctl repos import mysources michal/mcpctl michal/lab --project mcpctl mcpctl repos import mysources michal/mcpctl --project mcpctl --ssh ``` Discovery stores nothing — it is a live read. Import creates CodeRepo rows. Import is partial by design: it takes what it can and reports what it skipped with reasons, rather than failing the batch because one repo is empty or archived. An empty repo is flagged unavailable — there is nothing to clone. Imported repos carry **denormalized provenance** (`sourceProvider`, `sourceFullName`) and store no credential of their own — they inherit the forge's. Deleting a provider leaves its repos in place with their provenance intact, so where a repo came from outlives the connection. To attach a repo without a provider: ```sh mcpctl create repo mcpctl --project mcpctl \ --url https://mysources.co.uk/michal/mcpctl.git --branch main ``` ```sh mcpctl get repos mcpctl get gitproviders mcpctl describe repo mcpctl ``` ## Merge on accept When a task carrying a PR is accepted, mcpctl merges it through the repo's provider — see [epics.md](epics.md#the-human-gate). Two refusals matter: - **Stale head.** The accept records which commit was reviewed. If someone pushed after that, the merge is refused rather than landing code nobody approved. Re-review the current head and accept again. - **Conflict.** Reported as-is; the task stays un-merged. A failure always leaves the task un-merged and says why. Nothing reports success the board cannot back up. Repos with no `gitProviderId` cannot be merged through — the accept says so and tells you to import from a provider or merge in the forge. ## Post-merge failure CI can fail *after* a merge lands. Without a way to hear about it, the board keeps claiming `merged` for work that broke the build. The webhook closes that loop by moving the task to `reopened`. Add the signing secret to the provider's Secret and point the provider at it: ```sh mcpctl create secret gitea-token \ --data TOKEN= --data WEBHOOK= mcpctl create gitprovider mysources \ --url https://mysources.co.uk \ --secret gitea-token --key TOKEN \ --webhook-secret-key WEBHOOK ``` Then in the forge, add a webhook on **commit status** events: ``` URL: https:///api/v1/webhooks/git/mysources Content: application/json Secret: ``` > **It fails closed.** The endpoint is unauthenticated by nature — a forge calls > it — so it accepts nothing unless the provider has a signing key configured > *and* the signature verifies. **A provider without `--webhook-secret-key` > refuses every delivery**, which means post-merge failures silently never > reopen anything. If you want this loop, you must set the key. Accepted signature headers are `X-Gitea-Signature` and `X-Hub-Signature-256` (HMAC-SHA256 over the raw body, `sha256=` prefix optional), compared in constant time. Matching is precise, not approximate: a failing status carries a commit sha, and the accept path recorded exactly which commit each merge produced. A failure on a commit mcpctl did not merge is ignored — it is somebody else's business, and guessing at "whatever merged most recently" would reopen innocent work. Only `failure` and `error` states do anything. Success and pending are acknowledged and ignored. ## RBAC | Resource | Verbs | |---|---| | `gitproviders` | `view`, `create`, `edit`, `delete`, **`run`** | | `repos` | `view`, `create`, `edit`, `delete` | `run:gitproviders` covers discovery, import and probe. Those reach out to a third-party system with a stored credential, so they are `run`, not `view` — holding `view:gitproviders` should not let you drive the forge's API. ```sh mcpctl create rbac forge-user --role run --resource gitproviders ``` ## See also - [epics.md](epics.md) — the work items whose PRs get merged here - [agent-authority.md](agent-authority.md) — whose authority an agent acts under - [secret-backends.md](secret-backends.md) — where the forge credential lives