Skip to content
Merged
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
160 changes: 5 additions & 155 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ name: ci
on:
push:
branches: [main]
tags: ['v*']
pull_request:

env:
Expand Down Expand Up @@ -44,21 +43,16 @@ jobs:
- run: cargo clippy --workspace --all-targets --exclude rompatch-gui -- -D warnings

gui:
name: gui (macOS check + universal .dmg on tag)
name: gui (macOS check)
# macos-14 image build 20260512 ships a broken cargo proxy that
# invokes rustup-init on `cargo check`. macos-15 is the same arm64
# runner family and is already proven by the `test (macos-latest)`
# matrix entry on this workflow.
#
# This job only validates the GUI crate + frontend on PRs/main. The
# signed .dmg build and release publishing live in release-build.yml,
# invoked by release-please.yml when a release is cut.
runs-on: macos-15
# APPLE_SIGNING_ENABLED is true only when the Developer ID Application
# cert secret is populated. When false, the build still runs and ships
# an unsigned + sigstore-attested .dmg; the Apple-specific steps skip.
env:
APPLE_SIGNING_ENABLED: ${{ secrets.APPLE_CERTIFICATE_B64 != '' }}
permissions:
contents: write # softprops/action-gh-release needs this to create the release
id-token: write # sigstore OIDC for attestation
attestations: write # attestation storage
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
Expand Down Expand Up @@ -92,150 +86,6 @@ jobs:
run: |
cargo check -p rompatch-gui --release
cargo clippy -p rompatch-gui --all-targets -- -D warnings
# Guard against a release whose binary version (Cargo.toml) doesn't
# match the git tag. The updater manifest below derives its version
# from $GITHUB_REF_NAME while the installed app reports its version
# from Cargo.toml - if they disagree, every client gets stuck in an
# update loop because the "new" binary still reports the old version.
- name: verify tag matches Cargo.toml workspace version
if: startsWith(github.ref, 'refs/tags/')
run: |
set -euo pipefail
TAG_VERSION="${GITHUB_REF_NAME#v}"
CARGO_VERSION=$(cargo metadata --no-deps --format-version 1 \
| jq -r '.packages[] | select(.name=="rompatch-gui") | .version')
if [ "$TAG_VERSION" != "$CARGO_VERSION" ]; then
echo "::error::tag $GITHUB_REF_NAME (version $TAG_VERSION) does not match Cargo.toml ($CARGO_VERSION)"
exit 1
fi
# On tag pushes, produce a universal-apple-darwin .dmg.
# `cargo-binstall` pulls prebuilt tauri-cli binaries instead of
# building from source (~3-5 min saved per tagged release).
- name: install cargo-binstall
if: startsWith(github.ref, 'refs/tags/')
uses: cargo-bins/cargo-binstall@aaa84a43aec4955a42c5ffc65d258961e39f276e # v1.19.1
- name: install tauri-cli
if: startsWith(github.ref, 'refs/tags/')
run: cargo binstall tauri-cli@2.11.1 --no-confirm --locked
# Import the Developer ID Application cert into an ephemeral
# keychain so codesign can find it. Cleaned up at end-of-job.
- name: import Apple signing certificate
if: startsWith(github.ref, 'refs/tags/') && env.APPLE_SIGNING_ENABLED == 'true'
env:
APPLE_CERTIFICATE_B64: ${{ secrets.APPLE_CERTIFICATE_B64 }}
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }}
run: |
CERT_PATH="$RUNNER_TEMP/developer-id.p12"
KEYCHAIN_PATH="$RUNNER_TEMP/build.keychain-db"
echo "$APPLE_CERTIFICATE_B64" | base64 --decode > "$CERT_PATH"
security create-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
security set-keychain-settings -lut 21600 "$KEYCHAIN_PATH"
security unlock-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
security import "$CERT_PATH" -P "$APPLE_CERTIFICATE_PASSWORD" -A -t cert -f pkcs12 -k "$KEYCHAIN_PATH"
security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
security list-keychain -d user -s "$KEYCHAIN_PATH"
rm -f "$CERT_PATH"
# GitHub Actions sets referenced secrets to empty strings when
# they're missing rather than leaving the env var unset; Tauri 2
# treats APPLE_SIGNING_IDENTITY="" as "signing enabled, identity
# is empty" and the build fails looking up "" in the keychain.
# Split into two siblings so the unsigned path runs without any
# APPLE_* env vars present at all.
- name: tauri build universal .dmg (signed)
if: startsWith(github.ref, 'refs/tags/') && env.APPLE_SIGNING_ENABLED == 'true'
working-directory: crates/rompatch-gui
env:
APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }}
APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
run: cargo tauri build --target universal-apple-darwin
- name: tauri build universal .dmg (unsigned)
if: startsWith(github.ref, 'refs/tags/') && env.APPLE_SIGNING_ENABLED != 'true'
working-directory: crates/rompatch-gui
env:
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
run: cargo tauri build --target universal-apple-darwin
- name: verify notarization + signature
if: startsWith(github.ref, 'refs/tags/') && env.APPLE_SIGNING_ENABLED == 'true'
run: |
DMG=$(ls target/universal-apple-darwin/release/bundle/dmg/*.dmg | head -1)
xcrun stapler validate "$DMG"
spctl --assess --type open --context context:primary-signature "$DMG"
# Build the Tauri updater manifest. The endpoint configured in
# tauri.conf.json resolves `releases/latest/download/latest.json` to
# whichever release GitHub marks as "latest", so this file just needs
# to ship alongside the signed .app.tar.gz.
- name: build latest.json updater manifest
if: startsWith(github.ref, 'refs/tags/')
run: |
set -euo pipefail
VERSION="${GITHUB_REF_NAME#v}"
BUNDLE_DIR="target/universal-apple-darwin/release/bundle/macos"
TARBALL_PATH=$(ls "$BUNDLE_DIR"/*.app.tar.gz | head -1)
TARBALL=$(basename "$TARBALL_PATH")
SIG=$(cat "$TARBALL_PATH.sig")
URL="https://github.com/${{ github.repository }}/releases/download/${GITHUB_REF_NAME}/${TARBALL}"
# Tauri's updater matches the manifest's `platforms` map against
# the running process's target triple (darwin-aarch64 on Apple
# Silicon, darwin-x86_64 on Intel) - "darwin-universal" alone is
# not enough, the lookup misses on real Macs. Emit all three keys
# pointing at the same universal .app.tar.gz so both architectures
# resolve and the universal fallback exists too.
jq -n \
--arg v "$VERSION" \
--arg sig "$SIG" \
--arg url "$URL" \
--arg notes "See https://github.com/${{ github.repository }}/releases/tag/${GITHUB_REF_NAME} for details" \
'{
version: $v,
notes: $notes,
pub_date: (now | todate),
platforms: {
"darwin-aarch64": { signature: $sig, url: $url },
"darwin-x86_64": { signature: $sig, url: $url },
"darwin-universal":{ signature: $sig, url: $url }
}
}' > latest.json
cat latest.json
# Sigstore-backed build provenance attestation. Free, transparency-
# logged, and verifiable by anyone with `gh attestation verify`.
# Runs on every tag regardless of Apple-signing state. Covers both
# the .dmg (fresh install) and .app.tar.gz (in-app updater).
- name: attest release artifacts
if: startsWith(github.ref, 'refs/tags/')
uses: actions/attest-build-provenance@v2
with:
subject-path: |
target/universal-apple-darwin/release/bundle/dmg/*.dmg
target/universal-apple-darwin/release/bundle/macos/*.app.tar.gz
- name: upload .dmg artifact
if: startsWith(github.ref, 'refs/tags/')
uses: actions/upload-artifact@v4
with:
name: rompatch-gui-macos-universal
path: target/universal-apple-darwin/release/bundle/dmg/*.dmg
# Auto-create a GitHub Release with auto-generated release notes,
# the .dmg for fresh installs, and the .app.tar.gz + .sig + latest.json
# for the in-app updater.
- name: create GitHub Release
if: startsWith(github.ref, 'refs/tags/')
uses: softprops/action-gh-release@3bb12739c298aeb8a4eeaf626c5b8d85266b0e65 # v2.6.2
with:
generate_release_notes: true
files: |
target/universal-apple-darwin/release/bundle/dmg/*.dmg
target/universal-apple-darwin/release/bundle/macos/*.app.tar.gz
target/universal-apple-darwin/release/bundle/macos/*.app.tar.gz.sig
latest.json
- name: cleanup keychain
if: always() && startsWith(github.ref, 'refs/tags/') && env.APPLE_SIGNING_ENABLED == 'true'
run: |
security delete-keychain "$RUNNER_TEMP/build.keychain-db" || true

deny:
name: cargo-deny
Expand Down
204 changes: 204 additions & 0 deletions .github/workflows/release-build.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,204 @@
name: release-build

# Builds and publishes the signed universal macOS .dmg, the in-app
# updater bundle (.app.tar.gz + .sig + latest.json), and build
# attestations, then attaches them to the GitHub Release for `tag`.
#
# Invoked by release-please.yml when a release is cut. Also exposed via
# workflow_dispatch as a manual escape hatch (e.g. to re-run a failed
# artifact build against an existing tag/release).
on:
workflow_call:
inputs:
tag:
description: "Release tag to build (e.g. v0.3.2)"
required: true
type: string
workflow_dispatch:
inputs:
tag:
description: "Release tag to build (e.g. v0.3.2)"
required: true
type: string

env:
CARGO_TERM_COLOR: always
RUSTFLAGS: "-D warnings"

jobs:
build:
name: universal .dmg + release
runs-on: macos-15
# APPLE_SIGNING_ENABLED is true only when the Developer ID Application
# cert secret is populated. When false, the build still runs and ships
# an unsigned + sigstore-attested .dmg; the Apple-specific steps skip.
env:
APPLE_SIGNING_ENABLED: ${{ secrets.APPLE_CERTIFICATE_B64 != '' }}
TAG: ${{ inputs.tag }}
permissions:
contents: write # softprops/action-gh-release attaches to the release
id-token: write # sigstore OIDC for attestation
attestations: write # attestation storage
steps:
- uses: actions/checkout@v4
with:
ref: ${{ inputs.tag }}
- uses: pnpm/action-setup@v4
with:
version: 10
- uses: actions/setup-node@v4
with:
node-version: 22
cache: pnpm
cache-dependency-path: crates/rompatch-gui/ui/pnpm-lock.yaml
- uses: dtolnay/rust-toolchain@stable
with:
targets: aarch64-apple-darwin,x86_64-apple-darwin
- uses: Swatinem/rust-cache@v2
with:
key: gui
cache-bin: "false"
- name: install frontend deps
run: pnpm install --frozen-lockfile
working-directory: crates/rompatch-gui/ui
- name: typecheck frontend
run: pnpm typecheck
working-directory: crates/rompatch-gui/ui
- name: build frontend dist
run: pnpm build
working-directory: crates/rompatch-gui/ui
# Guard against a release whose binary version (Cargo.toml) doesn't
# match the git tag. The updater manifest below derives its version
# from the tag while the installed app reports its version from
# Cargo.toml - if they disagree, every client gets stuck in an
# update loop because the "new" binary still reports the old version.
- name: verify tag matches Cargo.toml workspace version
run: |
set -euo pipefail
TAG_VERSION="${TAG#v}"
CARGO_VERSION=$(cargo metadata --no-deps --format-version 1 \
| jq -r '.packages[] | select(.name=="rompatch-gui") | .version')
if [ "$TAG_VERSION" != "$CARGO_VERSION" ]; then
echo "::error::tag $TAG (version $TAG_VERSION) does not match Cargo.toml ($CARGO_VERSION)"
exit 1
fi
# `cargo-binstall` pulls prebuilt tauri-cli binaries instead of
# building from source (~3-5 min saved per tagged release).
- name: install cargo-binstall
uses: cargo-bins/cargo-binstall@aaa84a43aec4955a42c5ffc65d258961e39f276e # v1.19.1
- name: install tauri-cli
run: cargo binstall tauri-cli@2.11.1 --no-confirm --locked
# Import the Developer ID Application cert into an ephemeral
# keychain so codesign can find it. Cleaned up at end-of-job.
- name: import Apple signing certificate
if: env.APPLE_SIGNING_ENABLED == 'true'
env:
APPLE_CERTIFICATE_B64: ${{ secrets.APPLE_CERTIFICATE_B64 }}
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }}
run: |
CERT_PATH="$RUNNER_TEMP/developer-id.p12"
KEYCHAIN_PATH="$RUNNER_TEMP/build.keychain-db"
echo "$APPLE_CERTIFICATE_B64" | base64 --decode > "$CERT_PATH"
security create-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
security set-keychain-settings -lut 21600 "$KEYCHAIN_PATH"
security unlock-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
security import "$CERT_PATH" -P "$APPLE_CERTIFICATE_PASSWORD" -A -t cert -f pkcs12 -k "$KEYCHAIN_PATH"
security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
security list-keychain -d user -s "$KEYCHAIN_PATH"
rm -f "$CERT_PATH"
# GitHub Actions sets referenced secrets to empty strings when
# they're missing rather than leaving the env var unset; Tauri 2
# treats APPLE_SIGNING_IDENTITY="" as "signing enabled, identity
# is empty" and the build fails looking up "" in the keychain.
# Split into two siblings so the unsigned path runs without any
# APPLE_* env vars present at all.
- name: tauri build universal .dmg (signed)
if: env.APPLE_SIGNING_ENABLED == 'true'
working-directory: crates/rompatch-gui
env:
APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }}
APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
run: cargo tauri build --target universal-apple-darwin
- name: tauri build universal .dmg (unsigned)
if: env.APPLE_SIGNING_ENABLED != 'true'
working-directory: crates/rompatch-gui
env:
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
run: cargo tauri build --target universal-apple-darwin
- name: verify notarization + signature
if: env.APPLE_SIGNING_ENABLED == 'true'
run: |
DMG=$(ls target/universal-apple-darwin/release/bundle/dmg/*.dmg | head -1)
xcrun stapler validate "$DMG"
spctl --assess --type open --context context:primary-signature "$DMG"
# Build the Tauri updater manifest. The endpoint configured in
# tauri.conf.json resolves `releases/latest/download/latest.json` to
# whichever release GitHub marks as "latest", so this file just needs
# to ship alongside the signed .app.tar.gz.
- name: build latest.json updater manifest
run: |
set -euo pipefail
VERSION="${TAG#v}"
BUNDLE_DIR="target/universal-apple-darwin/release/bundle/macos"
TARBALL_PATH=$(ls "$BUNDLE_DIR"/*.app.tar.gz | head -1)
TARBALL=$(basename "$TARBALL_PATH")
SIG=$(cat "$TARBALL_PATH.sig")
URL="https://github.com/${{ github.repository }}/releases/download/${TAG}/${TARBALL}"
# Tauri's updater matches the manifest's `platforms` map against
# the running process's target triple (darwin-aarch64 on Apple
# Silicon, darwin-x86_64 on Intel) - "darwin-universal" alone is
# not enough, the lookup misses on real Macs. Emit all three keys
# pointing at the same universal .app.tar.gz so both architectures
# resolve and the universal fallback exists too.
jq -n \
--arg v "$VERSION" \
--arg sig "$SIG" \
--arg url "$URL" \
--arg notes "See https://github.com/${{ github.repository }}/releases/tag/${TAG} for details" \
'{
version: $v,
notes: $notes,
pub_date: (now | todate),
platforms: {
"darwin-aarch64": { signature: $sig, url: $url },
"darwin-x86_64": { signature: $sig, url: $url },
"darwin-universal":{ signature: $sig, url: $url }
}
}' > latest.json
cat latest.json
# Sigstore-backed build provenance attestation. Free, transparency-
# logged, and verifiable by anyone with `gh attestation verify`.
# Covers both the .dmg (fresh install) and .app.tar.gz (updater).
- name: attest release artifacts
uses: actions/attest-build-provenance@v2
with:
subject-path: |
target/universal-apple-darwin/release/bundle/dmg/*.dmg
target/universal-apple-darwin/release/bundle/macos/*.app.tar.gz
- name: upload .dmg artifact
uses: actions/upload-artifact@v4
with:
name: rompatch-gui-macos-universal
path: target/universal-apple-darwin/release/bundle/dmg/*.dmg
# Attach the artifacts to the release release-please already created
# for this tag. No generate_release_notes: release-please owns the
# release body (curated from the changelog); we only add files.
- name: attach artifacts to GitHub Release
uses: softprops/action-gh-release@3bb12739c298aeb8a4eeaf626c5b8d85266b0e65 # v2.6.2
with:
tag_name: ${{ inputs.tag }}
files: |
target/universal-apple-darwin/release/bundle/dmg/*.dmg
target/universal-apple-darwin/release/bundle/macos/*.app.tar.gz
target/universal-apple-darwin/release/bundle/macos/*.app.tar.gz.sig
latest.json
- name: cleanup keychain
if: always() && env.APPLE_SIGNING_ENABLED == 'true'
run: |
security delete-keychain "$RUNNER_TEMP/build.keychain-db" || true
Loading
Loading