docs: add LLM-operated Gitea workflow runbooks (#17)
Add docs/llm-workflow-runbooks.md — the final roadmap #10 deliverable: operational runbooks for LLM-operated Gitea workflows, built on the shipped canonical profiles + interactive menu + gated review/merge + audit logging. Covers: - Principle: the profile is the role, not the LLM (task-scoped, not assigned). - Canonical config: GITEA_MCP_CONFIG / GITEA_MCP_PROFILE, version, profiles, keychain + env auth references, precedence, legacy env-only fallback. - Interactive menu (python gitea_config.py menu): create author/reviewer profiles, generate Claude/Gemini/Codex launcher snippets, validate auth, check PR reviewer eligibility. - Thin-launcher pattern: LLM configs carry only command/args + the two GITEA_MCP_* vars — never raw tokens/passwords. - Migration away from duplicated GITEA_USER_*/GITEA_PASS_*/GITEA_SITE_* blocks; secrets referenced by keychain id or env var name only. - Per-workflow runbooks (create issue/children, implement+PR, review/request- changes/approve, merge, close-after-merge, stop-on-blocker) with safe prompts. - Fail-closed behavior table (unknown identity/profile, self-author, moved head, unexpected files, detected secrets, production/deploy) and no self-review/merge. Docs-only: no implementation code. Safe placeholder examples only (no real tokens, passwords, usernames, or private config). README links the new runbook. Closes #17. Refs #10. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -182,7 +182,10 @@ Notes:
|
|||||||
`gitea_get_profile` returns the full non-secret profile metadata so a workflow
|
`gitea_get_profile` returns the full non-secret profile metadata so a workflow
|
||||||
can inspect which runtime it is talking to before deciding to act.
|
can inspect which runtime it is talking to before deciding to act.
|
||||||
- See [`docs/gitea-execution-profiles.md`](docs/gitea-execution-profiles.md) for
|
- See [`docs/gitea-execution-profiles.md`](docs/gitea-execution-profiles.md) for
|
||||||
the full profile model.
|
the full profile model, and
|
||||||
|
[`docs/llm-workflow-runbooks.md`](docs/llm-workflow-runbooks.md) for the
|
||||||
|
task-scoped, profile-based runbooks (create/review/merge/close, thin
|
||||||
|
launchers, migration, fail-closed rules).
|
||||||
- **Audit logging (#18):** mutating actions emit a durable, redacted JSON audit
|
- **Audit logging (#18):** mutating actions emit a durable, redacted JSON audit
|
||||||
record — timestamp, action, result (`allowed`/`blocked`/`failed`/`succeeded`),
|
record — timestamp, action, result (`allowed`/`blocked`/`failed`/`succeeded`),
|
||||||
profile name + audit label, authenticated username, target repo/issue/PR,
|
profile name + audit label, authenticated username, target repo/issue/PR,
|
||||||
|
|||||||
@@ -0,0 +1,251 @@
|
|||||||
|
# LLM-Operated Gitea Workflow Runbooks
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
|
||||||
|
Runbooks for the common Gitea workflows an LLM performs through the `gitea-mcp`
|
||||||
|
package of the MCP Control Plane: creating issues, implementing them, opening
|
||||||
|
and reviewing pull requests, merging, and closing out — safely and
|
||||||
|
reproducibly.
|
||||||
|
|
||||||
|
These runbooks are **operational guidance only**. They add no tooling; the
|
||||||
|
behavior they rely on already exists (canonical runtime profiles, the
|
||||||
|
interactive setup menu, identity/eligibility checks, gated review/merge, and
|
||||||
|
audit logging). See [Related documents](#related-documents).
|
||||||
|
|
||||||
|
## Principle: the profile is the role, not the LLM
|
||||||
|
|
||||||
|
```text
|
||||||
|
The LLM is not the role.
|
||||||
|
The MCP execution profile used for the task is the role.
|
||||||
|
```
|
||||||
|
|
||||||
|
An LLM session is never permanently an "author," "reviewer," or "merger." Any
|
||||||
|
session may perform any of these roles — but only while operating through a
|
||||||
|
**task-appropriate profile** whose authenticated Gitea identity and allowed
|
||||||
|
operations fit the task. A task selects a profile; a profile is not assigned to
|
||||||
|
a model. See [`gitea-execution-profiles.md`](gitea-execution-profiles.md).
|
||||||
|
|
||||||
|
Example role-scoped instructions:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Use an author profile to implement issue #N and open a PR.
|
||||||
|
Use any eligible reviewer profile to review PR #N.
|
||||||
|
Use any eligible merger profile to merge PR #N if checks pass.
|
||||||
|
```
|
||||||
|
|
||||||
|
## Prerequisites: canonical config + thin launchers
|
||||||
|
|
||||||
|
Runtime profiles live in **one canonical JSON file**, referenced by every LLM
|
||||||
|
launcher. No client config contains raw credentials.
|
||||||
|
|
||||||
|
### Canonical config file
|
||||||
|
|
||||||
|
Selected by two environment variables:
|
||||||
|
|
||||||
|
- `GITEA_MCP_CONFIG` — path to the canonical file (e.g.
|
||||||
|
`~/.config/gitea-tools/profiles.json`).
|
||||||
|
- `GITEA_MCP_PROFILE` — the named profile to activate.
|
||||||
|
|
||||||
|
Shape (see [`../gitea-mcp.example.json`](../gitea-mcp.example.json)):
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"version": 1,
|
||||||
|
"profiles": {
|
||||||
|
"prgs-reviewer": {
|
||||||
|
"base_url": "https://gitea.example.invalid",
|
||||||
|
"username": "<reviewer-username>",
|
||||||
|
"auth": { "type": "keychain", "id": "prgs-reviewer-token" },
|
||||||
|
"default_owner": "<owner>",
|
||||||
|
"execution_profile": "gitea-reviewer"
|
||||||
|
},
|
||||||
|
"prgs-author": {
|
||||||
|
"base_url": "https://gitea.example.invalid",
|
||||||
|
"username": "<author-username>",
|
||||||
|
"auth": { "type": "env", "name": "GITEA_TOKEN_PRGS_AUTHOR" },
|
||||||
|
"default_owner": "<owner>",
|
||||||
|
"execution_profile": "gitea-author"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- `version` — canonical schema version (currently `1`).
|
||||||
|
- `profiles` — map of profile name → profile.
|
||||||
|
- `auth` — a **reference**, never an inline secret:
|
||||||
|
- **keychain**: `{ "type": "keychain", "id": "<service-id>" }` — the token is
|
||||||
|
read from the macOS keychain on demand.
|
||||||
|
- **env**: `{ "type": "env", "name": "<ENV_VAR_NAME>" }` — the token is read
|
||||||
|
from that environment variable.
|
||||||
|
|
||||||
|
Inline `token`/`password` keys are rejected. Token *values* are never stored in,
|
||||||
|
returned by, or logged from profile metadata. Precedence: explicit process env
|
||||||
|
vars override JSON profile values; the JSON profile fills only what the
|
||||||
|
environment leaves unset. With `GITEA_MCP_CONFIG` unset, behavior is exactly the
|
||||||
|
legacy environment-only mode.
|
||||||
|
|
||||||
|
### Thin launcher pattern
|
||||||
|
|
||||||
|
An LLM MCP launcher (Claude / Gemini / Codex) contains **only** command, args,
|
||||||
|
and the two `GITEA_MCP_*` variables — never a token or password:
|
||||||
|
|
||||||
|
```json
|
||||||
|
"gitea-tools": {
|
||||||
|
"command": "/path/to/Gitea-Tools/venv/bin/python3",
|
||||||
|
"args": ["/path/to/Gitea-Tools/mcp_server.py"],
|
||||||
|
"env": {
|
||||||
|
"GITEA_MCP_CONFIG": "/path/to/.config/gitea-tools/profiles.json",
|
||||||
|
"GITEA_MCP_PROFILE": "prgs-reviewer"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Run the same server as several launcher entries (e.g. `-author`, `-reviewer`,
|
||||||
|
`-merger`), each pointing at a different `GITEA_MCP_PROFILE`.
|
||||||
|
|
||||||
|
## Setup runbook — interactive menu
|
||||||
|
|
||||||
|
Create and manage profiles without hand-editing JSON:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python gitea_config.py menu
|
||||||
|
```
|
||||||
|
|
||||||
|
Menu options: list / add / edit / remove profiles · validate config · test
|
||||||
|
profile authentication · show authenticated user · generate launcher snippets
|
||||||
|
(Claude/Gemini/Codex) · check reviewer eligibility for a PR.
|
||||||
|
|
||||||
|
**Create an author + a reviewer profile:**
|
||||||
|
|
||||||
|
1. `add profile` → name `prgs-author`, base URL, username, default owner/repo,
|
||||||
|
execution profile `gitea-author`, auth type `keychain` or `env`.
|
||||||
|
- keychain: store the token now (hidden prompt); it goes to the keychain
|
||||||
|
under an id like `prgs-author-token` — never into the JSON.
|
||||||
|
- env: record a var name like `GITEA_TOKEN_PRGS_AUTHOR`; set that variable
|
||||||
|
yourself in the environment.
|
||||||
|
2. `add profile` again → name `prgs-reviewer`, execution profile
|
||||||
|
`gitea-reviewer`. Existing profiles are preserved.
|
||||||
|
3. `validate config` → confirm no problems.
|
||||||
|
4. `generate launcher snippets` → paste the printed snippet into each LLM
|
||||||
|
client's MCP config (it contains no secret).
|
||||||
|
5. `test profile authentication` → prints the resolved Gitea username (the only
|
||||||
|
time an API call is made, and only on request).
|
||||||
|
6. `check reviewer eligibility for a PR` → enter a PR number; prints the
|
||||||
|
authenticated user, the PR author, and `ELIGIBLE` / `INELIGIBLE`. Read-only —
|
||||||
|
it never approves or merges.
|
||||||
|
|
||||||
|
## Migration runbook — away from duplicated credential blocks
|
||||||
|
|
||||||
|
Old setups duplicated `GITEA_USER_*`, `GITEA_PASS_*`, and `GITEA_SITE_*` across
|
||||||
|
every LLM's `mcp_config.json` — duplicating profiles and exposing secrets.
|
||||||
|
|
||||||
|
1. For each instance/role, create one canonical profile (menu → `add profile`),
|
||||||
|
storing the secret in the keychain or an env var and referencing it by
|
||||||
|
id/name only.
|
||||||
|
2. `validate config`, then `test profile authentication` for each profile.
|
||||||
|
3. Replace each LLM's server block with the thin launcher (command + args +
|
||||||
|
`GITEA_MCP_CONFIG` + `GITEA_MCP_PROFILE`).
|
||||||
|
4. Delete the `GITEA_USER_*` / `GITEA_PASS_*` / `GITEA_SITE_*` blocks from every
|
||||||
|
LLM config.
|
||||||
|
5. Rotate any token that previously sat in a client config.
|
||||||
|
|
||||||
|
Legacy environment-only setups keep working unchanged until migrated.
|
||||||
|
|
||||||
|
## Workflow runbooks
|
||||||
|
|
||||||
|
Each runbook names the **profile role** it runs under, the steps, and a safe
|
||||||
|
prompt. Confirm the active profile first (`gitea_get_profile` / `gitea_whoami`).
|
||||||
|
|
||||||
|
### Create an issue / child issues
|
||||||
|
|
||||||
|
- **Profile:** issue-manager or author (any profile allowed to create issues).
|
||||||
|
- **Steps:** create the parent/roadmap issue; create child issues; apply the
|
||||||
|
minimal label set; link children to the parent.
|
||||||
|
- **Prompt:** `Using the issue-manager profile, create issue "<title>" with body
|
||||||
|
<body>, then create child issues for <list> and link them to the parent.`
|
||||||
|
|
||||||
|
### Implement an issue and open a PR
|
||||||
|
|
||||||
|
- **Profile:** author.
|
||||||
|
- **Steps:** claim the issue (`status:in-progress`); branch from latest
|
||||||
|
`master` (`feat/issue-<n>-...` / `fix/...` / `docs/...`); implement narrowly;
|
||||||
|
add/update tests if behavior changes; run the full suite; commit with an
|
||||||
|
issue-linked message; open a PR to `master`. **Do not** review or merge your
|
||||||
|
own PR.
|
||||||
|
- **Prompt:** `Use an author profile to implement issue #N and open a PR to
|
||||||
|
master. Do not self-review or self-merge.`
|
||||||
|
|
||||||
|
### Review a PR / request changes / approve
|
||||||
|
|
||||||
|
- **Profile:** reviewer (must be allowed to review/approve/request_changes, and
|
||||||
|
must **not** be the PR author).
|
||||||
|
- **Steps:** confirm identity + eligibility (menu eligibility check or
|
||||||
|
`gitea_check_pr_eligibility`); read the diff; confirm scope matches the linked
|
||||||
|
issue; post the review (`comment` / `request_changes` / `approve`) via the
|
||||||
|
gated review tool. Pin the reviewed head SHA where supported.
|
||||||
|
- **Prompt:** `Use any eligible reviewer profile to review PR #N. Approve only
|
||||||
|
if scope matches issue #M and checks pass; otherwise request changes.`
|
||||||
|
|
||||||
|
### Merge a PR
|
||||||
|
|
||||||
|
- **Profile:** merger (allowed to merge; must **not** be the PR author).
|
||||||
|
- **Steps:** confirm eligibility; require explicit confirmation
|
||||||
|
(`MERGE PR <n>`); optionally pin head SHA / changed-file set; merge only when
|
||||||
|
Gitea reports the PR mergeable (branch-protection checks satisfied). No force,
|
||||||
|
no ignore-checks.
|
||||||
|
- **Prompt:** `Use any eligible merger profile to merge PR #N if checks pass and
|
||||||
|
it is mergeable. Confirm with "MERGE PR N". Do not force-merge.`
|
||||||
|
|
||||||
|
### Close the issue after merge
|
||||||
|
|
||||||
|
- **Profile:** issue-manager or merger.
|
||||||
|
- **Steps:** verify remote `master` actually contains the merge; close the
|
||||||
|
issue (or rely on a `Closes #N` keyword); release `status:in-progress`;
|
||||||
|
clean up merged branches.
|
||||||
|
- **Prompt:** `After confirming master contains the merge of PR #N, close issue
|
||||||
|
#M and delete the merged branch.`
|
||||||
|
|
||||||
|
### Stop on blocker
|
||||||
|
|
||||||
|
- **Any profile.** If a required gate cannot be satisfied — identity
|
||||||
|
unverifiable, ineligible profile, self-authored PR, moved head, unexpected
|
||||||
|
files, detected secret, or any production/deploy behavior — **stop, report the
|
||||||
|
blocker, and take no mutating action.** Fail closed; never work around a gate.
|
||||||
|
|
||||||
|
## Fail-closed behavior
|
||||||
|
|
||||||
|
Before any mutating action the workflow verifies identity, active profile,
|
||||||
|
requested operation, target repo, target issue/PR, and (for review/merge) the PR
|
||||||
|
author. If any check cannot be satisfied, it **fails closed** — no mutation:
|
||||||
|
|
||||||
|
| Condition | Result |
|
||||||
|
|-----------|--------|
|
||||||
|
| Authenticated identity cannot be verified | blocked |
|
||||||
|
| Unknown / unconfigured profile | blocked |
|
||||||
|
| Profile not allowed the requested operation | blocked |
|
||||||
|
| Authenticated user **is** the PR author (approve/merge) | blocked (no self-review/-merge) |
|
||||||
|
| PR head SHA changed since review | blocked |
|
||||||
|
| PR's changed-file set differs from expected | blocked |
|
||||||
|
| PR not open, or Gitea reports it not mergeable | blocked |
|
||||||
|
| Secret / token detected in content | blocked |
|
||||||
|
| Production / deploy / Ops behavior requested | blocked (out of scope for gitea-mcp) |
|
||||||
|
|
||||||
|
All mutating attempts — allowed, blocked, failed, or succeeded — are audit-logged
|
||||||
|
with the profile and authenticated user when `GITEA_AUDIT_LOG` is set (see
|
||||||
|
[`safety-model.md`](safety-model.md)).
|
||||||
|
|
||||||
|
## Safety notes
|
||||||
|
|
||||||
|
- Never place raw tokens or passwords in any LLM MCP config; reference secrets
|
||||||
|
by keychain id or env var name only.
|
||||||
|
- Never self-review or self-merge; never bypass Gitea branch protections.
|
||||||
|
- No Jenkins / Ops / deploy / production behavior in `gitea-mcp`.
|
||||||
|
|
||||||
|
## Related documents
|
||||||
|
|
||||||
|
- [`gitea-execution-profiles.md`](gitea-execution-profiles.md) — the profile model.
|
||||||
|
- [`safety-model.md`](safety-model.md) — trust boundaries and audit logging.
|
||||||
|
- [`tool-boundaries.md`](tool-boundaries.md) — per-tool allowed operations.
|
||||||
|
- [`credential-isolation.md`](credential-isolation.md) — credential handling.
|
||||||
|
- [`release-workflows.md`](release-workflows.md) — release/merge workflow.
|
||||||
|
- [`../README.md`](../README.md) — canonical config, thin launchers, the menu.
|
||||||
Reference in New Issue
Block a user