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:
4
build.py
4
build.py
@@ -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."},
|
||||
]
|
||||
|
||||
|
||||
|
||||
16
registry.py
16
registry.py
@@ -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.
|
||||
|
||||
@@ -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 & Changelog</a>).</li>
|
||||
<code>VERSION</code> file (see <a href="../11_versioning_and_changelog/">11 – Versioning & Changelog</a>).</li>
|
||||
<li><code>check-remote-image</code> — if <code>aetos/<app>:<tag></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/<app>:<tag></code> and <code>:latest</code>.</li>
|
||||
<code>aetos/<app>:<tag></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 <app></code>.</li>
|
||||
<li><code>run-container</code> — <code>docker run -d --name <app> -p <host>:<container>
|
||||
--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><site></code></td>
|
||||
<td>site image / container name</td>
|
||||
<td><code>docs</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code><ENV_VAR></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 & 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>
|
||||
|
||||
@@ -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 & 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 <last-tag>..HEAD</code> and <code>git diff --stat <last-tag>..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 <last-tag>..HEAD</code> and <code>git diff --stat <last-tag>..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>
|
||||
|
||||
@@ -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 & 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 & 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 & 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.
@@ -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
@@ -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
@@ -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": {}
|
||||
}
|
||||
}
|
||||
@@ -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."
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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`.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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 <last-tag>..HEAD</code>, <code>git diff --stat <last-tag>..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/<repo>/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 <sha></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.
Reference in New Issue
Block a user