Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
189 changes: 189 additions & 0 deletions .github/workflows/release-cadence.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
name: Release Cadence

on:
schedule:
- cron: '0 15 * * 2' # Tuesdays 15:00 UTC
workflow_dispatch:

permissions:
contents: read
issues: write

env:
GLAMA_SERVER_SLUG: ${{ github.repository }}

jobs:
release-cadence:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6

- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6
with:
python-version: "3.12"

- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: "20"
cache: "npm"
cache-dependency-path: mcp-server/package-lock.json

- name: Build release cadence report
run: |
python - <<'PY'
import datetime as dt
import json
import os
import re
import subprocess
import urllib.error
import urllib.request
from pathlib import Path

today = dt.date.today()
monthly_sdk_due = today.day <= 7

def run(command: list[str], timeout: int = 45) -> tuple[bool, str]:
try:
result = subprocess.run(
command,
check=False,
capture_output=True,
text=True,
timeout=timeout,
)
except (OSError, subprocess.TimeoutExpired) as exc:
return False, str(exc)

output = (result.stdout or result.stderr or "").strip()
return result.returncode == 0, output

def read_sdk_version() -> str:
pyproject = Path("sdk/pyproject.toml").read_text(encoding="utf-8")
match = re.search(r'^version = "([^"]+)"$', pyproject, re.MULTILINE)
return match.group(1) if match else "unknown"

def read_mcp_version() -> str:
package = json.loads(Path("mcp-server/package.json").read_text(encoding="utf-8"))
return str(package.get("version", "unknown"))

def fetch_json(url: str) -> tuple[bool, object | str]:
try:
request = urllib.request.Request(
url,
headers={"User-Agent": "agentguard-release-cadence"},
)
with urllib.request.urlopen(request, timeout=30) as response:
return True, json.loads(response.read().decode("utf-8"))
except (OSError, urllib.error.URLError, json.JSONDecodeError) as exc:
return False, str(exc)

sdk_version = read_sdk_version()
mcp_version = read_mcp_version()
glama_slug = os.environ.get("GLAMA_SERVER_SLUG") or "bmdhodl/agent47"
npm_command = "npm.cmd" if os.name == "nt" else "npm"
npm_ok, npm_latest = run(
[npm_command, "view", "@agentguard47/mcp-server", "version"]
)
pypi_ok, pypi_payload = fetch_json("https://pypi.org/pypi/agentguard47/json")
glama_ok, glama_payload = fetch_json(
f"https://glama.ai/api/mcp/v1/servers/{glama_slug}"
)
Comment thread
bmdhodl marked this conversation as resolved.

pypi_latest = "unknown"
if pypi_ok and isinstance(pypi_payload, dict):
pypi_latest = str(pypi_payload.get("info", {}).get("version", "unknown"))

glama_tools = "unknown"
glama_env = "unknown"
if glama_ok and isinstance(glama_payload, dict):
tools = glama_payload.get("tools")
env = (
glama_payload.get("environmentVariablesJsonSchema")
or glama_payload.get("env")
or glama_payload.get("environment")
)
glama_tools = str(len(tools)) if isinstance(tools, list) else "unknown"
glama_env = "present" if env else "missing-or-unknown"

npm_status = npm_latest if npm_ok else f"unknown ({npm_latest})"
pypi_status = pypi_latest if pypi_ok else f"unknown ({pypi_payload})"
glama_status = (
f"{glama_tools} indexed tools; env schema {glama_env}"
if glama_ok
else f"unknown ({glama_payload})"
)

lines = [
f"# Release cadence report - {today.isoformat()}",
"",
"This issue is generated by `.github/workflows/release-cadence.yml`.",
"It schedules readiness review only. Publishing still requires a human.",
"",
"## Current package state",
"",
f"- SDK repo version: `{sdk_version}`",
f"- PyPI latest: `{pypi_status}`",
f"- MCP repo version: `{mcp_version}`",
f"- npm latest: `{npm_status}`",
f"- Glama API status: `{glama_status}`",
"",
"## MCP / Glama train",
"",
"- Cadence: review every Tuesday.",
"- Ship when MCP runtime, package metadata, tool schema, registry metadata, or Glama state changed.",
"- Hold when npm, MCP Registry, and Glama already match the current package.",
"",
"Checklist:",
"- [ ] Run `npm --prefix mcp-server test`.",
"- [ ] Run `python -m pytest sdk/tests/test_mcp_registry_metadata.py -v`.",
"- [ ] Run `python scripts/sdk_release_guard.py --check-mcp-npm`.",
"- [ ] Verify MCP Registry search reports the current npm version.",
"- [ ] Verify Glama exposes env schema and seven tools.",
"- [ ] Publish a Glama release only after npm and registry state are correct.",
"",
"## SDK train",
"",
f"- Monthly readiness due now: `{'yes' if monthly_sdk_due else 'no'}`",
"- Ship when accumulated runtime, API, docs, examples, or proof changes create user-visible value.",
"- Hotfix immediately for security, broken install, broken publish, or hosted-ingest contract regressions.",
"",
"Checklist:",
"- [ ] Run `python scripts/generate_pypi_readme.py --check`.",
"- [ ] Run `python scripts/sdk_release_guard.py`.",
"- [ ] Run CI-equivalent tests, lint, and bandit.",
"- [ ] Regenerate `sdk/PYPI_README.md` when release notes change.",
"- [ ] Confirm PyPI Trusted Publishing and attestations after publish.",
"",
"## Decision",
"",
"- [ ] MCP train ships this week.",
"- [ ] MCP train holds this week.",
"- [ ] SDK train ships this month.",
"- [ ] SDK train holds this month.",
"",
"Reference: `docs/release/cadence.md`.",
]

Path("release-cadence-report.md").write_text("\n".join(lines) + "\n", encoding="utf-8")
PY

- name: Create or update release queue issue
env:
GH_TOKEN: ${{ github.token }}
run: |
title="[release-cadence] Active release queue"
existing=$(gh issue list --search "\"${title}\" in:title state:open" --limit 1 --json number -q '.[0].number')

if [ -n "$existing" ]; then
gh issue comment "$existing" --body-file release-cadence-report.md
else
gh issue create --title "$title" --body-file release-cadence-report.md
fi

- name: Upload release cadence report
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: release-cadence-report
path: release-cadence-report.md
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,12 @@
repo MCP package version is actually published as npm latest without making
normal CI depend on the network.

### Release Operations
- Added a release cadence document that separates the weekly MCP / Glama
distribution train from the monthly SDK release train.
- Added a scheduled release cadence workflow that opens or updates one active
release queue issue with SDK, npm MCP, and Glama indexing status.

## 1.2.10

### Activation Proof Path
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -407,6 +407,7 @@ assert result.passed
| Managed sessions | [`docs/guides/managed-agent-sessions.md`](docs/guides/managed-agent-sessions.md) |
| Activation metrics design | [`docs/guides/activation-metrics-design.md`](docs/guides/activation-metrics-design.md) |
| Proof gallery | [`docs/examples/proof-gallery.md`](docs/examples/proof-gallery.md) |
| Release cadence | [`docs/release/cadence.md`](docs/release/cadence.md) |
| PyPI Trusted Publishing | [`docs/release/trusted-publishing.md`](docs/release/trusted-publishing.md) |

## Architecture
Expand Down
5 changes: 5 additions & 0 deletions docs/launch/distribution-execution.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,11 @@
This is the shortest path from "repo is ready" to "AgentGuard is in front of
real coding-agent users."

Ongoing release timing lives in
[`../release/cadence.md`](../release/cadence.md). Use this document for the
manual distribution steps, and use the cadence doc to decide when a new MCP,
Glama, or SDK release is actually due.

## Order of Operations

1. Official MCP Registry
Expand Down
110 changes: 110 additions & 0 deletions docs/release/cadence.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
# Release Cadence

AgentGuard has two release tracks:

- `agentguard47` Python SDK on PyPI.
- `@agentguard47/mcp-server` read-only MCP server on npm, MCP Registry, and
Glama.

Keep these tracks separate. Do not bump the SDK just to fix MCP directory
metadata, and do not publish the MCP package just because the SDK shipped.

## Cadence

| Track | Readiness review | Release target | Release when |
|---|---:|---:|---|
| MCP package and directories | Weekly, Tuesday | Same week if needed | `mcp-server/` code, package metadata, tool schema, environment schema, registry metadata, or Glama indexing needs a refresh |
| Python SDK | Monthly, first release-cadence run of the month | Same week if ready | Runtime behavior, public API, docs, examples, or release proof changed enough that users benefit from a new PyPI version |
| Hotfix | As needed | Same day | Security, broken install, broken publish, broken hosted-ingest contract, or a regression in local guard proof |

A no-release decision is valid. The release issue should say why the train is
held instead of forcing a version bump with no user-visible value.

## MCP Release Train

The MCP release train covers npm, MCP Registry, and Glama. Use
[`mcp-publishing.md`](mcp-publishing.md) for the publish commands.

Weekly readiness checks:

```bash
npm view @agentguard47/mcp-server version
node -p "require('./mcp-server/package.json').version"
npm --prefix mcp-server test
python -m pytest sdk/tests/test_mcp_registry_metadata.py -v
python scripts/sdk_release_guard.py --check-mcp-npm
curl "https://glama.ai/api/mcp/v1/servers/bmdhodl/agent47"
```

Release decision:

- Publish npm when `mcp-server/package.json`, `mcp-server/server.json`, runtime
tool behavior, or package files changed.
- Republish MCP Registry metadata after the npm package is live whenever public
registry search lags the current npm package.
- Publish a Glama release after npm and registry verification, using the same
MCP package version.
- If only Glama indexing is stale, do not change SDK or MCP runtime code. Run
the Glama build/test/release flow and record the outcome.

Post-release proof:

- npm latest matches `mcp-server/package.json`.
- `npx -y @agentguard47/mcp-server --help` starts.
- MCP Registry search reports the current npm package version.
- Glama public API exposes the environment schema.
- Glama tool pages render all seven tools: `query_traces`, `get_trace`,
`get_trace_decisions`, `get_alerts`, `get_usage`, `get_costs`, and
`check_budget`.
- `query_traces` keeps an A-grade rendered score page and states that it is
read-only, returns a JSON `traces` array, defaults to `limit=20`, and should
be used to find `trace_id` values before detail calls.

## SDK Release Train

The SDK release train is monthly by default. Patch releases can ship sooner for
hotfixes, but feature releases should wait until there is enough user-visible
value to justify the release.

Monthly readiness checks:

```bash
python scripts/generate_pypi_readme.py --check
python scripts/sdk_release_guard.py
python -m ruff check sdk/agentguard/ scripts/generate_pypi_readme.py scripts/sdk_preflight.py scripts/sdk_release_guard.py
python -m pytest sdk/tests/ -v --cov=agentguard --cov-report=term-missing --cov-fail-under=80
python -m bandit -r sdk/agentguard/ -s B101,B110,B112,B311 -q
```

Before tagging:

- `CHANGELOG.md` has a section for the intended SDK version.
- `sdk/pyproject.toml` matches the intended tag.
- `sdk/PYPI_README.md` is regenerated and checked.
- Hosted ingest proof is run when tracing, `HttpSink`, decision events, or the
dashboard ingest contract changed.
- `ops/03-ROADMAP_NOW_NEXT_LATER.md`, `ops/04-DEFINITION_OF_DONE.md`, and
`memory/state.md` are reviewed for drift.

After publish:

- GitHub Release exists for the tag.
- PyPI exposes the new version.
- PyPI release files show Trusted Publishing provenance and attestations.
- `pip install agentguard47==<version>` works from a clean environment.
- `agentguard doctor`, `agentguard demo`, and `agentguard quickstart` still run
without API keys.

## Scheduled Issue

The `Release Cadence` workflow opens or updates one active release queue issue.
It is a reminder and status collector only. It does not publish packages,
create tags, submit registry metadata, or call Glama admin endpoints.

Use the issue to record:

- current SDK version in repo and PyPI
- current MCP version in repo and npm
- Glama tool indexing status
- whether this week's MCP train ships, holds, or needs manual Glama action
- whether this month's SDK train ships, holds, or needs more proof
5 changes: 5 additions & 0 deletions docs/release/mcp-publishing.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,11 @@
Use this checklist when `mcp-server/package.json` is ahead of the public npm
package.

For timing and release-train ownership, see
[`cadence.md`](cadence.md). The short rule is: review MCP distribution weekly,
but publish only when package metadata, tool behavior, registry state, or Glama
state needs a new release.

The MCP package is separate from the Python SDK release:

- Python SDK: `agentguard47` on PyPI
Expand Down
1 change: 1 addition & 0 deletions scripts/generate_pypi_readme.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
"docs/guides/dashboard-contract.md",
"docs/guides/decision-tracing.md",
"docs/guides/managed-agent-sessions.md",
"docs/release/cadence.md",
"docs/release/trusted-publishing.md",
"examples/sticky_agent_proof.py",
}
Expand Down
1 change: 1 addition & 0 deletions sdk/PYPI_README.md
Original file line number Diff line number Diff line change
Expand Up @@ -409,6 +409,7 @@ assert result.passed
| Managed sessions | [`docs/guides/managed-agent-sessions.md`](https://github.com/bmdhodl/agent47/blob/main/docs/guides/managed-agent-sessions.md) |
| Activation metrics design | [`docs/guides/activation-metrics-design.md`](https://github.com/bmdhodl/agent47/blob/v1.2.10/docs/guides/activation-metrics-design.md) |
| Proof gallery | [`docs/examples/proof-gallery.md`](https://github.com/bmdhodl/agent47/blob/main/docs/examples/proof-gallery.md) |
| Release cadence | [`docs/release/cadence.md`](https://github.com/bmdhodl/agent47/blob/main/docs/release/cadence.md) |
| PyPI Trusted Publishing | [`docs/release/trusted-publishing.md`](https://github.com/bmdhodl/agent47/blob/main/docs/release/trusted-publishing.md) |

## Architecture
Expand Down
11 changes: 11 additions & 0 deletions sdk/tests/test_pypi_readme_sync.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,17 @@ def test_generated_pypi_readme_links_unreleased_sample_incident_to_main() -> Non
)


def test_generated_pypi_readme_links_unreleased_release_cadence_to_main() -> None:
module = _load_generator_module()

content = module.build_pypi_readme(REPO_ROOT)

assert (
"https://github.com/bmdhodl/agent47/blob/main/docs/release/cadence.md"
in content
)


def test_committed_pypi_readme_is_in_sync() -> None:
module = _load_generator_module()

Expand Down