# Release Process DBFlux uses **trunk-based development with short-lived release branches**. One long-lived branch (`main`) is the integration target; a `release/vX.Y` branch is cut per minor during stabilization and discarded after EOL. This document is the human-facing reference. The automated `dbflux-release` skill (`skills/dbflux-release/SKILL.md`) follows these same rules. ## Channels | Channel (source branch) | Tag pattern | GitHub release | |-----------------------------|---------------------|-----------------------------------| | **nightly** (`main` HEAD) | `nightly` (rolling) | prerelease, built by a daily cron | | **rc** (`release/vX.Y`) | `vX.Y.Z-rc.N` | prerelease, built on tag push | | **stable** (`release/vX.Y`) | `vX.Y.Z` | published, built on tag push | The `-dev.N` channel is **retired**. Nightly replaces it. Old `-dev.N` tags remain on GitHub but no new ones are created. ## Changelog Model Two artifacts, one authored source each: - **`CHANGELOG.md` in this repository is the changelog of record, written by hand.** It carries the curated, user-facing prose for each release. - **The GitHub release body is generated** by CI with [git-cliff](https://git-cliff.org) from conventional commits, for every channel. `cliff.toml` at the repo root configures it, and it is never hand-edited. One commit feeds both: its conventional type decides which section the generated notes use, and the bullet written under `## [Unreleased]` decides what the repository's changelog says. They are not copies of each other. The release body is terse (subject, PR number, section); the repository changelog is prose. ### Repository changelog (`CHANGELOG.md`) - Every user-visible change (`feat`, `fix`, `perf`) adds its bullet under `## [Unreleased]`, in the same commit as the change, under `### Added`, `### Fixed`, or `### Changed`. The PR template's checklist asks for it. - `[Unreleased]` collects the work that ships as the next **minor**. rc and nightly tags are transparent: they do not close it. - At the stable promote the heading is renamed **once**, to `## [X.Y.0] - `, on the release branch, in the same commit as the version bump (see Promote to Stable below). `main` opens a fresh `[Unreleased]` when the released section is carried back to it. - **Patches are the exception:** a `## [X.Y.Z]` section for a patch release is generated on the release branch with `git-cliff --prepend`. Patch sections are not curated. - Never run `git-cliff -o CHANGELOG.md`. A full regeneration collapses every historical section into one range from the last stable tag. Prepending, or editing by hand, is the only safe write. > **v0.7.0 transition:** patch sections have been generated by git-cliff since v0.7.x. The `## [0.6.0]` and `## [0.6.0-dev.N]` sections are hand-written baselines committed to `CHANGELOG.md`. They must never be regenerated — doing so would duplicate or collapse them. ### Release body (GitHub) - Composed by `.github/workflows/release.yml` from git-cliff's output. A `feat`, `fix`, or `perf` commit surfaces; a `chore`, `ci`, `docs`, `test`, `refactor`, or `style` commit is dropped. Security-relevant changes use `fix(security):` or a `Security:` footer. Breaking changes (`feat!:`, `fix!:`, or a `BREAKING CHANGE:` footer) always surface. - The stable body covers every user-visible commit since the previous **stable** tag. rc and nightly tags are transparent (`skip_tags` in `cliff.toml`). - Editing the published body in the GitHub UI is allowed, for an editorial intro or a correction. It does not touch `CHANGELOG.md`. - A stable tag whose version has no `## [X.Y.Z]` section in `CHANGELOG.md` fails the release job instead of publishing. ## Branches | Branch | Lifetime | Accepts | Tags produced | |----------------|-----------|--------------------------------------------------|--------------------------| | `main` | permanent | every new commit (features, fixes, refactors) | (none — nightly rolling) | | `release/vX.Y` | until EOL | cherry-picked fixes only (no new features) | `vX.Y.Z-rc.N`, `vX.Y.Z`, `vX.Y.(Z+1)` | ### Inviolable rules - A commit is **never** authored on a release branch. It always lands on `main` first, then is `git cherry-pick -x ` into the release branch. The only commits written there are the release's own version-artifact bumps, which carry the `CHANGELOG.md` heading rename, and the generated patch sections. - A release branch is **never** merged back into `main`. - **No new features** on a release branch once cut. Only bugfixes and the release's own version-artifact bumps. - `main` is always open for development. Every user-visible change adds its `[Unreleased]` bullet in the same commit that makes the change. ## Tags Tags must be annotated: ```bash git tag -a vX.Y.Z[-suffix.N] -m "vX.Y.Z[-suffix.N]" git push origin vX.Y.Z[-suffix.N] ``` The release workflow (`.github/workflows/release.yml`) classifies tags automatically: | Tag pattern (allowed source branch) | GitHub release kind | |-------------------------------------|---------------------| | `vX.Y.Z-rc.N` from `release/vX.Y` | prerelease | | `vX.Y.Z` from `release/vX.Y` | stable (published) | | anything else (safety net) | draft | ## Versioning Rules The workspace version (`Cargo.toml` `[workspace.package].version`) is the source of truth. All other manifests must stay in lockstep. **On `main`:** The manifest version is `X.(Y+1).0-dev.0`, where `X.Y` is the minor currently being stabilized on `release/vX.Y`. This marker is set when `release/vX.Y` is cut and stays on `main` through the entire stabilization window and beyond, until the next cut. It is a development marker only — no `-dev.N` releases are ever published. The nightly workflow derives `X.(Y+1).0-nightly+` from it by stripping the pre-release suffix and appending `-nightly+`. **On `release/vX.Y`:** - Next RC: if the last tag is `vX.Y.Z-rc.N` → `-rc.(N+1)`. If none → `-rc.0`. - Promote to stable: drop the RC suffix → `vX.Y.0`. - Patch: increment `Z` → `vX.Y.(Z+1)`. Never bump the minor on a release branch. ## Cycle Example: `0.7.0` 1. Features land on `main`, each adding its `## [Unreleased]` bullet to `CHANGELOG.md` in the same commit. 2. When ready to stabilize, cut `release/v0.7` from `main` HEAD. - On `release/v0.7`: bump every versioned artifact to `0.7.0-rc.0`. Commit and push. - On `main`: bump every versioned artifact to `0.8.0-dev.0`. Commit and push. `main` now targets the next minor. - On `main`: add `v0.7` to `web/versions.json`, not current. The site keeps serving `v0.6` at `/docs/`. - Tag `v0.7.0-rc.0` on the release branch. git-cliff renders the unreleased range as the RC body automatically. 3. A bug is found during RC: - Commit the fix on `main`. - `git cherry-pick -x ` into `release/v0.7`. - Bump to `v0.7.0-rc.1` and tag. 4. When clean, bump the release branch from `v0.7.0-rc.N` to `v0.7.0` and rename the top `CHANGELOG.md` heading to `## [0.7.0] - ` in that same commit. Tag `v0.7.0`. git-cliff renders the full range since `v0.6.0` as the stable release body. 5. `main` is already on `0.8.0-dev.0`, so no further version bump is needed after stable. One commit closes the released `[Unreleased]` section and opens a fresh one above it, and another moves `"current": true` in `web/versions.json` from `v0.6` to `v0.7`. 6. Patches (`v0.7.1`, `v0.7.2`, …) come from the same release branch via cherry-picks from `main`, and each one prepends a generated `## [0.7.N]` section to `CHANGELOG.md`. ## Artifact Identity and Signing Three separate things carry the "who built this" signal, and they do not overlap: | Layer | What it covers | Secret | When missing | |-------|----------------|--------|--------------| | GPG detached signature (`.asc`) and `.sha256` | The downloaded file | `GPG_PRIVATE_KEY`, `GPG_PASSPHRASE` | Build fails. Every release is signed. | | Build provenance attestation | The workflow run and commit that produced the artifact | none (keyless OIDC) | Always on. | | macOS code signature on the `.app` | The application identity the keychain and Gatekeeper look at | `MACOS_CERTIFICATE_P12`, `MACOS_CERTIFICATE_PASSWORD` | Bundle is signed ad-hoc with a warning. | Neither the Windows executable nor the installer is Authenticode-signed, so SmartScreen warns on first run. That needs a certificate from a CA and is tracked separately. ### macOS code signing macOS ties a keychain grant to the signature of the application that asked for it. An ad-hoc signature changes with every build, so without a stable identity each update makes the user re-enter their login password before DBFlux may read the saved database passwords again. Signing every release with one certificate makes that prompt appear once. A self-signed certificate is enough for the keychain. It does **not** satisfy Gatekeeper: the app stays "unidentified" on first launch until the project has a paid Developer ID certificate and notarisation, which the same two secrets would carry. Create the certificate once, on any machine with OpenSSL: ```bash scripts/macos-signing-cert.sh ~/secure/dbflux-signing ``` The script prints the two repository secrets to set. Keep the `.p12` and its password somewhere durable: issuing a new certificate later means every user re-authorises the keychain once more. The certificate is valid for ten years. The bundle is signed in `build.yml` before the DMG is created. The job imports the certificate into a temporary keychain, trusts it for code signing on the runner, signs, verifies with `codesign --verify --deep --strict`, and deletes the keychain. ### Windows executable identity `crates/dbflux/build.rs` embeds the channel icon and a `VERSIONINFO` block into `dbflux.exe` at build time, so Explorer, the taskbar, and the Open With dialog show the DBFlux icon and Properties shows the product name and version. The icons are committed under `packaging/icons/` (`dbflux.ico`, `dbflux-nightly.ico`), and the same files feed the portable zip and the installer shortcuts. Regenerate them from `resources/branding//` when the artwork changes: ```bash scripts/branding/generate-icons.sh ``` The script renders 48 px and larger from `mark.svg` (the full icon) and 32 px and smaller from `mark-small.svg` (the glyph). It also rebuilds the macOS `.icns` files beside them (`dbflux.icns`, `dbflux-nightly.icns`, adding 512 and 1024 px sizes), which land in the bundle as `AppIcon.icns`, the hicolor PNGs under `packaging/icons//apps/`, the in-app PNGs and the lockup `wordmark.svg` in each channel directory, and the site copies in `web/public/brand/`. It re-runs itself in a Nix shell when `rsvg-convert`, `icotool`, `png2icns`, or Python with `fonttools` and `uharfbuzz` are missing. ### AppImage external update information Stable and nightly AppImages embed external update information and ship with a generated `.zsync` sidecar, published next to the final signed image. Stable resolves through GitHub's `latest` release and nightly through the rolling `nightly` tag; each channel only advertises its own tag, so an update never moves an install across channels. RC releases embed no update information and stay manual. The sidecar and the embedded string are produced by appimagetool (`-u`) during the build and verified fail-closed by `build.yml`; the update is performed by an external tool such as AppImageUpdate — DBFlux itself does not self-update. ### Stable download names The `.deb` and `.rpm` packages carry their version in the file name (`dbflux_0.8.5_linux_amd64.deb`, `dbflux-0.8.5-1.x86_64.rpm`), so a `releases/latest/download/` link cannot point at them. `build.yml` also publishes byte-identical copies named `dbflux-linux-.deb` and `dbflux-linux-.rpm`, each with its own `.asc` and `.sha256`, and those are the names [Installation](INSTALL.md) links to. Before publishing, `release.yml` and `nightly.yml` run `scripts/check_release_links.py` against the downloaded artifacts. A `releases/latest/download/` link in the documentation, its translations or the site's install section that names a file the release does not carry fails the job instead of shipping a 404. ## Cut Procedure: `main` → `release/vX.Y` 1. Verify you are on `main`, clean tree, up to date with `origin/main`. 2. Verify `.github/workflows/release.yml` on `main` contains the `Classify release` job. If missing, fix on `main` first — otherwise stable tags will publish as drafts. 3. Create the branch (use a dedicated worktree if you use the bare-repo layout so `main` stays checked out): ```bash git worktree add ../release-vX.Y -b release/vX.Y main # or in a single-checkout repo: git checkout -b release/vX.Y ``` 4. On `release/vX.Y`: - Bump every versioned artifact to `X.Y.0-rc.0` (see [Files to Bump](#files-to-bump)). - Leave `CHANGELOG.md` alone. The branch carries the `[Unreleased]` block it inherited from `main`, and the fixes cherry-picked into it bring their own bullets. The heading is renamed at the stable promote, not here: an RC is not a release of record. - Commit: `chore(release): cut release/vX.Y at vX.Y.0-rc.0`. - Push: `git push -u origin release/vX.Y`. 5. Back on `main`: - Bump every versioned artifact to `X.(Y+1).0-dev.0` (main now targets the next minor). - Commit: `chore(version): move main to X.(Y+1).0-dev.0 marker`. - Push. 6. Still on `main`, register the minor on the website. `release/vX.Y` must already be on origin (step 4); see [Website](#website) for why. Add the entry right after `nightly`, without `current` — an RC is not the current release: ```json { "id": "nightly", "ref": "main", "noindex": true }, { "id": "vX.Y", "ref": "release/vX.Y" }, { "id": "vX.(Y-1)", "ref": "release/vX.(Y-1)", "current": true }, ``` - Commit: `chore(web): add vX.Y to the site versions`. - Push. 7. Tag `vX.Y.0-rc.0` on the release branch. The RC release body is generated from conventional commits automatically, so an RC needs no CHANGELOG step at all. ## Promote to Stable: `release/vX.Y` → `vX.Y.0` Run on `release/vX.Y` when the RC is clean: 1. Bump every versioned artifact from `X.Y.0-rc.N` to `X.Y.0`. 2. Close the changelog section: rename the top `## [Unreleased]` heading in `CHANGELOG.md` to `## [X.Y.0] - `, with today's date. The bullets collected on `main` and carried in by the cherry-picks are already the content of this release; nothing is generated into the file here. ```text ## [Unreleased] -> ## [X.Y.0] - 2026-07-31 ``` > **Warning:** do not prepend a generated section, and do NOT use `git-cliff -o CHANGELOG.md`. The repository changelog is curated; the release body is generated separately. 3. Commit: `chore(release): promote release/vX.Y to vX.Y.0`. 4. Tag `vX.Y.0` on the release branch and push branch + tag. 5. CI generates the stable release body from every user-visible commit since the previous stable tag. 6. On `main`, make the new minor the site's current release: in `web/versions.json`, move `"current": true` from `vX.(Y-1)` to `vX.Y`. ```json { "id": "vX.Y", "ref": "release/vX.Y", "current": true }, { "id": "vX.(Y-1)", "ref": "release/vX.(Y-1)" }, ``` - Commit: `chore(web): make vX.Y the current site version`. - Push. The site deploys from `main`, so this commit switches `/docs/` to `vX.Y` and the product version shown on the landing and compare pages to `X.Y.0`. The release workflow refuses to publish a stable tag whose version has no `## [X.Y.Z]` section in `CHANGELOG.md`, so step 2 cannot be skipped silently. ## Next Dev Cycle `main` is bumped to `X.(Y+1).0-dev.0` **when `release/vX.Y` is cut** (see Cut Procedure, step 5). No further bump to `main` is required after the stable tag. Nightly builds continue from `main` HEAD automatically, producing `X.(Y+1).0-nightly+` throughout the stabilization window. Once the stable tag is pushed, `main` gets one commit that closes the released section the same way (rename `## [Unreleased]` to `## [X.Y.0] - `) and opens a fresh `## [Unreleased]` above it, so the repository changelog keeps the released history. `7a13aceb` is an example of that commit. Work that landed on `main` after the cut and did not ship belongs under the new `[Unreleased]`, not in the released section; that split is the model's one hand-fold. ## Website The site publishes one documentation set per minor, listed in `web/versions.json` (the fields are described in `web/src/data/versions.ts`). It is not bumped per release: it changes at three points in a minor's life, always with a commit on `main`, because the site deploys from `main` (`.github/workflows/web.yml`). | Event | Change in `web/versions.json` | Commit | |-------|-------------------------------|--------| | Cut (`release/vX.Y` pushed) | add `{ "id": "vX.Y", "ref": "release/vX.Y" }` right after `nightly` | `chore(web): add vX.Y to the site versions` | | Stable (`vX.Y.0` tagged and pushed) | move `"current": true` to `vX.Y` | `chore(web): make vX.Y the current site version` | | EOL (before `release/vX.Y` is deleted) | repoint `vX.Y`'s `ref` to its last tag, e.g. `vX.Y.Z` | `chore(web): pin vX.Y site docs to vX.Y.Z` | - **The ref must exist on origin first.** `web/scripts/fetch-docs.ts` reads each entry from its git ref, fetching it from `origin` when the clone lacks it. A ref it cannot read is skipped with only a warning, but the build then fails rendering that version's pages (`No materialised documentation for version "vX.Y"`). An entry that lands before its branch is pushed, or that still names a deleted branch, breaks every site deploy from `main`. - **The product version is not typed anywhere.** The site reads it from each ref's `Cargo.toml`, so it follows the release branch's bumps (`X.Y.0-rc.N`, then `X.Y.0`, then patches) without a site change. - **Site copy for a new minor waits for the stable release.** Landing or compare-page text describing features of `vX.Y` must not reach `main` before the commit that makes `vX.Y` current; until then the site describes `vX.(Y-1)`. ## Files to Bump Per release, update all of the following to the exact same version: - `Cargo.toml` — `[workspace.package].version`. Workspace crates inherit via `version.workspace = true`. - `flake.nix` - `resources/windows/installer.iss` - Manual review (does not inherit): `examples/custom_driver/Cargo.toml`. After the GitHub Release artifacts for the tag are published, also update: - `nix/release-info.nix` — `version` + both prebuilt-tarball `url`s and `hash`es (see [Nix](#nix-this-repos-flake) below). This is a per-branch channel pointer. It requires the published artifacts, so it lands as a follow-up commit once the release workflow finishes. `web/versions.json` is not part of the per-release bump; it changes at the cut, the stable promote and the EOL of a minor (see [Website](#website)). The AUR `PKGBUILD` lives in an **external AUR repository**, not in this repo. It is bumped only for stable tags. ## How Nightly Works `.github/workflows/nightly.yml` runs daily at 03:17 UTC: 1. Reads the workspace version from `Cargo.toml`, strips any existing pre-release suffix, and appends `-nightly+` (e.g. `0.8.0-nightly+abc1234` when `main` carries `0.8.0-dev.0`). No `Cargo.toml` commit required. Because `main` tracks the **next** minor from the moment `release/vX.Y` is cut, the nightly version is always clearly ahead of the stabilizing line. 2. Calls `build.yml` with `channel: nightly`. 3. Computes the SHA256 SRI hash of each Linux tarball and regenerates `nix/nightly-info.nix` with the real hashes and the rolling release URLs. 4. Commits the updated `nix/nightly-info.nix` on top of the current `main` HEAD. This commit is **not pushed to `main`** — it becomes the sole target of the `nightly` tag. 5. Force-moves the `nightly` tag to the pin commit and pushes the tag. Pushing the tag is sufficient to make the commit reachable on the remote; no branch push is required. 6. Publishes or updates the rolling `nightly` GitHub prerelease with the new artifacts and a git-cliff-generated body covering commits since the last stable tag. The release's tag points at the pin commit, so `nix/nightly-info.nix` at `nightly` ref always matches the published artifacts. The nightly tag is force-pushed and the release is replaced on every run. Only the canonical repository (`0xErwin1/dbflux`) runs the schedule. **Skip when `main` has not advanced.** A scheduled run first compares the current `main` HEAD against the commit the last nightly was built from (`git rev-parse nightly^`, the pin commit's first parent). If they match, the run skips entirely: no rebuild, no tag move, no release churn. This avoids republishing an identical build under a fresh, non-reproducible hash that would needlessly break Nix pins. A manual `workflow_dispatch` run always builds, even with no new commits. ### Nix nightly package The workflow pins `nix/nightly-info.nix` at the `nightly` ref on every run. Downstream users get the prebuilt nightly binary without compiling from source: ```bash # Run nightly directly nix run github:0xErwin1/dbflux/nightly#dbflux-nightly # Install into a profile nix profile install github:0xErwin1/dbflux/nightly#dbflux-nightly ``` A from-source nightly (no hash pinning required) also works: ```bash nix run github:0xErwin1/dbflux/nightly#dbflux-source ``` **Do not consume `#dbflux-nightly` from `main`.** On `main`, `nix/nightly-info.nix` contains placeholder hashes that will not fetch. Always use the `nightly` ref as shown above. ## Cherry-Pick Discipline A release branch should never contain commits absent from `main`, except release-only commits (`chore(release): ...`, `chore(version): ...`). ```bash # On main: land the fix. git checkout main # ...commit, push... # On release branch: cherry-pick with -x to record the source SHA. git checkout release/vX.Y git cherry-pick -x ``` Audit: every non-release commit on `release/vX.Y` since branch-off should mention `(cherry picked from commit ...)` in its message. ```bash git log --grep='cherry picked from' release/vX.Y ``` ## Downstream Channels | Tag kind (GitHub Release) | AUR | Nix flake (this repo) | nixpkgs (future) | |-----------------------------|-------------|-----------------------------------------------|------------------| | nightly (prerelease) | skip | auto-pinned — `#dbflux-nightly` on nightly ref | skip | | `-rc.N` (prerelease) | skip | bump release branch's + main's `release-info` | skip | | Stable `vX.Y.Z` (published) | bump + push | bump release branch's + main's `release-info` | bump + PR | ### AUR AUR `pkgver` does not allow `-` (reserved for `pkgrel`). For stable releases the translation is a no-op (`pkgver=X.Y.Z`). For hypothetical AUR prereleases: - `vX.Y.Z-rc.N` → `pkgver=X.Y.Z.rc.N` ### Nix (this repo's flake) The flake exposes several packages on Linux (x86_64 and aarch64): | Package | What it provides | |-------------------|------------------------------------------------------| | `dbflux` (default) | Prebuilt stable/rc binary when available, source otherwise | | `dbflux-bin` | Explicit prebuilt from `nix/release-info.nix` | | `dbflux-source` | Source build via crane (all platforms) | | `dbflux-nightly` | Rolling nightly prebuilt from `nix/nightly-info.nix` (use `nightly` ref) | **Stable / RC (`nix/release-info.nix`):** per-branch channel pointer. `main` tracks the newest published tag of any kind; each `release/vX.Y` tracks its own line's newest. After a tag's artifacts publish, refresh `release-info.nix` on every branch whose channel that tag advances. ```bash ver=X.Y.Z for arch in amd64 arm64; do hex=$(curl -fsSL "https://github.com/0xErwin1/dbflux/releases/download/v$ver/dbflux-linux-$arch.tar.gz.sha256" | awk '{print $1}') nix-hash --to-sri --type sha256 "$hex" done ``` Update `version`, both `url`s, and both `hash`es in `nix/release-info.nix`. Verify locally: ```bash nix build .#dbflux-bin --no-link --print-out-paths ``` **Nightly (`nix/nightly-info.nix`):** auto-updated by the nightly workflow on the `nightly` ref. Do not update this file manually. Consume via: ```bash nix run github:0xErwin1/dbflux/nightly#dbflux-nightly ``` ### nixpkgs (future) Not yet upstream. When it is, only stable tags will get a PR to `NixOS/nixpkgs`. PR title convention: `dbflux: A -> B`. ## Anti-Patterns (refuse these) - Tagging `vX.Y.Z` or `vX.Y.Z-rc.N` while HEAD is on `main`. - Tagging an RC while HEAD is on `main`. - Merging `release/vX.Y` back into `main`. - Creating new features (non-fix commits) on a `release/*` branch. - Bumping minor or major version inside a `release/*` branch. - Pushing a tag without a clean working tree. - Pushing the AUR bump with `pkgver` containing a hyphen. - Cutting `release/vX.Y` from a `main` HEAD that does not contain the `Classify release` job in `release.yml`. - Creating new `-dev.N` tags (the channel is retired; use nightly instead). - Marking an RC minor `"current": true` in `web/versions.json`, or keeping the previous minor current after `vX.Y.0` is published. - Adding a `web/versions.json` entry whose ref is not yet on origin, or deleting a `release/vX.Y` branch that an entry still points at. ## Local Validation Before Tagging ```bash cargo check --workspace python3 scripts/lint.py fmt --check python3 scripts/lint.py clippy cargo test --workspace ``` `scripts/lint.py` covers only the first-party crates under `crates/` and never lints or reformats the vendored crates under `vendor/`. These fast suites do not cover the driver live integration tests: driver-specific suites backed by containers, local files, or a credential-gated Redshift cluster, documented in [tests/driver-live/README.md](../tests/driver-live/README.md). Run them before tagging an rc or stable. CI gates publication on the same suites — `release.yml` invokes `tests.yml`, and the `Create Release` job waits for it — but the suites exercise source code, not the built artifacts, and nightly builds are not test-gated (`nightly.yml` calls `build.yml` directly). ## Related - `.github/workflows/release.yml` — classification logic and artifact publishing - `.github/workflows/nightly.yml` — daily nightly build - `.github/workflows/build.yml` — reusable build jobs (called by release and nightly) - `.github/release-template.md` — installation section appended to every release body - `cliff.toml` — git-cliff configuration for changelog generation - `web/versions.json` — documentation versions the site publishes, and which one is current - `skills/dbflux-release/SKILL.md` — agent-facing skill that automates this process