From 46c2bffa0ec1a0e11d51dd0936af4d7b61c1c67d Mon Sep 17 00:00:00 2001 From: CPerezz Date: Wed, 6 May 2026 22:45:45 +0200 Subject: [PATCH 1/6] Release pipeline: GUI bundles, dry-run, SHA256SUMS, sigstore provenance MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --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. --- .github/workflows/release.yml | 218 +++++++++++++++++++++++++++++----- 1 file changed, 187 insertions(+), 31 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 049553b..d36e283 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,38 +1,38 @@ name: Release +# Triggered automatically by tag pushes (`v0.1.0`, `v0.1.0-rc1`, etc.) or +# manually via "Run workflow" for build-only dry-runs that exercise every +# matrix leg without creating a real GitHub release. on: push: - tags: - - 'v*' + tags: ['v*'] + workflow_dispatch: -# Need write access to create the release and upload assets. permissions: - contents: write + contents: write # creating the release + uploading assets + id-token: write # sigstore attestation OIDC token + attestations: write # actions/attest-build-provenance API jobs: - build: - name: build (${{ matrix.target }}) + # ------------------------------------------------------------------------- + # CLI binaries — `torpc` (daemon) + `torpc-proxy` (client). + # Four targets, the same set as before. No GUI here; that's its own job. + # ------------------------------------------------------------------------- + build-cli: + name: cli (${{ matrix.target }}) runs-on: ${{ matrix.os }} strategy: fail-fast: false matrix: include: - # Linux x86_64 - os: ubuntu-latest target: x86_64-unknown-linux-gnu - archive_ext: tar.gz - # macOS Intel - os: macos-13 target: x86_64-apple-darwin - archive_ext: tar.gz - # macOS Apple Silicon - os: macos-latest target: aarch64-apple-darwin - archive_ext: tar.gz - # Windows x86_64 - os: windows-latest target: x86_64-pc-windows-msvc - archive_ext: zip steps: - uses: actions/checkout@v4 @@ -43,20 +43,17 @@ jobs: - uses: Swatinem/rust-cache@v2 with: - key: ${{ matrix.target }} + prefix-key: "v0-rust-${{ env.ImageOS }}" + key: cli-${{ matrix.target }} - # Build the daemon and the CLI client. The Tauri GUI is intentionally - # excluded from the release for now — bundling needs platform-specific - # post-build steps (DMG, AppImage, MSI) that we'll add when there's - # demand. Source build of the GUI is documented in the README. - name: build daemon + CLI client run: | cargo build --release --target ${{ matrix.target }} --bin torpc cargo build --release --target ${{ matrix.target }} --bin torpc-proxy - # Pack each binary into the platform-conventional archive. macOS and - # Linux get tar.gz; Windows gets zip. Strip is implicit via the - # workspace [profile.release] `strip = true` setting. + # Pack the two CLI binaries into the platform-conventional archive. + # `strip = true` in `[profile.release]` already shrinks them; we just + # tarball/zip and emit a sibling `.sha256` for each. - name: package (Unix) if: runner.os != 'Windows' shell: bash @@ -84,16 +81,134 @@ jobs: } Get-ChildItem dist - - name: upload artifacts - uses: actions/upload-artifact@v4 + - uses: actions/upload-artifact@v4 with: - name: dist-${{ matrix.target }} + name: cli-${{ matrix.target }} path: dist/* if-no-files-found: error + # ------------------------------------------------------------------------- + # GUI bundle — Linux. Tauri's bundler produces .deb + .rpm + .AppImage in + # one pass via `cargo tauri build`. Renamed to a stable + # `torpc-proxy-gui--.` scheme before upload so the + # release-page asset list reads cleanly. + # ------------------------------------------------------------------------- + build-gui-linux: + name: gui (x86_64-unknown-linux-gnu) + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: dtolnay/rust-toolchain@stable + + - uses: Swatinem/rust-cache@v2 + with: + prefix-key: "v0-rust-${{ env.ImageOS }}" + key: gui-linux + + - name: install Tauri 2.x system deps + run: | + sudo apt-get update + sudo apt-get install -y \ + libwebkit2gtk-4.1-dev \ + libgtk-3-dev \ + libayatana-appindicator3-dev \ + librsvg2-dev \ + libsoup-3.0-dev \ + libjavascriptcoregtk-4.1-dev + + # `cargo install tauri-cli` from source takes ~3 min; the rust-cache + # picks it up from ~/.cargo/bin on subsequent runs. There's a + # precompiled binary on GitHub releases but the install path is + # less battle-tested across platforms; trade speed for reliability + # on a workflow that fires only on tag pushes. + - name: install tauri-cli + run: cargo install --locked --version "^2.0" tauri-cli + + - name: build GUI bundles + working-directory: torpc-proxy/torpc-proxy-gui + run: cargo tauri build + + - name: collect + rename bundles + shell: bash + run: | + set -euo pipefail + mkdir -p dist + version="${GITHUB_REF_NAME:-dev}" + target="x86_64-unknown-linux-gnu" + # Tauri lays bundles under target/release/bundle/{deb,rpm,appimage}. + while IFS= read -r f; do + ext="${f##*.}" + cp "$f" "dist/torpc-proxy-gui-${version}-${target}.${ext}" + done < <(find target/release/bundle -type f \ + \( -name "*.deb" -o -name "*.rpm" -o -name "*.AppImage" \)) + (cd dist && for f in *.deb *.rpm *.AppImage; do + shasum -a 256 "$f" > "$f.sha256" + done) + ls -la dist/ + + - uses: actions/upload-artifact@v4 + with: + name: gui-linux-x86_64-unknown-linux-gnu + path: dist/* + if-no-files-found: error + + # ------------------------------------------------------------------------- + # GUI bundle — macOS universal binary. `cargo tauri build --target + # universal-apple-darwin` builds for x86_64 + aarch64 and lipo's them + # together into a single `.app` + `.dmg`. We need both architectures' + # rustc targets installed for that to work. + # ------------------------------------------------------------------------- + build-gui-darwin: + name: gui (universal-apple-darwin) + runs-on: macos-13 + steps: + - uses: actions/checkout@v4 + + - uses: dtolnay/rust-toolchain@stable + with: + targets: x86_64-apple-darwin,aarch64-apple-darwin + + - uses: Swatinem/rust-cache@v2 + with: + prefix-key: "v0-rust-${{ env.ImageOS }}" + key: gui-darwin + + - name: install tauri-cli + run: cargo install --locked --version "^2.0" tauri-cli + + - name: build universal GUI bundle + working-directory: torpc-proxy/torpc-proxy-gui + run: cargo tauri build --target universal-apple-darwin + + - name: collect + rename DMG + shell: bash + run: | + set -euo pipefail + mkdir -p dist + version="${GITHUB_REF_NAME:-dev}" + # Universal output is at target/universal-apple-darwin/release/bundle/dmg. + dmg=$(find target/universal-apple-darwin/release/bundle/dmg -name "*.dmg" | head -1) + cp "$dmg" "dist/torpc-proxy-gui-${version}-universal-apple-darwin.dmg" + (cd dist && shasum -a 256 *.dmg > "torpc-proxy-gui-${version}-universal-apple-darwin.dmg.sha256") + ls -la dist/ + + - uses: actions/upload-artifact@v4 + with: + name: gui-darwin-universal + path: dist/* + if-no-files-found: error + + # ------------------------------------------------------------------------- + # Release. Aggregates all build artifacts, generates one combined + # SHA256SUMS file, attests build provenance via sigstore, and publishes + # to GitHub Releases — but only on real tag pushes. Manual workflow + # dispatches stop after artifact aggregation so the run summary is the + # download surface for dry-run testing. + # ------------------------------------------------------------------------- release: name: publish release - needs: build + needs: [build-cli, build-gui-linux, build-gui-darwin] runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 @@ -104,15 +219,56 @@ jobs: path: dist merge-multiple: true - - name: list collected artifacts - run: ls -la dist/ + # One aggregate SHA256SUMS file makes `sha256sum -c SHA256SUMS` a + # single command for downstream verification, instead of asking + # users to grab one .sha256 per asset. + - name: aggregate SHA256SUMS + shell: bash + run: | + set -euo pipefail + cd dist + # Hash only the real artifacts; skip per-file .sha256 sidecars. + shopt -s nullglob + shasum -a 256 *.tar.gz *.zip *.dmg *.AppImage *.deb *.rpm 2>/dev/null \ + | tee SHA256SUMS + ls -la + + # Sigstore-backed build provenance. `gh attestation verify + # --owner CPerezz` lets anyone independently confirm the binary + # came from this repo + this CI run, without trusting GitHub's + # word for it. + - name: attest build provenance + if: github.event_name == 'push' + uses: actions/attest-build-provenance@v2 + with: + subject-path: | + dist/*.tar.gz + dist/*.zip + dist/*.dmg + dist/*.AppImage + dist/*.deb + dist/*.rpm - # Use the tag annotation as the release body if present, else fall back - # to a generic message. softprops/action-gh-release auto-generates a - # release on the corresponding tag. + # Tag pushes only. Pre-release detection: tags with a `-` (e.g. + # `v0.1.0-rc1`, `v0.2.0-beta.3`) are marked as prerelease in the + # GitHub release UI; clean `v0.1.0` is stable. - name: publish release + if: github.event_name == 'push' uses: softprops/action-gh-release@v2 with: files: dist/* generate_release_notes: true fail_on_unmatched_files: true + prerelease: ${{ contains(github.ref, '-') }} + + # When triggered manually (workflow_dispatch), surface a heads-up + # so the run summary makes the build-only intent obvious. + - name: dry-run summary + if: github.event_name != 'push' + run: | + echo "## Dry-run build complete" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "Triggered manually via workflow_dispatch — no GitHub release created." >> $GITHUB_STEP_SUMMARY + echo "Artifacts are attached to this run for ~90 days; download from the run summary above." >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "To publish for real, push a tag like \`v0.1.0\` (stable) or \`v0.1.0-rc1\` (pre-release)." >> $GITHUB_STEP_SUMMARY From 1b59561ad724d2efadf7bed148f2df1e883a7305 Mon Sep 17 00:00:00 2001 From: CPerezz Date: Wed, 6 May 2026 22:58:49 +0200 Subject: [PATCH 2/6] Address PR review findings on the release pipeline MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- .github/workflows/release.yml | 100 ++++++++++++++++++++++++++-------- 1 file changed, 77 insertions(+), 23 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index d36e283..6dda3b7 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -71,6 +71,12 @@ jobs: if: runner.os == 'Windows' shell: pwsh run: | + # `pwsh` on GitHub runners defaults `$ErrorActionPreference` to + # `Continue`, which lets non-terminating errors from cmdlets like + # Compress-Archive / Get-FileHash slip through and produce + # malformed archives or empty .sha256 sidecars without failing + # the step. Force `Stop` so any cmdlet error is fatal. + $ErrorActionPreference = 'Stop' New-Item -ItemType Directory -Force -Path dist | Out-Null foreach ($bin in @("torpc", "torpc-proxy")) { $archive = "${bin}-${env:GITHUB_REF_NAME}-${{ matrix.target }}.zip" @@ -133,15 +139,30 @@ jobs: shell: bash run: | set -euo pipefail + shopt -s nullglob mkdir -p dist version="${GITHUB_REF_NAME:-dev}" target="x86_64-unknown-linux-gnu" - # Tauri lays bundles under target/release/bundle/{deb,rpm,appimage}. - while IFS= read -r f; do + # Tauri's bundler writes to the WORKSPACE target dir + # (`$GITHUB_WORKSPACE/target/release/bundle/{deb,rpm,appimage}`) + # because `torpc-proxy-gui` is a workspace member. If the GUI + # ever moves to its own workspace or sets a per-crate `target-dir`, + # this find root needs to follow. + mapfile -t bundles < <(find target/release/bundle -type f \ + \( -name "*.deb" -o -name "*.rpm" -o -name "*.AppImage" \)) + if [[ ${#bundles[@]} -eq 0 ]]; then + echo "No GUI bundles produced under target/release/bundle." >&2 + echo "Tauri's bundler emitted nothing — check the build step above." >&2 + exit 1 + fi + for f in "${bundles[@]}"; do ext="${f##*.}" cp "$f" "dist/torpc-proxy-gui-${version}-${target}.${ext}" - done < <(find target/release/bundle -type f \ - \( -name "*.deb" -o -name "*.rpm" -o -name "*.AppImage" \)) + done + # `shopt -s nullglob` makes the for-loop expand to nothing rather + # than to literal patterns when an extension is missing — the + # bundles[@] count check above already guards against zero, so + # this just keeps the loop safe per-extension. (cd dist && for f in *.deb *.rpm *.AppImage; do shasum -a 256 "$f" > "$f.sha256" done) @@ -188,9 +209,20 @@ jobs: mkdir -p dist version="${GITHUB_REF_NAME:-dev}" # Universal output is at target/universal-apple-darwin/release/bundle/dmg. - dmg=$(find target/universal-apple-darwin/release/bundle/dmg -name "*.dmg" | head -1) - cp "$dmg" "dist/torpc-proxy-gui-${version}-universal-apple-darwin.dmg" - (cd dist && shasum -a 256 *.dmg > "torpc-proxy-gui-${version}-universal-apple-darwin.dmg.sha256") + # Use `mapfile` + an explicit count check so we fail loudly + # instead of silently dropping a second DMG (Tauri sometimes + # produces a sidecar updater DMG when configured) or proceeding + # with an empty path. + mapfile -t dmgs < <(find target/universal-apple-darwin/release/bundle/dmg -name "*.dmg") + if [[ ${#dmgs[@]} -ne 1 ]]; then + echo "Expected exactly 1 DMG, found ${#dmgs[@]}: ${dmgs[*]:-}" >&2 + exit 1 + fi + renamed="dist/torpc-proxy-gui-${version}-universal-apple-darwin.dmg" + cp "${dmgs[0]}" "$renamed" + # Hash the renamed file by its specific path — `*.dmg` would have + # (incorrectly) hashed every match if the count check ever loosens. + (cd dist && shasum -a 256 "$(basename "$renamed")" > "$(basename "$renamed").sha256") ls -la dist/ - uses: actions/upload-artifact@v4 @@ -222,17 +254,37 @@ jobs: # One aggregate SHA256SUMS file makes `sha256sum -c SHA256SUMS` a # single command for downstream verification, instead of asking # users to grab one .sha256 per asset. + # + # `nullglob` makes missing extensions expand to nothing (rather than + # the literal pattern, which `shasum` would reject). The post-write + # `[[ -s ]]` assertion catches the worst-case "every glob expanded + # to empty → SHA256SUMS is zero bytes → release ships unchecksummed + # binaries" path. We deliberately don't `2>/dev/null` so any real + # I/O failure surfaces. - name: aggregate SHA256SUMS shell: bash run: | set -euo pipefail cd dist - # Hash only the real artifacts; skip per-file .sha256 sidecars. shopt -s nullglob - shasum -a 256 *.tar.gz *.zip *.dmg *.AppImage *.deb *.rpm 2>/dev/null \ - | tee SHA256SUMS + artifacts=( *.tar.gz *.zip *.dmg *.AppImage *.deb *.rpm ) + if [[ ${#artifacts[@]} -eq 0 ]]; then + echo "No artifacts found in dist/ — every build leg produced nothing." >&2 + ls -la + exit 1 + fi + shasum -a 256 "${artifacts[@]}" | tee SHA256SUMS + [[ -s SHA256SUMS ]] || { echo "SHA256SUMS is empty"; exit 1; } ls -la + # The per-file `.sha256` sidecars from each build leg are now + # redundant alongside the aggregate `SHA256SUMS`. Pruning them keeps + # the GitHub release page from listing two parallel verification + # surfaces (and stops them from being signed by attest-build- + # provenance below, which only signs the canonical artifacts). + - name: prune redundant per-file .sha256 sidecars + run: rm -f dist/*.sha256 + # Sigstore-backed build provenance. `gh attestation verify # --owner CPerezz` lets anyone independently confirm the binary # came from this repo + this CI run, without trusting GitHub's @@ -241,25 +293,27 @@ jobs: if: github.event_name == 'push' uses: actions/attest-build-provenance@v2 with: - subject-path: | - dist/*.tar.gz - dist/*.zip - dist/*.dmg - dist/*.AppImage - dist/*.deb - dist/*.rpm - - # Tag pushes only. Pre-release detection: tags with a `-` (e.g. - # `v0.1.0-rc1`, `v0.2.0-beta.3`) are marked as prerelease in the - # GitHub release UI; clean `v0.1.0` is stable. + # Single brace-expanded glob — if a particular extension has + # zero matches, the action treats the missing pattern less + # forgivingly than multiple separate patterns. This expands + # client-side via the shell that interprets the input. + subject-path: dist/* + + # Tag pushes only. Pre-release detection: tag refs with a `-` (e.g. + # `v0.1.0-rc1`, `v0.2.0-beta.3`) get the `prerelease` flag; clean + # `v0.1.0` is stable. `github.ref_name` is the unprefixed tag name + # (vs `github.ref` which is `refs/tags/v0.1.0`); using `_name` keeps + # the substring check tightly scoped to the tag itself. The `if:` + # gate above guarantees this only runs on tag pushes, so this + # expression is never evaluated against a branch ref. - name: publish release - if: github.event_name == 'push' + if: github.event_name == 'push' && success() uses: softprops/action-gh-release@v2 with: files: dist/* generate_release_notes: true fail_on_unmatched_files: true - prerelease: ${{ contains(github.ref, '-') }} + prerelease: ${{ contains(github.ref_name, '-') }} # When triggered manually (workflow_dispatch), surface a heads-up # so the run summary makes the build-only intent obvious. From cefd37ebcb1c8f62c0170df17027bd1d41b375ad Mon Sep 17 00:00:00 2001 From: CPerezz Date: Wed, 6 May 2026 23:09:30 +0200 Subject: [PATCH 3/6] ci: use -p torpc-proxy-cli for the CLI client build MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `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). --- .github/workflows/release.yml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 6dda3b7..d84dade 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -47,9 +47,14 @@ jobs: key: cli-${{ matrix.target }} - name: build daemon + CLI client + # `--bin torpc-proxy` doesn't resolve from the workspace root because + # the binary is defined in the `torpc-proxy-cli` package — Cargo's + # `--bin` flag searches default-run packages first and bails before + # finding it. Use `-p torpc-proxy-cli` to scope; `torpc` is the root + # package's default and works with `--bin torpc`. run: | cargo build --release --target ${{ matrix.target }} --bin torpc - cargo build --release --target ${{ matrix.target }} --bin torpc-proxy + cargo build --release --target ${{ matrix.target }} -p torpc-proxy-cli # Pack the two CLI binaries into the platform-conventional archive. # `strip = true` in `[profile.release]` already shrinks them; we just From d13a4955317aeb478246ef718ffb36544cc2b3f1 Mon Sep 17 00:00:00 2001 From: CPerezz Date: Wed, 6 May 2026 23:32:30 +0200 Subject: [PATCH 4/6] ci: sanitize version string for non-tag refs (slashes break paths) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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/.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. --- .github/workflows/release.yml | 34 ++++++++++++++++++++++++++++++---- 1 file changed, 30 insertions(+), 4 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index d84dade..73a22d6 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -59,14 +59,24 @@ jobs: # Pack the two CLI binaries into the platform-conventional archive. # `strip = true` in `[profile.release]` already shrinks them; we just # tarball/zip and emit a sibling `.sha256` for each. + # + # `GITHUB_REF_NAME` for tag pushes is the bare tag (`v0.1.0`); for + # workflow_dispatch on a branch it's the branch name (which may + # contain `/`, breaking filenames). Derive a safe `version` string: + # tags pass through verbatim, everything else gets `dev-`. - name: package (Unix) if: runner.os != 'Windows' shell: bash run: | set -euo pipefail + if [[ "$GITHUB_REF_TYPE" == "tag" ]]; then + version="$GITHUB_REF_NAME" + else + version="dev-${GITHUB_SHA:0:8}" + fi mkdir -p dist for bin in torpc torpc-proxy; do - archive="${bin}-${GITHUB_REF_NAME}-${{ matrix.target }}.tar.gz" + archive="${bin}-${version}-${{ matrix.target }}.tar.gz" tar -C "target/${{ matrix.target }}/release" -czf "dist/${archive}" "${bin}" (cd dist && shasum -a 256 "${archive}" > "${archive}.sha256") done @@ -82,9 +92,17 @@ jobs: # malformed archives or empty .sha256 sidecars without failing # the step. Force `Stop` so any cmdlet error is fatal. $ErrorActionPreference = 'Stop' + # Same sanitization as the Unix path: tags pass through verbatim, + # branches collapse to `dev-` so a slash in the branch + # name doesn't get interpreted as a path separator. + $version = if ($env:GITHUB_REF_TYPE -eq 'tag') { + $env:GITHUB_REF_NAME + } else { + "dev-" + $env:GITHUB_SHA.Substring(0, 8) + } New-Item -ItemType Directory -Force -Path dist | Out-Null foreach ($bin in @("torpc", "torpc-proxy")) { - $archive = "${bin}-${env:GITHUB_REF_NAME}-${{ matrix.target }}.zip" + $archive = "${bin}-${version}-${{ matrix.target }}.zip" $exe = "target/${{ matrix.target }}/release/${bin}.exe" Compress-Archive -Path $exe -DestinationPath "dist/${archive}" $hash = (Get-FileHash -Algorithm SHA256 "dist/${archive}").Hash.ToLower() @@ -146,7 +164,11 @@ jobs: set -euo pipefail shopt -s nullglob mkdir -p dist - version="${GITHUB_REF_NAME:-dev}" + if [[ "$GITHUB_REF_TYPE" == "tag" ]]; then + version="$GITHUB_REF_NAME" + else + version="dev-${GITHUB_SHA:0:8}" + fi target="x86_64-unknown-linux-gnu" # Tauri's bundler writes to the WORKSPACE target dir # (`$GITHUB_WORKSPACE/target/release/bundle/{deb,rpm,appimage}`) @@ -212,7 +234,11 @@ jobs: run: | set -euo pipefail mkdir -p dist - version="${GITHUB_REF_NAME:-dev}" + if [[ "$GITHUB_REF_TYPE" == "tag" ]]; then + version="$GITHUB_REF_NAME" + else + version="dev-${GITHUB_SHA:0:8}" + fi # Universal output is at target/universal-apple-darwin/release/bundle/dmg. # Use `mapfile` + an explicit count check so we fail loudly # instead of silently dropping a second DMG (Tauri sometimes From 1ce42cd329015577c34b229bf2e77d4d6bf48e7a Mon Sep 17 00:00:00 2001 From: CPerezz Date: Wed, 6 May 2026 23:53:50 +0200 Subject: [PATCH 5/6] ci: move macOS legs to macos-latest, cross-compile x86_64 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. --- .github/workflows/release.yml | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 73a22d6..84383eb 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -27,7 +27,11 @@ jobs: include: - os: ubuntu-latest target: x86_64-unknown-linux-gnu - - os: macos-13 + # Both macOS legs run on Apple Silicon (`macos-latest`). The + # x86_64 build cross-compiles via the rustc target installed + # by `dtolnay/rust-toolchain`. macos-13 (Intel) runners are + # queue-capacity-limited as GitHub winds them down. + - os: macos-latest target: x86_64-apple-darwin - os: macos-latest target: aarch64-apple-darwin @@ -209,7 +213,11 @@ jobs: # ------------------------------------------------------------------------- build-gui-darwin: name: gui (universal-apple-darwin) - runs-on: macos-13 + # Apple Silicon runner. Both rustc targets (x86_64 + aarch64) are + # installed below; Tauri's `--target universal-apple-darwin` does + # the lipo dance. macos-13 (Intel) is being phased out and was + # spending 20+ minutes in the runner queue. + runs-on: macos-latest steps: - uses: actions/checkout@v4 From 8fdfa9763995cf0dfb7297827e60c6c141ede966 Mon Sep 17 00:00:00 2001 From: CPerezz Date: Thu, 7 May 2026 00:14:25 +0200 Subject: [PATCH 6/6] ci: replace `mapfile` with portable while-read in macOS DMG collect MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- .github/workflows/release.yml | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 84383eb..e8d15e4 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -248,11 +248,15 @@ jobs: version="dev-${GITHUB_SHA:0:8}" fi # Universal output is at target/universal-apple-darwin/release/bundle/dmg. - # Use `mapfile` + an explicit count check so we fail loudly - # instead of silently dropping a second DMG (Tauri sometimes - # produces a sidecar updater DMG when configured) or proceeding - # with an empty path. - mapfile -t dmgs < <(find target/universal-apple-darwin/release/bundle/dmg -name "*.dmg") + # Use a portable while-read loop instead of `mapfile` — macOS + # ships bash 3.2 by default, which predates `mapfile` (bash 4+). + # The explicit count check still catches "no DMGs" and "multiple + # DMGs" (Tauri sometimes produces a sidecar updater DMG when + # configured) before we'd silently drop one. + dmgs=() + while IFS= read -r line; do + dmgs+=("$line") + done < <(find target/universal-apple-darwin/release/bundle/dmg -name "*.dmg") if [[ ${#dmgs[@]} -ne 1 ]]; then echo "Expected exactly 1 DMG, found ${#dmgs[@]}: ${dmgs[*]:-}" >&2 exit 1