feat(standards): drop the site CI variant + require changelog reconciliation before release

- Remove the push-triggered build-and-deploy-site pipeline and every "site"
  reference; Type B is now a single tag-triggered service pipeline
  (service-docker.yaml).
- Inventory now covers packages + the auth-server service only (repos.json
  11 -> 9; docs/mcp are no longer part of the standard).
- Versioning standard: reconcile [Unreleased] against the last tag before
  promoting, and always show proposed entries to the user for approval.
- Update registry.py compliance/release text and list_repos types, build.py
  component descriptions, and rebuild the MCP bundle + lib/wiki HTML.
This commit is contained in:
2026-10-17 00:41:52 +05:30
parent e33a6d6704
commit 104bf3eb1e
28 changed files with 234 additions and 353 deletions

View File

@@ -371,7 +371,7 @@ _DRONE_COMPONENTS: list[dict[str, str]] = [
{"kind": "drone-pypi", "path": "python-lib.yaml", "placeholder": "",
"description": "Drone pipeline: build + publish a Python package to the private PyPI index."},
{"kind": "drone-service", "path": "service-docker.yaml", "placeholder": "",
"description": "Drone pipeline: build, push, and deploy a Docker service or site."},
"description": "Drone pipeline: build, push, and deploy a Docker service."},
]
@@ -447,7 +447,7 @@ _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."},
"description": "Single-line SemVer source of truth for services."},
]

View File

@@ -105,10 +105,10 @@ _COMPLIANCE_TEXT = (
"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). The service must read its\n"
" version from `VERSION` (e.g. a build arg) — never hardcode it.\n\n"
"- **service** — root `VERSION`; root `CHANGELOG.md`; Drone file from\n"
" `get_drone_template(\"drone-service\")` (`service-docker.yaml`, tag-triggered\n"
" with a `tag == VERSION` guard). The service must read its version from\n"
" `VERSION` (e.g. a build arg) — never hardcode it.\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"
@@ -129,7 +129,7 @@ _RELEASE_TEXT = (
"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"
" - service: `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"
@@ -517,13 +517,13 @@ class StandardsMCPServer(MCPServer):
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"),
Each entry has: ``name``, ``type`` ("package" | "service"),
``path`` (relative to the aetos/ workspace root), ``version_source``
("pyproject.toml" | "VERSION"), ``drone`` (the template file),
``trigger`` ("tag" | "push"), ``docs`` kinds, and ``mcp``.
``trigger`` ("tag"), ``docs`` kinds, and ``mcp``.
Args:
type (str | None): Optional filter, e.g. "package" or "site".
type (str | None): Optional filter, e.g. "package" or "service".
Returns:
list[dict[str, Any]]: Matching repository entries.

View File

@@ -769,7 +769,7 @@ chosen by what the repository produces:</p>
<td><strong>B — Docker deployment</strong></td>
<td><code>templates/drone/service-docker.yaml</code></td>
<td>An image pushed to the registry and run as a container</td>
<td><code>auth-server</code>, <code>docs</code>, <code>mcp</code></td>
<td><code>auth-server</code></td>
</tr>
</tbody>
</table>
@@ -808,30 +808,25 @@ with <code>from_secret</code> — never commit credentials.</p>
</ul>
<hr />
<h2 id="type-b-docker-deployment-service-dockeryaml">Type B — Docker deployment (<code>service-docker.yaml</code>)</h2>
<p>For services and sites that ship a container image. The file contains <strong>two
pipelines</strong>, each mirroring a repo family:</p>
<ol>
<li><strong><code>build-and-deploy-service</code></strong> — triggered on <code>tag</code> (a long-running
API/worker service, e.g. <code>auth-server</code>):</li>
<p>For a service that ships a container image — a long-running API/worker, e.g.
<code>auth-server</code>. The file contains <strong>one pipeline</strong>, <code>build-and-deploy-service</code>,
triggered on <code>tag</code>:</p>
<ul>
<li><code>fetch-tags</code> — resolve the latest tag into <code>LATEST_TAG.txt</code>; fail if there
are no tags.</li>
are no tags.</li>
<li><code>check-version</code> — <strong>guard</strong>: fail unless <code>$DRONE_TAG</code> equals the repo-root
<code>VERSION</code> file (see <a href="../11_versioning_and_changelog/">11 – Versioning &amp; Changelog</a>).</li>
<code>VERSION</code> file (see <a href="../11_versioning_and_changelog/">11 – Versioning &amp; Changelog</a>).</li>
<li><code>check-remote-image</code> — if <code>aetos/&lt;app&gt;:&lt;tag&gt;</code> already exists locally, exit
<code>78</code> and skip the build.</li>
<code>78</code> and skip the build.</li>
<li><code>quality-gate</code> — install from <code>requirements.txt</code> and run
<code>black</code> / <code>ruff</code> / <code>mypy</code> / <code>pytest</code>.</li>
<code>black</code> / <code>ruff</code> / <code>mypy</code> / <code>pytest</code>.</li>
<li><code>build-image</code> — <code>docker build</code> with the private-index build args, tagged
<code>aetos/&lt;app&gt;:&lt;tag&gt;</code> and <code>:latest</code>.</li>
<code>aetos/&lt;app&gt;:&lt;tag&gt;</code> and <code>:latest</code>.</li>
<li><code>push-image</code> — login, re-tag with <code>$REGISTRY_HOST</code>, push both tags.</li>
<li><code>stop-old</code> — <code>docker rm -f &lt;app&gt;</code>.</li>
<li><code>run-container</code> — <code>docker run -d --name &lt;app&gt; -p &lt;host&gt;:&lt;container&gt;
--restart always</code> with the service's <code>-e</code> environment.</li>
<li><strong><code>build-and-deploy-site</code></strong> — triggered on <code>push</code> (a static/HTTP site, e.g.
<code>docs</code>, <code>mcp</code>): <code>build-image</code> → <code>push-image</code> → <code>stop-old</code> → <code>run-container</code>,
with no tag resolution, no quality gate, and no version guard (it ships
<code>:latest</code> only).</li>
</ol>
--restart always</code> with the service's <code>-e</code> environment.</li>
</ul>
<p>Secrets: <code>PIP_REPO_URL</code>, <code>PIP_USERNAME</code>, <code>PIP_PASSWORD</code> (service build args),
<code>REGISTRY_HOST</code>, <code>REGISTRY_USER</code>, <code>REGISTRY_PASS</code>, plus the service runtime env
(for example <code>MONGO_*</code>, <code>JWT_SECRET</code>).</p>
@@ -861,11 +856,6 @@ pipelines</strong>, each mirroring a repo family:</p>
<td><code>9003:8000</code></td>
</tr>
<tr>
<td><code>&lt;site&gt;</code></td>
<td>site image / container name</td>
<td><code>docs</code></td>
</tr>
<tr>
<td><code>&lt;ENV_VAR&gt;</code></td>
<td>runtime environment variable</td>
<td><code>MONGO_HOST</code></td>
@@ -883,8 +873,7 @@ pipelines</strong>, each mirroring a repo family:</p>
version to equal the tag; a service needs a root <code>VERSION</code> file that equals
the tag. See <a href="../11_versioning_and_changelog/">11 – Versioning &amp; Changelog</a>.</li>
<li>Do not rename the pipelines; tooling and runbooks key off
<code>build-and-publish-pypi</code>, <code>build-and-deploy-service</code>, and
<code>build-and-deploy-site</code>.</li>
<code>build-and-publish-pypi</code> and <code>build-and-deploy-service</code>.</li>
</ul>
<hr />
<h2 id="related">Related</h2>

View File

@@ -556,6 +556,21 @@
</span>
</a>
<nav class="md-nav" aria-label="The changelog">
<ul class="md-nav__list">
<li class="md-nav__item">
<a href="#reconcile-before-releasing" class="md-nav__link">
<span class="md-ellipsis">
Reconcile before releasing
</span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item">
@@ -771,6 +786,21 @@
</span>
</a>
<nav class="md-nav" aria-label="The changelog">
<ul class="md-nav__list">
<li class="md-nav__item">
<a href="#reconcile-before-releasing" class="md-nav__link">
<span class="md-ellipsis">
Reconcile before releasing
</span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item">
@@ -842,7 +872,7 @@
<h1 id="versioning-changelog">Versioning &amp; Changelog</h1>
<h2 id="summary">Summary</h2>
<p>Every Aetos artifact — library, service, or site — is versioned with
<p>Every Aetos artifact — library or service — is versioned with
<a href="https://semver.org/spec/v2.0.0.html">Semantic Versioning</a> and documents its
releases in a <a href="https://keepachangelog.com/en/1.1.0/">Keep a Changelog</a> file.
The <strong>version file is the source of truth</strong>, the <strong>git tag must equal it</strong>, and
@@ -867,7 +897,7 @@ and tag contract is exactly what those two CI shapes consume.</p>
<td><code>check-version</code> also reads it to skip duplicates</td>
</tr>
<tr>
<td>Service / site (Docker)</td>
<td>Service (Docker)</td>
<td><code>VERSION</code> file at the repo root</td>
<td>one line, <code>x.y.z</code>; the service <strong>reads</strong> it (build-arg) — never hardcode the version</td>
</tr>
@@ -934,16 +964,35 @@ one open <code>[Unreleased]</code> section with the change categories that apply
<li>Categories: <strong>Added</strong>, <strong>Changed</strong>, <strong>Deprecated</strong>, <strong>Removed</strong>, <strong>Fixed</strong>,
<strong>Security</strong>. Omit empty categories.</li>
<li>Security fixes get their own entry even if the change is otherwise a bug fix.</li>
<li>Keep <code>[Unreleased]</code> current: add an entry in the same change that introduces
the behaviour, not later.</li>
<li>The <code>[Unreleased]</code> heading is never renamed; copy its content into the new
version heading and clear it.</li>
</ul>
<h3 id="reconcile-before-releasing">Reconcile before releasing</h3>
<p><code>[Unreleased]</code> is a <strong>draft, not the source of truth</strong>. It is only trustworthy
when it was maintained alongside each change; a long-running branch or a change
that skipped the changelog leaves it stale, so it can silently under-report a
release. Verify it against the change before you promote:</p>
<ol>
<li>List what actually changed since the last release:
<code>git log &lt;last-tag&gt;..HEAD</code> and <code>git diff --stat &lt;last-tag&gt;..HEAD</code>.</li>
<li>Compare that against <code>[Unreleased]</code>; add an entry for every user-facing change
that is missing.</li>
<li><strong>Always show the proposed additions to the user and wait for approval</strong>
before promoting. If an entry is ambiguous, ask rather than guess.</li>
</ol>
<p>Only then promote. Never cut a release with a known gap between the diff and the
changelog.</p>
<hr />
<h2 id="tagging-strategy-mr-based">Tagging strategy (MR-based)</h2>
<p>The release version is known while the change is still a merge request, so the
bump travels with the change and only the tag is added afterwards:</p>
<ol>
<li><strong>In the MR</strong> — set the intended release version: <code>version</code> in
<code>pyproject.toml</code> (or the <code>VERSION</code> file) to <code>x.y.z</code>, and promote the
<li><strong>In the MR</strong> — reconcile <code>[Unreleased]</code> against the change (see
<a href="#reconcile-before-releasing">Reconcile before releasing</a>) and get the user's
approval for any additions. Then set the intended release version: <code>version</code>
in <code>pyproject.toml</code> (or the <code>VERSION</code> file) to <code>x.y.z</code>, and promote the
changelog (below). Changelog entries and link references name the
<strong>version/tag</strong>, never a commit id.</li>
<li><strong>Merge or squash</strong> the MR into <code>main</code>.</li>
@@ -970,8 +1019,11 @@ After the user confirms the MR is merged/squashed and says to proceed:</p>
<h2 id="release-checklist">Release checklist</h2>
<ol>
<li>Confirm the quality gate is green and <code>main</code> is up to date.</li>
<li>In the MR: move every <code>[Unreleased]</code> entry under the new version heading with
the target date (<code>## [x.y.z] - YYYY-MM-DD</code>); restore an empty <code>[Unreleased]</code>
<li>In the MR: reconcile <code>[Unreleased]</code> against the change being released —
<code>git log &lt;last-tag&gt;..HEAD</code> and <code>git diff --stat &lt;last-tag&gt;..HEAD</code> — add any
missing user-facing entries, and get the user's approval for the additions.
Then move every <code>[Unreleased]</code> entry under the new version heading with the
target date (<code>## [x.y.z] - YYYY-MM-DD</code>) and restore an empty <code>[Unreleased]</code>
on top.</li>
<li>In the MR: add the two compare-link references at the bottom of the
changelog, naming the version/tag (never a commit id).</li>

View File

@@ -557,9 +557,9 @@
</li>
<li class="md-nav__item">
<a href="#services-sites-type-b" class="md-nav__link">
<a href="#services-type-b" class="md-nav__link">
<span class="md-ellipsis">
Services &amp; sites (Type B)
Services (Type B)
</span>
</a>
@@ -711,9 +711,9 @@
</li>
<li class="md-nav__item">
<a href="#services-sites-type-b" class="md-nav__link">
<a href="#services-type-b" class="md-nav__link">
<span class="md-ellipsis">
Services &amp; sites (Type B)
Services (Type B)
</span>
</a>
@@ -837,14 +837,13 @@ Paths are relative to the <code>aetos/</code> workspace root.</p>
</tbody>
</table>
<hr />
<h2 id="services-sites-type-b">Services &amp; sites (Type B)</h2>
<h2 id="services-type-b">Services (Type B)</h2>
<p>A container image pushed to the registry and run on the Docker host. Version
source is the <code>VERSION</code> file; Drone file is <code>service-docker.yaml</code>.</p>
<table>
<thead>
<tr>
<th>Repo</th>
<th>Type</th>
<th>Path</th>
<th>Trigger</th>
<th>Docs</th>
@@ -853,29 +852,13 @@ source is the <code>VERSION</code> file; Drone file is <code>service-docker.yaml
<tbody>
<tr>
<td><code>auth-server</code></td>
<td>service</td>
<td><code>auth-server</code></td>
<td>tag</td>
<td>api, lib, wiki</td>
</tr>
<tr>
<td><code>docs</code></td>
<td>site</td>
<td><code>docs</code></td>
<td>push</td>
<td>—</td>
</tr>
<tr>
<td><code>mcp</code></td>
<td>site</td>
<td><code>mcp</code></td>
<td>push</td>
<td>—</td>
</tr>
</tbody>
</table>
<p>The <strong>service</strong> variant is tag-triggered and enforces <code>tag == VERSION</code>. The
<strong>site</strong> variant is push-triggered, ships <code>:latest</code>, and has no tag guard.</p>
<p>Tag-triggered and enforces <code>tag == VERSION</code>.</p>
<hr />
<h2 id="required-files-per-repo">Required files per repo</h2>
<table>
@@ -895,7 +878,7 @@ source is the <code>VERSION</code> file; Drone file is <code>service-docker.yaml
<td><code>python-lib.yaml</code></td>
</tr>
<tr>
<td>service / site</td>
<td>service</td>
<td><code>VERSION</code></td>
<td><code>CHANGELOG.md</code></td>
<td><code>service-docker.yaml</code></td>

File diff suppressed because one or more lines are too long

Binary file not shown.

View File

@@ -16,7 +16,7 @@
"kind": "drone-service",
"path": "service-docker.yaml",
"placeholder": "",
"description": "Drone pipeline: build, push, and deploy a Docker service or site."
"description": "Drone pipeline: build, push, and deploy a Docker service."
}
]
}

File diff suppressed because one or more lines are too long

View File

@@ -2,7 +2,7 @@
"module": "repository_inventory",
"content": {
"path": "repository_inventory",
"docstring": "# Repository Inventory\n\n## Summary\n\nThe canonical list of Aetos repositories covered by the CI and versioning\nstandards, with the artifact each one produces, its version source, and its\nDrone shape. Every entry maps to the contract in\n[11 – Versioning & Changelog](11_versioning_and_changelog.md) and the two CI\nfiles in [10 – Drone Pipelines](10_drone_pipelines.md).\n\nThe same data is published machine-readably as the `registry://repos` MCP\nresource (and the `list_repos` tool), sourced from `standards/src/repos.json`.\nPaths are relative to the `aetos/` workspace root.\n\n---\n\n## Packages (Type A)\n\nA wheel + sdist published to the private index. Version source is\n`pyproject.toml`; Drone file is `python-lib.yaml` (tag-triggered).\n\n| Repo | Path | Docs | MCP server |\n|------|------|------|------------|\n| `dagpipe` | `dagpipe` | lib, wiki | `dagpipe:8006` |\n| `doc-forge` | `doc-forge` | lib, wiki | `docforge:8001` |\n| `hexa` | `hexa` | lib, wiki | `hexa:8008` |\n| `mail-intake` | `mail-intake` | lib, wiki | `mail_intake:8002` |\n| `mongo-ops` | `mongo-ops` | lib, wiki | `mongo_ops:8007` |\n| `omniread` | `omniread` | lib, wiki | `omniread:8003` |\n| `openapi-first` | `openapi-first` | lib, wiki | `openapi_first:8004` |\n| `py-jwt` | `py-jwt` | lib, wiki | `jwtlib:8005` |\n\n---\n\n## Services & sites (Type B)\n\nA container image pushed to the registry and run on the Docker host. Version\nsource is the `VERSION` file; Drone file is `service-docker.yaml`.\n\n| Repo | Type | Path | Trigger | Docs |\n|------|------|------|---------|------|\n| `auth-server` | service | `auth-server` | tag | api, lib, wiki |\n| `docs` | site | `docs` | push | — |\n| `mcp` | site | `mcp` | push | — |\n\nThe **service** variant is tag-triggered and enforces `tag == VERSION`. The\n**site** variant is push-triggered, ships `:latest`, and has no tag guard.\n\n---\n\n## Required files per repo\n\n| Type | Version source | Changelog | Drone |\n|------|----------------|-----------|-------|\n| package | `pyproject.toml` | `CHANGELOG.md` | `python-lib.yaml` |\n| service / site | `VERSION` | `CHANGELOG.md` | `service-docker.yaml` |\n\nEvery covered repository must be able to satisfy `tag == version`:\nthe version file (never a commit id) is bumped in the release MR, the MR is\nmerged, and the tag is created on the merged commit.\n\n---\n\n## Reading this at runtime\n\n- Resource: `registry://repos`\n- Tool: `list_repos()`\n- Humans: this page (mirror of `standards/src/repos.json`).\n\n---\n\n## Related\n\n- [10 – Drone Pipelines](10_drone_pipelines.md) · [11 – Versioning & Changelog](11_versioning_and_changelog.md) · [13 – Library Registry](13_library_registry.md)\n",
"docstring": "# Repository Inventory\n\n## Summary\n\nThe canonical list of Aetos repositories covered by the CI and versioning\nstandards, with the artifact each one produces, its version source, and its\nDrone shape. Every entry maps to the contract in\n[11 – Versioning & Changelog](11_versioning_and_changelog.md) and the two CI\nfiles in [10 – Drone Pipelines](10_drone_pipelines.md).\n\nThe same data is published machine-readably as the `registry://repos` MCP\nresource (and the `list_repos` tool), sourced from `standards/src/repos.json`.\nPaths are relative to the `aetos/` workspace root.\n\n---\n\n## Packages (Type A)\n\nA wheel + sdist published to the private index. Version source is\n`pyproject.toml`; Drone file is `python-lib.yaml` (tag-triggered).\n\n| Repo | Path | Docs | MCP server |\n|------|------|------|------------|\n| `dagpipe` | `dagpipe` | lib, wiki | `dagpipe:8006` |\n| `doc-forge` | `doc-forge` | lib, wiki | `docforge:8001` |\n| `hexa` | `hexa` | lib, wiki | `hexa:8008` |\n| `mail-intake` | `mail-intake` | lib, wiki | `mail_intake:8002` |\n| `mongo-ops` | `mongo-ops` | lib, wiki | `mongo_ops:8007` |\n| `omniread` | `omniread` | lib, wiki | `omniread:8003` |\n| `openapi-first` | `openapi-first` | lib, wiki | `openapi_first:8004` |\n| `py-jwt` | `py-jwt` | lib, wiki | `jwtlib:8005` |\n\n---\n\n## Services (Type B)\n\nA container image pushed to the registry and run on the Docker host. Version\nsource is the `VERSION` file; Drone file is `service-docker.yaml`.\n\n| Repo | Path | Trigger | Docs |\n|------|------|---------|------|\n| `auth-server` | `auth-server` | tag | api, lib, wiki |\n\nTag-triggered and enforces `tag == VERSION`.\n\n---\n\n## Required files per repo\n\n| Type | Version source | Changelog | Drone |\n|------|----------------|-----------|-------|\n| package | `pyproject.toml` | `CHANGELOG.md` | `python-lib.yaml` |\n| service | `VERSION` | `CHANGELOG.md` | `service-docker.yaml` |\n\nEvery covered repository must be able to satisfy `tag == version`:\nthe version file (never a commit id) is bumped in the release MR, the MR is\nmerged, and the tag is created on the merged commit.\n\n---\n\n## Reading this at runtime\n\n- Resource: `registry://repos`\n- Tool: `list_repos()`\n- Humans: this page (mirror of `standards/src/repos.json`).\n\n---\n\n## Related\n\n- [10 – Drone Pipelines](10_drone_pipelines.md) · [11 – Versioning & Changelog](11_versioning_and_changelog.md) · [13 – Library Registry](13_library_registry.md)\n",
"objects": {}
}
}

File diff suppressed because one or more lines are too long

View File

@@ -2,7 +2,7 @@
"module": "wiki_development_releasing",
"content": {
"path": "wiki_development_releasing",
"docstring": "# Releasing & Versioning\n\nHow to cut a release of any Aetos repository. The rule lives in the reference\npage [Versioning & Changelog](../lib/11_versioning_and_changelog/); this is the\nrunbook you follow.\n\nEvery repository is one of two Drone shapes — a **PyPI package** or a **Docker\ndeployment** ([Drone Pipelines](../lib/10_drone_pipelines/)). Both release the\nsame way: the version bump and changelog promotion go **in the merge request**,\nand the tag is created on the merged commit.\n\n---\n\n## 🧾 Before you start\n\n- You have an open **merge request** holding the change, and `main` is green\n (the Drone quality gate passed on the last run).\n- You know whether this is a **breaking**, **feature**, or **fix** release.\n- The version file exists:\n - package → `version` in `pyproject.toml`\n - service / site → `VERSION` at the repo root\n\n---\n\n## 🚦 Pick the next version\n\nThe version file and the tag must be identical. For `0.y.z`, `0.y` is the major\nline (see the reference page); once at `1.0.0`, normal SemVer applies.\n\n| Change | `0.y.z` → | `1.0.0+` → |\n|--------|-----------|------------|\n| Breaking | `0.(y+1).0` | `(x+1).0.0` |\n| Feature | `0.y.(z+1)` | `x.(y+1).0` |\n| Fix | `0.y.(z+1)` | `x.y.(z+1)` |\n\n---\n\n## 🌿 Release in the MR\n\nThe version is known while the change is still a merge request, so the bump\nrides along with it. Do both edits **in the MR**:\n\n- **Promote the changelog** in `CHANGELOG.md`: move the `[Unreleased]` content\n under a new `## [x.y.z] - YYYY-MM-DD` heading, leave a fresh empty\n `[Unreleased]` on top, and add the compare links — naming the **version/tag**,\n never a commit id:\n\n ```markdown\n [Unreleased]: https://git.aetoskia.com/aetos/<repo>/compare/x.y.z...HEAD\n [x.y.z]: https://git.aetoskia.com/aetos/<repo>/compare/x.y.(z-1)...x.y.z\n ```\n\n- **Bump the version file** to the intended release version: `version = \"x.y.z\"`\n in `pyproject.toml` (packages) or the `VERSION` file (services/sites).\n\n---\n\n## 🏷️ Tag the merged commit\n\n1. **Merge or squash** the MR into `main`.\n2. **Create the tag** on the resulting commit: `git tag x.y.z` (lightweight).\n3. **Push the tag** (`git push origin x.y.z`) — Drone builds the release.\n\n### 🤖 Agent-assisted tagging\n\nThe agent does not author or merge the MR and never tags before the merge. After\nyou confirm the MR is merged/squashed and tell it to proceed, the agent:\n\n1. Waits for that explicit go-ahead.\n2. Finds the commit that introduced the bump:\n - package:\n `git log -S 'version = \"x.y.z\"' -n 1 --format='%H %s' -- pyproject.toml`\n - service / site:\n `git log -S 'x.y.z' -n 1 --format='%H %s' -- VERSION`\n3. Verifies that commit's version file equals `x.y.z`, then tags **only** that\n commit: `git tag x.y.z <sha>`.\n4. Does **not** push — it asks you to run `git push origin x.y.z`.\n\n---\n\n## 🤖 What Drone does\n\n- **Type A (package):** asserts `tag == pyproject version`, then skips if that\n version already exists on the index, runs the quality gate, builds, and\n uploads the wheel + sdist.\n- **Type B (service):** asserts `tag == VERSION`, resolves the tag, skips if the\n image already exists, runs the quality gate, builds, pushes, and restarts the\n container.\n- **Type B (site):** triggered on push, ships `:latest` and has no tag guard.\n\nIf the guard fails, nothing is published — fix the version file, amend the\ncommit, and move the tag **only** if it was never pushed.\n\n---\n\n## 🛟 Backfill / recovery\n\nIf an older tag never published (a past version-file mismatch, an index\noutage):\n\n- **Package:** run the `backfill-pypi-from-tags` pipeline (custom event). It\n checks out every tag, verifies `tag == pyproject version` at that commit,\n and uploads the ones missing from the index. Already-fixed history is never\n rewritten.\n- **Service:** rebuild the image for the missing tag with a fresh commit and a\n new patch version; do not move a published tag.\n\n---\n\n## 🔁 If a release is wrong\n\n- Never reuse or force-move a pushed tag.\n- Fix forward: bump to the next patch, promote the changelog, tag again.\n\n---\n\n## Related\n\n- [Testing & Quality Gates](01_testing.md) · [Versioning & Changelog](../lib/11_versioning_and_changelog/) · [Drone Pipelines](../lib/10_drone_pipelines/)\n",
"docstring": "# Releasing & Versioning\n\nHow to cut a release of any Aetos repository. The rule lives in the reference\npage [Versioning & Changelog](../lib/11_versioning_and_changelog/); this is the\nrunbook you follow.\n\nEvery repository is one of two Drone shapes — a **PyPI package** or a **Docker\ndeployment** ([Drone Pipelines](../lib/10_drone_pipelines/)). Both release the\nsame way: the version bump and changelog promotion go **in the merge request**,\nand the tag is created on the merged commit.\n\n---\n\n## 🧾 Before you start\n\n- You have an open **merge request** holding the change, and `main` is green\n (the Drone quality gate passed on the last run).\n- You know whether this is a **breaking**, **feature**, or **fix** release.\n- The version file exists:\n - package → `version` in `pyproject.toml`\n - service → `VERSION` at the repo root\n\n---\n\n## 🚦 Pick the next version\n\nThe version file and the tag must be identical. For `0.y.z`, `0.y` is the major\nline (see the reference page); once at `1.0.0`, normal SemVer applies.\n\n| Change | `0.y.z` → | `1.0.0+` → |\n|--------|-----------|------------|\n| Breaking | `0.(y+1).0` | `(x+1).0.0` |\n| Feature | `0.y.(z+1)` | `x.(y+1).0` |\n| Fix | `0.y.(z+1)` | `x.y.(z+1)` |\n\n---\n\n## 🌿 Release in the MR\n\nThe version is known while the change is still a merge request, so the bump\nrides along with it. Do all edits **in the MR**:\n\n- **Reconcile the changelog first.** `[Unreleased]` is a **draft, not the source\n of truth**: diff the change against the last tag\n (`git log <last-tag>..HEAD`, `git diff --stat <last-tag>..HEAD`) and add an\n entry for every user-facing change that is missing. **Always show the proposed\n additions to the user and wait for approval** before promoting — ask rather\n than guess when an entry is ambiguous.\n\n- **Promote the changelog** in `CHANGELOG.md`: move the (now reconciled)\n `[Unreleased]` content under a new `## [x.y.z] - YYYY-MM-DD` heading, leave a\n fresh empty `[Unreleased]` on top, and add the compare links — naming the\n **version/tag**, never a commit id:\n\n ```markdown\n [Unreleased]: https://git.aetoskia.com/aetos/<repo>/compare/x.y.z...HEAD\n [x.y.z]: https://git.aetoskia.com/aetos/<repo>/compare/x.y.(z-1)...x.y.z\n ```\n\n- **Bump the version file** to the intended release version: `version = \"x.y.z\"`\n in `pyproject.toml` (packages) or the `VERSION` file (services).\n\n---\n\n## 🏷️ Tag the merged commit\n\n1. **Merge or squash** the MR into `main`.\n2. **Create the tag** on the resulting commit: `git tag x.y.z` (lightweight).\n3. **Push the tag** (`git push origin x.y.z`) — Drone builds the release.\n\n### 🤖 Agent-assisted tagging\n\nThe agent does not author or merge the MR and never tags before the merge. After\nyou confirm the MR is merged/squashed and tell it to proceed, the agent:\n\n1. Waits for that explicit go-ahead.\n2. Finds the commit that introduced the bump:\n - package:\n `git log -S 'version = \"x.y.z\"' -n 1 --format='%H %s' -- pyproject.toml`\n - service:\n `git log -S 'x.y.z' -n 1 --format='%H %s' -- VERSION`\n3. Verifies that commit's version file equals `x.y.z`, then tags **only** that\n commit: `git tag x.y.z <sha>`.\n4. Does **not** push — it asks you to run `git push origin x.y.z`.\n\n---\n\n## 🤖 What Drone does\n\n- **Type A (package):** asserts `tag == pyproject version`, then skips if that\n version already exists on the index, runs the quality gate, builds, and\n uploads the wheel + sdist.\n- **Type B (service):** asserts `tag == VERSION`, resolves the tag, skips if the\n image already exists, runs the quality gate, builds, pushes, and restarts the\n container.\n\nIf the guard fails, nothing is published — fix the version file, amend the\ncommit, and move the tag **only** if it was never pushed.\n\n---\n\n## 🛟 Backfill / recovery\n\nIf an older tag never published (a past version-file mismatch, an index\noutage):\n\n- **Package:** run the `backfill-pypi-from-tags` pipeline (custom event). It\n checks out every tag, verifies `tag == pyproject version` at that commit,\n and uploads the ones missing from the index. Already-fixed history is never\n rewritten.\n- **Service:** rebuild the image for the missing tag with a fresh commit and a\n new patch version; do not move a published tag.\n\n---\n\n## 🔁 If a release is wrong\n\n- Never reuse or force-move a pushed tag.\n- Fix forward: bump to the next patch, promote the changelog, tag again.\n\n---\n\n## Related\n\n- [Testing & Quality Gates](01_testing.md) · [Versioning & Changelog](../lib/11_versioning_and_changelog/) · [Drone Pipelines](../lib/10_drone_pipelines/)\n",
"objects": {}
}
}

View File

@@ -13,7 +13,7 @@
"kind": "version-file",
"path": "VERSION",
"placeholder": "",
"description": "Single-line SemVer source of truth for services and sites."
"description": "Single-line SemVer source of truth for services."
}
]
}

View File

@@ -144,26 +144,6 @@
"wiki"
],
"mcp": null
},
{
"name": "docs",
"type": "site",
"path": "docs",
"version_source": "VERSION",
"drone": "service-docker.yaml",
"trigger": "push",
"docs": [],
"mcp": null
},
{
"name": "mcp",
"type": "site",
"path": "mcp",
"version_source": "VERSION",
"drone": "service-docker.yaml",
"trigger": "push",
"docs": [],
"mcp": null
}
]
}

View File

@@ -5,6 +5,13 @@ 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).
<!--
Keep [Unreleased] current as you land changes. On release, reconcile it against
the change (git log / git diff since the last tag), add any missing user-facing
entries, and get the user's approval before promoting it into a version heading.
See standards: Versioning & Changelog (registry://release).
-->
## [Unreleased]
### Added

View File

@@ -1,29 +1,25 @@
# Drone standard — Type B: Docker deployment
#
# Canonical pipelines for services that build an image, push it to the private
# registry, and run it on the Docker host (auth-server, docs, mcp).
# Canonical pipeline for a service that builds an image, pushes it to the
# private registry, and runs it on the Docker host (auth-server).
#
# Two variants ship together:
# build-and-deploy-service — a long-running API/worker service: fetch the git
# tag, skip if the image already exists, run the
# quality gate, build, push, restart the container.
# build-and-deploy-site — a static/HTTP site: build, push, restart, on
# every push (no tag, no quality gate).
# build-and-deploy-service — fetch the git tag, fail unless it equals the
# repo-root VERSION file, skip if the image already
# exists, run the quality gate, build, push, and
# restart the container.
#
# Secrets (Drone): PIP_REPO_URL, PIP_USERNAME, PIP_PASSWORD (service only),
# REGISTRY_HOST, REGISTRY_USER, REGISTRY_PASS, plus the service's runtime env
# (for example MONGO_* / JWT_SECRET).
# Secrets (Drone): PIP_REPO_URL, PIP_USERNAME, PIP_PASSWORD, REGISTRY_HOST,
# REGISTRY_USER, REGISTRY_PASS, plus the service's runtime env (for example
# MONGO_* / JWT_SECRET).
#
# Placeholders to set per repo:
# <app> image / container / network name (e.g. auth-server).
# aetos/<app> image repository on the private registry.
# <host-port> published host port; <container-port> the in-image port.
# <site> site image name for the site variant (e.g. docs, mcp).
# <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.
# Version guard: the tag-triggered pipeline fails unless the git tag equals the
# repo-root VERSION file (see the versioning standard).
---
kind: pipeline
type: docker
@@ -203,75 +199,3 @@ trigger:
event:
- tag
---
kind: pipeline
type: docker
name: build-and-deploy-site
platform:
os: linux
arch: arm64
workspace:
path: /drone/src
volumes:
- name: dockersock
host:
path: /var/run/docker.sock
steps:
- name: build-image
image: docker:24
volumes:
- name: dockersock
path: /var/run/docker.sock
commands:
- echo "Building Docker image for aetos/<site>..."
- docker build -t aetos/<site>:latest .
- name: push-image
image: docker:24
environment:
REGISTRY_HOST:
from_secret: REGISTRY_HOST
REGISTRY_USER:
from_secret: REGISTRY_USER
REGISTRY_PASS:
from_secret: REGISTRY_PASS
volumes:
- name: dockersock
path: /var/run/docker.sock
commands:
- echo "Logging into registry $REGISTRY_HOST ..."
- echo "$REGISTRY_PASS" | docker login $REGISTRY_HOST -u "$REGISTRY_USER" --password-stdin
- docker tag aetos/<site>:latest $REGISTRY_HOST/aetos/<site>:latest
- echo "Pushing image to $REGISTRY_HOST..."
- docker push $REGISTRY_HOST/aetos/<site>:latest
- name: stop-old
image: docker:24
volumes:
- name: dockersock
path: /var/run/docker.sock
commands:
- echo "Stopping old container..."
- docker rm -f <site> || true
- name: run-container
image: docker:24
volumes:
- name: dockersock
path: /var/run/docker.sock
commands:
- echo "Starting aetos/<site>:latest ..."
- |
docker run -d \
--name <site> \
-p <host-port>:<container-port> \
--restart always \
aetos/<site>:latest
trigger:
event:
- push

View File

@@ -20,9 +20,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Security
<!--
In the release merge request, promote the entries above into a new heading and
leave a fresh, empty [Unreleased] section. Name the version/tag, never a commit
id:
In the release merge request, first reconcile the entries above against the
change: diff it against the last tag (`git log <last-tag>..HEAD`,
`git diff --stat <last-tag>..HEAD`) and add any missing user-facing entries.
`[Unreleased]` is a draft — always show the additions to the user for approval
before promoting. Then promote the entries above into a new heading and leave a
fresh, empty [Unreleased] section. Name the version/tag, never a commit id:
## [0.1.0] - YYYY-MM-DD

View File

@@ -9,7 +9,7 @@ chosen by what the repository produces:
| Type | File | Produces | Repos |
|------|------|----------|-------|
| **A — PyPI package** | `templates/drone/python-lib.yaml` | A wheel + sdist on the private index | `dagpipe`, `doc-forge`, `hexa`, `mail-intake`, `mongo-ops`, `omniread`, `openapi-first`, `py-jwt` |
| **B — Docker deployment** | `templates/drone/service-docker.yaml` | An image pushed to the registry and run as a container | `auth-server`, `docs`, `mcp` |
| **B — Docker deployment** | `templates/drone/service-docker.yaml` | An image pushed to the registry and run as a container | `auth-server` |
Both templates run on `platform: linux / arm64` with the workspace at
`/drone/src`. Copy the matching template to `<repo>/.drone.yml`; the Type A file
@@ -50,29 +50,24 @@ Secrets: `PIP_REPO_URL`, `PIP_USERNAME`, `PIP_PASSWORD`.
## Type B — Docker deployment (`service-docker.yaml`)
For services and sites that ship a container image. The file contains **two
pipelines**, each mirroring a repo family:
For a service that ships a container image — a long-running API/worker, e.g.
`auth-server`. The file contains **one pipeline**, `build-and-deploy-service`,
triggered on `tag`:
1. **`build-and-deploy-service`** — triggered on `tag` (a long-running
API/worker service, e.g. `auth-server`):
- `fetch-tags` — resolve the latest tag into `LATEST_TAG.txt`; fail if there
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
`78` and skip the build.
- `quality-gate` — install from `requirements.txt` and run
`black` / `ruff` / `mypy` / `pytest`.
- `build-image` — `docker build` with the private-index build args, tagged
`aetos/<app>:<tag>` and `:latest`.
- `push-image` — login, re-tag with `$REGISTRY_HOST`, push both tags.
- `stop-old` — `docker rm -f <app>`.
- `run-container` — `docker run -d --name <app> -p <host>:<container>
--restart always` with the service's `-e` environment.
2. **`build-and-deploy-site`** — triggered on `push` (a static/HTTP site, e.g.
`docs`, `mcp`): `build-image` → `push-image` → `stop-old` → `run-container`,
with no tag resolution, no quality gate, and no version guard (it ships
`:latest` only).
- `fetch-tags` — resolve the latest tag into `LATEST_TAG.txt`; fail if there
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
`78` and skip the build.
- `quality-gate` — install from `requirements.txt` and run
`black` / `ruff` / `mypy` / `pytest`.
- `build-image` — `docker build` with the private-index build args, tagged
`aetos/<app>:<tag>` and `:latest`.
- `push-image` — login, re-tag with `$REGISTRY_HOST`, push both tags.
- `stop-old` — `docker rm -f <app>`.
- `run-container` — `docker run -d --name <app> -p <host>:<container>
--restart always` with the service's `-e` environment.
Secrets: `PIP_REPO_URL`, `PIP_USERNAME`, `PIP_PASSWORD` (service build args),
`REGISTRY_HOST`, `REGISTRY_USER`, `REGISTRY_PASS`, plus the service runtime env
@@ -85,7 +80,6 @@ Placeholders to fill in per repo:
| `<app>` | service image / container name | `auth-server` |
| `aetos/<app>` | image repository on the registry | `aetos/auth-server` |
| `<host-port>` / `<container-port>` | published : in-image port | `9003:8000` |
| `<site>` | site image / container name | `docs` |
| `<ENV_VAR>` | runtime environment variable | `MONGO_HOST` |
---
@@ -100,8 +94,7 @@ Placeholders to fill in per repo:
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
`build-and-publish-pypi`, `build-and-deploy-service`, and
`build-and-deploy-site`.
`build-and-publish-pypi` and `build-and-deploy-service`.
---

View File

@@ -2,7 +2,7 @@
## Summary
Every Aetos artifact — library, service, or site — is versioned with
Every Aetos artifact — library or service — 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
@@ -19,7 +19,7 @@ and tag contract is exactly what those two CI shapes consume.
| 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`; the service **reads** it (build-arg) — never hardcode the version |
| Service (Docker) | `VERSION` file at the repo root | one line, `x.y.z`; the service **reads** it (build-arg) — never hardcode the version |
Rules:
@@ -84,9 +84,28 @@ Conventions:
- 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.
- Keep `[Unreleased]` current: add an entry in the same change that introduces
the behaviour, not later.
- The `[Unreleased]` heading is never renamed; copy its content into the new
version heading and clear it.
### Reconcile before releasing
`[Unreleased]` is a **draft, not the source of truth**. It is only trustworthy
when it was maintained alongside each change; a long-running branch or a change
that skipped the changelog leaves it stale, so it can silently under-report a
release. Verify it against the change before you promote:
1. List what actually changed since the last release:
`git log <last-tag>..HEAD` and `git diff --stat <last-tag>..HEAD`.
2. Compare that against `[Unreleased]`; add an entry for every user-facing change
that is missing.
3. **Always show the proposed additions to the user and wait for approval**
before promoting. If an entry is ambiguous, ask rather than guess.
Only then promote. Never cut a release with a known gap between the diff and the
changelog.
---
## Tagging strategy (MR-based)
@@ -94,8 +113,10 @@ Conventions:
The release version is known while the change is still a merge request, so the
bump travels with the change and only the tag is added afterwards:
1. **In the MR** — set the intended release version: `version` in
`pyproject.toml` (or the `VERSION` file) to `x.y.z`, and promote the
1. **In the MR** — reconcile `[Unreleased]` against the change (see
[Reconcile before releasing](#reconcile-before-releasing)) and get the user's
approval for any additions. Then set the intended release version: `version`
in `pyproject.toml` (or the `VERSION` file) to `x.y.z`, and promote the
changelog (below). Changelog entries and link references name the
**version/tag**, never a commit id.
2. **Merge or squash** the MR into `main`.
@@ -125,8 +146,11 @@ After the user confirms the MR is merged/squashed and says to proceed:
## Release checklist
1. Confirm the quality gate is green and `main` is up to date.
2. In the MR: move every `[Unreleased]` entry under the new version heading with
the target date (`## [x.y.z] - YYYY-MM-DD`); restore an empty `[Unreleased]`
2. In the MR: reconcile `[Unreleased]` against the change being released —
`git log <last-tag>..HEAD` and `git diff --stat <last-tag>..HEAD` — add any
missing user-facing entries, and get the user's approval for the additions.
Then move every `[Unreleased]` entry under the new version heading with the
target date (`## [x.y.z] - YYYY-MM-DD`) and restore an empty `[Unreleased]`
on top.
3. In the MR: add the two compare-link references at the bottom of the
changelog, naming the version/tag (never a commit id).

View File

@@ -32,19 +32,16 @@ A wheel + sdist published to the private index. Version source is
---
## Services & sites (Type B)
## Services (Type B)
A container image pushed to the registry and run on the Docker host. Version
source is the `VERSION` file; Drone file is `service-docker.yaml`.
| Repo | Type | Path | Trigger | Docs |
|------|------|------|---------|------|
| `auth-server` | service | `auth-server` | tag | api, lib, wiki |
| `docs` | site | `docs` | push | — |
| `mcp` | site | `mcp` | push | — |
| Repo | Path | Trigger | Docs |
|------|------|---------|------|
| `auth-server` | `auth-server` | tag | api, lib, wiki |
The **service** variant is tag-triggered and enforces `tag == VERSION`. The
**site** variant is push-triggered, ships `:latest`, and has no tag guard.
Tag-triggered and enforces `tag == VERSION`.
---
@@ -53,7 +50,7 @@ The **service** variant is tag-triggered and enforces `tag == VERSION`. The
| Type | Version source | Changelog | Drone |
|------|----------------|-----------|-------|
| package | `pyproject.toml` | `CHANGELOG.md` | `python-lib.yaml` |
| service / site | `VERSION` | `CHANGELOG.md` | `service-docker.yaml` |
| service | `VERSION` | `CHANGELOG.md` | `service-docker.yaml` |
Every covered repository must be able to satisfy `tag == version`:
the version file (never a commit id) is bumped in the release MR, the MR is

View File

@@ -92,26 +92,6 @@
"trigger": "tag",
"docs": ["api", "lib", "wiki"],
"mcp": null
},
{
"name": "docs",
"type": "site",
"path": "docs",
"version_source": "VERSION",
"drone": "service-docker.yaml",
"trigger": "push",
"docs": [],
"mcp": null
},
{
"name": "mcp",
"type": "site",
"path": "mcp",
"version_source": "VERSION",
"drone": "service-docker.yaml",
"trigger": "push",
"docs": [],
"mcp": null
}
]
}

View File

@@ -5,6 +5,13 @@ 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).
<!--
Keep [Unreleased] current as you land changes. On release, reconcile it against
the change (git log / git diff since the last tag), add any missing user-facing
entries, and get the user's approval before promoting it into a version heading.
See standards: Versioning & Changelog (registry://release).
-->
## [Unreleased]
### Added

View File

@@ -1,29 +1,25 @@
# Drone standard — Type B: Docker deployment
#
# Canonical pipelines for services that build an image, push it to the private
# registry, and run it on the Docker host (auth-server, docs, mcp).
# Canonical pipeline for a service that builds an image, pushes it to the
# private registry, and runs it on the Docker host (auth-server).
#
# Two variants ship together:
# build-and-deploy-service — a long-running API/worker service: fetch the git
# tag, skip if the image already exists, run the
# quality gate, build, push, restart the container.
# build-and-deploy-site — a static/HTTP site: build, push, restart, on
# every push (no tag, no quality gate).
# build-and-deploy-service — fetch the git tag, fail unless it equals the
# repo-root VERSION file, skip if the image already
# exists, run the quality gate, build, push, and
# restart the container.
#
# Secrets (Drone): PIP_REPO_URL, PIP_USERNAME, PIP_PASSWORD (service only),
# REGISTRY_HOST, REGISTRY_USER, REGISTRY_PASS, plus the service's runtime env
# (for example MONGO_* / JWT_SECRET).
# Secrets (Drone): PIP_REPO_URL, PIP_USERNAME, PIP_PASSWORD, REGISTRY_HOST,
# REGISTRY_USER, REGISTRY_PASS, plus the service's runtime env (for example
# MONGO_* / JWT_SECRET).
#
# Placeholders to set per repo:
# <app> image / container / network name (e.g. auth-server).
# aetos/<app> image repository on the private registry.
# <host-port> published host port; <container-port> the in-image port.
# <site> site image name for the site variant (e.g. docs, mcp).
# <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.
# Version guard: the tag-triggered pipeline fails unless the git tag equals the
# repo-root VERSION file (see the versioning standard).
---
kind: pipeline
type: docker
@@ -203,75 +199,3 @@ trigger:
event:
- tag
---
kind: pipeline
type: docker
name: build-and-deploy-site
platform:
os: linux
arch: arm64
workspace:
path: /drone/src
volumes:
- name: dockersock
host:
path: /var/run/docker.sock
steps:
- name: build-image
image: docker:24
volumes:
- name: dockersock
path: /var/run/docker.sock
commands:
- echo "Building Docker image for aetos/<site>..."
- docker build -t aetos/<site>:latest .
- name: push-image
image: docker:24
environment:
REGISTRY_HOST:
from_secret: REGISTRY_HOST
REGISTRY_USER:
from_secret: REGISTRY_USER
REGISTRY_PASS:
from_secret: REGISTRY_PASS
volumes:
- name: dockersock
path: /var/run/docker.sock
commands:
- echo "Logging into registry $REGISTRY_HOST ..."
- echo "$REGISTRY_PASS" | docker login $REGISTRY_HOST -u "$REGISTRY_USER" --password-stdin
- docker tag aetos/<site>:latest $REGISTRY_HOST/aetos/<site>:latest
- echo "Pushing image to $REGISTRY_HOST..."
- docker push $REGISTRY_HOST/aetos/<site>:latest
- name: stop-old
image: docker:24
volumes:
- name: dockersock
path: /var/run/docker.sock
commands:
- echo "Stopping old container..."
- docker rm -f <site> || true
- name: run-container
image: docker:24
volumes:
- name: dockersock
path: /var/run/docker.sock
commands:
- echo "Starting aetos/<site>:latest ..."
- |
docker run -d \
--name <site> \
-p <host-port>:<container-port> \
--restart always \
aetos/<site>:latest
trigger:
event:
- push

View File

@@ -20,9 +20,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Security
<!--
In the release merge request, promote the entries above into a new heading and
leave a fresh, empty [Unreleased] section. Name the version/tag, never a commit
id:
In the release merge request, first reconcile the entries above against the
change: diff it against the last tag (`git log <last-tag>..HEAD`,
`git diff --stat <last-tag>..HEAD`) and add any missing user-facing entries.
`[Unreleased]` is a draft — always show the additions to the user for approval
before promoting. Then promote the entries above into a new heading and leave a
fresh, empty [Unreleased] section. Name the version/tag, never a commit id:
## [0.1.0] - YYYY-MM-DD

View File

@@ -18,7 +18,7 @@ and the tag is created on the merged commit.
- 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
- service → `VERSION` at the repo root
---
@@ -38,12 +38,19 @@ line (see the reference page); once at `1.0.0`, normal SemVer applies.
## 🌿 Release in the MR
The version is known while the change is still a merge request, so the bump
rides along with it. Do both edits **in the MR**:
rides along with it. Do all edits **in the MR**:
- **Promote the changelog** in `CHANGELOG.md`: move the `[Unreleased]` content
under a new `## [x.y.z] - YYYY-MM-DD` heading, leave a fresh empty
`[Unreleased]` on top, and add the compare links — naming the **version/tag**,
never a commit id:
- **Reconcile the changelog first.** `[Unreleased]` is a **draft, not the source
of truth**: diff the change against the last tag
(`git log <last-tag>..HEAD`, `git diff --stat <last-tag>..HEAD`) and add an
entry for every user-facing change that is missing. **Always show the proposed
additions to the user and wait for approval** before promoting — ask rather
than guess when an entry is ambiguous.
- **Promote the changelog** in `CHANGELOG.md`: move the (now reconciled)
`[Unreleased]` content under a new `## [x.y.z] - YYYY-MM-DD` heading, leave a
fresh empty `[Unreleased]` on top, and add the compare links — naming the
**version/tag**, never a commit id:
```markdown
[Unreleased]: https://git.aetoskia.com/aetos/<repo>/compare/x.y.z...HEAD
@@ -51,7 +58,7 @@ rides along with it. Do both edits **in the MR**:
```
- **Bump the version file** to the intended release version: `version = "x.y.z"`
in `pyproject.toml` (packages) or the `VERSION` file (services/sites).
in `pyproject.toml` (packages) or the `VERSION` file (services).
---
@@ -70,8 +77,8 @@ you confirm the MR is merged/squashed and tell it to proceed, the agent:
2. Finds the commit that introduced the bump:
- package:
`git log -S 'version = "x.y.z"' -n 1 --format='%H %s' -- pyproject.toml`
- service / site:
`git log -S 'x.y.z' -n 1 --format='%H %s' -- VERSION`
- service:
`git log -S 'x.y.z' -n 1 --format='%H %s' -- VERSION`
3. Verifies that commit's version file equals `x.y.z`, then tags **only** that
commit: `git tag x.y.z <sha>`.
4. Does **not** push — it asks you to run `git push origin x.y.z`.
@@ -86,7 +93,6 @@ you confirm the MR is merged/squashed and tell it to proceed, the agent:
- **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.

View File

@@ -636,7 +636,7 @@ and the tag is created on the merged commit.</p>
<li>You know whether this is a <strong>breaking</strong>, <strong>feature</strong>, or <strong>fix</strong> release.</li>
<li>The version file exists:</li>
<li>package → <code>version</code> in <code>pyproject.toml</code></li>
<li>service / site → <code>VERSION</code> at the repo root</li>
<li>service → <code>VERSION</code> at the repo root</li>
</ul>
<hr />
<h2 id="pick-the-next-version">🚦 Pick the next version</h2>
@@ -671,12 +671,22 @@ line (see the reference page); once at <code>1.0.0</code>, normal SemVer applies
<hr />
<h2 id="release-in-the-mr">🌿 Release in the MR</h2>
<p>The version is known while the change is still a merge request, so the bump
rides along with it. Do both edits <strong>in the MR</strong>:</p>
rides along with it. Do all edits <strong>in the MR</strong>:</p>
<ul>
<li><strong>Promote the changelog</strong> in <code>CHANGELOG.md</code>: move the <code>[Unreleased]</code> content
under a new <code>## [x.y.z] - YYYY-MM-DD</code> heading, leave a fresh empty
<code>[Unreleased]</code> on top, and add the compare links — naming the <strong>version/tag</strong>,
never a commit id:</li>
<li>
<p><strong>Reconcile the changelog first.</strong> <code>[Unreleased]</code> is a <strong>draft, not the source
of truth</strong>: diff the change against the last tag
(<code>git log &lt;last-tag&gt;..HEAD</code>, <code>git diff --stat &lt;last-tag&gt;..HEAD</code>) and add an
entry for every user-facing change that is missing. <strong>Always show the proposed
additions to the user and wait for approval</strong> before promoting — ask rather
than guess when an entry is ambiguous.</p>
</li>
<li>
<p><strong>Promote the changelog</strong> in <code>CHANGELOG.md</code>: move the (now reconciled)
<code>[Unreleased]</code> content under a new <code>## [x.y.z] - YYYY-MM-DD</code> heading, leave a
fresh empty <code>[Unreleased]</code> on top, and add the compare links — naming the
<strong>version/tag</strong>, never a commit id:</p>
</li>
</ul>
<div class="language-markdown highlight"><table class="highlighttable"><tr><td class="linenos"><div class="linenodiv"><pre><span></span><span class="normal"><a href="#__codelineno-0-1">1</a></span>
<span class="normal"><a href="#__codelineno-0-2">2</a></span></pre></div></td><td class="code"><div><pre><span></span><code><span id="__span-0-1"><a id="__codelineno-0-1" name="__codelineno-0-1"></a>[<span class="nl">Unreleased</span>]: <span class="na">https://git.aetoskia.com/aetos/&lt;repo&gt;/compare/x.y.z...HEAD</span>
@@ -684,7 +694,7 @@ rides along with it. Do both edits <strong>in the MR</strong>:</p>
</span></code></pre></div></td></tr></table></div>
<ul>
<li><strong>Bump the version file</strong> to the intended release version: <code>version = "x.y.z"</code>
in <code>pyproject.toml</code> (packages) or the <code>VERSION</code> file (services/sites).</li>
in <code>pyproject.toml</code> (packages) or the <code>VERSION</code> file (services).</li>
</ul>
<hr />
<h2 id="tag-the-merged-commit">🏷️ Tag the merged commit</h2>
@@ -701,8 +711,8 @@ you confirm the MR is merged/squashed and tell it to proceed, the agent:</p>
<li>Finds the commit that introduced the bump:</li>
<li>package:
<code>git log -S 'version = "x.y.z"' -n 1 --format='%H %s' -- pyproject.toml</code></li>
<li>service / site:
<code>git log -S 'x.y.z' -n 1 --format='%H %s' -- VERSION</code></li>
<li>service:
<code>git log -S 'x.y.z' -n 1 --format='%H %s' -- VERSION</code></li>
<li>Verifies that commit's version file equals <code>x.y.z</code>, then tags <strong>only</strong> that
commit: <code>git tag x.y.z &lt;sha&gt;</code>.</li>
<li>Does <strong>not</strong> push — it asks you to run <code>git push origin x.y.z</code>.</li>
@@ -716,7 +726,6 @@ you confirm the MR is merged/squashed and tell it to proceed, the agent:</p>
<li><strong>Type B (service):</strong> asserts <code>tag == VERSION</code>, resolves the tag, skips if the
image already exists, runs the quality gate, builds, pushes, and restarts the
container.</li>
<li><strong>Type B (site):</strong> triggered on push, ships <code>:latest</code> and has no tag guard.</li>
</ul>
<p>If the guard fails, nothing is published — fix the version file, amend the
commit, and move the tag <strong>only</strong> if it was never pushed.</p>

File diff suppressed because one or more lines are too long

Binary file not shown.