Skip to main content
Glama
README.md
# 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

A3.7/5.0

Scored across 12 tools

Disambiguation5/5

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.

Naming Consistency3/5

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.

Tool Count5/5

12 tools is well-scoped for a release management server. Each tool serves a necessary step in the release lifecycle without unnecessary duplication.

Completeness5/5

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.

Maintenance

ActivitySlowing
ResponsivenessSyncing