Derive OCI publish mode from trigger to stop twin-publish race (GOV-15) #382

Merged
David merged 2 commits from fix/GOV-15-twin-trigger-publish-mode into main 2026-06-26 18:02:27 +02:00
Owner

What

Ports the GOV-13 twin-trigger publish-mode fix into mokosh-server's .forgejo/workflows/build-oci-image.yml and its oci-build/get-tags.nu helper. Part of the GOV-15 per-repo rollout.

Why

The build workflow triggers on push to both branches: [main] and tags: [v*] and resolved its tag set from git describe via oci-build/get-tags.nu --joined, which returned [vX.Y.Z, latest] on a tagged commit. A release commit is simultaneously a main push and a v* tag push, so both events fired, both built the same image, and both raced to overwrite the mutable :latest tag (and 409'd on immutable version artifacts). This is the race tracked as governance GOV-13 / claude-run CLAUDE-122.

How

Derive the publish mode from the workflow trigger rather than from git describe: a tag ref resolves to the release train and publishes only :vX.Y.Z, a main push resolves to the latest train and publishes only :latest, and a manual workflow_dispatch resolves to a dry-run that builds and prints but mutates nothing. The two push runs now write disjoint destinations and never race. oci-build/get-tags.nu is replaced with the canonical --mode version returning a { mode, train, tag, describe } record; the workflow gains workflow_dispatch dry_run/simulate_tag inputs, a resolve step that maps the trigger to a single mode/train/tag, a dry-run-gated build-and-push (--load with no cache export on dry-run, --push --cache-to otherwise), and a dry-run-aware URL print.

Scope

Minimal port: the image name, registry/secret wiring, build-args, Dockerfile path, and the type=gha cache strategy are unchanged. Only the trigger-derived publish-mode logic changed.

Verification

Local nu/yaml gate per the GOV-15 recipe, all green: YAML parses, nu-check on the helper returns true, the four functional modes resolve the expected tags (release -> v9.9.9, latest -> latest, dry-run --simulate-tag v9.9.9 -> release/v9.9.9, dry-run -> latest/latest), and both error paths (--mode bogus, --mode release with no version) exit non-zero. The repo's E2E (staging) CI check is always-red by design (staging/prod split) and is not a gate here.

## What Ports the GOV-13 twin-trigger publish-mode fix into mokosh-server's `.forgejo/workflows/build-oci-image.yml` and its `oci-build/get-tags.nu` helper. Part of the GOV-15 per-repo rollout. ## Why The build workflow triggers on push to both `branches: [main]` and `tags: [v*]` and resolved its tag set from `git describe` via `oci-build/get-tags.nu --joined`, which returned `[vX.Y.Z, latest]` on a tagged commit. A release commit is simultaneously a main push and a `v*` tag push, so both events fired, both built the same image, and both raced to overwrite the mutable `:latest` tag (and 409'd on immutable version artifacts). This is the race tracked as governance GOV-13 / claude-run CLAUDE-122. ## How Derive the publish mode from the workflow trigger rather than from `git describe`: a tag ref resolves to the `release` train and publishes only `:vX.Y.Z`, a main push resolves to the `latest` train and publishes only `:latest`, and a manual `workflow_dispatch` resolves to a `dry-run` that builds and prints but mutates nothing. The two push runs now write disjoint destinations and never race. `oci-build/get-tags.nu` is replaced with the canonical `--mode` version returning a `{ mode, train, tag, describe }` record; the workflow gains `workflow_dispatch` `dry_run`/`simulate_tag` inputs, a resolve step that maps the trigger to a single mode/train/tag, a dry-run-gated build-and-push (`--load` with no cache export on dry-run, `--push --cache-to` otherwise), and a dry-run-aware URL print. ## Scope Minimal port: the image name, registry/secret wiring, build-args, Dockerfile path, and the `type=gha` cache strategy are unchanged. Only the trigger-derived publish-mode logic changed. ## Verification Local nu/yaml gate per the GOV-15 recipe, all green: YAML parses, `nu-check` on the helper returns `true`, the four functional modes resolve the expected tags (`release` -> `v9.9.9`, `latest` -> `latest`, `dry-run --simulate-tag v9.9.9` -> release/`v9.9.9`, `dry-run` -> latest/`latest`), and both error paths (`--mode bogus`, `--mode release` with no version) exit non-zero. The repo's `E2E (staging)` CI check is always-red by design (staging/prod split) and is not a gate here.
fix(ci): derive publish mode from trigger to stop twin-publish race
All checks were successful
E2E / Playwright against staging (pull_request) Successful in 1m1s
Check / fmt + clippy + build + tests (pull_request) Successful in 2m31s
Integration / integration tests (pull_request) Successful in 13m17s
4ae07e44ce
The build-oci-image workflow triggers on push to both main and v* tags and resolved its tag set from git describe via oci-build/get-tags.nu, which returned [vX.Y.Z, latest] on a tagged commit. A release commit is simultaneously a main push and a v* tag push, so both events fired, both built the same image, and both raced to overwrite the mutable :latest tag (and 409'd on immutable version artifacts).

Derive the publish mode from the trigger instead: a tag ref resolves to the release train and publishes only :vX.Y.Z, a main push resolves to the latest train and publishes only :latest, and a manual workflow_dispatch resolves to dry-run which builds and prints but mutates nothing. The two push runs now write disjoint destinations and never race.

get-tags.nu is replaced with the canonical --mode version; the workflow gains workflow_dispatch dry_run/simulate_tag inputs, a resolve step that maps the trigger to a single mode/train/tag, a dry-run-gated build-and-push (--load with no cache export on dry-run, --push --cache-to otherwise), and a dry-run-aware URL print. Image name, registry/secret wiring, build-args, Dockerfile path, and the type=gha cache strategy are unchanged.

#GOV-15
#GOV-13
Merge branch 'main' into fix/GOV-15-twin-trigger-publish-mode
All checks were successful
E2E / Playwright against staging (pull_request) Successful in 1m44s
Check / fmt + clippy + build + tests (pull_request) Successful in 5m38s
Integration / integration tests (pull_request) Successful in 8m20s
Create release / Gate (release-branch merges only) (pull_request) Successful in 0s
Create release / Create release from merged PR (pull_request) Has been skipped
817b4d9048
David merged commit 8007fb93a8 into main 2026-06-26 18:02:27 +02:00
David deleted branch fix/GOV-15-twin-trigger-publish-mode 2026-06-26 18:02:27 +02:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
psa-systems/mokosh-server!382
No description provided.