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

@@ -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.