Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
83 changes: 61 additions & 22 deletions .github/workflows/v2-release.yml
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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:
Expand All @@ -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 <prev-v2-tag>..<this-tag>`.
# 2. Otherwise auto-generate from `git log <prev-tag>..<this-tag>`.
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
Expand All @@ -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'
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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' }}
Expand Down Expand Up @@ -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…"
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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}."
12 changes: 6 additions & 6 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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).

Expand Down Expand Up @@ -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).
Expand All @@ -71,11 +71,11 @@ 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

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).
Expand Down
56 changes: 38 additions & 18 deletions v2/release.sh
Original file line number Diff line number Diff line change
@@ -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")"
Expand All @@ -32,20 +40,34 @@ 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 <<EOF
Usage: $0 [--major|--minor|--patch] [--beta|--rc|--alpha] [--set X.Y.Z]
Usage: $0 [--major|--minor|--patch] [--beta|--rc|--alpha] [--set X.Y.Z] [--yes]
bump kind: --patch (default) | --minor | --major
pre-release: --beta | --rc | --alpha | --preview
explicit: --set X.Y.Z[-tag] (overrides bump kind, used verbatim)
--yes, -y: skip all confirmation prompts (non-interactive / CI use)

Examples:
$0 # 0.1.0 -> 0.1.1
$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
}
Expand All @@ -60,6 +82,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
Expand All @@ -75,8 +98,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 ----------------------------------------------------
Expand Down Expand Up @@ -114,7 +136,7 @@ else
fi
fi

TAG="v2-$NEW"
TAG="$TAG_PREFIX-$NEW"
echo "New version: $NEW"
echo "Tag: $TAG"

Expand All @@ -123,8 +145,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 ---------------------------------

Expand Down Expand Up @@ -209,8 +230,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
Expand Down
Loading