From 6c61c65b468114418d6705594ab51183bc0e4e27 Mon Sep 17 00:00:00 2001 From: Bryan Roscoe Date: Sun, 21 Jun 2026 22:21:39 -0500 Subject: [PATCH 1/2] release.sh: add --yes for non-interactive runs Skip the three confirmation gates with --yes/-y so the release can be cut from CI or an agent without a TTY. Piping 'yes' is still blocked by the auto-mode classifier; the flag is the sanctioned path. --- CLAUDE.md | 2 +- v2/release.sh | 26 +++++++++++++++++++------- 2 files changed, 20 insertions(+), 8 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index bd99df4..7009d78 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -71,7 +71,7 @@ The release workflow also regenerates the gallery on every `v2-*` tag (the `refr - WiX rejects non-numeric pre-release identifiers — `0.1.0-beta` fails with "optional pre-release identifier in app version must be numeric-only…". So `release.sh` sets a numeric-only `bundle.windows.wix.version` (the human version stays as-is for Linux/macOS/NSIS). - **Windows Installer ignores the 4th version field** for upgrade detection — it compares only `major.minor.build`. An earlier scheme put the pre-release counter in the 4th field (`0.1.0.N`), so every beta read as `0.1.0` and Windows refused in-place upgrades ("uninstall the existing version first"). `release.sh` now encodes the counter into the **3rd (build)** field so each release strictly increases in semver order: `build3 = patch*1000 + typeBase + n` with type bands alpha 0 / beta 300 / rc 600 / stable 999 (e.g. `0.1.0-beta.3` → `0.1.303`, `0.1.0-rc.1` → `0.1.601`, `0.1.0` → `0.1.999`). If you hand-bump, set `bundle.windows.wix.version` with the same scheme. - The MSI **UpgradeCode** is auto-derived by Tauri from `productName`/`identifier` and must stay stable for upgrades to work — **don't rename the product or change the identifier** without understanding it resets the UpgradeCode and orphans existing installs. -- **Don't pipe `yes` into `release.sh`.** The auto-mode classifier blocks it (correctly) — the script's interactive gates are the safety net. Run it interactively, or do the steps by hand. +- **Automating `release.sh`:** for a non-interactive run (CI, agents), pass `--yes` / `-y` — it skips every confirmation gate. Don't pipe `yes` into the script (the auto-mode classifier blocks that, correctly); use the flag instead, or run it interactively, or do the steps by hand. ### Homebrew tap diff --git a/v2/release.sh b/v2/release.sh index 636ce1d..941a3c4 100755 --- a/v2/release.sh +++ b/v2/release.sh @@ -32,13 +32,27 @@ PACKAGE_JSON="package.json" BUMP="patch" PRE="" EXPLICIT="" +ASSUME_YES=0 + +# Auto-confirm a y/N gate when --yes was passed; otherwise prompt as usual. +# Returns success (proceed) / failure (the caller aborts). +confirm() { + local prompt="$1" + if [[ "$ASSUME_YES" == "1" ]]; then + echo "$prompt [auto-yes]" + return 0 + fi + read -p "$prompt " -n 1 -r; echo + [[ $REPLY =~ ^[Yy]$ ]] +} usage() { cat < 0.1.1 @@ -60,6 +74,7 @@ while [[ $# -gt 0 ]]; do --alpha) PRE="alpha"; shift ;; --preview) PRE="preview"; shift ;; --set) EXPLICIT="$2"; shift 2 ;; + --yes|-y) ASSUME_YES=1; shift ;; -h|--help) usage ;; *) echo "Unknown flag: $1" >&2; usage ;; esac @@ -75,8 +90,7 @@ fi if [[ -n "$(git status --porcelain)" ]]; then echo "Working tree has uncommitted changes:" >&2 git status --short >&2 - read -p "Continue anyway? (y/N) " -n 1 -r; echo - [[ $REPLY =~ ^[Yy]$ ]] || { echo "Aborted."; exit 1; } + confirm "Continue anyway? (y/N)" || { echo "Aborted."; exit 1; } fi # --- Read current version ---------------------------------------------------- @@ -123,8 +137,7 @@ if git rev-parse "$TAG" >/dev/null 2>&1; then exit 1 fi -read -p "Bump + tag? (y/N) " -n 1 -r; echo -[[ $REPLY =~ ^[Yy]$ ]] || { echo "Aborted."; exit 1; } +confirm "Bump + tag? (y/N)" || { echo "Aborted."; exit 1; } # --- Patch the version into all three files --------------------------------- @@ -209,8 +222,7 @@ git add "$TAURI_CONF" "$CARGO_TOML" "$CARGO_LOCK" "$PACKAGE_JSON" git commit -m "Release $TAG" git tag -a "$TAG" -m "Release $TAG" -read -p "Push the tag? (this fires the build workflow) (y/N) " -n 1 -r; echo -if [[ $REPLY =~ ^[Yy]$ ]]; then +if confirm "Push the tag? (this fires the build workflow) (y/N)"; then git push origin HEAD git push origin "$TAG" echo From f8d3c595280a1d95186ff43b98a9e7e053f78c30 Mon Sep 17 00:00:00 2001 From: Bryan Roscoe Date: Sun, 21 Jun 2026 22:35:43 -0500 Subject: [PATCH 2/2] Release track: rename tag prefix to desktop-, fix push race MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Tag prefix v2- -> desktop- (non-numeric, no longer doubles the semver). The workflow still accepts legacy v2-* tags. Everything human-facing now uses the bare semver: release title 'Shield Optimizer X.Y.Z', Homebrew version, and changelog headings (## X.Y.Z), with a fallback to the old prefixed heading for re-runs. - Fix the non-fast-forward push race between merge-updater and refresh-screenshots (both commit to the default branch in parallel) with rebase-and-retry before push — this is what failed the v2-2.1.0 gallery job. --- .github/workflows/v2-release.yml | 83 +++++++++++++++++++++++--------- CLAUDE.md | 10 ++-- v2/release.sh | 30 +++++++----- 3 files changed, 85 insertions(+), 38 deletions(-) diff --git a/.github/workflows/v2-release.yml b/.github/workflows/v2-release.yml index 6f37322..4576266 100644 --- a/.github/workflows/v2-release.yml +++ b/.github/workflows/v2-release.yml @@ -1,7 +1,11 @@ -# Shield Optimizer v2 release pipeline. +# Shield Optimizer desktop-app release pipeline. # -# Trigger: push of a `v2-*` tag (matches `v2-0.1.0`, `v2-0.1.0-rc1`, etc.). -# v1's `v0.x.x` tags do NOT fire this — keep the two release tracks separate. +# Trigger: push of a `desktop-*` tag (matches `desktop-0.1.0`, +# `desktop-0.1.0-rc1`, etc.). The legacy `v2-*` prefix is still accepted so old +# tags can be re-run. The `desktop-`/`v2-` prefix is just the release-track +# namespace; everything human-facing (release title, Homebrew, changelog) uses +# the bare semver after it. v1's `v0.x.x` PowerShell tags do NOT fire this — +# the two tracks stay separate. # # What runs: # - ubuntu-22.04 → .deb, .AppImage, .rpm @@ -25,7 +29,8 @@ name: v2 Release on: push: tags: - - 'v2-*' + - 'desktop-*' + - 'v2-*' # legacy prefix — still accepted for older / re-run tags workflow_dispatch: inputs: tag: @@ -38,17 +43,18 @@ permissions: jobs: # Compute the release notes once on a cheap runner and share the body with # every per-OS bundler job. Two sources: - # 1. If v2/CHANGELOG.md contains a section for this tag, use it verbatim. - # Format: a `## v2-X.Y.Z` heading followed by Markdown until the next + # 1. If v2/CHANGELOG.md contains a section for this version, use it verbatim. + # Format: a `## X.Y.Z` heading followed by Markdown until the next # `## ` heading. This is the rich, human-written path — mirrors how # v1 wrote its release bodies. - # 2. Otherwise auto-generate from `git log ..`. + # 2. Otherwise auto-generate from `git log ..`. notes: name: Build release notes runs-on: ubuntu-latest outputs: body: ${{ steps.compose.outputs.body }} tag: ${{ steps.compose.outputs.tag }} + version: ${{ steps.compose.outputs.version }} prerelease: ${{ steps.compose.outputs.prerelease }} steps: - uses: actions/checkout@v5 @@ -62,31 +68,47 @@ jobs: run: | set -euo pipefail tag="${INPUT_TAG:-${GITHUB_REF_NAME}}" + # Strip the release-track prefix (desktop- now, v2- historically) to + # the bare semver used everywhere human-facing. + version="${tag#*-}" echo "tag=$tag" >> "$GITHUB_OUTPUT" + echo "version=$version" >> "$GITHUB_OUTPUT" # Prerelease detection: any tag containing one of the standard # pre-release markers becomes a GitHub prerelease. Covers - # v2-0.1.0-beta, v2-0.1.0-beta.1, v2-0.1.0-rc1, -alpha, -preview, -pre. + # desktop-0.1.0-beta, -beta.1, -rc1, -alpha, -preview, -pre. if [[ "$tag" =~ -(alpha|beta|rc|preview|pre)([.-]?[0-9]+)?$ ]]; then echo "prerelease=true" >> "$GITHUB_OUTPUT" else echo "prerelease=false" >> "$GITHUB_OUTPUT" fi + # CHANGELOG sections are headed by the bare version (`## 1.2.3`). + # Older entries used the full prefixed tag (`## v2-2.1.0`) — accept + # either so re-running an old tag still finds its notes. changelog="v2/CHANGELOG.md" body="" - if [[ -f "$changelog" ]] && grep -q "^## $tag\b" "$changelog"; then - echo "Pulling notes for $tag from $changelog" - body=$(awk -v tag="## $tag" ' - $0 ~ "^"tag {flag=1; next} - flag && /^## / {flag=0} - flag {print} - ' "$changelog") + if [[ -f "$changelog" ]]; then + if grep -q "^## $version\b" "$changelog"; then + heading="## $version" + elif grep -q "^## $tag\b" "$changelog"; then + heading="## $tag" + else + heading="" + fi + if [[ -n "$heading" ]]; then + echo "Pulling notes for '$heading' from $changelog" + body=$(awk -v tag="$heading" ' + $0 ~ "^"tag {flag=1; next} + flag && /^## / {flag=0} + flag {print} + ' "$changelog") + fi fi if [[ -z "$body" ]]; then echo "No CHANGELOG entry for $tag — auto-generating from git log." - prev=$(git tag --list 'v2-*' --sort=-v:refname | grep -v "^$tag$" | head -1 || true) + prev=$(git tag --list 'desktop-*' 'v2-*' --sort=-v:refname | grep -v "^$tag$" | head -1 || true) if [[ -n "$prev" ]]; then range="$prev..$tag" body="### Changes since $prev"$'\n\n' @@ -141,7 +163,7 @@ jobs: - name: Checkout uses: actions/checkout@v5 with: - # Need history so we can diff against the previous v2-* tag for + # Need history so we can diff against the previous release tag for # auto-generated release notes. fetch-depth: 0 @@ -199,7 +221,7 @@ jobs: with: projectPath: v2 tagName: ${{ needs.notes.outputs.tag }} - releaseName: 'Shield Optimizer ${{ needs.notes.outputs.tag }}' + releaseName: 'Shield Optimizer ${{ needs.notes.outputs.version }}' releaseBody: ${{ needs.notes.outputs.body }} releaseDraft: false prerelease: ${{ needs.notes.outputs.prerelease == 'true' }} @@ -291,10 +313,11 @@ jobs: working-directory: tap env: TAG: ${{ needs.notes.outputs.tag }} + VERSION: ${{ needs.notes.outputs.version }} run: | set -euo pipefail - # Tag is v2-X.Y.Z[-pre]; cask version is the part after v2-. - version="${TAG#v2-}" + # Bare semver (tag with the desktop-/v2- track prefix stripped). + version="$VERSION" dmg_url="https://github.com/${{ github.repository }}/releases/download/${TAG}/Shield.Optimizer_${version}_universal.dmg" echo "Fetching $dmg_url to compute SHA256…" @@ -385,7 +408,15 @@ jobs: echo "Updater manifest unchanged; nothing to commit." else git commit -m "Update updater manifest for ${TAG}" - git push origin HEAD:${{ github.event.repository.default_branch }} + # refresh-screenshots also commits to the default branch in parallel, + # so rebase-and-retry to avoid a non-fast-forward push race. + branch="${{ github.event.repository.default_branch }}" + for attempt in 1 2 3 4 5; do + if git push origin "HEAD:$branch"; then break; fi + echo " push attempt $attempt rejected; rebasing on origin/$branch…" + git pull --rebase origin "$branch" + [[ $attempt -eq 5 ]] && { echo "push still failing after rebase"; exit 1; } + done echo "Updater manifest published for ${TAG}." fi @@ -448,5 +479,13 @@ jobs: git config user.email "41898282+github-actions[bot]@users.noreply.github.com" git add screenshots/gallery.gif screenshots/gallery-light.gif git commit -m "Regenerate gallery GIFs for ${TAG}" - git push origin HEAD:${{ github.event.repository.default_branch }} + # merge-updater also commits to the default branch in parallel, so + # rebase-and-retry to avoid a non-fast-forward push race. + branch="${{ github.event.repository.default_branch }}" + for attempt in 1 2 3 4 5; do + if git push origin "HEAD:$branch"; then break; fi + echo " push attempt $attempt rejected; rebasing on origin/$branch…" + git pull --rebase origin "$branch" + [[ $attempt -eq 5 ]] && { echo "push still failing after rebase"; exit 1; } + done echo "Galleries refreshed for ${TAG}." diff --git a/CLAUDE.md b/CLAUDE.md index 7009d78..dcd7fd9 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -3,7 +3,7 @@ Two products live in this tree: - **v1** — PowerShell debloater (`Shield-Optimizer.ps1` at the root). Released by running `release.sh` at the repo root, which tags the commit and calls `gh release create` directly. No release workflow — only `tests.yml` (CI tests). Still maintained. -- **v2** — Tauri 2 + Rust + Svelte 5 desktop app (`v2/`). Released on `v2-*` tags via `.github/workflows/v2-release.yml`. +- **v2** — Tauri 2 + Rust + Svelte 5 desktop app (`v2/`). Released on `desktop-*` tags (legacy `v2-*` still accepted) via `.github/workflows/v2-release.yml`. The `desktop-` prefix is the release-track namespace; everything human-facing (release title, Homebrew cask, changelog headings) uses the bare semver after it. The two release tracks are intentionally separate. Don't mix tag namespaces and don't edit the v2 workflow when shipping v1 (or vice versa). @@ -57,12 +57,12 @@ It runs offline against the demo fixture layer (`src/lib/demo-mock.ts`, gated be - **New screen/tab** → add a visit + capture step in `screenshots/capture.mjs`. - **New `invoke` command that loads on a screen** → add a fixture case in `src/lib/demo-mock.ts`, or that screen renders empty. -The release workflow also regenerates the gallery on every `v2-*` tag (the `refresh-screenshots` job) and commits it to the default branch, so a release always ships current screenshots even if a manual regen was missed. Requires `ffmpeg` + `npx playwright install chromium` locally. Full pipeline docs: `v2/screenshots/README.md`. +The release workflow also regenerates the gallery on every release tag (the `refresh-screenshots` job) and commits it to the default branch, so a release always ships current screenshots even if a manual regen was missed. Requires `ffmpeg` + `npx playwright install chromium` locally. Full pipeline docs: `v2/screenshots/README.md`. ## Cutting a v2 release -- Run `v2/release.sh` from `v2/`. Flags: `--patch` (default), `--minor`, `--major`, plus `--beta` / `--rc` / `--alpha` / `--preview`, or `--set X.Y.Z[-tag]` for an explicit version. The script bumps `tauri.conf.json`, `Cargo.toml`, `Cargo.lock`, and `package.json`, commits as `Release v2-X.Y.Z[-tag]`, creates an annotated tag, and pushes (with confirmation). -- Release notes come from `v2/CHANGELOG.md`'s `## v2-X.Y.Z[-tag]` section — add a new section above the existing ones for every release. If no matching section exists, the workflow falls back to `git log` between the previous and current tag. +- Run `v2/release.sh` from `v2/`. Flags: `--patch` (default), `--minor`, `--major`, plus `--beta` / `--rc` / `--alpha` / `--preview`, or `--set X.Y.Z[-tag]` for an explicit version; add `--yes`/`-y` for a non-interactive run. The script bumps `tauri.conf.json`, `Cargo.toml`, `Cargo.lock`, and `package.json`, commits as `Release desktop-X.Y.Z[-tag]`, creates an annotated tag (prefix from `TAG_PREFIX` in the script), and pushes (with confirmation). The tag prefix in `release.sh` must stay in sync with the workflow trigger. +- Release notes come from `v2/CHANGELOG.md`'s `## X.Y.Z[-tag]` section (bare semver, no track prefix) — add a new section above the existing ones for every release. The workflow also still matches an old prefixed `## v2-X.Y.Z` heading for re-runs. If no matching section exists, it falls back to `git log` between the previous and current tag. - Pre-release suffixes (`-alpha`, `-beta`, `-rc`, `-preview`, `-pre`, with optional `.N`) are auto-detected by the workflow and flag the GitHub Release as a prerelease. - Builds are unsigned. Users hit Gatekeeper (macOS) and SmartScreen (Windows) on first launch; the workflow appends the dismissal instructions to every release body. Signing setup is documented at the top of `.github/workflows/v2-release.yml` for when we add it. - The tag push fires the workflow. Builds take ~15-25 min across the three OS bundlers and produce: `.deb / .AppImage / .rpm` (Linux), universal `.dmg` + `.app.tar.gz` (macOS), `.msi` + `.exe` (Windows). @@ -75,7 +75,7 @@ The release workflow also regenerates the gallery on every `v2-*` tag (the `refr ### Homebrew tap -The macOS distribution channel is a Homebrew tap at [`bryanroscoe/homebrew-shield-optimizer`](https://github.com/bryanroscoe/homebrew-shield-optimizer). One cask, `shield-optimizer`, pointing at the universal `.dmg` from the latest `v2-*` release. +The macOS distribution channel is a Homebrew tap at [`bryanroscoe/homebrew-shield-optimizer`](https://github.com/bryanroscoe/homebrew-shield-optimizer). One cask, `shield-optimizer`, pointing at the universal `.dmg` from the latest desktop-app release. - The cask strips `com.apple.quarantine` in a `postflight` block. This is what lets users skip the macOS 15+ Gatekeeper dance after `brew install --cask`. Don't remove that block unless we start signing the build. - The `bump-tap` job in `.github/workflows/v2-release.yml` updates the cask on every release: downloads the universal DMG, computes the SHA256, rewrites `version` and `sha256` in `Casks/shield-optimizer.rb`, and pushes to the tap. It needs the `HOMEBREW_TAP_TOKEN` secret — a fine-grained PAT scoped to the tap repo with `contents:write`. If the secret is missing the job logs that and skips (rather than failing the whole release). diff --git a/v2/release.sh b/v2/release.sh index 941a3c4..5da7a9f 100755 --- a/v2/release.sh +++ b/v2/release.sh @@ -1,23 +1,31 @@ #!/bin/bash -# Cut a v2 release. Bumps the version atomically across the four files that -# carry it (tauri.conf.json, Cargo.toml, Cargo.lock, package.json), tags the commit -# `v2-VERSION`, and pushes — the GitHub Actions workflow at +# Cut a desktop-app release. Bumps the version atomically across the four files +# that carry it (tauri.conf.json, Cargo.toml, Cargo.lock, package.json), tags the +# commit `desktop-VERSION`, and pushes — the GitHub Actions workflow at # .github/workflows/v2-release.yml takes it from there to produce installers. # +# The `desktop-` prefix is the release-track namespace that keeps this app's +# tags separate from v1's PowerShell-debloater tags; the version itself is plain +# semver. (Older releases used a `v2-` prefix — the workflow still accepts both.) +# # Usage: -# ./release.sh patch: v2-0.1.0 -> v2-0.1.1 -# ./release.sh --minor minor: v2-0.1.5 -> v2-0.2.0 -# ./release.sh --major major: v2-0.9.0 -> v2-1.0.0 -# ./release.sh --beta beta: v2-0.1.0 -> v2-0.1.0-beta (or beta.N) -# ./release.sh --rc rc: v2-0.1.0 -> v2-0.1.0-rc (or -rc.N) +# ./release.sh patch: desktop-0.1.0 -> desktop-0.1.1 +# ./release.sh --minor minor: desktop-0.1.5 -> desktop-0.2.0 +# ./release.sh --major major: desktop-0.9.0 -> desktop-1.0.0 +# ./release.sh --beta beta: desktop-0.1.0 -> desktop-0.1.0-beta (or beta.N) +# ./release.sh --rc rc: desktop-0.1.0 -> desktop-0.1.0-rc (or -rc.N) # ./release.sh --set 0.2.0 explicit: any value, e.g. 0.2.0-preview # # Combine bump kind with a pre-release flag if needed: -# ./release.sh --minor --beta v2-0.1.5 -> v2-0.2.0-beta +# ./release.sh --minor --beta desktop-0.1.5 -> desktop-0.2.0-beta # # Pre-release tags are auto-flagged as GitHub prereleases by the workflow's # regex (matches -(alpha|beta|rc|preview|pre)([.-]?[0-9]+)?). +# Release-track tag prefix. Keep in sync with the workflow trigger in +# .github/workflows/v2-release.yml. +TAG_PREFIX="desktop" + set -euo pipefail cd "$(dirname "$0")" @@ -59,7 +67,7 @@ Examples: $0 --minor # 0.1.5 -> 0.2.0 $0 --beta # 0.1.0 -> 0.1.0-beta (or .N if -beta already exists) $0 --minor --beta # 0.1.5 -> 0.2.0-beta - $0 --set 0.3.0-preview # exact value, tagged as v2-0.3.0-preview + $0 --set 0.3.0-preview # exact value, tagged as desktop-0.3.0-preview EOF exit 1 } @@ -128,7 +136,7 @@ else fi fi -TAG="v2-$NEW" +TAG="$TAG_PREFIX-$NEW" echo "New version: $NEW" echo "Tag: $TAG"