Skip to content

Release pipeline: GUI bundles, dry-run, SHA256SUMS, sigstore provenance (PR 2 of 3)#6

Merged
CPerezz merged 6 commits into
mainfrom
refactor/phase-6-release-pipeline
May 7, 2026
Merged

Release pipeline: GUI bundles, dry-run, SHA256SUMS, sigstore provenance (PR 2 of 3)#6
CPerezz merged 6 commits into
mainfrom
refactor/phase-6-release-pipeline

Conversation

@CPerezz
Copy link
Copy Markdown
Owner

@CPerezz CPerezz commented May 6, 2026

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

Trigger Behavior
Tag push (v*) Builds + publishes a GitHub release. Tags with a - are marked pre-release.
Manual workflow_dispatch Builds everything, skips publish. Artifacts available for 90 days from the run summary.

Only 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-msvc

GUI — two new jobs:

  • build-gui-linux (Ubuntu) — Tauri produces .deb + .rpm + .AppImage in one pass
  • build-gui-darwin (macOS-13) — universal .dmg covering 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 / .zip
  • torpc-proxy-${VERSION}-${TARGET}.tar.gz / .zip
  • torpc-proxy-gui-${VERSION}-${TARGET}.{dmg,AppImage,deb,rpm}

Each with a .sha256 sibling. Manual dispatches use ${GITHUB_REF_NAME:-dev}.

Verification surface

  • SHA256SUMS — single aggregate file in the release. sha256sum -c SHA256SUMS from one place instead of per-asset sidecars.
  • Sigstore build provenance via actions/attest-build-provenance@v2. Independent verification: gh attestation verify <file> --owner CPerezz confirms the binary came from this repo + this CI run.
  • Pre-release detection: prerelease: ${{ contains(github.ref, '-') }} — tags like v0.1.0-rc1 get the prerelease flag, clean v0.1.0 doesn't.

CPerezz added 6 commits May 6, 2026 22:45
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.
@CPerezz CPerezz merged commit 9c6adac into main May 7, 2026
11 checks passed
@CPerezz CPerezz deleted the refactor/phase-6-release-pipeline branch May 7, 2026 05:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant