Skip to main content

Versioning & Release Policy

fulla's version numbering scheme, bump decision rules, release cadence, pre-release and patch channels, and the standard operating procedure (SOP) for shipping a release.

This document is the single source of truth for versioning governance. The "how" of release engineering (CI pipeline, signing, SBOM) is implemented by .github/workflows/release.yml; this document answers "when to release, what to bump, and why". When the two conflict, this document wins — fix the pipeline.

Related documents:

  • SDK Runtime Contract §2 declares the ABI / source-level SemVer commitments and the deprecation process; this document expands on their versioning-governance side.
  • CI/CD Guide describes where the release pipeline sits in the overall CI.

1. Version Numbering Scheme​

1.1 SemVer 2.0.0​

fulla follows Semantic Versioning 2.0.0:

MAJOR.MINOR.PATCH[-prerelease]
1 . 0 . 0 -rc.1
SegmentBump trigger (summary; see the decision table in §2)Compatibility commitment
MAJORBreaking changeNone — users must change their code
MINORNew functionality, backwards compatibleSource compatible
PATCHBackwards-compatible defect fixesSource compatible
prerelease-alpha.N / -beta.N / -rc.NNo commitment

The boundary of "source compatible" for v1.x is defined by SDK Runtime Contract §2: it covers only the source-level API of the public headers under libs/*/include/fulla/** and makes no binary-ABI commitment.

1.2 Single source of truth for the version number (SSoT)​

ComponentVersion sourceSync validation
C++ libraries + serverMAJOR/MINOR/PATCH in cmake/Version.cmake✅ tools/api-diff/api_diff.py cross-checks Version.cmake / CMakeLists.txt project(VERSION) / conanfile.py version
Docker imagesRead from Version.cmake by release.ymlGHCR tag = <version>
DB schemaThe numbering of apps/server/migrations/V0NN_*.sqlNot coupled to the product version (see §6)

The first step of any release is editing cmake/Version.cmake; version drift across the three locations is intercepted by api-diff in the version-check job of release.yml.


2. Version Bump Decision Table​

Which bump does a change trigger — MAJOR / MINOR / PATCH? Decide with the table below. When multiple rows match, take the highest level (MAJOR > MINOR > PATCH).

Change type→ MAJOR→ MINOR→ PATCH
SDK public header removed / renamed / signature changed / default argument changed (judged BREAKING by api-diff)✅
Behavioral semantics change of a public API (meaning of return values, error codes, side effects, protocol field semantics)✅
Raise of the minimum C++ standard / compiler version✅
Major-version dependency upgrade of Drogon / Postgres / Redis✅
Config option removed, or default value changed with no compatible old behavior✅
Breaking migration of the DB schema (column drop / type change without backfill / rename)✅
New SDK API / new OAuth2 endpoint / new OIDC claim✅
New optional parameter / field on an existing API (with default value)✅
New optional config option (old configs keep working)✅
New optional dependency✅
Performance optimization (no public API change)✅
feat: conventional commit (no !)✅
fix: conventional commit — API behavior regressing back to "correct"✅
Security vulnerability fix (CVE-type, no API change)✅
Docs / tests / CI fixes (if a release is decided)✅
Purely docs: / test: / chore: / build: / ci: commitsNo release

Conventional Commits → bump automatic mapping​

The default mapping from commit prefix to bump (an ! suffix or a BREAKING CHANGE: footer forces MAJOR):

feat: → MINOR feat!: → MAJOR
fix: → PATCH fix!: → MAJOR
perf: → PATCH perf!: → MAJOR
refactor: → no release (unless !)
docs/test/chore/build/ci: → no release

A scope does not change the default mapping, but a maintainer may raise the level based on the scope (see §3). The commit parser in cliff.toml is already aligned with the table above.


3. The security-hardening "gray zone" — an explicit trade-off statement​

One category of changes arising from OAuth/OIDC compliance audits is special: they tighten previously lenient behavior (e.g. enforcing https redirect_uri, enforcing PKCE, requiring client_secret for the refresh grant of CONFIDENTIAL clients). Such changes:

  • From a strict SemVer perspective: breaking (downstreams relying on the old lenient behavior break).
  • Industry practice: mostly shipped within a MINOR bump, prominently flagged in the Release Notes.

fulla's trade-off:

Security hardening ships within a MINOR and does not force a MAJOR. Rationale: the previous "lenient behavior" was itself a spec violation (a bug); fixing it is a return to correctness, not an intentional change of product semantics. But every such change must be explicitly listed in the ⚠️ Breaking (security hardening) section of the Release Notes, together with migration guidance.

This is an explicit trade-off, not a vague compromise. If the impact of a particular hardening is assessed as genuinely broad (e.g. removing an entire grant type), it should still go through the MAJOR + pre-release channel (see §5).


4. Release Cadence​

A hybrid model: periodic MINOR + on-demand PATCH + emergency security hotfix.

EventTrigger
Scheduled MINOR (new features)Every 4–6 weeks; or when ≥ 3 feat: commits have accumulated
PATCH (bug fix)When ≥ 5 fix: commits have accumulated; or when a user-reported bug has been fixed
Emergency security PATCHImmediately after a P0 / CVE vulnerability fix (without waiting for the cadence)
MAJORWhen breaking changes have accumulated; must go through the pre-release channel (§5)

The cadence is guidance, not dogma: skipping a cycle when nothing release-worthy changed is perfectly fine; conversely, a P0 security fix always ships immediately.


5. Pre-release Channel​

Before an official MAJOR release, go through a staged pre-release ladder:

v2.0.0-alpha.1 → alpha.2 → … → v2.0.0-beta.1 → … → v2.0.0-rc.1 → … → v2.0.0
StageSemanticsAccepted changes
alpha.NFunctionality may be incomplete; CI not guaranteed to passAnything (including new features, behavior adjustments)
beta.NFeature freeze, feedback gatheringBug fixes + non-breaking feedback-driven adjustments
rc.NRelease candidateOnly P0/P1 bug fixes
(suffix removed)Official releaseNo new changes accepted

Image tag rules:

  • Official release → tagged <version> and latest
  • pre-release → tagged only <version> (e.g. v2.0.0-rc.1), not latest

Current pipeline status: the tag trigger pattern of release.yml, v[0-9]+.[0-9]+.[0-9]+, accepts no suffix, so pre-release tags currently do not trigger a release. Enabling the pre-release channel requires extending that regex to match v[0-9]+.[0-9]+.[0-9]+(-[a-z]+\.[0-9]+)? and, in the github-release job, deciding from whether the tag contains - whether to mark the GitHub Release as "Pre-release" and skip the latest manifest merge. This is a pending pipeline change of this policy (see §11).


6. DB Schema Versioning Is Decoupled from the Product Version​

fulla uses numbered migrations (V001_* … V0NN_*) whose numbers increment independently.

  • Additive migration (new table / new column / new index) = backwards compatible → triggers a MINOR assessment.
  • Breaking migration (column drop / type change without backfill / rename) → triggers a MAJOR assessment.
  • The schema version table only records the migration application history and does not map to MAJOR.MINOR.PATCH.

7. Release Branch and Patch Release​

After v1.2.0 is released, the mainline develops v1.3.0. If a P0 vulnerability is found in v1.2.0:

master: ──●──●──●──●──●──●──→ (developing v1.3.0)
\
release/1.2: └──●(cherry-pick fix)──● tag v1.2.1

Conventions:

  • Branch naming: release/<MAJOR>.<MINOR> (e.g. release/1.2).
  • The branch accepts only cherry-picked bug fixes, no new features.
  • Each patch release cuts a v<MAJOR>.<MINOR>.<PATCH> tag, which triggers release.yml.
  • Maintenance window: patches are maintained only for the latest release branch. The previous branch is EOL once a new minor is released (no LTS — see §8).

8. LTS (Long-Term Support)​

No LTS at the current stage. Only the latest minor's patch releases are maintained. Whether to introduce LTS (à la the Node.js / Kubernetes model) will be reconsidered when the downstream user base grows and upgrade costs become visible.


9. latest Tag Semantics and Production Deployment​

  • :latest points to the latest official release (pre-releases excluded).
  • deploy/docker/docker-compose.prod.yml uses ${FULLA_VERSION:-latest}: a default for deployment convenience.
  • ⚠️ Production deployments should pin an explicit version number (FULLA_VERSION=1.2.0) instead of relying on latest — it rolls uncontrollably whenever a new version is published.

10. Deprecation Process​

Consistent with SDK Runtime Contract §2:

  1. At the current MINOR release, annotate the deprecated API with [[deprecated("Use X instead; removed in vN.0")]].
  2. Record the deprecation + migration guidance in the Deprecated section of the Release Notes.
  3. Keep it for at least one MINOR cycle (two recommended).
  4. Remove it in the next MAJOR.

Non-SDK deprecations (config options, endpoint parameters) follow the same "annotate → transition → remove" process, using Release Notes plus a LOG_WARNING at config load time as the annotation mechanism.


11. Release Standard Operating Procedure (SOP)​

11.1 Official MINOR / PATCH (from master)​

# 1. Confirm master is green (CI fully passing)
git checkout master && git pull

# 2. Update the version number SSoT
# Edit MINOR or PATCH in cmake/Version.cmake

# 3. Validate the API surface (critical step)
python3 tools/api-diff/api_diff.py
# - additive drift (new headers / new declarations) → allowed for MINOR, ratify:
# python3 tools/api-diff/api_diff.py --update-baseline
# - breaking drift (removed / changed declarations) → MAJOR must be confirmed
# bumped first; for changes that do not affect the consumed surface (private
# members / include reordering, etc.), after review:
# python3 tools/api-diff/api_diff.py --force --update-baseline

# 4. Generate a CHANGELOG draft, then curate manually
git cliff --unreleased --tag vX.Y.Z --prepend CHANGELOG.md
# Manual editing essentials:
# - Categorize into Added / Fixed / Changed / Security / Deprecated / ⚠️ Breaking
# - Put security hardening into the ⚠️ Breaking (security hardening) section + migration guidance
# - Drop entries with no information value

# 5. Commit the version number + baseline + CHANGELOG
git add cmake/Version.cmake tools/api-diff/*.baseline CHANGELOG.md
git commit -m "chore(release): vX.Y.Z"

# 6. Tag and push — triggers release.yml
git tag vX.Y.Z
git push origin master --tags

release.yml completes automatically: version-check → SDK tarball → multi-arch images → cosign signing → SBOM → GitHub Release (with git-cliff-generated notes + verification guidance).

11.2 Emergency Security PATCH (from a release branch)​

# 1. Cherry-pick the fix commit onto the release/<MAJOR>.<MINOR> branch
git checkout release/1.2
git cherry-pick <fix-commit-sha>

# 2. Bump PATCH on that branch (same steps 2–6 as 11.1, but targeting the release branch)

11.3 MAJOR (via the pre-release channel)​

# 1. Accumulate breaking changes on master (or a dedicated candidate branch)
# 2. Bump MAJOR, then tag prereleases in sequence:
git tag v2.0.0-alpha.1 && git push --tags # → alpha stage
# ... feedback iterations ...
git tag v2.0.0-beta.1 && git push --tags # → beta stage
git tag v2.0.0-rc.1 && git push --tags # → rc stage (only P0/P1 fixes)
# 3. Once rc passes, drop the suffix for the official release:
git tag v2.0.0 && git push --tags
# 4. After the official release, create the release/2.0 branch
git checkout -b release/2.0 v2.0.0 && git push origin release/2.0

⚠️ As stated in §5: pre-release tags currently do not trigger release.yml. Complete the pipeline change (see the §12 backlog) before enabling this channel.


12. Backlog (Gaps Between This Policy and the Current State)​

#ItemProblem it solves
T1Write this document (✅ this file)No written bump rules before
T2Execute the first official release since v1.0.0 (v1.0.1 or v1.1.0)840 commits piled up after v1.0.0, unreleased
T3Extend the release.yml tag trigger pattern + latest skip logic, enabling the pre-release channelPrerelease tags currently do not trigger a release
T4Add a latest warning cross-reference to the prod deployment doc (docker-deployment.md)The latest default poses a rolling-update risk in production
T5Define the release branch naming convention and add a pointer in the README (create the branch when first actually needed)The patch release process is not yet instantiated

T1 is this file; T2 is the immediate priority; T3–T5 can land when their scenarios first occur.