diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 8c3fd0b..fa63c99 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,13 +1,16 @@ name: Release -# A pushed tag publishes the release that every client updates from: legacy -# Win32 clients (v0.5.0 and earlier) through the GitHub API's +# A pushed plain vX.Y.Z tag publishes the release that every client updates +# from: legacy Win32 clients (v0.5.0 and earlier) through the GitHub API's # `/releases/latest`, Tauri clients through its `latest.json` asset. A # dispatch run builds the same assets into a draft, which neither reaches. +# Suffixed tags (vX.Y.Z-suffix) are dispatch-only: both client comparators +# ignore the suffix, so publishing one would ship an rc as a stable update +# and hide the final vX.Y.Z behind it. on: push: - tags: ['v*.*.*'] + tags: ['v[0-9]+.[0-9]+.[0-9]+'] workflow_dispatch: inputs: tag: @@ -43,6 +46,10 @@ jobs: echo "::error::Tag '$tag' is not of the form vX.Y.Z or vX.Y.Z-suffix." exit 1 fi + if [ "$GITHUB_EVENT_NAME" != workflow_dispatch ] && [[ "$tag" == *-* ]]; then + echo "::error::Tag '$tag' has a suffix; suffixed tags are for draft dispatch runs only and are never published." + exit 1 + fi expected="${tag#v}" cargo_version=$(sed -nE 's/^version[[:space:]]*=[[:space:]]*"([^"]+)".*/\1/p' Cargo.toml | head -n 1) if [ "$cargo_version" != "$expected" ]; then diff --git a/docs/release-process.md b/docs/release-process.md index 9324563..55214ad 100644 --- a/docs/release-process.md +++ b/docs/release-process.md @@ -1,7 +1,9 @@ # Cutting a release -The `release.yml` workflow builds and publishes on every pushed tag -matching `v*.*.*`. The workflow asserts that the pushed tag matches +The `release.yml` workflow builds and publishes on every pushed plain +`vX.Y.Z` tag. Pushing a tag with a suffix (`vX.Y.Z-rc1`) publishes +nothing: the updaters ignore suffixes, so an rc would reach every user +as a stable release. The workflow asserts that the pushed tag matches `Cargo.toml` `version` and **fails fast on mismatch**, so the order below matters: bump Cargo *before* you tag. @@ -31,7 +33,9 @@ below matters: bump Cargo *before* you tag. ## Testing without a real tag Use the workflow's `workflow_dispatch` input with a throwaway tag -like `v0.0.0-test`. The release is created as a **draft**, so it +that carries a suffix, like `v0.0.0-test`. The suffix keeps the tag +push from starting a publishing run; never use a plain `vX.Y.Z` tag for +a dry run, because pushing it publishes a release marked latest. The release is created as a **draft**, so it does not show up on the public Releases feed or trigger self-updates for users. The tag must already exist and point at a commit whose `Cargo.toml` diff --git a/plans/261021-1028-tauri-v2-port/phase-10-ci-release.md b/plans/261021-1028-tauri-v2-port/phase-10-ci-release.md index 61a63d3..8e97f99 100644 --- a/plans/261021-1028-tauri-v2-port/phase-10-ci-release.md +++ b/plans/261021-1028-tauri-v2-port/phase-10-ci-release.md @@ -150,6 +150,7 @@ host has no patchelf and no display. - **`make-latest-json.js` scope.** Besides the listed guards it requires the exact asset set (any unexpected file fails), so a stray upload cannot slip into a release. It writes `/latest.json` and ignores an existing one, so a re-run is safe. - **yamllint.** The repo had no yamllint config, and the default rules reject every GitHub workflow (`on:` as a truthy key, no `---`, 80-column lines), `ci.yml` included. `.yamllint.yaml` relaxes exactly those three rules (line length 140), so the check is reproducible. - **Draft dry-run tag.** The tag check also runs for dispatch, so `v0.0.0-test` must point at a commit whose `Cargo.toml` says `0.0.0-test`; `docs/release-process.md` says so. +- **Suffixed tags never publish.** The push trigger matches only plain `vX.Y.Z`, and `prepare` also fails a non-dispatch run on a suffixed tag. Before this, pushing `v0.0.0-test` (or an rc tag) started a run that published it with `--latest`, and both client comparators ignore the suffix. ## Residual risk @@ -179,7 +180,7 @@ script, not against a real draft. | A missing secret silently produces an unsigned build | L x H | The signing step fails hard when the env var is empty. | | An old client picks the wrong asset | L x H | Exactly one `*.exe` asset, enforced by `make-latest-json.js`. | | The Tauri release is not the one `/releases/latest` returns | L x H | Tag runs publish non-draft, non-prerelease, `--latest`; check with `gh api repos/tiennm99/claude-code-usage-bubble/releases/latest -q .tag_name` after publishing. | -| A draft dry run reaches users | L x H | `/releases/latest` skips drafts; both drafts are deleted after inspection. | +| A draft dry run reaches users | L x H | `/releases/latest` skips drafts; the dry-run tag carries a suffix, which the push trigger and `prepare` refuse to publish; both drafts are deleted after inspection. | ## Rollback