fix: atomic release versions, immutable image tags, conditional cd_path #22

Merged
KofTwentyTwo merged 1 commits from feature/atomic-release-versions into develop 2026-10-11 19:48:10 +00:00
KofTwentyTwo commented 2026-10-11 18:57:23 +00:00 (Migrated from github.com)

Problem

Version collisions. Website-Backend #65/#66 (0.1.24) and again #80/#79 (0.1.32, today 18:42–18:50Z) merged minutes apart and both pipelines computed the same GitVersion version. The first pipeline rolled its image out. The second overwrote the image tag in GHCR. github-release used --target main, so the tag landed on whatever main pointed at by then. The GitOps bump was a no-op. Result: prod ran 2d7cb7a under v0.1.32 -> d212ec5, and GHCR 0.1.32 now holds a different image than prod.

cd_path check failure. The generator always rendered cd_path: into update_cd_repo. Repos whose .munitor.yml pins an older orb_version (Website-Backend/Frontend develop at 0.2, Concilium 0.2, makers4 0.1) fail with Unexpected argument(s): cd_path.

Fix

  • allocate_version (new, in mvn/gradle/node build_and_test on main): reserves vX.Y.Z by POST /git/refs. GitHub creates the ref only if it is absent, so concurrent pipelines serialize on it:

    • created: this commit owns the version
    • exists on this commit: a rerun, so reuse it
    • exists on another commit: take the next patch and retry

    Existing tags are never moved. This runs before the version is baked into the jar or image.

  • guard_image_tags (new, before build/push in docker_build_push): every image now carries the org.opencontainers.image.revision label. The guard refuses to push a main or supplemental version tag that already exists for a different commit or without the label, and fails closed on registry errors.

  • update_cd_repo: on main, verifies vX.Y.Z points at CIRCLE_SHA1 before bumping GitOps, and fails loudly otherwise. The source commit is recorded in the GitOps commit body.

  • github_release: targets CIRCLE_SHA1 (not main), and refuses if the tag points elsewhere.

  • Generator: cd_path is wrapped in ##IF_CD_EXPLICIT_PATH##, so it is rendered only when cd.path is set. Packed scripts are regenerated.

No new orb parameters, so generated pipelines stay compatible.

Tests

  • tests/test_allocate_version.sh (12 tests): free version, rerun reuse, the #65/#66 collision gets the next patch, tags never move, annotated tags, multiple taken versions skipped, non-main/pre-release skipped, API errors and missing token fail. It also runs 8 concurrent allocators against a fake refs API with atomic create-if-absent (hard-link) semantics, which must yield 8 distinct contiguous versions, each tag owned by its committer, and concurrent reruns that create no extra tags. Mutation check: treating "already exists" as success reproduces the incident (all 8 get 0.1.24, 4 tests fail).
  • tests/test_release_guards.sh (13 tests): the registry guard with a fake docker (free, rerun, collision, supplemental collision, unlabeled, registry error), step ordering in jobs, release target, CD ownership check, and cd_path omitted without cd.path but kept with it.
  • shellcheck clean on all touched scripts.

🤖 Generated with Claude Code

## Problem **Version collisions.** Website-Backend #65/#66 (0.1.24) and again #80/#79 (0.1.32, today 18:42–18:50Z) merged minutes apart and both pipelines computed the same GitVersion version. The first pipeline rolled its image out. The second overwrote the image tag in GHCR. `github-release` used `--target main`, so the tag landed on whatever `main` pointed at by then. The GitOps bump was a no-op. Result: prod ran `2d7cb7a` under `v0.1.32 -> d212ec5`, and GHCR `0.1.32` now holds a different image than prod. **`cd_path` check failure.** The generator always rendered `cd_path:` into `update_cd_repo`. Repos whose `.munitor.yml` pins an older `orb_version` (Website-Backend/Frontend `develop` at 0.2, Concilium 0.2, makers4 0.1) fail with `Unexpected argument(s): cd_path`. ## Fix - **`allocate_version`** (new, in mvn/gradle/node `build_and_test` on `main`): reserves `vX.Y.Z` by `POST /git/refs`. GitHub creates the ref only if it is absent, so concurrent pipelines serialize on it: - created: this commit owns the version - exists on this commit: a rerun, so reuse it - exists on another commit: take the next patch and retry Existing tags are never moved. This runs before the version is baked into the jar or image. - **`guard_image_tags`** (new, before build/push in `docker_build_push`): every image now carries the `org.opencontainers.image.revision` label. The guard refuses to push a main or supplemental version tag that already exists for a different commit or without the label, and fails closed on registry errors. - **`update_cd_repo`**: on `main`, verifies `vX.Y.Z` points at `CIRCLE_SHA1` before bumping GitOps, and fails loudly otherwise. The source commit is recorded in the GitOps commit body. - **`github_release`**: targets `CIRCLE_SHA1` (not `main`), and refuses if the tag points elsewhere. - **Generator**: `cd_path` is wrapped in `##IF_CD_EXPLICIT_PATH##`, so it is rendered only when `cd.path` is set. Packed scripts are regenerated. No new orb parameters, so generated pipelines stay compatible. ## Tests - `tests/test_allocate_version.sh` (12 tests): free version, rerun reuse, the #65/#66 collision gets the next patch, tags never move, annotated tags, multiple taken versions skipped, non-main/pre-release skipped, API errors and missing token fail. It also runs **8 concurrent allocators** against a fake refs API with atomic create-if-absent (hard-link) semantics, which must yield 8 distinct contiguous versions, each tag owned by its committer, and concurrent reruns that create no extra tags. Mutation check: treating "already exists" as success reproduces the incident (all 8 get 0.1.24, 4 tests fail). - `tests/test_release_guards.sh` (13 tests): the registry guard with a fake `docker` (free, rerun, collision, supplemental collision, unlabeled, registry error), step ordering in jobs, release target, CD ownership check, and `cd_path` omitted without `cd.path` but kept with it. - shellcheck clean on all touched scripts. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Sign in to join this conversation.