feat(standards): MR-based tag strategy + repository inventory (registry://repos)
Versioning standard now documents the MR flow: bump pyproject.toml/VERSION and promote CHANGELOG in the MR (referencing the version/tag, not a commit id), merge/squash, then tag the resulting commit. Adds the agent-assisted tagging procedure (wait for the go-ahead, find the version-bump commit, tag only it, ask the user to push) to both the reference page and the releasing wiki runbook. Adds the repo inventory (src/repos.json -> mcp/repos.json), the registry://repos resource + list_repos tool, the 15_repository_inventory lib page, and two MCP prompts: compliance (remediate repos) and release (agent tagging).
This commit is contained in:
108
registry.py
108
registry.py
@@ -12,6 +12,7 @@ Resources:
|
||||
registry://components (the template component index)
|
||||
registry://drone (the CI template index)
|
||||
registry://release (the release template index)
|
||||
registry://repos (the repository inventory)
|
||||
|
||||
Tools:
|
||||
ping (from the base server)
|
||||
@@ -23,12 +24,15 @@ Tools:
|
||||
get_drone_template (kind -> exact .drone.yml template)
|
||||
list_release_templates (the changelog + version-file artefacts)
|
||||
get_release_template (kind -> exact release template file)
|
||||
list_repos (the covered repositories: type, path, version source)
|
||||
|
||||
Prompts:
|
||||
intake (the questions to gather before building)
|
||||
build (the two axes -> deterministic build prompt)
|
||||
start_app (combined entry: intake, then build)
|
||||
trial (self-contained trial build into a scratch dir)
|
||||
compliance (remediate repos against the CI + versioning standard)
|
||||
release (agent-assisted tagging after the release MR is merged)
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -94,6 +98,45 @@ def _build_text(app_name: str, resources: str, pipelines: str, auth: str) -> str
|
||||
)
|
||||
|
||||
|
||||
_COMPLIANCE_TEXT = (
|
||||
"# Compliance — align repositories with the CI + versioning standard\n\n"
|
||||
"Read `docs://modules/drone_pipelines` and `docs://modules/versioning_and_changelog`,\n"
|
||||
"then enumerate the working set with `registry://repos` (or `list_repos()`).\n\n"
|
||||
"For each repository, bring it to the standard for its `type`:\n\n"
|
||||
"- **package** — `version` in `pyproject.toml`; root `CHANGELOG.md`; Drone file\n"
|
||||
" from `get_drone_template(\"drone-pypi\")` (`python-lib.yaml`, tag-triggered).\n"
|
||||
"- **service / site** — root `VERSION`; root `CHANGELOG.md`; Drone file from\n"
|
||||
" `get_drone_template(\"drone-service\")` (`service-docker.yaml`; tag-triggered\n"
|
||||
" service variant, push-triggered site variant).\n\n"
|
||||
"Steps:\n"
|
||||
"1. Inspect each repo (`git -C <path> tag`, version file, `CHANGELOG.md`,\n"
|
||||
" `.drone.yml`) and list the gaps — never assume.\n"
|
||||
"2. Add missing files from `get_release_template(\"changelog\"/\"version-file\")`.\n"
|
||||
"3. Replace `.drone.yml` with the guarded template for the repo's type.\n"
|
||||
"4. Align the version file with the intended tag.\n"
|
||||
"5. Commit per repo (one focused commit); **do not push or tag** without the\n"
|
||||
" user's explicit go-ahead.\n"
|
||||
)
|
||||
|
||||
_RELEASE_TEXT = (
|
||||
"# Release — tag the merged commit\n\n"
|
||||
"The version bump and changelog promotion live **in the release merge\n"
|
||||
"request**; the tag is created on the commit that results from merging it.\n"
|
||||
"See `docs://modules/versioning_and_changelog`.\n\n"
|
||||
"Do this only after the user confirms the MR is merged/squashed and tells you\n"
|
||||
"to proceed — never tag before the merge.\n\n"
|
||||
"1. Wait for that explicit go-ahead.\n"
|
||||
"2. Find the commit that introduced the version bump `x.y.z`:\n"
|
||||
" - package: `git log -S 'version = \"x.y.z\"' -n 1 --format='%H %s' -- pyproject.toml`\n"
|
||||
" - service / site: `git log -S 'x.y.z' -n 1 --format='%H %s' -- VERSION`\n"
|
||||
"3. Verify that commit's version file equals `x.y.z` (the Drone guard enforces\n"
|
||||
" `tag == version`).\n"
|
||||
"4. Tag **only** that commit, lightweight and bare: `git tag x.y.z <sha>`.\n"
|
||||
"5. **Do not push.** Ask the user to run `git push origin x.y.z`, then Drone\n"
|
||||
" builds the release.\n"
|
||||
)
|
||||
|
||||
|
||||
class StandardsMCPServer(MCPServer):
|
||||
"""MCP server for the `standards` bundle with discovery/scaffolding tools.
|
||||
|
||||
@@ -146,6 +189,13 @@ class StandardsMCPServer(MCPServer):
|
||||
return {"error": "not_found", "path": str(path), "components": []}
|
||||
return json.loads(path.read_bytes())
|
||||
|
||||
def _repos(self) -> dict[str, Any]:
|
||||
"""Load the repository inventory."""
|
||||
path = Path(self.mcp_root) / "repos.json"
|
||||
if not path.exists():
|
||||
return {"error": "not_found", "path": str(path), "repos": []}
|
||||
return json.loads(path.read_bytes())
|
||||
|
||||
def _find_release_component(self, kind: str) -> dict[str, Any] | None:
|
||||
for entry in self._release().get("components", []):
|
||||
if entry.get("kind") == kind:
|
||||
@@ -258,9 +308,19 @@ class StandardsMCPServer(MCPServer):
|
||||
return self._drone()
|
||||
|
||||
@self.app.resource("registry://release")
|
||||
def release() -> dict[str, Any]:
|
||||
def release_templates() -> dict[str, Any]:
|
||||
return self._release()
|
||||
|
||||
@self.app.resource("registry://repos")
|
||||
def repos() -> dict[str, Any]:
|
||||
"""Return the repository inventory.
|
||||
|
||||
Use this to enumerate the repositories covered by the CI and
|
||||
versioning standards, each with its type, path, version source, and
|
||||
Drone shape. Paths are relative to the aetos/ workspace root.
|
||||
"""
|
||||
return self._repos()
|
||||
|
||||
@self.app.resource("docs://spec")
|
||||
def spec() -> dict[str, Any]:
|
||||
"""Return the pinned auth-server OpenAPI contract.
|
||||
@@ -452,6 +512,27 @@ class StandardsMCPServer(MCPServer):
|
||||
}
|
||||
return self._read_release_component(entry)
|
||||
|
||||
@self.app.tool()
|
||||
def list_repos(type: str | None = None) -> list[dict[str, Any]]:
|
||||
"""List the repositories covered by the CI and versioning standards.
|
||||
|
||||
Each entry has: ``name``, ``type`` ("package" | "service" | "site"),
|
||||
``path`` (relative to the aetos/ workspace root), ``version_source``
|
||||
("pyproject.toml" | "VERSION"), ``drone`` (the template file),
|
||||
``trigger`` ("tag" | "push"), ``docs`` kinds, and ``mcp``.
|
||||
|
||||
Args:
|
||||
type (str | None): Optional filter, e.g. "package" or "site".
|
||||
|
||||
Returns:
|
||||
list[dict[str, Any]]: Matching repository entries.
|
||||
"""
|
||||
repos = self._repos().get("repos", [])
|
||||
if type:
|
||||
needle = type.strip().lower()
|
||||
return [r for r in repos if str(r.get("type", "")).lower() == needle]
|
||||
return repos
|
||||
|
||||
@self.app.prompt()
|
||||
def intake() -> str:
|
||||
"""Return the questions to gather before building an app.
|
||||
@@ -544,3 +625,28 @@ class StandardsMCPServer(MCPServer):
|
||||
"back so the standard can be amended.\n\n"
|
||||
f"Delete `{target}` once you are satisfied.\n"
|
||||
)
|
||||
|
||||
@self.app.prompt()
|
||||
def compliance() -> str:
|
||||
"""Return the procedure to align repositories with the standard.
|
||||
|
||||
Use this to audit and remediate the repos listed in
|
||||
`registry://repos` against the Drone and versioning standards.
|
||||
|
||||
Returns:
|
||||
str: The compliance/remediation prompt.
|
||||
"""
|
||||
return _COMPLIANCE_TEXT
|
||||
|
||||
@self.app.prompt()
|
||||
def release() -> str:
|
||||
"""Return the agent-assisted tagging procedure.
|
||||
|
||||
Use after the user confirms the release MR is merged/squashed: find
|
||||
the commit that bumped the version file, tag only that commit, and
|
||||
ask the user to push the tag.
|
||||
|
||||
Returns:
|
||||
str: The release/tagging prompt.
|
||||
"""
|
||||
return _RELEASE_TEXT
|
||||
|
||||
Reference in New Issue
Block a user