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 anMCP 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 servicescan pin them, release branches with iterated tags (v1.0.0-1, -2, ...), a QA gate, adeploy 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 whatvariables 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 withoutan 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 thestate repo (reports/<coordinate>.md). - No LLM in the engine — ever.
release explaindeterministically fetches a failedjob's log tail; the model on the client side (Copilot, Claude, ...) interprets it inthe same chat.ports.LogExplainerstays as an extension point if you want a hostedmodel, but nothing requires one.
Install
Requires Python 3.13+ and uv.
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
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" | 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 themodel never guesses which release you mean, and ask to "advance" freely — reconcilenever double-fires, so an over-eager prompt costs nothing.
Adapt to your product
- Copy examples/release-manifest.yaml and describe yourrepos, 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_KINDSinsrc/release_agent/core/edits.py. - Have an LLM endpoint? Implement
ports.LogExplainerand wire it insrc/release_agent/bootstrap.py.
Development
uv run pytest # unit + engine tests against an in-memory fake GitLab
uv run ruff check .
License
MIT