feat(standards): add versioning + changelog standard, release templates, and Drone version guard
Add lib page 11 (SemVer contract, pre-1.0 rule, tag==version invariant, Keep a Changelog), the releasing wiki runbook, and canonical release templates (CHANGELOG.md, VERSION). Vendor them into the MCP bundle as templates/release + release.json and expose registry://release with list/get tools. Add a changelog component to the app template. Enforce the tag<->version contract in both Drone shapes: Type A check-version and the backfill loop verify pyproject version; Type B adds a VERSION guard step.
This commit is contained in:
79
build.py
79
build.py
@@ -239,6 +239,8 @@ _TEMPLATE_COMPONENTS: list[dict[str, str]] = [
|
|||||||
"description": "OpenAPI contract skeleton."},
|
"description": "OpenAPI contract skeleton."},
|
||||||
{"kind": "pyproject", "path": "pyproject.toml", "placeholder": "",
|
{"kind": "pyproject", "path": "pyproject.toml", "placeholder": "",
|
||||||
"description": "Packaging + dependency pins."},
|
"description": "Packaging + dependency pins."},
|
||||||
|
{"kind": "changelog", "path": "CHANGELOG.md", "placeholder": "",
|
||||||
|
"description": "Keep a Changelog file with an [Unreleased] section (versioning standard)."},
|
||||||
{"kind": "dockerfile", "path": "Dockerfile", "placeholder": "",
|
{"kind": "dockerfile", "path": "Dockerfile", "placeholder": "",
|
||||||
"description": "API + worker image with private-index wiring."},
|
"description": "API + worker image with private-index wiring."},
|
||||||
{"kind": "dockerignore", "path": ".dockerignore", "placeholder": "",
|
{"kind": "dockerignore", "path": ".dockerignore", "placeholder": "",
|
||||||
@@ -412,6 +414,82 @@ def build_drone_templates(folder: Path, dry_run: bool = False) -> None:
|
|||||||
f"({len(manifest['components'])} components)")
|
f"({len(manifest['components'])} components)")
|
||||||
|
|
||||||
|
|
||||||
|
# ── Release standard bundle ───────────────────────────────────────────────────
|
||||||
|
|
||||||
|
# The release artefacts shipped in ``src/templates/release``. One canonical
|
||||||
|
# changelog (Keep a Changelog + SemVer) and the service version file. See
|
||||||
|
# standards "11_versioning_and_changelog".
|
||||||
|
_RELEASE_COMPONENTS: list[dict[str, str]] = [
|
||||||
|
{"kind": "changelog", "path": "CHANGELOG.md", "placeholder": "",
|
||||||
|
"description": "Keep a Changelog skeleton: one [Unreleased] section + compare links."},
|
||||||
|
{"kind": "version-file", "path": "VERSION", "placeholder": "",
|
||||||
|
"description": "Single-line SemVer source of truth for services and sites."},
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def build_release_manifest(template_root: Path) -> dict[str, object]:
|
||||||
|
"""Describe every artefact in the vendored release standard.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
template_root (Path): The vendored ``templates/release`` directory.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict[str, object]: Manifest with the component entries and the
|
||||||
|
tag == version invariant.
|
||||||
|
"""
|
||||||
|
components: list[dict[str, str]] = []
|
||||||
|
for entry in _RELEASE_COMPONENTS:
|
||||||
|
rel = entry["path"]
|
||||||
|
if not (template_root / rel).exists():
|
||||||
|
raise SystemExit(f"[ERR] release template missing: {rel}")
|
||||||
|
components.append({
|
||||||
|
"kind": entry["kind"],
|
||||||
|
"path": rel,
|
||||||
|
"placeholder": entry["placeholder"],
|
||||||
|
"description": entry["description"],
|
||||||
|
})
|
||||||
|
return {
|
||||||
|
"project": "standards",
|
||||||
|
"template": "templates/release",
|
||||||
|
"invariant": "git tag == version file (enforced by the Drone guard)",
|
||||||
|
"components": components,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def build_release_templates(folder: Path, dry_run: bool = False) -> None:
|
||||||
|
"""Vendor ``folder/src/templates/release`` into ``<name>/mcp/templates/release``.
|
||||||
|
|
||||||
|
Ships the canonical ``CHANGELOG.md`` and ``VERSION`` in the MCP bundle so an
|
||||||
|
agent can drop them into a repo, and emits ``release.json`` describing them.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
folder (Path): The hub folder owning ``src/templates/release``.
|
||||||
|
dry_run (bool): Print the plan without executing.
|
||||||
|
"""
|
||||||
|
name = folder.name
|
||||||
|
src = folder / "src" / "templates" / "release"
|
||||||
|
if not src.is_dir():
|
||||||
|
return
|
||||||
|
out_dir = folder / "mcp" / "templates" / "release"
|
||||||
|
if dry_run:
|
||||||
|
print(f"[REL] {name} {src.relative_to(ROOT)}/** -> {out_dir.relative_to(ROOT)}/**")
|
||||||
|
return
|
||||||
|
|
||||||
|
if out_dir.exists():
|
||||||
|
shutil.rmtree(out_dir)
|
||||||
|
out_dir.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
shutil.copytree(
|
||||||
|
src, out_dir,
|
||||||
|
ignore=shutil.ignore_patterns("__pycache__", "*.pyc", ".git"),
|
||||||
|
)
|
||||||
|
manifest = build_release_manifest(out_dir)
|
||||||
|
(folder / "mcp" / "release.json").write_text(
|
||||||
|
json.dumps(manifest, indent=2, ensure_ascii=False), encoding="utf-8"
|
||||||
|
)
|
||||||
|
print(f"[REL] {name} release templates vendored to {out_dir.relative_to(ROOT)}/ "
|
||||||
|
f"({len(manifest['components'])} components)")
|
||||||
|
|
||||||
|
|
||||||
# ── CLI ──────────────────────────────────────────────────────────────────────
|
# ── CLI ──────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
|
||||||
@@ -432,6 +510,7 @@ def main() -> None:
|
|||||||
build_bundle(folder, dry_run=args.dry_run)
|
build_bundle(folder, dry_run=args.dry_run)
|
||||||
build_templates(folder, dry_run=args.dry_run)
|
build_templates(folder, dry_run=args.dry_run)
|
||||||
build_drone_templates(folder, dry_run=args.dry_run)
|
build_drone_templates(folder, dry_run=args.dry_run)
|
||||||
|
build_release_templates(folder, dry_run=args.dry_run)
|
||||||
build_html(folder, dry_run=args.dry_run)
|
build_html(folder, dry_run=args.dry_run)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
72
registry.py
72
registry.py
@@ -11,6 +11,7 @@ Resources:
|
|||||||
registry://libraries (the structured catalog)
|
registry://libraries (the structured catalog)
|
||||||
registry://components (the template component index)
|
registry://components (the template component index)
|
||||||
registry://drone (the CI template index)
|
registry://drone (the CI template index)
|
||||||
|
registry://release (the release template index)
|
||||||
|
|
||||||
Tools:
|
Tools:
|
||||||
ping (from the base server)
|
ping (from the base server)
|
||||||
@@ -20,6 +21,8 @@ Tools:
|
|||||||
get_app_template (the whole app skeleton as a path -> content map)
|
get_app_template (the whole app skeleton as a path -> content map)
|
||||||
list_drone_templates (the two CI shapes: PyPI package / Docker deployment)
|
list_drone_templates (the two CI shapes: PyPI package / Docker deployment)
|
||||||
get_drone_template (kind -> exact .drone.yml template)
|
get_drone_template (kind -> exact .drone.yml template)
|
||||||
|
list_release_templates (the changelog + version-file artefacts)
|
||||||
|
get_release_template (kind -> exact release template file)
|
||||||
|
|
||||||
Prompts:
|
Prompts:
|
||||||
intake (the questions to gather before building)
|
intake (the questions to gather before building)
|
||||||
@@ -136,6 +139,32 @@ class StandardsMCPServer(MCPServer):
|
|||||||
return {"error": "not_found", "path": str(path), "components": []}
|
return {"error": "not_found", "path": str(path), "components": []}
|
||||||
return json.loads(path.read_bytes())
|
return json.loads(path.read_bytes())
|
||||||
|
|
||||||
|
def _release(self) -> dict[str, Any]:
|
||||||
|
"""Load the release (versioning + changelog) template manifest."""
|
||||||
|
path = Path(self.mcp_root) / "release.json"
|
||||||
|
if not path.exists():
|
||||||
|
return {"error": "not_found", "path": str(path), "components": []}
|
||||||
|
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:
|
||||||
|
return entry
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _read_release_component(self, entry: dict[str, Any]) -> dict[str, Any]:
|
||||||
|
"""Read a vendored release standard file."""
|
||||||
|
rel = entry["path"]
|
||||||
|
path = Path(self.mcp_root) / "templates" / "release" / rel
|
||||||
|
if not path.exists():
|
||||||
|
return {"error": "not_found", "path": str(path)}
|
||||||
|
return {
|
||||||
|
"kind": entry["kind"],
|
||||||
|
"path": rel,
|
||||||
|
"placeholder": entry.get("placeholder"),
|
||||||
|
"content": path.read_text(encoding="utf-8"),
|
||||||
|
}
|
||||||
|
|
||||||
def _find_drone_component(self, kind: str) -> dict[str, Any] | None:
|
def _find_drone_component(self, kind: str) -> dict[str, Any] | None:
|
||||||
for entry in self._drone().get("components", []):
|
for entry in self._drone().get("components", []):
|
||||||
if entry.get("kind") == kind:
|
if entry.get("kind") == kind:
|
||||||
@@ -228,6 +257,10 @@ class StandardsMCPServer(MCPServer):
|
|||||||
def drone() -> dict[str, Any]:
|
def drone() -> dict[str, Any]:
|
||||||
return self._drone()
|
return self._drone()
|
||||||
|
|
||||||
|
@self.app.resource("registry://release")
|
||||||
|
def release() -> dict[str, Any]:
|
||||||
|
return self._release()
|
||||||
|
|
||||||
@self.app.resource("docs://spec")
|
@self.app.resource("docs://spec")
|
||||||
def spec() -> dict[str, Any]:
|
def spec() -> dict[str, Any]:
|
||||||
"""Return the pinned auth-server OpenAPI contract.
|
"""Return the pinned auth-server OpenAPI contract.
|
||||||
@@ -380,6 +413,45 @@ class StandardsMCPServer(MCPServer):
|
|||||||
}
|
}
|
||||||
return self._read_drone_component(entry)
|
return self._read_drone_component(entry)
|
||||||
|
|
||||||
|
@self.app.tool()
|
||||||
|
def list_release_templates() -> list[dict[str, Any]]:
|
||||||
|
"""List the release/versioning template artefacts.
|
||||||
|
|
||||||
|
These are the files every repo carries for releases: a Keep a
|
||||||
|
Changelog ``CHANGELOG.md`` and, for services, a root ``VERSION``
|
||||||
|
file. See standards "11_versioning_and_changelog".
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
list[dict[str, Any]]:
|
||||||
|
Entries with ``kind``, ``path``, ``placeholder`` and
|
||||||
|
``description``. Fetch one with :func:`get_release_template`.
|
||||||
|
"""
|
||||||
|
return self._release().get("components", [])
|
||||||
|
|
||||||
|
@self.app.tool()
|
||||||
|
def get_release_template(kind: str) -> dict[str, Any]:
|
||||||
|
"""Return the exact release standard file for a component kind.
|
||||||
|
|
||||||
|
Use this to drop the canonical changelog / version file into a repo
|
||||||
|
instead of inventing one. See :func:`list_release_templates` for
|
||||||
|
valid kinds ("changelog", "version-file").
|
||||||
|
|
||||||
|
Args:
|
||||||
|
kind (str): Component kind, e.g. "changelog" or "version-file".
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict[str, Any]: ``{kind, path, placeholder, content}`` or an
|
||||||
|
error entry when the kind is unknown.
|
||||||
|
"""
|
||||||
|
entry = self._find_release_component(kind)
|
||||||
|
if entry is None:
|
||||||
|
return {
|
||||||
|
"error": "unknown_kind",
|
||||||
|
"kind": kind,
|
||||||
|
"available": [c["kind"] for c in self._release().get("components", [])],
|
||||||
|
}
|
||||||
|
return self._read_release_component(entry)
|
||||||
|
|
||||||
@self.app.prompt()
|
@self.app.prompt()
|
||||||
def intake() -> str:
|
def intake() -> str:
|
||||||
"""Return the questions to gather before building an app.
|
"""Return the questions to gather before building an app.
|
||||||
|
|||||||
@@ -186,6 +186,12 @@
|
|||||||
"placeholder": "",
|
"placeholder": "",
|
||||||
"description": "Packaging + dependency pins."
|
"description": "Packaging + dependency pins."
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"kind": "changelog",
|
||||||
|
"path": "CHANGELOG.md",
|
||||||
|
"placeholder": "",
|
||||||
|
"description": "Keep a Changelog file with an [Unreleased] section (versioning standard)."
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"kind": "dockerfile",
|
"kind": "dockerfile",
|
||||||
"path": "Dockerfile",
|
"path": "Dockerfile",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"project": "standards",
|
"project": "standards",
|
||||||
"type": "standards",
|
"type": "standards",
|
||||||
"modules_count": 14,
|
"modules_count": 15,
|
||||||
"source": "hand-authored"
|
"source": "hand-authored"
|
||||||
}
|
}
|
||||||
File diff suppressed because one or more lines are too long
@@ -2,7 +2,7 @@
|
|||||||
"module": "index",
|
"module": "index",
|
||||||
"content": {
|
"content": {
|
||||||
"path": "index",
|
"path": "index",
|
||||||
"docstring": "# Standards Reference\n\nThe reference half of the Standards description: the enforceable rules,\npatterns, and the internal library catalog that agents consult while coding.\n\n- **Build Recipe** — the ordered, agentic checklist for a new app.\n- **Layering Rules** — the strict request → domain → repository layering and\n the rules that enforce it.\n- **Composition Root** — a single composition point wiring the whole app.\n- **Spec-First Routing** — the OpenAPI contract as the routing source of truth.\n- **CRUD Template** — the standardized generic resource endpoint scaffold.\n- **Pipeline Scaffold** — the deterministic pipeline skeleton for multi-stage\n jobs.\n- **Worker & Progress** — queued work, progress reporting, and visibility.\n- **Human-in-the-Loop** — guardrails and approvals inside automations.\n- **Infra Adapters** — how infrastructure connects without leaking into domain.\n- **Drone Pipelines** — the two CI shapes: a PyPI package or a Docker deployment.\n- **Genericity Matrix** — what is library-backed, template-provided, or authored.\n- **Auth Integration** — consuming the shared auth-server service (spec-driven,\n no auth code).\n- **Library Registry** — the catalog of internal libraries behind all of it.\n\nRead the **Wiki** for the philosophy and rationale; these pages are the\nreference that mirrors the `standards` MCP bundle.\n",
|
"docstring": "# Standards Reference\n\nThe reference half of the Standards description: the enforceable rules,\npatterns, and the internal library catalog that agents consult while coding.\n\n- **Build Recipe** — the ordered, agentic checklist for a new app.\n- **Layering Rules** — the strict request → domain → repository layering and\n the rules that enforce it.\n- **Composition Root** — a single composition point wiring the whole app.\n- **Spec-First Routing** — the OpenAPI contract as the routing source of truth.\n- **CRUD Template** — the standardized generic resource endpoint scaffold.\n- **Pipeline Scaffold** — the deterministic pipeline skeleton for multi-stage\n jobs.\n- **Worker & Progress** — queued work, progress reporting, and visibility.\n- **Human-in-the-Loop** — guardrails and approvals inside automations.\n- **Infra Adapters** — how infrastructure connects without leaking into domain.\n- **Drone Pipelines** — the two CI shapes: a PyPI package or a Docker deployment.\n- **Versioning & Changelog** — SemVer, the tag↔version contract, and the release\n changelog.\n- **Genericity Matrix** — what is library-backed, template-provided, or authored.\n- **Auth Integration** — consuming the shared auth-server service (spec-driven,\n no auth code).\n- **Library Registry** — the catalog of internal libraries behind all of it.\n\nRead the **Wiki** for the philosophy and rationale; these pages are the\nreference that mirrors the `standards` MCP bundle.\n",
|
||||||
"objects": {}
|
"objects": {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
8
standards/mcp/modules/versioning_and_changelog.json
Normal file
8
standards/mcp/modules/versioning_and_changelog.json
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"module": "versioning_and_changelog",
|
||||||
|
"content": {
|
||||||
|
"path": "versioning_and_changelog",
|
||||||
|
"docstring": "# Versioning & Changelog\n\n## Summary\n\nEvery Aetos artifact — library, service, or site — is versioned with\n[Semantic Versioning](https://semver.org/spec/v2.0.0.html) and documents its\nreleases in a [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) file.\nThe **version file is the source of truth**, the **git tag must equal it**, and\na Drone guard enforces that equality before anything is published. Releases are\ncut manually; a tag is what triggers the pipeline.\n\nThis page pairs with [10 – Drone Pipelines](10_drone_pipelines.md): the version\nand tag contract is exactly what those two CI shapes consume.\n\n---\n\n## The version contract\n\n| Artifact | Source of truth | Notes |\n|----------|-----------------|-------|\n| Library (PyPI package) | `version` in `pyproject.toml` | `check-version` also reads it to skip duplicates |\n| Service / site (Docker) | `VERSION` file at the repo root | one line, `x.y.z` |\n\nRules:\n\n1. **One version per repository.** No per-module versions.\n2. **Tag = version.** The git tag is bare SemVer (`0.0.6`), no `v` prefix, and\n it MUST match the version file byte-for-byte. The Drone guard fails the\n build otherwise — this is what stops a tag from silently publishing nothing.\n3. **A release is a tag.** Bump the version file, promote the changelog, commit,\n tag, push. Everything else is automated by Drone.\n4. **Never reuse a tag.** If a release is wrong, cut a new patch version.\n\n### Pre-1.0 (0.y.z)\n\nWhile a package is `0.y.z`, **`0.y` is treated as the major line**:\n\n- breaking change → `0.(y+1).0`\n- new feature → `0.y.(z+1)`\n- bug fix → `0.y.(z+1)`\n\nOnce a package reaches `1.0.0`, normal SemVer applies. Do not ship `1.0.0`\nuntil the public API is considered stable.\n\n---\n\n## The changelog\n\n`CHANGELOG.md` at the repo root, in Keep a Changelog form. It always has exactly\none open `[Unreleased]` section with the change categories that apply:\n\n```markdown\n## [Unreleased]\n\n### Added\n### Changed\n### Deprecated\n### Removed\n### Fixed\n### Security\n```\n\nOn release you **promote** it — never delete history:\n\n```markdown\n## [0.0.6] - 2026-03-07\n\n### Added\n- ...\n```\n\nand add a link reference at the bottom:\n\n```markdown\n[Unreleased]: https://git.aetoskia.com/aetos/<repo>/compare/0.0.6...HEAD\n[0.0.6]: https://git.aetoskia.com/aetos/<repo>/compare/0.0.5...0.0.6\n```\n\nConventions:\n\n- Write entries for humans: one line each, imperative, referencing the user\n impact, not the commit hash.\n- Categories: **Added**, **Changed**, **Deprecated**, **Removed**, **Fixed**,\n **Security**. Omit empty categories.\n- Security fixes get their own entry even if the change is otherwise a bug fix.\n- The `[Unreleased]` heading is never renamed; copy its content into the new\n version heading and clear it.\n\n---\n\n## Release checklist\n\n1. Confirm the quality gate is green on `main`.\n2. Move every `[Unreleased]` entry under the new version heading with today's\n date (`## [x.y.z] - YYYY-MM-DD`); restore an empty `[Unreleased]` on top.\n3. Add the two compare-link references at the bottom of the changelog.\n4. Set the version file: `pyproject.toml` (`version = \"x.y.z\"`) or `VERSION`.\n5. Commit: `chore(release): x.y.z`.\n6. Tag: `git tag x.y.z` and push the tag (`git push origin x.y.z`).\n7. Watch Drone: Type A publishes the wheel/sdist, Type B builds and deploys the\n image. Both first assert `tag == version`.\n8. Do not reuse the tag if the run fails — fix and cut `x.y.(z+1)`.\n\n---\n\n## Why the guard matters\n\nThe version file and the tag are two facts that must agree; nothing in git\nenforces that by itself. A mismatch is not a theoretical risk — a package can be\ntagged `0.0.2` while its `pyproject.toml` still says `0.0.1`, and the publish\nstep (which compares the **file** against the index) then skips the build\nentirely, so `0.0.2` never reaches the index while looking like a release. The\nDrone guard turns that silent no-op into a hard failure.\n\n---\n\n## Related\n\n- [10 – Drone Pipelines](10_drone_pipelines.md) · [01 – Build Recipe](01_build_recipe.md) · [13 – Library Registry](13_library_registry.md)\n",
|
||||||
|
"objects": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -49,6 +49,11 @@
|
|||||||
"resource": "docs://modules/drone_pipelines",
|
"resource": "docs://modules/drone_pipelines",
|
||||||
"title": "Drone Pipelines"
|
"title": "Drone Pipelines"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"module": "versioning_and_changelog",
|
||||||
|
"resource": "docs://modules/versioning_and_changelog",
|
||||||
|
"title": "Versioning & Changelog"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"module": "genericity_matrix",
|
"module": "genericity_matrix",
|
||||||
"resource": "docs://modules/genericity_matrix",
|
"resource": "docs://modules/genericity_matrix",
|
||||||
|
|||||||
19
standards/mcp/release.json
Normal file
19
standards/mcp/release.json
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"project": "standards",
|
||||||
|
"template": "templates/release",
|
||||||
|
"invariant": "git tag == version file (enforced by the Drone guard)",
|
||||||
|
"components": [
|
||||||
|
{
|
||||||
|
"kind": "changelog",
|
||||||
|
"path": "CHANGELOG.md",
|
||||||
|
"placeholder": "",
|
||||||
|
"description": "Keep a Changelog skeleton: one [Unreleased] section + compare links."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "version-file",
|
||||||
|
"path": "VERSION",
|
||||||
|
"placeholder": "",
|
||||||
|
"description": "Single-line SemVer source of truth for services and sites."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
22
standards/mcp/templates/app/CHANGELOG.md
Normal file
22
standards/mcp/templates/app/CHANGELOG.md
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
# Changelog
|
||||||
|
|
||||||
|
All notable changes to this project are documented here.
|
||||||
|
|
||||||
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/)
|
||||||
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
|
## [Unreleased]
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
## [0.1.0] - YYYY-MM-DD
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- Initial blueprint app scaffolded from the Aetos standard.
|
||||||
|
|
||||||
|
[Unreleased]: <repo-url>/compare/0.1.0...HEAD
|
||||||
|
[0.1.0]: <repo-url>/releases/tag/0.1.0
|
||||||
@@ -12,6 +12,10 @@
|
|||||||
# Secrets (Drone): PIP_REPO_URL, PIP_USERNAME, PIP_PASSWORD.
|
# Secrets (Drone): PIP_REPO_URL, PIP_USERNAME, PIP_PASSWORD.
|
||||||
# Copy to <repo>/.drone.yml unchanged; the package name and version are read
|
# Copy to <repo>/.drone.yml unchanged; the package name and version are read
|
||||||
# from pyproject.toml, so no per-repo edits are required.
|
# from pyproject.toml, so no per-repo edits are required.
|
||||||
|
#
|
||||||
|
# Version guard: the tag build fails unless the git tag equals the version in
|
||||||
|
# pyproject.toml (see the versioning standard). This stops a tag from looking
|
||||||
|
# like a release while silently publishing nothing.
|
||||||
---
|
---
|
||||||
kind: pipeline
|
kind: pipeline
|
||||||
type: docker
|
type: docker
|
||||||
@@ -37,6 +41,11 @@ steps:
|
|||||||
commands:
|
commands:
|
||||||
- PACKAGE_NAME=$(grep -E '^name\s*=' pyproject.toml | head -1 | cut -d'"' -f2)
|
- PACKAGE_NAME=$(grep -E '^name\s*=' pyproject.toml | head -1 | cut -d'"' -f2)
|
||||||
- VERSION=$(grep -E '^version\s*=' pyproject.toml | head -1 | cut -d'"' -f2)
|
- VERSION=$(grep -E '^version\s*=' pyproject.toml | head -1 | cut -d'"' -f2)
|
||||||
|
- |
|
||||||
|
if [ "$DRONE_TAG" != "$VERSION" ]; then
|
||||||
|
echo "Tag '$DRONE_TAG' does not match pyproject version '$VERSION'; refusing to publish."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
- echo "Checking if $PACKAGE_NAME==$VERSION exists on $PIP_REPO_URL ..."
|
- echo "Checking if $PACKAGE_NAME==$VERSION exists on $PIP_REPO_URL ..."
|
||||||
- |
|
- |
|
||||||
if curl -fsSL -u "$PIP_USERNAME:$PIP_PASSWORD" "$PIP_REPO_URL/simple/$PACKAGE_NAME/" | grep -qE "${PACKAGE_NAME}-${VERSION}(-|\.tar\.gz|\.whl)"; then
|
if curl -fsSL -u "$PIP_USERNAME:$PIP_PASSWORD" "$PIP_REPO_URL/simple/$PACKAGE_NAME/" | grep -qE "${PACKAGE_NAME}-${VERSION}(-|\.tar\.gz|\.whl)"; then
|
||||||
@@ -147,6 +156,12 @@ steps:
|
|||||||
|
|
||||||
git checkout --force "$TAG"
|
git checkout --force "$TAG"
|
||||||
|
|
||||||
|
TAG_VERSION=$(grep -E '^version\s*=' pyproject.toml | head -1 | cut -d'"' -f2)
|
||||||
|
if [ "$TAG" != "$TAG_VERSION" ]; then
|
||||||
|
echo "Tag $TAG != pyproject version $TAG_VERSION at that commit; skipping"
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
echo "Building $VERSION"
|
echo "Building $VERSION"
|
||||||
rm -rf dist
|
rm -rf dist
|
||||||
python -m build
|
python -m build
|
||||||
|
|||||||
@@ -20,6 +20,10 @@
|
|||||||
# <host-port> published host port; <container-port> the in-image port.
|
# <host-port> published host port; <container-port> the in-image port.
|
||||||
# <site> site image name for the site variant (e.g. docs, mcp).
|
# <site> site image name for the site variant (e.g. docs, mcp).
|
||||||
# <env-var> runtime environment variable consumed by the service.
|
# <env-var> runtime environment variable consumed by the service.
|
||||||
|
#
|
||||||
|
# Version guard: the tag-triggered service variant fails unless the git tag
|
||||||
|
# equals the repo-root VERSION file (see the versioning standard). The
|
||||||
|
# push-triggered site variant ships :latest only and has no version to guard.
|
||||||
---
|
---
|
||||||
kind: pipeline
|
kind: pipeline
|
||||||
type: docker
|
type: docker
|
||||||
@@ -64,6 +68,22 @@ steps:
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
- name: check-version
|
||||||
|
image: alpine:3.20
|
||||||
|
commands:
|
||||||
|
- VERSION=$(cat VERSION 2>/dev/null | tr -d '\n')
|
||||||
|
- echo "tag=$DRONE_TAG version=$VERSION"
|
||||||
|
- |
|
||||||
|
if [ -z "$VERSION" ]; then
|
||||||
|
echo "VERSION file missing or empty; add it (see versioning standard)."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
- |
|
||||||
|
if [ "$DRONE_TAG" != "$VERSION" ]; then
|
||||||
|
echo "Tag '$DRONE_TAG' does not match VERSION '$VERSION'; refusing to deploy."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
- name: check-remote-image
|
- name: check-remote-image
|
||||||
image: docker:24
|
image: docker:24
|
||||||
volumes:
|
volumes:
|
||||||
|
|||||||
36
standards/mcp/templates/release/CHANGELOG.md
Normal file
36
standards/mcp/templates/release/CHANGELOG.md
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
# Changelog
|
||||||
|
|
||||||
|
All notable changes to this project are documented here.
|
||||||
|
|
||||||
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/)
|
||||||
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
|
## [Unreleased]
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
### Deprecated
|
||||||
|
|
||||||
|
### Removed
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
### Security
|
||||||
|
|
||||||
|
<!--
|
||||||
|
On release, promote the entries above into a new heading with today's date and
|
||||||
|
leave a fresh, empty [Unreleased] section:
|
||||||
|
|
||||||
|
## [0.1.0] - YYYY-MM-DD
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- Initial release.
|
||||||
|
|
||||||
|
Then update the compare links below. See the standard:
|
||||||
|
Versioning & Changelog (registry://release).
|
||||||
|
-->
|
||||||
|
|
||||||
|
[Unreleased]: <repo-url>/compare/0.1.0...HEAD
|
||||||
|
[0.1.0]: <repo-url>/releases/tag/0.1.0
|
||||||
1
standards/mcp/templates/release/VERSION
Normal file
1
standards/mcp/templates/release/VERSION
Normal file
@@ -0,0 +1 @@
|
|||||||
|
0.0.0
|
||||||
@@ -25,9 +25,10 @@ needs no edits, the Type B file needs its placeholders filled in (below).
|
|||||||
For internal libraries. The file contains **two pipelines**:
|
For internal libraries. The file contains **two pipelines**:
|
||||||
|
|
||||||
1. **`build-and-publish-pypi`** — triggered on `tag`:
|
1. **`build-and-publish-pypi`** — triggered on `tag`:
|
||||||
- `check-version` — read `name` / `version` from `pyproject.toml` and query
|
- `check-version` — **guard**: fail unless `$DRONE_TAG` equals the version in
|
||||||
|
`pyproject.toml`; then read `name` / `version` and query
|
||||||
`$PIP_REPO_URL/simple/<package>/`; if that version already exists, exit
|
`$PIP_REPO_URL/simple/<package>/`; if that version already exists, exit
|
||||||
`78` and skip the whole build.
|
`78` and skip the whole build (see [11 – Versioning & Changelog](11_versioning_and_changelog.md)).
|
||||||
- `quality-gate` — install the package with its `[dev]` extras from the
|
- `quality-gate` — install the package with its `[dev]` extras from the
|
||||||
private index, then `black --check`, `ruff check`, `mypy`, `pydoclint`,
|
private index, then `black --check`, `ruff check`, `mypy`, `pydoclint`,
|
||||||
`pytest`.
|
`pytest`.
|
||||||
@@ -36,8 +37,9 @@ For internal libraries. The file contains **two pipelines**:
|
|||||||
2. **`backfill-pypi-from-tags`** — triggered on `custom`:
|
2. **`backfill-pypi-from-tags`** — triggered on `custom`:
|
||||||
- `fetch-tags` — `git fetch --tags --force`.
|
- `fetch-tags` — `git fetch --tags --force`.
|
||||||
- `build-and-upload-missing` — walk every tag in version order, skip the ones
|
- `build-and-upload-missing` — walk every tag in version order, skip the ones
|
||||||
already on the index, `git checkout` the rest, build, and upload. This is
|
already on the index, `git checkout` the rest, verify `tag == pyproject
|
||||||
the disaster-recovery / replay path.
|
version` at that commit, build, and upload. This is the disaster-recovery /
|
||||||
|
replay path.
|
||||||
|
|
||||||
Secrets: `PIP_REPO_URL`, `PIP_USERNAME`, `PIP_PASSWORD`.
|
Secrets: `PIP_REPO_URL`, `PIP_USERNAME`, `PIP_PASSWORD`.
|
||||||
|
|
||||||
@@ -55,6 +57,8 @@ pipelines**, each mirroring a repo family:
|
|||||||
API/worker service, e.g. `auth-server`):
|
API/worker service, e.g. `auth-server`):
|
||||||
- `fetch-tags` — resolve the latest tag into `LATEST_TAG.txt`; fail if there
|
- `fetch-tags` — resolve the latest tag into `LATEST_TAG.txt`; fail if there
|
||||||
are no tags.
|
are no tags.
|
||||||
|
- `check-version` — **guard**: fail unless `$DRONE_TAG` equals the repo-root
|
||||||
|
`VERSION` file (see [11 – Versioning & Changelog](11_versioning_and_changelog.md)).
|
||||||
- `check-remote-image` — if `aetos/<app>:<tag>` already exists locally, exit
|
- `check-remote-image` — if `aetos/<app>:<tag>` already exists locally, exit
|
||||||
`78` and skip the build.
|
`78` and skip the build.
|
||||||
- `quality-gate` — install from `requirements.txt` and run
|
- `quality-gate` — install from `requirements.txt` and run
|
||||||
@@ -67,7 +71,8 @@ pipelines**, each mirroring a repo family:
|
|||||||
--restart always` with the service's `-e` environment.
|
--restart always` with the service's `-e` environment.
|
||||||
2. **`build-and-deploy-site`** — triggered on `push` (a static/HTTP site, e.g.
|
2. **`build-and-deploy-site`** — triggered on `push` (a static/HTTP site, e.g.
|
||||||
`docs`, `mcp`): `build-image` → `push-image` → `stop-old` → `run-container`,
|
`docs`, `mcp`): `build-image` → `push-image` → `stop-old` → `run-container`,
|
||||||
with no tag resolution and no quality gate.
|
with no tag resolution, no quality gate, and no version guard (it ships
|
||||||
|
`:latest` only).
|
||||||
|
|
||||||
Secrets: `PIP_REPO_URL`, `PIP_USERNAME`, `PIP_PASSWORD` (service build args),
|
Secrets: `PIP_REPO_URL`, `PIP_USERNAME`, `PIP_PASSWORD` (service build args),
|
||||||
`REGISTRY_HOST`, `REGISTRY_USER`, `REGISTRY_PASS`, plus the service runtime env
|
`REGISTRY_HOST`, `REGISTRY_USER`, `REGISTRY_PASS`, plus the service runtime env
|
||||||
@@ -91,6 +96,9 @@ Placeholders to fill in per repo:
|
|||||||
- Do not inline secrets; always use `from_secret`.
|
- Do not inline secrets; always use `from_secret`.
|
||||||
- Do not drop the `check-version` / `check-remote-image` skip — it guards the
|
- Do not drop the `check-version` / `check-remote-image` skip — it guards the
|
||||||
index and the running container against duplicate publishes.
|
index and the running container against duplicate publishes.
|
||||||
|
- Do not drop the tag↔version guard. A package needs its `pyproject.toml`
|
||||||
|
version to equal the tag; a service needs a root `VERSION` file that equals
|
||||||
|
the tag. See [11 – Versioning & Changelog](11_versioning_and_changelog.md).
|
||||||
- Do not rename the pipelines; tooling and runbooks key off
|
- Do not rename the pipelines; tooling and runbooks key off
|
||||||
`build-and-publish-pypi`, `build-and-deploy-service`, and
|
`build-and-publish-pypi`, `build-and-deploy-service`, and
|
||||||
`build-and-deploy-site`.
|
`build-and-deploy-site`.
|
||||||
@@ -99,4 +107,4 @@ Placeholders to fill in per repo:
|
|||||||
|
|
||||||
## Related
|
## Related
|
||||||
|
|
||||||
- [01 – Build Recipe](01_build_recipe.md) · [09 – Infra Adapters](09_infra_adaptors.md) · [13 – Library Registry](13_library_registry.md)
|
- [01 – Build Recipe](01_build_recipe.md) · [09 – Infra Adapters](09_infra_adaptors.md) · [11 – Versioning & Changelog](11_versioning_and_changelog.md) · [13 – Library Registry](13_library_registry.md)
|
||||||
|
|||||||
119
standards/src/lib/11_versioning_and_changelog.md
Normal file
119
standards/src/lib/11_versioning_and_changelog.md
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
# Versioning & Changelog
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
Every Aetos artifact — library, service, or site — is versioned with
|
||||||
|
[Semantic Versioning](https://semver.org/spec/v2.0.0.html) and documents its
|
||||||
|
releases in a [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) file.
|
||||||
|
The **version file is the source of truth**, the **git tag must equal it**, and
|
||||||
|
a Drone guard enforces that equality before anything is published. Releases are
|
||||||
|
cut manually; a tag is what triggers the pipeline.
|
||||||
|
|
||||||
|
This page pairs with [10 – Drone Pipelines](10_drone_pipelines.md): the version
|
||||||
|
and tag contract is exactly what those two CI shapes consume.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## The version contract
|
||||||
|
|
||||||
|
| Artifact | Source of truth | Notes |
|
||||||
|
|----------|-----------------|-------|
|
||||||
|
| Library (PyPI package) | `version` in `pyproject.toml` | `check-version` also reads it to skip duplicates |
|
||||||
|
| Service / site (Docker) | `VERSION` file at the repo root | one line, `x.y.z` |
|
||||||
|
|
||||||
|
Rules:
|
||||||
|
|
||||||
|
1. **One version per repository.** No per-module versions.
|
||||||
|
2. **Tag = version.** The git tag is bare SemVer (`0.0.6`), no `v` prefix, and
|
||||||
|
it MUST match the version file byte-for-byte. The Drone guard fails the
|
||||||
|
build otherwise — this is what stops a tag from silently publishing nothing.
|
||||||
|
3. **A release is a tag.** Bump the version file, promote the changelog, commit,
|
||||||
|
tag, push. Everything else is automated by Drone.
|
||||||
|
4. **Never reuse a tag.** If a release is wrong, cut a new patch version.
|
||||||
|
|
||||||
|
### Pre-1.0 (0.y.z)
|
||||||
|
|
||||||
|
While a package is `0.y.z`, **`0.y` is treated as the major line**:
|
||||||
|
|
||||||
|
- breaking change → `0.(y+1).0`
|
||||||
|
- new feature → `0.y.(z+1)`
|
||||||
|
- bug fix → `0.y.(z+1)`
|
||||||
|
|
||||||
|
Once a package reaches `1.0.0`, normal SemVer applies. Do not ship `1.0.0`
|
||||||
|
until the public API is considered stable.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## The changelog
|
||||||
|
|
||||||
|
`CHANGELOG.md` at the repo root, in Keep a Changelog form. It always has exactly
|
||||||
|
one open `[Unreleased]` section with the change categories that apply:
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
## [Unreleased]
|
||||||
|
|
||||||
|
### Added
|
||||||
|
### Changed
|
||||||
|
### Deprecated
|
||||||
|
### Removed
|
||||||
|
### Fixed
|
||||||
|
### Security
|
||||||
|
```
|
||||||
|
|
||||||
|
On release you **promote** it — never delete history:
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
## [0.0.6] - 2026-03-07
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- ...
|
||||||
|
```
|
||||||
|
|
||||||
|
and add a link reference at the bottom:
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
[Unreleased]: https://git.aetoskia.com/aetos/<repo>/compare/0.0.6...HEAD
|
||||||
|
[0.0.6]: https://git.aetoskia.com/aetos/<repo>/compare/0.0.5...0.0.6
|
||||||
|
```
|
||||||
|
|
||||||
|
Conventions:
|
||||||
|
|
||||||
|
- Write entries for humans: one line each, imperative, referencing the user
|
||||||
|
impact, not the commit hash.
|
||||||
|
- Categories: **Added**, **Changed**, **Deprecated**, **Removed**, **Fixed**,
|
||||||
|
**Security**. Omit empty categories.
|
||||||
|
- Security fixes get their own entry even if the change is otherwise a bug fix.
|
||||||
|
- The `[Unreleased]` heading is never renamed; copy its content into the new
|
||||||
|
version heading and clear it.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Release checklist
|
||||||
|
|
||||||
|
1. Confirm the quality gate is green on `main`.
|
||||||
|
2. Move every `[Unreleased]` entry under the new version heading with today's
|
||||||
|
date (`## [x.y.z] - YYYY-MM-DD`); restore an empty `[Unreleased]` on top.
|
||||||
|
3. Add the two compare-link references at the bottom of the changelog.
|
||||||
|
4. Set the version file: `pyproject.toml` (`version = "x.y.z"`) or `VERSION`.
|
||||||
|
5. Commit: `chore(release): x.y.z`.
|
||||||
|
6. Tag: `git tag x.y.z` and push the tag (`git push origin x.y.z`).
|
||||||
|
7. Watch Drone: Type A publishes the wheel/sdist, Type B builds and deploys the
|
||||||
|
image. Both first assert `tag == version`.
|
||||||
|
8. Do not reuse the tag if the run fails — fix and cut `x.y.(z+1)`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Why the guard matters
|
||||||
|
|
||||||
|
The version file and the tag are two facts that must agree; nothing in git
|
||||||
|
enforces that by itself. A mismatch is not a theoretical risk — a package can be
|
||||||
|
tagged `0.0.2` while its `pyproject.toml` still says `0.0.1`, and the publish
|
||||||
|
step (which compares the **file** against the index) then skips the build
|
||||||
|
entirely, so `0.0.2` never reaches the index while looking like a release. The
|
||||||
|
Drone guard turns that silent no-op into a hard failure.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Related
|
||||||
|
|
||||||
|
- [10 – Drone Pipelines](10_drone_pipelines.md) · [01 – Build Recipe](01_build_recipe.md) · [13 – Library Registry](13_library_registry.md)
|
||||||
@@ -15,6 +15,8 @@ patterns, and the internal library catalog that agents consult while coding.
|
|||||||
- **Human-in-the-Loop** — guardrails and approvals inside automations.
|
- **Human-in-the-Loop** — guardrails and approvals inside automations.
|
||||||
- **Infra Adapters** — how infrastructure connects without leaking into domain.
|
- **Infra Adapters** — how infrastructure connects without leaking into domain.
|
||||||
- **Drone Pipelines** — the two CI shapes: a PyPI package or a Docker deployment.
|
- **Drone Pipelines** — the two CI shapes: a PyPI package or a Docker deployment.
|
||||||
|
- **Versioning & Changelog** — SemVer, the tag↔version contract, and the release
|
||||||
|
changelog.
|
||||||
- **Genericity Matrix** — what is library-backed, template-provided, or authored.
|
- **Genericity Matrix** — what is library-backed, template-provided, or authored.
|
||||||
- **Auth Integration** — consuming the shared auth-server service (spec-driven,
|
- **Auth Integration** — consuming the shared auth-server service (spec-driven,
|
||||||
no auth code).
|
no auth code).
|
||||||
|
|||||||
@@ -64,6 +64,7 @@ nav:
|
|||||||
- Human-in-the-Loop: 08_human_in_loop.md
|
- Human-in-the-Loop: 08_human_in_loop.md
|
||||||
- Infra Adapters: 09_infra_adaptors.md
|
- Infra Adapters: 09_infra_adaptors.md
|
||||||
- Drone Pipelines: 10_drone_pipelines.md
|
- Drone Pipelines: 10_drone_pipelines.md
|
||||||
|
- Versioning & Changelog: 11_versioning_and_changelog.md
|
||||||
- Genericity Matrix: 12_genericity_matrix.md
|
- Genericity Matrix: 12_genericity_matrix.md
|
||||||
- Auth Integration: 14_auth_integration.md
|
- Auth Integration: 14_auth_integration.md
|
||||||
- Library Registry: 13_library_registry.md
|
- Library Registry: 13_library_registry.md
|
||||||
@@ -68,3 +68,4 @@ nav:
|
|||||||
- App Components: 02_components.md
|
- App Components: 02_components.md
|
||||||
- Start an App: 03_start_an_app.md
|
- Start an App: 03_start_an_app.md
|
||||||
- Testing & Quality: 04_development/01_testing.md
|
- Testing & Quality: 04_development/01_testing.md
|
||||||
|
- Releasing & Versioning: 04_development/02_releasing.md
|
||||||
22
standards/src/templates/app/CHANGELOG.md
Normal file
22
standards/src/templates/app/CHANGELOG.md
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
# Changelog
|
||||||
|
|
||||||
|
All notable changes to this project are documented here.
|
||||||
|
|
||||||
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/)
|
||||||
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
|
## [Unreleased]
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
## [0.1.0] - YYYY-MM-DD
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- Initial blueprint app scaffolded from the Aetos standard.
|
||||||
|
|
||||||
|
[Unreleased]: <repo-url>/compare/0.1.0...HEAD
|
||||||
|
[0.1.0]: <repo-url>/releases/tag/0.1.0
|
||||||
@@ -12,6 +12,10 @@
|
|||||||
# Secrets (Drone): PIP_REPO_URL, PIP_USERNAME, PIP_PASSWORD.
|
# Secrets (Drone): PIP_REPO_URL, PIP_USERNAME, PIP_PASSWORD.
|
||||||
# Copy to <repo>/.drone.yml unchanged; the package name and version are read
|
# Copy to <repo>/.drone.yml unchanged; the package name and version are read
|
||||||
# from pyproject.toml, so no per-repo edits are required.
|
# from pyproject.toml, so no per-repo edits are required.
|
||||||
|
#
|
||||||
|
# Version guard: the tag build fails unless the git tag equals the version in
|
||||||
|
# pyproject.toml (see the versioning standard). This stops a tag from looking
|
||||||
|
# like a release while silently publishing nothing.
|
||||||
---
|
---
|
||||||
kind: pipeline
|
kind: pipeline
|
||||||
type: docker
|
type: docker
|
||||||
@@ -37,6 +41,11 @@ steps:
|
|||||||
commands:
|
commands:
|
||||||
- PACKAGE_NAME=$(grep -E '^name\s*=' pyproject.toml | head -1 | cut -d'"' -f2)
|
- PACKAGE_NAME=$(grep -E '^name\s*=' pyproject.toml | head -1 | cut -d'"' -f2)
|
||||||
- VERSION=$(grep -E '^version\s*=' pyproject.toml | head -1 | cut -d'"' -f2)
|
- VERSION=$(grep -E '^version\s*=' pyproject.toml | head -1 | cut -d'"' -f2)
|
||||||
|
- |
|
||||||
|
if [ "$DRONE_TAG" != "$VERSION" ]; then
|
||||||
|
echo "Tag '$DRONE_TAG' does not match pyproject version '$VERSION'; refusing to publish."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
- echo "Checking if $PACKAGE_NAME==$VERSION exists on $PIP_REPO_URL ..."
|
- echo "Checking if $PACKAGE_NAME==$VERSION exists on $PIP_REPO_URL ..."
|
||||||
- |
|
- |
|
||||||
if curl -fsSL -u "$PIP_USERNAME:$PIP_PASSWORD" "$PIP_REPO_URL/simple/$PACKAGE_NAME/" | grep -qE "${PACKAGE_NAME}-${VERSION}(-|\.tar\.gz|\.whl)"; then
|
if curl -fsSL -u "$PIP_USERNAME:$PIP_PASSWORD" "$PIP_REPO_URL/simple/$PACKAGE_NAME/" | grep -qE "${PACKAGE_NAME}-${VERSION}(-|\.tar\.gz|\.whl)"; then
|
||||||
@@ -147,6 +156,12 @@ steps:
|
|||||||
|
|
||||||
git checkout --force "$TAG"
|
git checkout --force "$TAG"
|
||||||
|
|
||||||
|
TAG_VERSION=$(grep -E '^version\s*=' pyproject.toml | head -1 | cut -d'"' -f2)
|
||||||
|
if [ "$TAG" != "$TAG_VERSION" ]; then
|
||||||
|
echo "Tag $TAG != pyproject version $TAG_VERSION at that commit; skipping"
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
echo "Building $VERSION"
|
echo "Building $VERSION"
|
||||||
rm -rf dist
|
rm -rf dist
|
||||||
python -m build
|
python -m build
|
||||||
|
|||||||
@@ -20,6 +20,10 @@
|
|||||||
# <host-port> published host port; <container-port> the in-image port.
|
# <host-port> published host port; <container-port> the in-image port.
|
||||||
# <site> site image name for the site variant (e.g. docs, mcp).
|
# <site> site image name for the site variant (e.g. docs, mcp).
|
||||||
# <env-var> runtime environment variable consumed by the service.
|
# <env-var> runtime environment variable consumed by the service.
|
||||||
|
#
|
||||||
|
# Version guard: the tag-triggered service variant fails unless the git tag
|
||||||
|
# equals the repo-root VERSION file (see the versioning standard). The
|
||||||
|
# push-triggered site variant ships :latest only and has no version to guard.
|
||||||
---
|
---
|
||||||
kind: pipeline
|
kind: pipeline
|
||||||
type: docker
|
type: docker
|
||||||
@@ -64,6 +68,22 @@ steps:
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
- name: check-version
|
||||||
|
image: alpine:3.20
|
||||||
|
commands:
|
||||||
|
- VERSION=$(cat VERSION 2>/dev/null | tr -d '\n')
|
||||||
|
- echo "tag=$DRONE_TAG version=$VERSION"
|
||||||
|
- |
|
||||||
|
if [ -z "$VERSION" ]; then
|
||||||
|
echo "VERSION file missing or empty; add it (see versioning standard)."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
- |
|
||||||
|
if [ "$DRONE_TAG" != "$VERSION" ]; then
|
||||||
|
echo "Tag '$DRONE_TAG' does not match VERSION '$VERSION'; refusing to deploy."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
- name: check-remote-image
|
- name: check-remote-image
|
||||||
image: docker:24
|
image: docker:24
|
||||||
volumes:
|
volumes:
|
||||||
|
|||||||
36
standards/src/templates/release/CHANGELOG.md
Normal file
36
standards/src/templates/release/CHANGELOG.md
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
# Changelog
|
||||||
|
|
||||||
|
All notable changes to this project are documented here.
|
||||||
|
|
||||||
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/)
|
||||||
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
|
## [Unreleased]
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
### Deprecated
|
||||||
|
|
||||||
|
### Removed
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
### Security
|
||||||
|
|
||||||
|
<!--
|
||||||
|
On release, promote the entries above into a new heading with today's date and
|
||||||
|
leave a fresh, empty [Unreleased] section:
|
||||||
|
|
||||||
|
## [0.1.0] - YYYY-MM-DD
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- Initial release.
|
||||||
|
|
||||||
|
Then update the compare links below. See the standard:
|
||||||
|
Versioning & Changelog (registry://release).
|
||||||
|
-->
|
||||||
|
|
||||||
|
[Unreleased]: <repo-url>/compare/0.1.0...HEAD
|
||||||
|
[0.1.0]: <repo-url>/releases/tag/0.1.0
|
||||||
1
standards/src/templates/release/VERSION
Normal file
1
standards/src/templates/release/VERSION
Normal file
@@ -0,0 +1 @@
|
|||||||
|
0.0.0
|
||||||
107
standards/src/wiki/04_development/02_releasing.md
Normal file
107
standards/src/wiki/04_development/02_releasing.md
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
# Releasing & Versioning
|
||||||
|
|
||||||
|
How to cut a release of any Aetos repository. The rule lives in the reference
|
||||||
|
page [Versioning & Changelog](../lib/11_versioning_and_changelog/); this is the
|
||||||
|
runbook you follow.
|
||||||
|
|
||||||
|
Every repository is one of two Drone shapes — a **PyPI package** or a **Docker
|
||||||
|
deployment** ([Drone Pipelines](../lib/10_drone_pipelines/)). Both are
|
||||||
|
released the same way: bump the version file, promote the changelog, tag.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🧾 Before you start
|
||||||
|
|
||||||
|
- `main` is green (the Drone quality gate passed on the last run).
|
||||||
|
- You know whether this is a **breaking**, **feature**, or **fix** release.
|
||||||
|
- The version file exists:
|
||||||
|
- package → `version` in `pyproject.toml`
|
||||||
|
- service / site → `VERSION` at the repo root
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚦 Pick the next version
|
||||||
|
|
||||||
|
The version file and the tag must be identical. For `0.y.z`, `0.y` is the major
|
||||||
|
line (see the reference page); once at `1.0.0`, normal SemVer applies.
|
||||||
|
|
||||||
|
| Change | `0.y.z` → | `1.0.0+` → |
|
||||||
|
|--------|-----------|------------|
|
||||||
|
| Breaking | `0.(y+1).0` | `(x+1).0.0` |
|
||||||
|
| Feature | `0.y.(z+1)` | `x.(y+1).0` |
|
||||||
|
| Fix | `0.y.(z+1)` | `x.y.(z+1)` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ✍️ Promote the changelog
|
||||||
|
|
||||||
|
In `CHANGELOG.md`:
|
||||||
|
|
||||||
|
1. Rename the content under `[Unreleased]` into a new
|
||||||
|
`## [x.y.z] - YYYY-MM-DD` heading (keep the sections that apply).
|
||||||
|
2. Leave a fresh, empty `## [Unreleased]` on top.
|
||||||
|
3. Add the compare links at the bottom:
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
[Unreleased]: https://git.aetoskia.com/aetos/<repo>/compare/x.y.z...HEAD
|
||||||
|
[x.y.z]: https://git.aetoskia.com/aetos/<repo>/compare/x.y.(z-1)...x.y.z
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔢 Bump the version file
|
||||||
|
|
||||||
|
- **Package:** edit `version = "x.y.z"` in `pyproject.toml`.
|
||||||
|
- **Service / site:** edit the `VERSION` file.
|
||||||
|
|
||||||
|
Then commit and tag:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add CHANGELOG.md pyproject.toml # or VERSION
|
||||||
|
git commit -m "chore(release): x.y.z"
|
||||||
|
git tag x.y.z
|
||||||
|
git push origin HEAD
|
||||||
|
git push origin x.y.z
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🤖 What Drone does
|
||||||
|
|
||||||
|
- **Type A (package):** asserts `tag == pyproject version`, then skips if that
|
||||||
|
version already exists on the index, runs the quality gate, builds, and
|
||||||
|
uploads the wheel + sdist.
|
||||||
|
- **Type B (service):** asserts `tag == VERSION`, resolves the tag, skips if the
|
||||||
|
image already exists, runs the quality gate, builds, pushes, and restarts the
|
||||||
|
container.
|
||||||
|
- **Type B (site):** triggered on push, ships `:latest` and has no tag guard.
|
||||||
|
|
||||||
|
If the guard fails, nothing is published — fix the version file, amend the
|
||||||
|
commit, and move the tag **only** if it was never pushed.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🛟 Backfill / recovery
|
||||||
|
|
||||||
|
If an older tag never published (a past version-file mismatch, an index
|
||||||
|
outage):
|
||||||
|
|
||||||
|
- **Package:** run the `backfill-pypi-from-tags` pipeline (custom event). It
|
||||||
|
checks out every tag, verifies `tag == pyproject version` at that commit,
|
||||||
|
and uploads the ones missing from the index. Already-fixed history is never
|
||||||
|
rewritten.
|
||||||
|
- **Service:** rebuild the image for the missing tag with a fresh commit and a
|
||||||
|
new patch version; do not move a published tag.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔁 If a release is wrong
|
||||||
|
|
||||||
|
- Never reuse or force-move a pushed tag.
|
||||||
|
- Fix forward: bump to the next patch, promote the changelog, tag again.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Related
|
||||||
|
|
||||||
|
- [Testing & Quality Gates](01_testing.md) · [Versioning & Changelog](../lib/11_versioning_and_changelog/) · [Drone Pipelines](../lib/10_drone_pipelines/)
|
||||||
Reference in New Issue
Block a user