release-mcp
# release-agent
Deterministic, **non-blocking** release preparation across a fleet of GitLab repositories —
a reconciler engine over a **declarative manifest**, exposed as a CLI (`release`) and an
**MCP server** (`release-mcp`) so you can drive releases from GitHub Copilot Chat, Claude,
or any other MCP client in plain language.
Built for the reality of multi-repo products: libraries that must publish before services
can pin them, release branches with iterated tags (`v1.0.0-1`, `-2`, ...), a QA gate, a
deploy repo that pins the final versions — and CI pipelines that take 40–50 minutes.
The engine never waits: every command returns in seconds, pipelines run on their own time,
and an idempotent `reconcile` advances whatever is ready whenever you come back.
## How it works
- **Manifest** (`release-manifest.yaml`) — declares your repos, their dependency DAG,
which files to edit (maven properties, python dependencies, yaml keys), and what
variables each pipeline needs. See [examples/release-manifest.yaml](examples/release-manifest.yaml).
The engine hard-codes zero product knowledge — adoption is pure configuration.
- **Run state** — one JSON per release, stored in a small dedicated GitLab project.
Shared visibility, full history, optimistic locking; any teammate can resume any release.
- **Reconciler** — walks the DAG; per node: create branch → commit version pins → cut tag →
trigger pipeline (via the API, so per-release variables travel with the build) → poll →
record produced versions → unblock dependents. Nothing is ever created twice.
- **Gates** — a manual checkpoint (e.g. `qa-signoff`) the engine will not pass without
an explicit `release approve`.
- **Captures & report** — manifest-declared regexes grep job logs for values of interest
(Sonar URLs, image digests); `release report` assembles the whole release — tags,
pipelines, versions, captured values — into markdown, optionally published to the
state repo (`reports/<coordinate>.md`).
- **No LLM in the engine — ever.** `release explain` deterministically fetches a failed
job's log tail; the model on the *client* side (Copilot, Claude, ...) interprets it in
the same chat. `ports.LogExplainer` stays as an extension point if you want a hosted
model, but nothing requires one.
## Install
Requires Python 3.13+ and [uv](https://docs.astral.sh/uv/).
```bash
git clone <this repo> && cd release_agent
uv sync
uv run release --help
```
## Configure
Everything is environment variables (per-developer PAT model):
| Variable | Required | Meaning |
|---|---|---|
| `RELEASE_AGENT_GITLAB_URL` | yes | GitLab base URL, e.g. `https://gitlab.example.com` |
| `RELEASE_AGENT_GITLAB_TOKEN` | yes | PAT with `api` scope (falls back to `GITLAB_TOKEN`) |
| `RELEASE_AGENT_BOT_TOKEN` | no | Second identity used to approve MRs on repos with `merge_request: { bot_approve: true }` |
| `RELEASE_AGENT_STATE_PROJECT` | yes* | Project holding run states + manifest, e.g. `group/release-state` |
| `RELEASE_AGENT_STATE_BRANCH` | no | Branch in the state project (default `main`) |
| `RELEASE_AGENT_MANIFEST_PATH` | no | Manifest path in the state project (default `release-manifest.yaml`) |
| `RELEASE_AGENT_MANIFEST_FILE` | no | Local manifest file (overrides the state project copy) |
| `RELEASE_AGENT_STATE_DIR` | yes* | Local state directory instead of a state project (single-user/dev) |
\* one of `STATE_PROJECT` / `STATE_DIR` is required.
## Use — CLI
```bash
release start 1.0.0 --env sit \
-i jar_bundle_1_version=1.0.1 -i pydantic_version=10.0.0 -i jar_bundle_2_version=2.3.0
release status 1.0.0 # render the DAG, tags, pipeline URLs
release reconcile 1.0.0 # idempotent tick — run it whenever, it never double-fires
release bump 1.0.0 service-a # next vX.Y.Z-(N+1) after a QA fix
release approve 1.0.0 qa-signoff # clear the manual gate
release explain 1.0.0 service-a # advisory: why did the pipeline fail?
```
## Use — MCP (Copilot Chat, Claude, ...)
Register the stdio server with your client, e.g. VS Code `.vscode/mcp.json`:
```json
{
"servers": {
"release-agent": {
"type": "stdio",
"command": "uv",
"args": ["run", "--directory", "/path/to/release_agent", "release-mcp"],
"env": {
"RELEASE_AGENT_GITLAB_URL": "https://gitlab.example.com",
"RELEASE_AGENT_GITLAB_TOKEN": "${input:gitlab-token}",
"RELEASE_AGENT_STATE_PROJECT": "group/release-state"
}
}
}
}
```
Then just talk: *"start release 1.0.0 on sit with jar-bundle-2 2.3.0, jar-bundle-1 jar 1.0.1,
pydantic 10.0.0"* → the model calls `release_start`; later, *"advance the release"* →
`release_reconcile`. Tools exposed: `release_start`, `release_status`, `release_reconcile`,
`release_approve`, `release_bump`, `release_explain`, `get_manifest`.
### Prompt cookbook
| You say | Tool the model calls |
|---|---|
| "Start release 1.2.0 on sit — jar-bundle-1 jar 1.0.2, pydantic 10.1.0, jar-bundle-2 2.4.0" | `release_plan` (dry-run shown for confirmation) → `release_start` |
| "What exactly would starting 1.2.0 do?" | `release_plan` — branches, MRs, predicted tags, edits, variables; nothing touched |
| "Where's release 1.2.0?" / "Did the jar-bundle-2 build finish?" | `release_status` |
| "Advance the release" / "Pipelines look done, continue" | `release_reconcile` (idempotent — always safe) |
| "Why is service-a stuck?" | `release_status` → explains e.g. an `AWAITING_MERGE` MR with its link |
| "QA signed off, approve the release" | `release_approve` |
| "Why did service-b fail?" | `release_explain` → model interprets the failed job's log |
| "Fix is merged on service-b's release branch, rebuild it" | `release_bump` (warns if the deploy repo pinned the old tag) |
| "Which files get edited during a release? What depends on what?" | `get_manifest` |
| "What was the last released version of service-a?" | `release_tags` — reads the repo's tags live, no run state needed |
| "What releases are in flight?" / "What did we ship last?" | `list_releases` — all coordinates with progress + attention flags |
| "Here's my updated manifest — is it valid?" | `validate_manifest` — full validation before you commit it |
| "Take 1.2.0 as far as it can go and tell me what's blocking" | chains reconcile → status → summary |
| "Give me the release report" / "…and publish it" | `release_report` — tags, pipelines, versions, log-captured values (e.g. Sonar URLs) |
Habits that keep it reliable: **always name the coordinate** ("release 1.2.0") so the
model never guesses which release you mean, and ask to "advance" freely — reconcile
never double-fires, so an over-eager prompt costs nothing.
## Adapt to your product
1. Copy [examples/release-manifest.yaml](examples/release-manifest.yaml) and describe your
repos, tiers, edits, and pipeline variables.
2. Create a `release-state` project in GitLab, commit the manifest there.
3. Gate your CI release/publish jobs to run only from API-triggered tag pipelines
(`$CI_COMMIT_TAG && $CI_PIPELINE_SOURCE == "api"` — purely additive, design §9).
On GitLab 17.7+ also set each project's *"Minimum role to use pipeline variables"*
to `developer`, or API-triggered pipelines with variables are rejected with HTTP 400.
4. Need a new file mutation? Add one function to `EDIT_KINDS` in
[src/release_agent/core/edits.py](src/release_agent/core/edits.py).
5. Have an LLM endpoint? Implement `ports.LogExplainer` and wire it in
[src/release_agent/bootstrap.py](src/release_agent/bootstrap.py).
## Development
```bash
uv run pytest # unit + engine tests against an in-memory fake GitLab
uv run ruff check .
```
## License
MIT
TDQS
Scored across 12 tools
Each tool has a clearly distinct purpose: manifest retrieval, release listing, approval, bumping, explanation, planning, reconciliation, reporting, starting, status, tags, and validation. No two tools overlap in intent.
Most tools follow the 'release_verb' pattern, but three (get_manifest, list_releases, validate_manifest) lack the prefix, creating inconsistency. The naming is still descriptive but not uniform.
12 tools is well-scoped for a release management server. Each tool serves a necessary step in the release lifecycle without unnecessary duplication.
The tool set covers the full release workflow: planning, starting, approving, bumping, reconciling, reporting, status checking, and tag fetching. Validation and explanation tools add robustness. No obvious gaps for common release operations.