c4e539c7f7
Automate the documented release-tag checklist (#48) without bypassing safety gates. scripts/release-tag: - Requires a SemVer tag (vMAJOR.MINOR.PATCH); validates before any git/network. - Fetch/prune first, then refuses: dirty worktree, non-master branch, local master != remote master, HEAD not on remote master, and an existing local or remote tag of the same name. - Runs the full suite by default; --skip-tests is an explicit opt-out that warns. - Creates an ANNOTATED tag (git tag -a), never lightweight. - Safe by default: no push unless --push; --dry-run prints planned actions and changes nothing. Supports --notes-file <path> for the annotation message. - Prints: commit, tag, tests_run, tag_created, tag_pushed. - Env injection points for testing/CI: RELEASE_TAG_REMOTE, RELEASE_TAG_TEST_CMD. tests/test_release_tag.py (14 cases): valid SemVer dry-run; invalid version; dirty worktree; non-master; master/remote mismatch; existing tag; missing notes-file; annotated-not-lightweight; no-push-without-flag; push-only-with-flag; notes-file message; --skip-tests warns; default runs tests (fail blocks tag, pass tags). Each test builds a throwaway repo with a LOCAL bare remote (cloned, not pushed) and stubs the test command — no network, no real tags, no pushing from the project repo. Docs: reference scripts/release-tag from the runbook, SKILL, and the release-tag template (script preferred; manual steps are the fallback). Full suite 305 passed / 0 failures; bash -n clean; git diff --check clean; no secrets. Closes #50. Refs #48. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
394 lines
17 KiB
Markdown
394 lines
17 KiB
Markdown
# 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.
|
||
|
||
> For the **project-agnostic** version of these operating rules (issue-first,
|
||
> isolated worktrees, no self-review/merge, profile safety, cleanup, fail-closed)
|
||
> that can be copied into any repository, see the reusable skill
|
||
> [`skills/llm-project-workflow/SKILL.md`](../skills/llm-project-workflow/SKILL.md)
|
||
> and its `templates/`. This runbook is the Gitea-specific application of it.
|
||
|
||
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).
|
||
|
||
For cross-project use, copy the portable workflow skill at
|
||
[`../skills/llm-project-workflow/SKILL.md`](../skills/llm-project-workflow/SKILL.md).
|
||
It extracts the issue-first, isolated-worktree, no-self-review, profile-safety,
|
||
merge-cleanup, fail-closed, and recovery rules into a reusable package that can
|
||
be adapted to other repositories.
|
||
|
||
## 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
|
||
./scripts/gitea-config-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.
|
||
|
||
In a real terminal the menu takes a **single keypress** (no Enter), **Enter**
|
||
quits the main menu and cancels/back-outs of any submenu, and you pick a profile
|
||
from a **numbered list** instead of typing its name. Non-interactive runs
|
||
(pipes/tests) fall back to line input and never block.
|
||
|
||
**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`).
|
||
|
||
## Branch worktree isolation
|
||
|
||
All LLM implementation and review work happens in an isolated branch worktree
|
||
under `branches/`. The main repository checkout is an orchestration checkout:
|
||
use it for status checks, issue creation/claiming, and creating worktrees, but
|
||
do not edit tracked repository files there.
|
||
|
||
**Issue → branch → worktree → PR → cleanup.** Every implementation branch is
|
||
tied to an issue number so the work is traceable end to end:
|
||
|
||
| Stage | Form |
|
||
|-------|------|
|
||
| Issue | `#123` (claimed with `status:in-progress`) |
|
||
| Branch | `(fix\|feat\|docs\|chore)/issue-123-<slug>` (review: `review/pr-456-<slug>`) |
|
||
| Worktree | `branches/fix-issue-123-<slug>` (slashes → hyphens) |
|
||
| PR | body says `Closes #123` (closes) or `Refs #123` (related) |
|
||
| Cleanup | remove remote+local branch + worktree folder; drop `status:in-progress` |
|
||
|
||
`scripts/worktree-start` **rejects** implementation branches that are not
|
||
issue-linked (use `--allow-unlinked` only for genuine exceptions). When claiming,
|
||
post a comment like
|
||
`Claimed. Branch: fix/issue-123-<slug>. Worktree: branches/fix-issue-123-<slug>.`
|
||
Gitea has no native issue→branch API field (only a PR's head branch), so this
|
||
linkage is enforced by branch name + claim comment + PR body + cleanup.
|
||
|
||
Branch folders are ignored by git via `branches/`, so dirty work in one issue
|
||
does not block starting an unrelated issue in a separate branch folder. No LLM
|
||
may edit another issue's branch folder unless explicitly assigned to that issue.
|
||
No LLM may clean another issue's branch folder unless the PR is merged or closed
|
||
and cleanup is explicitly part of the task.
|
||
|
||
Implementation work and review work must use separate branch folders. For
|
||
example, an implementation branch might live under
|
||
`branches/fix-issue-123-example`, while a review branch for the resulting PR
|
||
uses its own folder.
|
||
|
||
Issue creation and claiming may happen from the orchestration checkout:
|
||
|
||
1. Create or identify the tracking issue.
|
||
2. Claim it with `status:in-progress`.
|
||
3. Create the issue branch worktree.
|
||
4. `cd` into the branch worktree and perform all file edits there.
|
||
|
||
Preferred helper:
|
||
|
||
```bash
|
||
scripts/worktree-start fix/issue-123-example
|
||
cd branches/fix-issue-123-example
|
||
```
|
||
|
||
Because `venv/` is ignored and not copied into new worktrees, run checks with a
|
||
known Python interpreter. Either create a venv inside the branch folder, or use
|
||
the orchestration checkout's venv by explicit path.
|
||
|
||
Equivalent manual commands:
|
||
|
||
```bash
|
||
git fetch prgs --prune
|
||
git worktree add -b fix/issue-123-example branches/fix-issue-123-example prgs/master
|
||
cd branches/fix-issue-123-example
|
||
```
|
||
|
||
For review work, create a separate **detached** review worktree instead of
|
||
reusing the author's implementation folder:
|
||
|
||
```bash
|
||
scripts/worktree-review fix/issue-123-example # → branches/review-fix-issue-123-example
|
||
```
|
||
|
||
Cleanup is explicit and only after merge or close. Use the helper (it fetches/
|
||
prunes first, refuses to remove a dirty worktree, and only safe-deletes a merged
|
||
branch), or the equivalent manual commands:
|
||
|
||
```bash
|
||
scripts/worktree-clean --delete-branch fix/issue-123-example
|
||
# equivalent manual commands:
|
||
cd <main-repo>
|
||
git fetch prgs --prune
|
||
git worktree remove branches/fix-issue-123-example
|
||
git branch -d fix/issue-123-example
|
||
```
|
||
|
||
All three helpers accept `--dry-run` to print the exact commands/paths without
|
||
touching anything.
|
||
|
||
### 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`); create an isolated branch
|
||
worktree from latest `master` under `branches/` (`feat/issue-<n>-...` /
|
||
`fix/...` / `docs/...`); `cd` into that worktree; implement narrowly; add or
|
||
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)).
|
||
|
||
## Releases and version tags
|
||
|
||
Versions follow SemVer — **`vMAJOR.MINOR.PATCH`**, using **`v0.x.y`** while
|
||
unstable. Pick the bump by the largest change since the last tag:
|
||
|
||
- **PATCH** — bug fixes, docs, tests, wrappers, non-breaking workflow polish.
|
||
- **MINOR** — new MCP tools, new workflow helpers, new config features;
|
||
backward-compatible behavior.
|
||
- **MAJOR** — breaking config/schema/API behavior or a changed MCP contract.
|
||
|
||
Tags are **annotated** (`git tag -a`), created **only from the exact commit on
|
||
remote `master`**, **only after the full suite passes**, and carry release notes
|
||
referencing the merged PRs/issues. **Never tag** feature branches, dirty
|
||
worktrees, unreviewed or self-authored work, or commits not on remote `master`.
|
||
|
||
Release runbook (see [`../skills/llm-project-workflow/templates/release-tag.md`](../skills/llm-project-workflow/templates/release-tag.md)):
|
||
|
||
1. `git fetch prgs --prune`.
|
||
2. Confirm local `master` equals `prgs/master` (`0 0`) and the tree is clean.
|
||
3. Run the full test suite; stop on failure.
|
||
4. Review merged issues/PRs since the last tag
|
||
(`git log --oneline <last-tag>..prgs/master`).
|
||
5. Choose the version bump.
|
||
6. `git tag -a <vX.Y.Z> prgs/master -m "<notes referencing #issues / PRs>"`.
|
||
7. `git push prgs <vX.Y.Z>`; add release notes if the forge supports it.
|
||
|
||
`scripts/release-tag` automates steps 1–7 with these gates built in (SemVer
|
||
check, fetch/prune, on-master, clean tree, local==remote master, HEAD on remote
|
||
master, no duplicate tag, tests run unless `--skip-tests`, annotated tag only).
|
||
It is **safe by default** — no push unless `--push`, and `--dry-run` changes
|
||
nothing:
|
||
|
||
```bash
|
||
scripts/release-tag --dry-run v0.4.0
|
||
scripts/release-tag v0.4.0 --notes-file /tmp/release-notes.md
|
||
scripts/release-tag v0.4.0 --notes-file /tmp/release-notes.md --push
|
||
```
|
||
|
||
## 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
|
||
|
||
- [`../skills/llm-project-workflow/SKILL.md`](../skills/llm-project-workflow/SKILL.md) — portable cross-project LLM workflow skill.
|
||
- [`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.
|