This file is the operator playbook for cutting a new version of artemis. Reader: a maintainer with push rights to the artemis repo. Downstream-deployment pin updates (helm/kustomize/ArgoCD/etc.) are operator-specific and live outside this file.
artemis follows Semantic Versioning 2.0.0 with a single pre-1.0 caveat: until v1.0.0 is cut, a MINOR bump may introduce a backwards-incompatible API change. Each such change is called out in the changelog with a **[BREAKING]** marker and a one-line migration note.
Post-1.0, the standard semver contract applies: MAJOR for breaks, MINOR for additive features, PATCH for fixes.
| Conventional Commit prefix | Pre-1.0 bump | Post-1.0 bump |
|---|---|---|
feat(*): |
MINOR |
MINOR |
fix(*):, perf(*):, refactor(*): |
PATCH |
PATCH |
any body with BREAKING CHANGE: |
MINOR loud |
MAJOR |
chore(deps): |
PATCH |
PATCH |
test(*):, docs(*):, ci(*):, chore(*): (non-deps), style(*):, build(*): |
no release | no release |
A release is cut only when the unreleased section contains at least one feat, fix, perf, refactor, or chore(deps) commit. Pure test/docs/chore drift accumulates until a behaviour-bearing commit ships alongside it.
Cut v1.0.0 when the API surface is declared frozen — practically, after GET /api/site/{site}/alias/{mode}, the sites-registry CRUD, and the deploy/promote/rollback verbs have settled in production CLI use without breaking changes for two consecutive minor releases.
The flow is operator-driven, not CI-driven. CI only validates and publishes; the human picks the bump.
# What would the next release contain?
git-cliff --unreleased
# What does git-cliff think the next version should be?
git-cliff --bumped-version--bumped-version reads [bump] rules in cliff.toml. Use it as a sanity check, not as the authoritative bump — the operator owns the final call (e.g. --bumped-version cannot detect a quiet behaviour break behind a refactor prefix).
# Replace v0.2.0 with the version chosen in step 1.
git tag -a v0.2.0 -m "v0.2.0 — <one-line summary>"Tags are annotated (-a), never lightweight, so the tag carries authorship + a message that survives git describe.
git-cliff -o CHANGELOG.md
git add CHANGELOG.md
git commit -m "chore(release): v0.2.0"The release-chore commit lands on main after the tag so the tag itself points at the last behaviour-bearing commit (operators looking at helm list will see a release version that ties cleanly to the deployed code, not to a doc commit).
If the tag now points at the wrong commit because step 3 was committed first, fix with:
git tag -d v0.2.0
git tag -a v0.2.0 HEAD~1 -m "v0.2.0 — <summary>"git push origin main
git push origin v0.2.0The tag push fires .github/workflows/docker-ghcr.yml (trigger: push.tags: ['v[0-9]+.[0-9]+.[0-9]+', 'v[0-9]+.[0-9]+.[0-9]+-*']). The workflow:
- builds + pushes the image to
ghcr.io/freecodecamp/artemis, emitting tags0.2.0(full semver —docker/metadata-actiontype=semver,pattern={{version}}strips the leadingvfrom the git tag; the registry tag is the bare semver)0.2(major.minor, for floating point-release pins — samev-stripping)sha-<full-sha>(immutable audit anchor; always emitted)
- embeds
VERSION=0.2.0+COMMIT=<full-sha>into the binary via-X main.version=… -X main.commit=…(visible in the startup log lineartemis: starting version=0.2.0 commit=<sha>).
Git tag (v0.2.0) and registry tag (0.2.0) intentionally differ by the v prefix — this is the docker/metadata-action default and the broader OCI-registry convention. Don't try to "fix" the asymmetry by adding pattern=v{{version}}; downstream tooling (helm, kustomize, ArgoCD image-updater) expects the bare semver.
The same workflow is also workflow_dispatch-able for ad-hoc builds off main; those emit only sha-<full-sha>, main, and latest — never a semver tag.
GitHub Release notes are auto-published as part of the same docker-ghcr.yml run. After the build+push job finishes the image, two extra steps fire only on tag push: Generate release notes invokes orhun/git-cliff-action with --current --strip all to render the body for the tag directly from the commit log + cliff.toml, and Publish GitHub Release (softprops/action-gh-release pinned to v3.0.0) creates / updates the Release object with that body. The action is idempotent — re-running against an existing release updates rather than failing.
The notes come from the git history walked between the previous tag and the current one — not from CHANGELOG.md state at the tagged commit. Decoupling them lets the release-chore commit (chore(release): vX.Y.Z) land after the tag without breaking the workflow: the tag can keep pointing at the last behaviour-bearing commit per step 2 while still publishing a populated GH Release body. The checkout step uses fetch-depth: 0 + fetch-tags: true so git-cliff can resolve --current against the full ref graph.
Once the workflow finishes, downstream deployments pin the new release. Resolve the digest:
docker buildx imagetools inspect ghcr.io/freecodecamp/artemis:X.Y.Z \
--format '{{.Manifest.Digest}}'The pin format is image.tag: "X.Y.Z@sha256:<digest>" — bare semver (no v-prefix; docker/metadata-action strips it from the git tag per OCI convention) plus the @sha256:<digest> immutable anchor. Never use tag: X.Y.Z without the digest, and never use tag: latest in production. Deployment mechanics (helm/kustomize/ArgoCD/etc.) are operator-specific.
Behaviour-bearing warnings emitted by the running service. Each entry lists the log event, the removal trigger, and the replacement contract.
| event | emitted when | removal trigger | replacement |
|---|---|---|---|
promote.legacy_bare |
POST /api/site/{site}/promote with empty / zero-valued body |
one release after first appearance — flip the empty-body branch to 400 Bad Request in the sprint following the release that ships the warn |
{"deployId": "<id>"} (direct-write) and/or {"expectedCurrent": "<id>"} (CAS) |
Telemetry consumers can grep event=promote.legacy_bare in the artemis access log to find remaining callers before the flip.
If v0.3.x is current but v0.2.x is still pinned in some downstream deployment and needs a fix:
git checkout -b release/v0.2.x v0.2.0- Cherry-pick the fix commit.
git tag -a v0.2.1 -m "v0.2.1 — <hotfix summary>"- Push branch + tag. Open a PR to merge
release/v0.2.xback intomain(so the fix also lands forward).
git-cliff(Rust): reads Conventional Commits, emitsCHANGELOG.md. Install viabrew install git-cliff(macOS) orcargo install git-cliff. Config:cliff.tomlat repo root.- No Node toolchain is required.
release-pleasewas evaluated and rejected to keep this Go service free of JS dev-deps.
- Operators map deployed releases back to changelog entries via the semver portion of
image.tag. Even though@sha256:<digest>is the load-bearing pin, theX.Y.Zsemver prefix (nov, per OCI tag convention) gives agrep-able human anchor that survives future tag-format migrations. Git tags carry thev(v0.2.0); registry tags do not (0.2.0) — intentional, consistent with docker/metadata-action defaults. - Tags are local-cheap, push-discoverable. CI builds run only on
v*tag push, so a typo in step 2 is a soft failure (the tag can be deleted before step 4). MINOR-may-breakpre-1.0 is preserved in this file (not just incliff.tomlcomments) because operators readingCHANGELOG.mdneed to see the caveat without diving into the tooling config.