This file is the operator playbook for cutting a new version of artemis. Reader: a maintainer with merge rights to the artemis repo. Downstream-deployment pin updates (helm/kustomize/ArgoCD/etc.) are operator-specific and live outside this file.
Releases are driven by release-please in manifest mode. The operator does not tag by hand — release-please reads Conventional Commits on main and maintains a standing release PR; merging that PR cuts the tag, publishes the GitHub Release, and triggers the image build.
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. This caveat is enforced mechanically by bump-minor-pre-major: true in release-please-config.json — a BREAKING CHANGE commit on a 0.x line bumps MINOR, not MAJOR.
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 |
feat: deliberately bumps MINOR even pre-1.0 — bump-patch-for-minor-pre-major is left off. A release PR is opened only when the unreleased commits contain at least one releasable change (feat, fix, perf, refactor, chore(deps)); pure test/docs/chore drift accumulates silently 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. Force it with a Release-As: 1.0.0 footer (see step 2) or by editing the release PR.
The flow is PR-driven. release-please computes the bump from commit history; the operator owns the merge and may override the version before merging.
Every push to main runs .github/workflows/release.yml. The release-please job opens or updates a PR titled chore(main): release X.Y.Z that:
- bumps
version.txtand.release-please-manifest.jsonto the computed version, and - regenerates the
CHANGELOG.mdsection from the Conventional Commits since the last release.
Read the PR. The diff is the proposed release: the version bump and the rendered changelog. Edit the changelog body directly in the PR if wording needs work — release-please preserves manual edits to its PR.
release-please's computed bump is a strong default, not gospel — it cannot detect a quiet behaviour break hidden behind a refactor: prefix. To force a specific version, add a Release-As: footer to any commit that lands on main (e.g. an empty commit), then let release-please re-groom the PR:
git commit --allow-empty -m "chore: release 0.4.0" -m "Release-As: 0.4.0"
git push origin mainMerging the release PR is the release. release-please then:
- creates the annotated git tag
vX.Y.Zat the merge commit (include-v-in-tagsdefaults true — git tags carry thev), - publishes the GitHub Release with the changelog body, and
- emits
release_created=true, which gates thebuild-and-pushjob in the same workflow run.
Note — tag points at the release commit. Unlike a hand-tagged flow, the tag sits on the release-PR merge commit (version bump + changelog), not on the last behaviour-bearing commit. The image is still built from that exact commit (
checkoutusesref: ${{ needs.release-please.outputs.sha }}), sohelm listversion ↔ deployed code stays consistent.
The build-and-push job builds and pushes to ghcr.io/freecodecamp/artemis, emitting tags:
0.2.0(full semver — release-please'sversionoutput is already v-stripped; the registry tag is the bare semver),0.2(major.minor, for floating point-release pins — same bare form),sha-<full-sha>(immutable audit anchor; always emitted),latest(newest release; the job only runs on a real release, solatestalways maps to a published version).
It embeds VERSION=0.2.0 + COMMIT=<full-sha> into the binary via -X main.version=… -X main.commit=… (visible in the startup log line artemis: starting version=0.2.0 commit=<sha>).
Git tag (v0.2.0) and registry tag (0.2.0) intentionally differ by the v prefix — release-please's git-tag default plus the OCI-registry convention. Do not "fix" the asymmetry; downstream tooling (helm, kustomize, ArgoCD image-updater) expects the bare semver.
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) 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.
| File | Role |
|---|---|
release-please-config.json |
release-type: simple, bump-minor-pre-major: true. Language-agnostic — no Node/JS toolchain pulled in. |
.release-please-manifest.json |
Source of truth for the current released version ({".": "X.Y.Z"}). release-please reads + bumps this. |
version.txt |
The simple strategy mirrors the current version here; embedded nowhere at runtime (build identity comes from the workflow's build-args). |
.github/workflows/release.yml |
The release-please + build-and-push two-job workflow. |
Because the build runs in the same workflow run as release-please (gated on release_created), the older tag-push race that produced intermittent Bad credentials at the metadata step no longer applies. If build-and-push fails for a transient reason, re-run the failed jobs — the tag and release already exist, so the replay is idempotent:
gh run rerun <run-id> --failedIf release-please itself fails to open or update the PR, confirm the release-please job has contents: write + pull-requests: write and that repo settings allow Actions to create PRs (Settings → Actions → General → Allow GitHub Actions to create and approve pull requests).
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, run release-please against a maintenance branch:
git checkout -b release/v0.2.x v0.2.0and push the branch.- Cherry-pick the fix commit(s) onto it.
- Point a release-please run at that branch via the
target-branchinput (a dedicatedworkflow_dispatchinvocation or a branch-scoped workflow). release-please opens a release PR againstrelease/v0.2.x; merging it cutsv0.2.1and builds the image off that branch. - Open a PR to merge
release/v0.2.xback intomainso the fix also lands forward.
release-pleaseruns entirely in CI viagoogleapis/release-please-action(pinned by SHA inrelease.yml). No local tool and no Node toolchain are required for a release — the action is a runner-side dependency, not a repo dev-dep.release-please(thenode/go/etc. release-types) was configured assimpleprecisely to keep this Go service free of language-specific manifest coupling.
- 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. Git tags carry thev(v0.2.0); registry tags do not (0.2.0) — intentional, consistent with release-please + docker/metadata-action defaults. - The release decision is a PR review, not a local tag. The diff under review is exactly what ships (version + changelog), so a mistaken bump is caught before merge, and the version can be overridden with a
Release-As:footer. MINOR-may-breakpre-1.0 is enforced bybump-minor-pre-major: trueand documented here so operators readingCHANGELOG.mdsee the caveat without diving into the tooling config.