ci(release): decouple latest/release publish trains and add dry-run (FJ-24) #23

Merged
David merged 1 commit from ci/decouple-publish-trains-dry-run-fj-24 into main 2026-05-29 00:50:41 +02:00
Owner

Summary

Fully decouples the latest and release publish trains and adds a zero-mutation dry-run to both binary build workflows (FJ-24). The publish mode is now derived from the workflow trigger, not from git describe, so a release tag publishes only its immutable versioned package and a main push publishes only latest plus the manifest.

What changed

  • oci-build/get-tags.nu is now a pure resolver that takes --mode (release | latest | dry-run), --ref-name, and --simulate-tag, and returns a single { mode, train, tag, describe } record. It returns the single tag appropriate to the mode ([<version>] or latest) instead of [<version>, latest]. git describe is kept ONLY to fill the manifest tag field; it no longer decides the train. Both workflows call this one resolver so they cannot drift.
  • Publish mode is derived from the trigger: a refs/tags/v* push (github.ref_type == tag) maps to release, a main push maps to latest, and workflow_dispatch maps to dry-run.
  • Release (version-tag) uploads are write-once: the publish step probes the <version> artifact URL and fails the build on HTTP 200 instead of pre-deleting and re-uploading. No 404 window, no silent replacement of a released artifact. The latest train keeps delete-then-reupload (overwrite) semantics.
  • The latest-train manifest step is gated to the latest train (if: steps.resolve.outputs.train == 'latest'), so a release build never writes latest/version-<arch>.json and never resets latest to the release commit.
  • Both build-binary.yml and build-binary-windows.yml accept workflow_dispatch with dry_run (boolean, default true) and simulate_tag (string, optional). A dry-run builds the binary, computes every delete/upload URL, prints exactly what it WOULD do, and mutates nothing. A non-empty simulate_tag (e.g. v9.9.9) makes the dry-run exercise and print the release publish path from a non-tag ref, so the release publish can be validated before just create-release is run.
  • No post-publish smoke test is added (per the issue's explicit decision). No change to create-release.yml or the just create-release bump logic.

Validation

  • oci-build/get-tags.nu exercised across all modes locally (nu 0.112.2): release -> tag=v1.2.3, latest -> tag=latest, dry-run (no sim) -> train=latest, dry-run --simulate-tag v9.9.9 -> train=release tag=v9.9.9. Missing-version and unknown-mode both exit non-zero.
  • Both workflow YAML files parse and expose push + workflow_dispatch triggers.

Acceptance criteria

  • A v* tag build publishes the binary ONLY to the <version> path; no latest binary, no manifest.
  • A main push publishes ONLY latest + latest/version-<arch>.json; no <version> path.
  • Publish mode is derived from the trigger ref type / event, not git describe.
  • Release uploads are write-once (no pre-delete; existing <version> artifact fails the build); latest retains overwrite semantics.
  • Both workflows accept workflow_dispatch with dry_run defaulting to true; dry-run prints every action and performs none.
  • simulate_tag makes the dry-run exercise and print the release publish path from a non-tag ref.
  • get-tags.nu returns a single tag for the mode; doc comment and the two workflows updated to share the logic.
  • create-release flow unchanged; latest continues to track main and is not reset to the release commit.
  • No post-publish smoke test added.

Resolves FJ-24.

🤖 Generated with Claude Code

## Summary Fully decouples the `latest` and `release` publish trains and adds a zero-mutation dry-run to both binary build workflows (FJ-24). The publish mode is now derived from the workflow trigger, not from `git describe`, so a release tag publishes only its immutable versioned package and a `main` push publishes only `latest` plus the manifest. ## What changed - `oci-build/get-tags.nu` is now a pure resolver that takes `--mode` (`release` | `latest` | `dry-run`), `--ref-name`, and `--simulate-tag`, and returns a single `{ mode, train, tag, describe }` record. It returns the single tag appropriate to the mode (`[<version>]` or `latest`) instead of `[<version>, latest]`. `git describe` is kept ONLY to fill the manifest `tag` field; it no longer decides the train. Both workflows call this one resolver so they cannot drift. - Publish mode is derived from the trigger: a `refs/tags/v*` push (`github.ref_type == tag`) maps to `release`, a `main` push maps to `latest`, and `workflow_dispatch` maps to `dry-run`. - Release (version-tag) uploads are write-once: the publish step probes the `<version>` artifact URL and fails the build on HTTP 200 instead of pre-deleting and re-uploading. No 404 window, no silent replacement of a released artifact. The `latest` train keeps delete-then-reupload (overwrite) semantics. - The latest-train manifest step is gated to the `latest` train (`if: steps.resolve.outputs.train == 'latest'`), so a release build never writes `latest/version-<arch>.json` and never resets `latest` to the release commit. - Both `build-binary.yml` and `build-binary-windows.yml` accept `workflow_dispatch` with `dry_run` (boolean, default `true`) and `simulate_tag` (string, optional). A dry-run builds the binary, computes every delete/upload URL, prints exactly what it WOULD do, and mutates nothing. A non-empty `simulate_tag` (e.g. `v9.9.9`) makes the dry-run exercise and print the `release` publish path from a non-tag ref, so the release publish can be validated before `just create-release` is run. - No post-publish smoke test is added (per the issue's explicit decision). No change to `create-release.yml` or the `just create-release` bump logic. ## Validation - `oci-build/get-tags.nu` exercised across all modes locally (nu 0.112.2): `release` -> `tag=v1.2.3`, `latest` -> `tag=latest`, `dry-run` (no sim) -> `train=latest`, `dry-run --simulate-tag v9.9.9` -> `train=release tag=v9.9.9`. Missing-version and unknown-mode both exit non-zero. - Both workflow YAML files parse and expose `push` + `workflow_dispatch` triggers. ## Acceptance criteria - [x] A `v*` tag build publishes the binary ONLY to the `<version>` path; no `latest` binary, no manifest. - [x] A `main` push publishes ONLY `latest` + `latest/version-<arch>.json`; no `<version>` path. - [x] Publish mode is derived from the trigger ref type / event, not `git describe`. - [x] Release uploads are write-once (no pre-delete; existing `<version>` artifact fails the build); `latest` retains overwrite semantics. - [x] Both workflows accept `workflow_dispatch` with `dry_run` defaulting to `true`; dry-run prints every action and performs none. - [x] `simulate_tag` makes the dry-run exercise and print the `release` publish path from a non-tag ref. - [x] `get-tags.nu` returns a single tag for the mode; doc comment and the two workflows updated to share the logic. - [x] `create-release` flow unchanged; `latest` continues to track `main` and is not reset to the release commit. - [x] No post-publish smoke test added. Resolves FJ-24. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
ci(release): decouple latest/release publish trains and add dry-run (FJ-24)
All checks were successful
Check / fmt + clippy + build + tests (pull_request) Successful in 38s
Create release / Create release from merged PR (pull_request) Has been skipped
d678fdfbd2
Derive the publish mode from the workflow trigger instead of `git describe`: a `refs/tags/v*` push is `release`, a push to `main` is `latest`, and a `workflow_dispatch` is `dry-run`. The mode, effective train, single publish tag and `describe` (for the manifest `tag` field only) are resolved once in `oci-build/get-tags.nu` so the Linux and Windows workflows share the logic and cannot drift.

Release builds now publish ONLY the immutable `<version>` Generic Package, write-once: the publish step probes the artifact URL first and fails the build on HTTP 200 rather than pre-deleting and re-uploading, so a released artifact can never be silently replaced and there is no 404 window. Release builds no longer touch the rolling `latest` binary, and the latest-train manifest step is gated to the `latest` train so a release never resets `latest/version-<arch>.json` to the release commit.

Main pushes publish ONLY the `latest` binary plus the per-arch manifest, keeping the delete-then-reupload (overwrite) semantics the rolling tag needs.

Add `workflow_dispatch` to both workflows with `dry_run` (boolean, default true) and `simulate_tag` (string, optional) inputs. A dry-run builds the binary, computes every delete/upload URL, and prints exactly what it WOULD do while mutating nothing. A non-empty `simulate_tag` (e.g. v9.9.9) makes the dry-run exercise and print the `release` publish path from a non-tag ref, so the release publish can be validated before `just create-release` is run. No post-publish smoke test is added.

#FJ-24
David merged commit 1dcdfefe41 into main 2026-05-29 00:50:41 +02:00
David deleted branch ci/decouple-publish-trains-dry-run-fj-24 2026-05-29 00:50:41 +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
pandoras-box/forgejo-cli!23
No description provided.