Skip to main content
Glama

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 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.

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 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 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.

  5. Have an LLM endpoint? Implement ports.LogExplainer and 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