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. 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 explicitrelease approve.Captures & report — manifest-declared regexes grep job logs for values of interest (Sonar URLs, image digests);
release reportassembles 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 explaindeterministically fetches a failed job's log tail; the model on the client side (Copilot, Claude, ...) interprets it in the same chat.ports.LogExplainerstays as an extension point if you want a hosted model, but nothing requires one.
Install
Requires Python 3.13+ and uv.
git clone <this repo> && cd release_agent
uv sync
uv run release --helpConfigure
Everything is environment variables (per-developer PAT model):
Variable | Required | Meaning |
| yes | GitLab base URL, e.g. |
| yes | PAT with |
| no | Second identity used to approve MRs on repos with |
| yes* | Project holding run states + manifest, e.g. |
| no | Branch in the state project (default |
| no | Manifest path in the state project (default |
| no | Local manifest file (overrides the state project copy) |
| yes* | Local state directory instead of a state project (single-user/dev) |
* one of STATE_PROJECT / STATE_DIR is required.
Use — CLI
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:
{
"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" |
|
"What exactly would starting 1.2.0 do?" |
|
"Where's release 1.2.0?" / "Did the jar-bundle-2 build finish?" |
|
"Advance the release" / "Pipelines look done, continue" |
|
"Why is service-a stuck?" |
|
"QA signed off, approve the release" |
|
"Why did service-b fail?" |
|
"Fix is merged on service-b's release branch, rebuild it" |
|
"Which files get edited during a release? What depends on what?" |
|
"What was the last released version of service-a?" |
|
"What releases are in flight?" / "What did we ship last?" |
|
"Here's my updated manifest — is it valid?" |
|
"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" |
|
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
Copy examples/release-manifest.yaml and describe your repos, tiers, edits, and pipeline variables.
Create a
release-stateproject in GitLab, commit the manifest there.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" todeveloper, or API-triggered pipelines with variables are rejected with HTTP 400.Need a new file mutation? Add one function to
EDIT_KINDSin src/release_agent/core/edits.py.Have an LLM endpoint? Implement
ports.LogExplainerand wire it in src/release_agent/bootstrap.py.
Development
uv run pytest # unit + engine tests against an in-memory fake GitLab
uv run ruff check .License
MIT