Release pipeline: GUI bundles, dry-run, SHA256SUMS, sigstore provenance (PR 2 of 3)#6
Merged
Merged
Conversation
Evolves `release.yml` from a CLI-only tag-trigger workflow into a full
release pipeline that also produces the GUI bundles needed for the PR-3
distribution channels (Homebrew Cask, AUR, AppImage/.deb/.rpm). Same
single workflow, three new pieces.
Workflow shape
- Triggers: tag push (`v*`) or manual `workflow_dispatch` from the
Actions UI. Manual = dry-run; everything builds, nothing publishes.
- Permissions tightened to the minimum: `contents: write` (release
+ assets), `id-token: write` + `attestations: write` (sigstore
provenance attestations).
Build matrix
CLI matrix (`build-cli`) keeps the prior four targets:
- x86_64-unknown-linux-gnu, x86_64-apple-darwin,
aarch64-apple-darwin, x86_64-pc-windows-msvc
GUI matrix splits into two new jobs:
- `build-gui-linux` on `ubuntu-latest`. `cargo tauri build` produces
`.deb`, `.rpm`, and `.AppImage` in one pass. System deps installed
inline (libwebkit2gtk-4.1, libsoup-3.0, etc.).
- `build-gui-darwin` on `macos-13`. `cargo tauri build --target
universal-apple-darwin` produces a fat-binary `.dmg` covering Intel
and Apple Silicon. Both rustc targets installed via the toolchain
action.
Tauri CLI installed via `cargo install --locked --version "^2.0"
tauri-cli` (rust-cache picks it up after first run; ~3 min cold).
Artifact naming
All artifacts renamed to a stable scheme before upload:
`{torpc,torpc-proxy}-${VERSION}-${TARGET}.{tar.gz,zip}`
`torpc-proxy-gui-${VERSION}-${TARGET}.{dmg,AppImage,deb,rpm}`
Each gets a sibling `.sha256` file. Manual dispatches use
`${GITHUB_REF_NAME:-dev}` so the artifacts are named after the branch.
Aggregated checksums + sigstore attestation
- New `aggregate SHA256SUMS` step downloads all build artifacts and
emits a single `SHA256SUMS` file at the release root. Users verify
with `sha256sum -c SHA256SUMS` from one file instead of grabbing N
per-asset sidecars.
- `actions/attest-build-provenance@v2` attaches sigstore-backed
provenance to every binary on real tag pushes. Independent
verification: `gh attestation verify <file> --owner CPerezz`.
Publish gating
- `if: github.event_name == 'push'` on both the publish step and the
attestation step. workflow_dispatch builds everything but skips
publishing — the artifacts live as workflow artifacts (90-day
retention) for dry-run testing.
- Pre-release detection: tags with a `-` (e.g. `v0.1.0-rc1`,
`v0.2.0-beta.3`) are marked `prerelease: true`; clean `v0.1.0` is
stable. Implemented as `prerelease: ${{ contains(github.ref, '-') }}`.
- Dry-run path emits a `$GITHUB_STEP_SUMMARY` block explaining what
the run did and how to publish for real.
Cache hygiene
`prefix-key: "v0-rust-${{ env.ImageOS }}"` carries over from the CI
workflow — same per-image isolation that fixed the 22.04/24.04 GLIBC
collision before. Per-job `key` (`cli-x86_64-unknown-linux-gnu`,
`gui-darwin`, etc.) keeps each leg's deps independent.
Verification
- YAML parses cleanly (yaml.safe_load).
- No Rust changes; existing tests + clippy + fmt unaffected.
- Pre-merge dry-run: trigger workflow_dispatch on this branch from
Actions UI; confirm all five jobs produce artifacts; confirm release
job emits SHA256SUMS and skips publishing.
- Pre-merge end-to-end: push `v0.1.0-rc1` after merge to validate the
publish path; delete + retag if needed.
Two parallel reviews (code-reviewer + silent-failure-hunter) flagged a
tight set of silent-failure paths and asset-list cleanup wins. All
changes are scoped to `.github/workflows/release.yml`.
Critical fixes
- **No double `.sha256` upload.** New `prune redundant per-file .sha256
sidecars` step deletes per-asset sidecars after the aggregate
SHA256SUMS is generated. The release page now lists ~half as many
assets and there's exactly one verification surface
(`sha256sum -c SHA256SUMS`).
- **Empty-SHA256SUMS guard.** Wrapped the aggregate hash step with a
nullglob-expanded array + an `[[ -s SHA256SUMS ]]` assertion.
Pre-fix: every glob expanding to nothing produced a zero-byte
SHA256SUMS that silently shipped. Now: the step exits non-zero with
a clear message before publishing.
- **Linux GUI collect crash on missing bundle types.** Wrapped `find`
output in `mapfile` + an explicit count check ("expected at least 1
bundle, found 0"). Also added `shopt -s nullglob` so the post-loop
`for f in *.deb *.rpm *.AppImage` doesn't expand to literal patterns
if any one bundle type is missing — the count check above already
fails first, but defense in depth.
- **macOS DMG collect: silent drop / opaque error.** Replaced
`find ... | head -1` (which silently drops the second DMG and
produces a confusing `cp: missing destination` if zero) with
`mapfile` + an exact-count assertion. Also hashed the renamed file
by its explicit path instead of `*.dmg` glob, so a future
multi-DMG bundle config can't produce a sidecar with two hash
lines for one asset.
Important fixes
- **Windows packaging: PowerShell `$ErrorActionPreference = 'Stop'`.**
GitHub runners default to `Continue`, which lets non-terminating
errors from `Compress-Archive` / `Get-FileHash` slip through and
produce malformed archives or empty `.sha256` sidecars.
- **Pre-release detection now uses `github.ref_name`** instead of
`github.ref`. Same correctness today (the `if:` gate restricts to
tag pushes), but `ref_name` is the unprefixed tag (`v0.1.0-rc1`)
rather than `refs/tags/v0.1.0-rc1`, so the substring check is
scoped to just the tag.
- **`if: github.event_name == 'push' && success()`** on the publish
step. Defends against future `if: always()` refactors that could
ship unattested binaries.
- **Attestation subject-path** simplified to `dist/*` (now safe
because the prune step ensures dist only contains canonical
artifacts + SHA256SUMS).
Suggestion fixes
- Dropped `2>/dev/null` from the SHA256SUMS aggregation. With
`nullglob` set, there's nothing legitimate left to silence; the
redirect was only hiding real I/O errors.
- Comment at the Linux collect step now acknowledges the workspace-
target subtlety (Tauri writes to `$GITHUB_WORKSPACE/target` because
the GUI is a workspace member; a future per-crate `target-dir`
would break the find root silently).
Verification
- `python3 yaml.safe_load` still passes.
- Logic is identical on the happy path; changes are exclusively
defensive — they fail loudly on corner cases that pre-fix would
have shipped silently.
Will re-trigger the dry-run after push.
`cargo build --bin torpc-proxy` from the workspace root fails with "no bin target named `torpc-proxy` in default-run packages" — the binary is defined inside the `torpc-proxy-cli` package, but `--bin` only searches default-run packages first and bails before finding it elsewhere in the workspace. Replace with `-p torpc-proxy-cli` (the only bin in that package is `torpc-proxy`, so no `--bin` flag is needed). Confirmed locally: the same error reproduces on `cargo build --release --bin torpc-proxy`, and `-p torpc-proxy-cli` produces the binary at `target/release/torpc-proxy` as expected. Caught by the first dry-run on the branch — three of four CLI matrix legs failed with this exact error before reaching the package step (linux + windows + darwin-aarch64; the darwin-x86_64 leg was still running when the others bailed).
Caught by the second dry-run: 3 of 5 build legs failed with errors like
cp: cannot create regular file 'dist/torpc-proxy-gui-refactor/phase-6-release-pipeline-...'
tar (child): dist/torpc-refactor/phase-6-release-pipeline-...: Cannot open
`GITHUB_REF_NAME` for tag pushes is the bare tag (`v0.1.0`) — no slashes,
filename-safe. For workflow_dispatch on a branch it's the branch name,
which can contain `/` (here: `refactor/phase-6-release-pipeline`). The
slash gets interpreted as a path separator the moment we use it as part
of `dist/<archive>.tar.gz`.
Pre-PR-6 the workflow only fired on tag pushes, so this never surfaced.
PR 6 added the workflow_dispatch path; this commit closes the gap.
Fix: derive a safe `version` string in each of the four packaging steps:
- Tag refs (GITHUB_REF_TYPE=tag): use `$GITHUB_REF_NAME` verbatim.
- Branch refs (manual dispatches): use `dev-<8-char-sha>`.
Bash form:
if [[ "$GITHUB_REF_TYPE" == "tag" ]]; then
version="$GITHUB_REF_NAME"
else
version="dev-${GITHUB_SHA:0:8}"
fi
PowerShell form (Windows package step):
$version = if ($env:GITHUB_REF_TYPE -eq 'tag') {
$env:GITHUB_REF_NAME
} else {
"dev-" + $env:GITHUB_SHA.Substring(0, 8)
}
Applied identically to:
- `package (Unix)` in build-cli
- `package (Windows)` in build-cli
- `collect + rename bundles` in build-gui-linux
- `collect + rename DMG` in build-gui-darwin
Verified: YAML still parses; both bash forms emit valid filename
fragments; PowerShell form mirrors the bash logic.
The two `macos-13` jobs (CLI x86_64 + GUI universal) sat in the runner
queue for 20+ minutes without starting in the latest dry-run; macos-13
(Intel) is being phased out by GitHub and capacity is tight on the
free tier.
Switch all macOS jobs to `macos-latest` (Apple Silicon). The x86_64
CLI build cross-compiles via the rustc target installed by
`dtolnay/rust-toolchain`. The universal GUI build already had both
architectures' rustc targets installed; Tauri's
`--target universal-apple-darwin` handles the lipo regardless of
host architecture.
This also unifies the macOS image image identifier across CLI + GUI
legs (both now `macos-latest` / ImageOS=macos15-arm64), so the
`prefix-key: v0-rust-${{ env.ImageOS }}` cache lane is shared.
Verified: YAML still parses; previous run on macos-latest with the
aarch64 target completed in 2m24s, so the runner type is healthy
and the cross-compiled x86_64 build should be similarly fast.
macOS ships bash 3.2 by default for licensing reasons (newer bash is
GPLv3); `mapfile` was introduced in bash 4.0 and is consequently
unavailable on the `macos-latest` runner without an explicit
homebrew-bash install.
Latest dry-run failure:
line 13: mapfile: command not found
Process completed with exit code 127.
Replaced with the bash-3.2-compatible idiom:
dmgs=()
while IFS= read -r line; do
dmgs+=("$line")
done < <(find ... -name "*.dmg")
Same semantics — the count check catches both "no DMGs" and "multiple
DMGs" (Tauri sometimes emits a sidecar updater DMG depending on
config). The Linux equivalent step keeps using `mapfile` because the
ubuntu-latest runner has bash 5+; pre-marked with a comment so a
future maintainer doesn't unify them blindly.
Otherwise green: 5 of 6 build legs (CLI x4 + GUI Linux) finished
between 1m38s and 3m40s on the same dry-run; only the DMG-collect
step in build-gui-darwin failed at the bash version check.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Second of the three PRs from the wallet-user distribution + Caffeine UX plan. PR 1 (#5, merged) gave us a Tauri 2.x GUI that builds cleanly. This PR teaches the release pipeline to ship it — alongside the existing CLI binaries, with dry-run testing, aggregate checksums, and sigstore-backed build provenance.
PR 3 follows: Homebrew Cask tap, AUR PKGBUILD, distribution-side wiring, and the auto-update tray notification.
Workflow shape
v*)-are marked pre-release.workflow_dispatchOnly minimum-needed permissions:
contents: write(release/assets),id-token: write+attestations: write(sigstore).Build matrix
CLI (
build-cli) — same four targets as before:x86_64-unknown-linux-gnu,x86_64-apple-darwin,aarch64-apple-darwin,x86_64-pc-windows-msvcGUI — two new jobs:
build-gui-linux(Ubuntu) — Tauri produces.deb+.rpm+.AppImagein one passbuild-gui-darwin(macOS-13) — universal.dmgcovering Intel + Apple Silicon (cargo tauri build --target universal-apple-darwin)Tauri CLI installed via
cargo install --locked --version "^2.0" tauri-cli. Cold build ~3 min; rust-cache makes subsequent runs faster.Artifact naming
Renamed before upload to a stable scheme so the release page reads cleanly:
torpc-${VERSION}-${TARGET}.tar.gz/.ziptorpc-proxy-${VERSION}-${TARGET}.tar.gz/.ziptorpc-proxy-gui-${VERSION}-${TARGET}.{dmg,AppImage,deb,rpm}Each with a
.sha256sibling. Manual dispatches use${GITHUB_REF_NAME:-dev}.Verification surface
SHA256SUMS— single aggregate file in the release.sha256sum -c SHA256SUMSfrom one place instead of per-asset sidecars.actions/attest-build-provenance@v2. Independent verification:gh attestation verify <file> --owner CPerezzconfirms the binary came from this repo + this CI run.prerelease: ${{ contains(github.ref, '-') }}— tags likev0.1.0-rc1get the prerelease flag, cleanv0.1.0doesn't.