feat: adopt role-based branching strategy (ADR-013)

release/3.x -> main, release/4.x -> next, master -> archive/v2-legacy
(retired, was byte-identical to release/2.x). Branch identity is now
role-based, not version-numbered -- main and next are fixed names
that never get renamed at a major-version cutover; release/N.x is
reserved exclusively for already-superseded majors, created only at
the moment they're archived.

This directly fixes the structural cause of the bug found 2026-08-30:
build.yml hardcoded literal branch-name comparisons for the
testing/beta channel, and ADR-006's original "master -> beta channel"
rule was never superseded when release/3.x took over as the active
branch, leaving master as a silent, stale duplicate of release/2.x
that CI still treated as a legitimate publish source. Rewrote the
version-resolution logic to match roles instead: refs/heads/main,
refs/heads/next, and a refs/heads/release/*.x pattern that auto-
catches any future archived major with zero code changes needed.
Also tightened the push/PR triggers from a bare wildcard to an
explicit allowlist, and switched the release-notes CHANGELOG link to
a tag-relative permalink instead of a floating branch reference.

Research (DEP-14, openedx/frontend-base #273, semantic-release's
branch-pattern config) is cited in ADR-013; the openedx precedent in
particular fixed this identical bug the same way.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Kevin Adams
2026-08-30 09:43:33 -04:00
co-authored by Claude Sonnet 5
parent 687bdd8f7f
commit 8a4750378c
9 changed files with 240 additions and 37 deletions
@@ -1,7 +1,7 @@
# ADR-006: Package Versioning Strategy
**Date**: 2026-05-15
**Status**: Accepted
**Status**: Superseded by ADR-013 (branch-to-channel mapping section only — the semver scheme below is still in effect)
**Deciders**: Kevin Adams
## Context
@@ -57,3 +57,11 @@ Branch-to-channel mapping (not version mapping):
- `feature_*` → alpha channel
- `master` → beta/testing channel
- Tagged release (`v3.0.0`) → stable channel
> **Superseded 2026-08-30** — this branch-to-channel mapping is the section ADR-013
> replaces. `master` stopped being maintained shortly after this was written and the
> mapping was never updated when `release/3.x` took over as the active branch,
> which caused a real production bug. See ADR-013 for the current, role-based
> mapping (`main`/`next`/`release/N.x`). Left as-written above per this project's
> ADR rule against editing an ADR's original decision text — this note is the
> pointer forward, not a correction of the historical record.
@@ -280,20 +280,24 @@ version-specific testing this ADR's implementation work needs
## Implementation branch
Development for this ADR happens on `release/4.x` (created 2026-08-29,
branched from `release/3.x`) — CI already resolves pushes there to
`<VERSION>~alpha+<sha>` / `truenas-proxmox-snapshots` per the existing
`build.yml` version-bucketing logic (see ADR references below), no workflow
changes were needed for that part. Standing it up did surface one real,
previously-dormant CI bug worth noting here since it's a direct consequence of
this ADR's work existing on its own branch: the "Publish to GitHub Pages
testing dist" step ran for both `testing` and `development` channel builds and
did `rm -rf pool/testing` first, so the first `release/4.x` push briefly
overwrote the public testing dist with an unrelated alpha build. Fixed in
`build.yml` (restricted that step to `channel == 'testing'` only) and verified
live; `$VERSION` in `TrueNAS.pm` stays at the `release/3.x` value (`3.2.5`)
until real implementation code lands here, per the branch's own alpha-channel
versioning.
Development for this ADR happens on the branch created 2026-08-29 as
`release/4.x` (branched from what was then `release/3.x`) — CI resolves pushes
there to `<VERSION>~alpha+<sha>` / `truenas-proxmox-snapshots`. Standing it up
surfaced one real, previously-dormant CI bug worth noting here since it's a
direct consequence of this ADR's work existing on its own branch: the
"Publish to GitHub Pages testing dist" step ran for both `testing` and
`development` channel builds and did `rm -rf pool/testing` first, so the first
push to this branch briefly overwrote the public testing dist with an
unrelated alpha build. Fixed in `build.yml` (restricted that step to
`channel == 'testing'` only) and verified live.
**2026-08-30 — renamed to `next`** as part of [ADR-013](ADR-013-branching-strategy.md)'s
role-based branch naming: `release/N.x` names are now reserved exclusively for
already-superseded majors, so the in-progress v4.0.0 work moved to the fixed
name `next` (and the old `release/3.x` became `main`). Same content, same
history, just a new name — `$VERSION` in `TrueNAS.pm` stays at the `main`
branch's value (`3.2.5`) until real implementation code lands here, per the
branch's own alpha-channel versioning.
## References
@@ -305,7 +309,8 @@ versioning.
- [[project_truenas_lab_versions]] (test lab plan for verifying the unconfirmed items above)
- `scripts/truenas-ws-diag.pl` (this repo — the read-only diagnostic tool used for the 2026-08-29 live verification above)
- `docs/architecture.md` §9 (multipath's inheritance of this ADR's transport work, and the multipath docs added 2026-08-30)
- `release/4.x` branch (implementation work for this ADR lives here; see Implementation branch above)
- `next` branch (implementation work for this ADR lives here, renamed from `release/4.x` 2026-08-30; see Implementation branch above)
- ADR-013 (branching strategy — role-based branch naming, why this ADR's branch was renamed)
- `truenas/truenas_jsonrpc` (protocol spec) and `truenas/api_client` (Python reference client) on GitHub
- [boomshankerx/proxmox-truenas](https://github.com/boomshankerx/proxmox-truenas) (independent AGPL-3.0 successor project, reference only — see Prior Art above)
- TrueNAS Jira NAS-135643 (live 25.04.0 bug in `iscsi.target.query` over JSON-RPC)
@@ -0,0 +1,170 @@
# ADR-013: Role-Based Branching Strategy
**Date**: 2026-08-30
**Status**: Accepted
**Deciders**: Kevin Adams
**Supersedes**: ADR-006 (branch-to-channel mapping section only — that ADR's semver scheme is unaffected and still in effect)
## Context
This repo has accumulated three live version lines plus one abandoned one, and the
CI pipeline's branch-to-channel logic never kept up:
- `master` — the original branch. ADR-006 (2026-05-15) declared *"`master` → beta/testing
channel"* as a permanent rule. `master` was abandoned shortly after v3.0's "clean slate"
rewrite began on a new branch, and nobody wrote a superseding ADR when that happened.
As of this ADR, `master` is byte-identical to `release/2.x` (same commit) — a fully
redundant, silently-stale duplicate that `build.yml` still treated as a legitimate
testing-channel source.
- `release/2.x` — the actual, intentional archive of the v2.x line, still receiving
occasional patches for legacy users.
- `release/3.x` — the current, actively-developed v3.x line, and the actual GitHub
default branch (moved there when v3.0 began, but never renamed to reflect that).
- `release/4.x` — created 2026-08-29 for v4.0.0/Rivendell design work (ADR-012).
**This directly caused a real bug, same day `release/4.x` was created**: `build.yml`'s
version-resolution logic hardcodes literal branch-name comparisons —
`refs/heads/master || refs/heads/release/3.x` for the "testing" channel, everything
else matching `refs/heads/release/*` for "development" — and a separate publish step
did `rm -rf pool/testing` before republishing. The first push to `release/4.x`
matched the generic `release/*` bucket, which shared a publish path with the
`release/3.x`-specific one, and briefly overwrote the public testing apt dist with an
unrelated alpha build (fixed same-day, see ADR-012's Implementation Branch section).
**The structural problem**: every time a new major version's branch becomes "the
current one," someone has to remember to find and update the hardcoded branch-name
literal in `build.yml` (and separately, in README.md, CLAUDE.md, docs/architecture.md,
and the release-notes template). Miss any one of them, and you get exactly the class
of bug above. `ADR-010`'s apt dist-track model (`v2`/`v3`/`v4`/`testing`) already
solves this problem correctly for package *distribution* — dist tracks are named by
fixed suite identity, never renamed, and users never get silently promoted across a
major boundary. Git branch naming had no equivalent discipline.
### Research (2026-08-30)
- **Debian's DEP-14** (git packaging repo layout spec) states the underlying
principle directly: development branches should be named after their *role*
(`debian/unstable`, `debian/experimental`), not a value that migrates — DEP-14
explicitly avoids ever naming a branch `debian/stable`, because "stable" is a
rolling label pointing at a different codename over time.
- **`openedx/frontend-base` issue #273** hit this exact bug class and fixed it the
same way this ADR adopts: renamed their always-current branch to a fixed,
non-version-named identity (`release`), reserving version-numbered branch names
*exclusively* for already-superseded majors, created only at the moment they're
demoted.
- **semantic-release** (widely-adopted release-automation tooling) models
branch-to-channel mapping as declarative, pattern-matched config rather than
hardcoded literals — its default config auto-recognizes `N.x`/`N.N.x`-named
branches as maintenance lines via regex, not an enumerated list.
- Node.js and Django both keep the *policy* of which lines are current/maintained/EOL
in a document or metadata file, never in build-script conditionals.
Full research trail available on request — not reproduced in full here to keep this
ADR focused on the decision.
## Decision
### Branch roles (fixed names, assigned once, never renamed again)
| Branch | Role | Renamed at cutover? |
|---|---|---|
| `main` | The current stable major's development branch (was `release/3.x`) | No — content moves forward, name stays |
| `next` | The in-progress next major, pre-cutover (was `release/4.x`) | No — same |
| `release/N.x` | An **already-superseded** major, archived at the moment it's demoted (e.g. `release/2.x`) | N/A — these are the only branches that ever get a version-numbered name, and only once, at archival time |
| `archive/v2-legacy` | The old `master` branch, retired (was byte-identical to `release/2.x`) | N/A — dead, kept only for discoverability |
`main` and `next` are never renamed. At the next major-version cutover (when `next`'s
content is ready to ship as the new current major): branch `main`'s tip off into a new
`release/{old-major}.x` archive first, then let `main` continue forward as what used
to be `next`'s content, then free up `next` for whatever comes after. No file in this
repo will ever need a branch-name edit for a cutover again — `build.yml`, README, and
every doc reference `main`/`next` by their fixed role name.
### Why not keep `release/N.x` for the current line too (rejected)
The alternative (keep today's naming, just add a config pointer for "which
`release/N.x` is current") was considered and explicitly rejected in favor of the
above — it still leaves version-numbered names carrying a rolling role, which is the
root property DEP-14 and the openedx precedent both identify as the actual defect,
not just a missing pointer. `release/4.x` was one day old with zero external
references when this decision was made, making now the cheapest possible time to fix
this properly rather than defer it to the next cutover.
### What was actually renamed (2026-08-30)
| Old name | New name | Why |
|---|---|---|
| `release/3.x` | `main` | Current stable major; also the GitHub default branch — `main` is the modern idiomatic default-branch name, and this cutover already required retiring the old `master`, making this a natural pairing |
| `release/4.x` | `next` | In-progress v4.0.0/Rivendell work (ADR-012) — not yet a superseded major, so it doesn't earn a `release/N.x` name under this model |
| `master` | `archive/v2-legacy` | Fully redundant with `release/2.x` (same commit); kept renamed rather than deleted so old clones/bookmarks aren't silently broken |
| `release/2.x` | *(unchanged)* | Already correctly named under this model — an archived major that still takes real patches |
All renames used GitHub's branch-rename API (`POST /repos/{owner}/{repo}/branches/{branch}/rename`),
which preserves commit history, auto-updates the default-branch pointer, and redirects
old references — not a delete+recreate.
### `build.yml` changes
- `push`/`pull_request` triggers changed from a bare `["**"]` wildcard to an explicit
`[main, next, "release/*.x"]` allowlist — closes the "any branch, including a stray
one, silently triggers full CI" exposure that the wildcard trigger had.
- Version-resolution logic rewritten to match roles, not literals:
```bash
elif [[ "$REF" == "refs/heads/main" ]]; then
CHANNEL="testing" # was: refs/heads/master || refs/heads/release/3.x
elif [[ "$REF" == "refs/heads/next" || "$REF" =~ ^refs/heads/release/.+\.x$ ]]; then
CHANNEL="development" # was: refs/heads/release/*
```
A future archived major (`release/5.x`, eventually) is automatically caught by the
pattern — no code change needed. `next` and archived `release/*.x` branches still
share one channel/Cloudsmith repo (`truenas-proxmox-snapshots`) for now, same as
before this ADR; splitting them into separate channels is possible future work, not
done here, since neither `next` nor `release/2.x` currently has enough push volume
to need it.
- The "Publish to GitHub Pages testing dist" step already keyed off `channel ==
'testing'` (fixed same-day as the `release/4.x` incident, see ADR-012) rather than a
branch-name literal — no further change needed there, it "just worked" once the
branches were renamed.
- The draft-release template's CHANGELOG.md link changed from a branch-relative link
(`.../blob/release/3.x/CHANGELOG.md`, which would have gone stale at every cutover
too) to a tag-relative permalink (`.../blob/${{ github.ref_name }}/CHANGELOG.md`) —
this step only ever runs on an actual version tag, so the tag name is always
correct and never needs updating.
### Docs updated to match
`README.md`, `CLAUDE.md`, `docs/architecture.md`, `ROADMAP.md`, and ADR-012 all had
hardcoded `release/3.x`/`master` references — fixed to reference `main`/`next` by
role, except where a reference was genuinely historical (e.g. ROADMAP's record of
what v3.0.0 was released from), which was annotated with the rename rather than
rewritten.
## Consequences
- No file in this repo should ever again need a branch-name edit at a major-version
cutover — that was the entire point. If a future cutover *does* require touching
`build.yml`'s branch logic, that's a signal this ADR's model has a gap worth
revisiting.
- `git checkout main` replaces `git checkout release/3.x` as daily muscle memory —
a one-time adjustment.
- Apt end users are unaffected — ADR-010's dist tracks (`v2`/`v3`/`v4`/`testing`) are
already fully decoupled from git branch names; nothing in a user's `sources.list`
references a branch.
- `archive/v2-legacy` and `release/2.x` currently point at the same commit and will
drift apart only if `release/2.x` receives a future patch `archive/v2-legacy` does
not — this is intentional; `archive/v2-legacy` is frozen, `release/2.x` is not.
- The next major-version cutover (v4.0.0/Rivendell shipping) is the first real test
of this model: branch `main`'s pre-cutover tip into `release/3.x` (recreating that
name, now correctly meaning "the archived v3.x line"), then let `main` continue as
`next`'s content. Worth a short runbook entry when that day actually arrives.
## References
- ADR-006 (superseded — branch-to-channel mapping section only)
- ADR-009, ADR-010, ADR-011 (versioning/dist-track/packaging precedent this ADR is
consistent with)
- ADR-012 (the `release/4.x`→`next` rename and the same-day CI bug this ADR's
research was prompted by)
- [DEP-14: Recommended layout for Git packaging repositories](https://dep-team.pages.debian.net/deps/dep14/)
- [openedx/frontend-base issue #273](https://github.com/openedx/frontend-base/issues/273) — the closest direct precedent found
- [semantic-release branches configuration](https://semantic-release.gitbook.io/semantic-release/usage/configuration)
+2 -1
View File
@@ -28,10 +28,11 @@ ADRs document significant decisions made about this project. They follow an RFC-
| [ADR-003](ADR-003-apt-repo-hosting.md) | APT Repository Hosting | Accepted |
| [ADR-004](ADR-004-cleanup-on-failure.md) | Transactional Cleanup on API Operation Failure | Accepted |
| [ADR-005](ADR-005-bearer-token-authentication.md) | Bearer Token Authentication as Primary Auth Method | Accepted |
| [ADR-006](ADR-006-versioning-strategy.md) | Package Versioning Strategy | Accepted |
| [ADR-006](ADR-006-versioning-strategy.md) | Package Versioning Strategy | Superseded by ADR-013 (branch-mapping section only) |
| [ADR-007](ADR-007-freenas-vs-truenas-naming.md) | FreeNAS vs TrueNAS Module Naming | Accepted |
| [ADR-008](ADR-008-pve-version-support-matrix.md) | PVE Version Support Matrix | Superseded by ADR-009 |
| [ADR-009](ADR-009-pve-version-support-v3.md) | PVE Version Support Matrix — v3.x and v4.0 Versioning Strategy | Accepted |
| [ADR-010](ADR-010-apt-dist-tracks.md) | APT Repository Dist Tracks — Per-Major-Version Isolation | Accepted |
| [ADR-011](ADR-011-apt-components-optional-features.md) | APT Components for Optional Feature Packages | Accepted |
| [ADR-012](ADR-012-websocket-transport-v4.md) | WebSocket JSON-RPC 2.0 Transport for v4.0.0 (Rivendell) | Draft |
| [ADR-013](ADR-013-branching-strategy.md) | Role-Based Branching Strategy | Accepted |
+23 -14
View File
@@ -2,10 +2,15 @@ name: CI / Build / Publish
on:
push:
branches: ["**"]
# main = current stable major (fixed name, never renamed at a version
# cutover). next = the in-progress next major, pre-cutover (also fixed).
# release/*.x = archived, already-superseded majors (e.g. release/2.x) --
# matched by pattern so a newly-archived major needs no workflow edit.
# See ADR-013.
branches: [main, next, "release/*.x"]
tags: ["v*.*.*"]
pull_request:
branches: [master, "release/3.x"]
branches: [main, next, "release/*.x"]
env:
PACKAGE_NAME: truenas-proxmox
@@ -94,11 +99,14 @@ jobs:
fetch-depth: 0
# ── Version resolution ─────────────────────────────────────────────────
# Version strategy:
# Tagged release (v3.x.x) → 3.x.x-1 (stable channel)
# release/3.x branch → <ver>~beta+<sha> (testing channel)
# master branch → <ver>~beta+<sha> (testing channel)
# release/* branches → <ver>~alpha+<sha> (development)
# Version strategy (see ADR-013 -- branch identity is role-based, not
# version-numbered, specifically so this logic never needs editing at a
# major-version cutover again):
# Tagged release (vX.Y.Z) → X.Y.Z-1 (stable channel)
# main (current stable) → <ver>~beta+<sha> (testing channel)
# next (in-progress next major) → <ver>~alpha+<sha> (development)
# release/*.x (archived, superseded majors, e.g. release/2.x)
# → <ver>~alpha+<sha> (development)
# everything else / PRs → <ver>~dev+<sha> (no publish)
#
# Tilde (~) sorts BELOW the base version in dpkg, so pre-release builds
@@ -123,12 +131,12 @@ jobs:
echo "::warning::Tag version (${REF#refs/tags/v}) does not match \$VERSION in TrueNAS.pm ($BASE_VERSION)"
fi
elif [[ "$REF" == refs/heads/master || "$REF" == refs/heads/release/3.x ]]; then
elif [[ "$REF" == "refs/heads/main" ]]; then
VERSION="${BASE_VERSION}~beta+${SHORT_SHA}"
CHANNEL="testing"
CLOUDSMITH_REPO="truenas-proxmox-testing"
elif [[ "$REF" == refs/heads/release/* ]]; then
elif [[ "$REF" == "refs/heads/next" || "$REF" =~ ^refs/heads/release/.+\.x$ ]]; then
VERSION="${BASE_VERSION}~alpha+${SHORT_SHA}"
CHANNEL="development"
CLOUDSMITH_REPO="truenas-proxmox-snapshots"
@@ -474,12 +482,13 @@ jobs:
git push
- name: Publish to GitHub Pages testing dist
# Only real release/3.x / master beta builds go here. "development"
# channel (other release/* branches, e.g. release/4.x) must NOT land
# in this dist — it's shared/public, and this step does `rm -rf
# Only real "main" beta builds go here (see ADR-013). "development"
# channel (next, or an archived release/*.x branch) must NOT land in
# this dist — it's shared/public, and this step does `rm -rf
# pool/testing` before copying in the new build, which would silently
# overwrite the real testing dist with an unrelated branch's alpha
# build (confirmed happening 2026-08-30 when release/4.x was created).
# build (confirmed happening 2026-08-30 when the old release/4.x
# branch, since renamed to "next", was created).
# No GitHub Pages publish target exists yet for "development" — it
# just builds/scans/uploads as a workflow artifact until one is added.
if: needs.build.outputs.channel == 'testing'
@@ -596,4 +605,4 @@ jobs:
See [README → Installation](https://github.com/TheGrandWazoo/truenas-proxmox#installation).
### Changes
See [CHANGELOG.md](https://github.com/TheGrandWazoo/truenas-proxmox/blob/release/3.x/CHANGELOG.md).
See [CHANGELOG.md](https://github.com/TheGrandWazoo/truenas-proxmox/blob/${{ github.ref_name }}/CHANGELOG.md).
+7 -2
View File
@@ -42,10 +42,15 @@ Everything lives in this repo — there is no external packer repo.
| Trigger | Version format | Cloudsmith repo |
|---------|---------------|-----------------|
| `v*.*.*` tag | `X.Y.Z-1` | `truenas-proxmox` (stable) |
| `release/3.x` or `master` branch | `X.Y.Z~beta+<sha>` | `truenas-proxmox-testing` |
| other `release/*` branches | `X.Y.Z~alpha+<sha>` | `truenas-proxmox-snapshots` |
| `main` branch (current stable major) | `X.Y.Z~beta+<sha>` | `truenas-proxmox-testing` |
| `next` branch (in-progress next major) | `X.Y.Z~alpha+<sha>` | `truenas-proxmox-snapshots` |
| `release/*.x` branches (archived, superseded majors) | `X.Y.Z~alpha+<sha>` | `truenas-proxmox-snapshots` |
| PRs / feature branches | `X.Y.Z~dev+<sha>` | not published |
Branch identity is role-based, not version-numbered (see ADR-013) — `main` and `next` are fixed
names that never get renamed at a major-version cutover; only already-superseded majors get a
version-numbered `release/N.x` archive name. `master` no longer exists (renamed to `archive/v2-legacy`).
`$VERSION` in `TrueNAS.pm` is the single source of truth. A version tag that doesn't match emits a CI warning.
## Authentication
+2 -1
View File
@@ -294,7 +294,8 @@ For a full upgrade path matrix — including what to do when v4 ships — see [d
### Testing / Beta Release
For early access to new features (may be unstable). Beta builds are published on
every push to `release/3.x` — always the latest build, not accumulated history.
every push to `main` (the current stable major's development branch) — always the
latest build, not accumulated history.
```bash
# Import the GPG key (skip if already done for the stable track)
+1 -1
View File
@@ -7,7 +7,7 @@ This file captures release scope, business decisions, and deferred items. It is
## Released — v3.0.0 (TrueNAS Custom Plugin)
**Released:** 2026-05-31
**Branch:** `release/3.x` (default branch)
**Branch:** `release/3.x` (default branch at the time; renamed to `main` 2026-08-30, see ADR-013)
**GitHub Release:** https://github.com/TheGrandWazoo/freenas-proxmox/releases/tag/v3.0.0
Full rewrite as a native `PVE::Storage::Custom` plugin. No patching of PVE files, no SSH, full TrueNAS REST API, bearer token auth only.
+6 -2
View File
@@ -370,10 +370,14 @@ lint → build → security → publish
| Git ref | Debian version string | Channel | Cloudsmith repo | GitHub Pages dist |
|---|---|---|---|---|
| `refs/tags/v3.0.0` | `3.0.0-1` | stable | `truenas-proxmox` | `v3` |
| `refs/heads/master` or `refs/heads/release/3.x` | `3.0.0~beta+abc1234` | testing | `truenas-proxmox-testing` | — |
| `refs/heads/release/*` (other) | `3.0.0~alpha+abc1234` | development | `truenas-proxmox-snapshots` | — |
| `refs/heads/main` (current stable major) | `3.0.0~beta+abc1234` | testing | `truenas-proxmox-testing` | — |
| `refs/heads/next` (in-progress next major) | `3.0.0~alpha+abc1234` | development | `truenas-proxmox-snapshots` | — |
| `refs/heads/release/*.x` (archived majors) | `3.0.0~alpha+abc1234` | development | `truenas-proxmox-snapshots` | — |
| feature branches, PRs | `3.0.0~dev+abc1234` | none | not published |
Branch naming is role-based, not version-numbered — see [ADR-013](../.claude/cos/adrs/ADR-013-branching-strategy.md).
`master` was retired (renamed to `archive/v2-legacy`) as part of that change.
The tilde (`~`) in the Debian version sorts below the base version in `dpkg`, guaranteeing pre-release builds never auto-upgrade over a stable release.
### Running the Build Locally