From 263401a3409b71d680a1bffa1f6d3d4a594c4e49 Mon Sep 17 00:00:00 2001 From: Owen McGirr Date: Sun, 9 Aug 2026 17:56:12 +0100 Subject: [PATCH 1/4] Add notarized macOS release workflow --- .github/workflows/release-macos.yml | 213 ++++++++++++++++++++++++++++ README.md | 8 +- docs/macos-releases.md | 60 ++++++++ scripts/verify-macos-release.sh | 90 ++++++++++++ src-tauri/tauri.conf.json | 1 + src-tauri/tests/config.rs | 1 + 6 files changed, 372 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/release-macos.yml create mode 100644 docs/macos-releases.md create mode 100755 scripts/verify-macos-release.sh diff --git a/.github/workflows/release-macos.yml b/.github/workflows/release-macos.yml new file mode 100644 index 0000000..46abe87 --- /dev/null +++ b/.github/workflows/release-macos.yml @@ -0,0 +1,213 @@ +name: Release macOS + +on: + push: + tags: + - 'v*' + workflow_dispatch: + inputs: + tag: + description: Release tag to publish, for example v1.0.0-beta.1 + required: true + type: string + +permissions: + contents: write + +concurrency: + group: release-macos-${{ github.ref_name || inputs.tag }} + cancel-in-progress: false + +jobs: + release-macos: + name: Sign, notarize, and publish Apple Silicon DMG + runs-on: macos-15 + environment: production + timeout-minutes: 60 + + env: + RELEASE_EVENT_NAME: ${{ github.event_name }} + RELEASE_INPUT_TAG: ${{ inputs.tag }} + RELEASE_REF_NAME: ${{ github.ref_name }} + APPLE_SIGNING_IDENTITY: ${{ vars.APPLE_SIGNING_IDENTITY }} + APPLE_TEAM_ID: ${{ vars.APPLE_TEAM_ID }} + APPLE_API_ISSUER: ${{ secrets.APPLE_API_ISSUER }} + APPLE_API_KEY: ${{ secrets.APPLE_API_KEY }} + TIMBERLOGS_API_KEY: ${{ secrets.TIMBERLOGS_API_KEY }} + SWITCHIFY_TELEMETRY_ENDPOINT: ${{ vars.TIMBERLOGS_ENDPOINT }} + + steps: + - name: Resolve release tag + shell: bash + run: | + set -euo pipefail + release_tag="$RELEASE_REF_NAME" + if [[ "$RELEASE_EVENT_NAME" == 'workflow_dispatch' ]]; then + release_tag="$RELEASE_INPUT_TAG" + fi + if [[ ! "$release_tag" =~ ^v[0-9]+\.[0-9]+\.[0-9]+([+-][0-9A-Za-z.-]+)?$ ]]; then + echo "Release tag must be a semantic version beginning with v. Received: $release_tag" >&2 + exit 1 + fi + echo "RELEASE_TAG=$release_tag" >> "$GITHUB_ENV" + + - name: Checkout release tag + uses: actions/checkout@v6 + with: + ref: ${{ github.event_name == 'workflow_dispatch' && inputs.tag || github.ref }} + + - name: Set up Node.js + uses: actions/setup-node@v6 + with: + node-version: 24 + cache: npm + cache-dependency-path: package-lock.json + + - name: Set up Rust + uses: dtolnay/rust-toolchain@stable + with: + toolchain: 1.97.1 + targets: aarch64-apple-darwin + + - name: Install dependencies + run: npm ci + + - name: Verify release configuration + shell: bash + run: | + set -euo pipefail + node_version="$(node -p "require('./package.json').version")" + tauri_version="$(node -p "require('./src-tauri/tauri.conf.json').version")" + expected_tag="v${node_version}" + if [[ "$node_version" != "$tauri_version" ]]; then + echo "package.json version $node_version does not match Tauri version $tauri_version." >&2 + exit 1 + fi + if [[ "$RELEASE_TAG" != "$expected_tag" ]]; then + echo "Release tag $RELEASE_TAG does not match app version $expected_tag." >&2 + exit 1 + fi + if [[ "$APPLE_SIGNING_IDENTITY" != 'Developer ID Application: '* ]]; then + echo 'APPLE_SIGNING_IDENTITY must name a Developer ID Application certificate.' >&2 + exit 1 + fi + if [[ "$APPLE_SIGNING_IDENTITY" != *"($APPLE_TEAM_ID)" ]]; then + echo 'APPLE_SIGNING_IDENTITY does not contain the configured APPLE_TEAM_ID.' >&2 + exit 1 + fi + for required_name in APPLE_API_ISSUER APPLE_API_KEY TIMBERLOGS_API_KEY SWITCHIFY_TELEMETRY_ENDPOINT; do + if [[ -z "${!required_name:-}" ]]; then + echo "$required_name is not configured for the production environment." >&2 + exit 1 + fi + done + + - name: Install Developer ID certificate + shell: bash + env: + APPLE_CERTIFICATE_BASE64: ${{ secrets.APPLE_CERTIFICATE_BASE64 }} + APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }} + run: | + set -euo pipefail + if [[ -z "$APPLE_CERTIFICATE_BASE64" || -z "$APPLE_CERTIFICATE_PASSWORD" ]]; then + echo 'The production Developer ID certificate is not configured.' >&2 + exit 1 + fi + certificate_path="$RUNNER_TEMP/switchify-developer-id.p12" + keychain_path="$RUNNER_TEMP/switchify-signing.keychain-db" + keychain_password="$(openssl rand -base64 32)" + echo "::add-mask::$keychain_password" + printf '%s' "$APPLE_CERTIFICATE_BASE64" | base64 --decode > "$certificate_path" + chmod 600 "$certificate_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 "$certificate_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-keychains -d user -s "$keychain_path" + identity_output="$(security find-identity -v -p codesigning "$keychain_path")" + if ! grep -Fq "\"$APPLE_SIGNING_IDENTITY\"" <<< "$identity_output"; then + echo "The configured Developer ID identity was not found in the imported certificate." >&2 + exit 1 + fi + echo "APPLE_KEYCHAIN_PATH=$keychain_path" >> "$GITHUB_ENV" + + - name: Install notarization API key + shell: bash + env: + APPLE_API_PRIVATE_KEY: ${{ secrets.APPLE_API_PRIVATE_KEY }} + run: | + set -euo pipefail + if [[ -z "$APPLE_API_PRIVATE_KEY" ]]; then + echo 'APPLE_API_PRIVATE_KEY is not configured for the production environment.' >&2 + exit 1 + fi + api_key_path="$RUNNER_TEMP/AuthKey_${APPLE_API_KEY}.p8" + printf '%s' "$APPLE_API_PRIVATE_KEY" > "$api_key_path" + chmod 600 "$api_key_path" + echo "APPLE_API_KEY_PATH=$api_key_path" >> "$GITHUB_ENV" + + - name: Build signed and notarized app and DMG + run: npm run tauri build -- --bundles app,dmg --target aarch64-apple-darwin + + - name: Verify signed release + shell: bash + run: ./scripts/verify-macos-release.sh "$RELEASE_TAG" "$APPLE_SIGNING_IDENTITY" "$APPLE_TEAM_ID" + + - name: Stage release assets + shell: bash + run: | + set -euo pipefail + mkdir -p dist + dmg_path="$(find src-tauri/target/aarch64-apple-darwin/release/bundle/dmg -maxdepth 1 -type f -name '*.dmg' -print -quit)" + if [[ -z "$dmg_path" ]]; then + echo 'No notarized DMG was produced.' >&2 + exit 1 + fi + cp "$dmg_path" dist/ + asset_name="$(basename "$dmg_path")" + asset_hash="$(shasum -a 256 "dist/$asset_name" | awk '{print $1}')" + printf '%s %s\n' "$asset_hash" "$asset_name" > dist/SHA256SUMS-macos.txt + + - name: Publish GitHub release + shell: bash + env: + GH_TOKEN: ${{ github.token }} + run: | + set -euo pipefail + shopt -s nullglob + assets=(dist/*) + if (( ${#assets[@]} == 0 )); then + echo 'No release assets found in dist.' >&2 + exit 1 + fi + if gh release view "$RELEASE_TAG" >/dev/null 2>&1; then + gh release upload "$RELEASE_TAG" "${assets[@]}" --clobber + else + gh release create "$RELEASE_TAG" "${assets[@]}" \ + --title "$RELEASE_TAG" \ + --generate-notes \ + --verify-tag + fi + + - name: Upload workflow artifacts + uses: actions/upload-artifact@v7 + with: + name: switchify-pc-macos-${{ env.RELEASE_TAG }} + path: dist/* + if-no-files-found: error + + - name: Remove temporary signing material + if: always() + shell: bash + run: | + if [[ -n "${APPLE_KEYCHAIN_PATH:-}" ]]; then + security delete-keychain "$APPLE_KEYCHAIN_PATH" 2>/dev/null || true + fi + rm -f \ + "$RUNNER_TEMP/switchify-developer-id.p12" \ + "$RUNNER_TEMP/AuthKey_${APPLE_API_KEY:-missing}.p8" diff --git a/README.md b/README.md index 832e1fe..908b259 100644 --- a/README.md +++ b/README.md @@ -58,6 +58,12 @@ cargo test --manifest-path src-tauri/Cargo.toml Rust tests use fake input adapters and never control the local pointer or keyboard. Native checks and unsigned bundles run on Windows and macOS in `.github/workflows/ci.yml`. +## macOS releases + +Production macOS releases are Apple Silicon DMGs signed with an Apple-issued Developer ID Application certificate, submitted to Apple's notarization service, and stapled for offline Gatekeeper verification. A `v*` tag or manual release run creates or updates the matching GitHub Release after verifying the tag, app version, architecture, nested-code signatures, hardened runtime, secure timestamp, and notarization tickets. + +The production certificate and App Store Connect API key are held only in the GitHub `production` environment and imported into an ephemeral runner keychain. They are separate from the machine-local `Switchify PC Development` identity used by `npm run macos:run`. See [macOS production releases](docs/macos-releases.md) for certificate creation, GitHub configuration, release, recovery, and rotation instructions. + ## Diagnostics Switchify keeps up to 500 sanitized diagnostic events locally in `diagnostic-history.jsonl`. The history covers application startup, Bluetooth and Accessibility transitions, disconnects, runtime failures, and update checks. It never stores typed text, command payloads, pairing secrets, device names, or full paths; malformed or unwritable history is ignored so diagnostics cannot prevent startup. @@ -86,7 +92,7 @@ Existing public Git history, tags, releases, update metadata, and installer down ## Development boundaries -- The macOS development identity is local-only. The application has no production Developer ID signing, notarization, or release publishing workflow; macOS CI builds with `--no-sign`. Windows production packages use the locally available Certum/SimplySign identity and are not published automatically. +- Pull-request and `main` macOS CI remains unsigned. Only authorized release tags and manual recovery runs can access the production Developer ID and notarization credentials. Windows production packages continue to use the locally available Certum/SimplySign identity and are not published automatically. - Linux may appear in capability data but is not a supported Bluetooth target. - Windows Grid 3 output uses the native `Sensory_SwitchInput` broadcast contract. Grid 3 is omitted from macOS capabilities and profiles. - Update installation requires a signed Tauri update feed. Local development builds can only report updater configuration errors. diff --git a/docs/macos-releases.md b/docs/macos-releases.md new file mode 100644 index 0000000..23b2b41 --- /dev/null +++ b/docs/macos-releases.md @@ -0,0 +1,60 @@ +# macOS production releases + +Switchify PC is distributed outside the Mac App Store as an Apple Silicon DMG. Production releases use an Apple-issued **Developer ID Application** certificate and Apple notarization. The self-signed `Switchify PC Development` identity remains exclusively for local Accessibility testing through `npm run macos:run`. + +## Create and preserve the certificate + +1. In Keychain Access, create a certificate signing request using the Apple Developer account email. Select **Saved to disk** and **Let me specify key pair information**, then use RSA 2048-bit keys. +2. In Apple Developer Certificates, create a **Developer ID Application** certificate from that CSR. Download and install the certificate on the Mac that generated the CSR so it joins the existing private key. +3. Verify it appears under **My Certificates** and in `security find-identity -v -p codesigning` with its full `Developer ID Application: … (TEAMID)` identity. +4. Export the certificate and private key together as a password-protected `.p12`. Keep an encrypted offline backup of the `.p12` and its password. Never commit either one. +5. In App Store Connect → Users and Access → Integrations, create a team API key with **Developer** access. Download the `.p8` immediately; Apple only permits downloading it once. + +Do not revoke or replace the certificate during normal renewal. Revocation invalidates future signing and can require an emergency rotation. Keep the local `Switchify PC Development`, earlier development, POC, and Preview identities untouched. + +## Configure GitHub + +Create a `production` environment. Allow deployments from release tags matching `v*` and from `main` for manually dispatched recovery runs. No reviewer gate is required. Add a repository tag ruleset so only maintainers can create or update `v*` tags. + +Configure these environment secrets: + +| Name | Value | +| --- | --- | +| `APPLE_CERTIFICATE_BASE64` | Base64-encoded contents of the encrypted `.p12` | +| `APPLE_CERTIFICATE_PASSWORD` | Password used when exporting the `.p12` | +| `APPLE_API_ISSUER` | App Store Connect API issuer UUID | +| `APPLE_API_KEY` | App Store Connect API key ID | +| `APPLE_API_PRIVATE_KEY` | Complete contents of the downloaded `.p8` file | +| `TIMBERLOGS_API_KEY` | Production telemetry API key | + +Configure these environment variables: + +| Name | Value | +| --- | --- | +| `APPLE_SIGNING_IDENTITY` | Full `Developer ID Application: … (TEAMID)` identity | +| `APPLE_TEAM_ID` | Apple Developer team ID from the identity | +| `TIMBERLOGS_ENDPOINT` | Production HTTPS telemetry endpoint | + +Encode the certificate without line wrapping on macOS: + +```bash +base64 -i Switchify-PC-Developer-ID.p12 | tr -d '\n' +``` + +The workflow writes credentials only beneath the ephemeral runner directory, imports the certificate into a temporary keychain, masks its generated keychain password, and deletes the material during cleanup. Pull-request and `main` CI never receive production signing secrets. + +## Publish a release + +Keep `package.json` and `src-tauri/tauri.conf.json` versions identical, then create and push the corresponding tag, for example `v1.0.0-beta.1`. The release workflow checks out that exact tag, builds on an Apple Silicon runner, signs and notarizes the app and DMG, validates Gatekeeper and stapled tickets, and creates or updates the matching GitHub Release. + +The workflow can be manually dispatched with an existing tag to recover or replace a macOS asset. It never modifies earlier tags or release assets. The published DMG is accompanied by `SHA256SUMS-macos.txt`. + +Updater feed generation and updater signing are intentionally separate work. Publishing a DMG does not make the in-app updater functional. + +## Rotation and troubleshooting + +- Track the Developer ID certificate expiry date and rotate it before expiry by issuing a new certificate, updating all certificate-related secrets together, and validating a release candidate before retiring the old certificate. +- If import fails, verify the `.p12` contains both certificate and private key and that its password is correct. +- If the identity check fails, copy the exact identity from `security find-identity -v -p codesigning`; it must start with `Developer ID Application:` and end with the configured team ID. +- If notarization fails, inspect the Tauri/notarytool output for unsigned nested code, missing hardened runtime, timestamp failures, or rejected entitlements. Do not bypass notarization or stapling. +- If Gatekeeper or stapling validation fails after Apple accepted the upload, rerun the workflow once before replacing credentials; Apple ticket availability can briefly lag. diff --git a/scripts/verify-macos-release.sh b/scripts/verify-macos-release.sh new file mode 100755 index 0000000..759ddba --- /dev/null +++ b/scripts/verify-macos-release.sh @@ -0,0 +1,90 @@ +#!/bin/bash + +set -euo pipefail + +if [[ $# -ne 3 ]]; then + echo "Usage: $0 " >&2 + exit 64 +fi + +release_tag="$1" +signing_identity="$2" +team_id="$3" +project_directory="$(cd "$(dirname "$0")/.." && pwd)" +bundle_directory="${project_directory}/src-tauri/target/aarch64-apple-darwin/release/bundle" + +if [[ ! "$release_tag" =~ ^v[0-9]+\.[0-9]+\.[0-9]+([+-][0-9A-Za-z.-]+)?$ ]]; then + echo "Invalid release tag: $release_tag" >&2 + exit 1 +fi +if [[ "$signing_identity" != 'Developer ID Application: '* ]]; then + echo 'The release identity must be a Developer ID Application certificate.' >&2 + exit 1 +fi +if [[ -z "$team_id" || "$signing_identity" != *"($team_id)" ]]; then + echo 'The release identity does not match the expected Apple team.' >&2 + exit 1 +fi + +app_path="$(find "${bundle_directory}/macos" -maxdepth 1 -type d -name '*.app' -print -quit)" +dmg_path="$(find "${bundle_directory}/dmg" -maxdepth 1 -type f -name '*.dmg' -print -quit)" + +if [[ -z "$app_path" || ! -d "$app_path" ]]; then + echo "No application bundle found under ${bundle_directory}/macos." >&2 + exit 1 +fi +if [[ -z "$dmg_path" || ! -f "$dmg_path" ]]; then + echo "No DMG found under ${bundle_directory}/dmg." >&2 + exit 1 +fi + +main_executable="${app_path}/Contents/MacOS/switchify-pc" +if [[ ! -x "$main_executable" ]]; then + echo "The expected application executable is missing: $main_executable" >&2 + exit 1 +fi + +codesign --verify --deep --strict --verbose=2 "$app_path" +codesign --verify --strict --verbose=2 "$dmg_path" + +app_details="$(codesign -dvvv "$app_path" 2>&1)" +if ! grep -Fq "Authority=${signing_identity}" <<< "$app_details"; then + echo 'The application was not signed with the configured Developer ID identity.' >&2 + exit 1 +fi +if ! grep -Fq "TeamIdentifier=${team_id}" <<< "$app_details"; then + echo 'The application signature has the wrong Apple team identifier.' >&2 + exit 1 +fi +if ! grep -Eq 'flags=.*\(runtime\)' <<< "$app_details"; then + echo 'The application signature does not enable hardened runtime.' >&2 + exit 1 +fi +if ! grep -Fq 'Timestamp=' <<< "$app_details"; then + echo 'The application signature does not contain a secure timestamp.' >&2 + exit 1 +fi + +while IFS= read -r -d '' candidate; do + if file "$candidate" | grep -Fq 'Mach-O'; then + candidate_details="$(codesign -dvv "$candidate" 2>&1)" + if ! grep -Fq "Authority=${signing_identity}" <<< "$candidate_details" || \ + ! grep -Fq "TeamIdentifier=${team_id}" <<< "$candidate_details"; then + echo "Nested executable uses a different signing identity: $candidate" >&2 + exit 1 + fi + fi +done < <(find "${app_path}/Contents" -type f -print0) + +architectures="$(lipo -archs "$main_executable")" +if [[ "$architectures" != 'arm64' ]]; then + echo "Expected an Apple Silicon-only executable, found: $architectures" >&2 + exit 1 +fi + +xcrun stapler validate "$app_path" +xcrun stapler validate "$dmg_path" +spctl --assess --type execute --verbose=4 "$app_path" +spctl --assess --type open --context context:primary-signature --verbose=4 "$dmg_path" + +echo "Verified signed, hardened, notarized Apple Silicon release ${release_tag}." diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index ca1b414..e6e0dd6 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -48,6 +48,7 @@ "nsis": { "installMode": "perMachine" } }, "macOS": { + "hardenedRuntime": true, "minimumSystemVersion": "13.0", "infoPlist": "Info.plist" } diff --git a/src-tauri/tests/config.rs b/src-tauri/tests/config.rs index e9d6dfb..f108291 100644 --- a/src-tauri/tests/config.rs +++ b/src-tauri/tests/config.rs @@ -31,4 +31,5 @@ fn application_configuration_uses_the_promoted_identity() { macos["bundle"]["macOS"]["signingIdentity"], "Switchify PC Development" ); + assert_eq!(config["bundle"]["macOS"]["hardenedRuntime"], true); } From 1bce369be54d5b9785922579b25f30c2bceaadb2 Mon Sep 17 00:00:00 2001 From: Owen McGirr Date: Sun, 9 Aug 2026 17:57:55 +0100 Subject: [PATCH 2/4] Avoid retired identity name in release docs --- docs/macos-releases.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/macos-releases.md b/docs/macos-releases.md index 23b2b41..039092a 100644 --- a/docs/macos-releases.md +++ b/docs/macos-releases.md @@ -10,7 +10,7 @@ Switchify PC is distributed outside the Mac App Store as an Apple Silicon DMG. P 4. Export the certificate and private key together as a password-protected `.p12`. Keep an encrypted offline backup of the `.p12` and its password. Never commit either one. 5. In App Store Connect → Users and Access → Integrations, create a team API key with **Developer** access. Download the `.p8` immediately; Apple only permits downloading it once. -Do not revoke or replace the certificate during normal renewal. Revocation invalidates future signing and can require an emergency rotation. Keep the local `Switchify PC Development`, earlier development, POC, and Preview identities untouched. +Do not revoke or replace the certificate during normal renewal. Revocation invalidates future signing and can require an emergency rotation. Keep the local `Switchify PC Development`, earlier development, POC, and retired identities untouched. ## Configure GitHub From 4cb433cde80a30028c9ebe2586d72bb402d78986 Mon Sep 17 00:00:00 2001 From: Owen McGirr Date: Sun, 9 Aug 2026 19:49:50 +0100 Subject: [PATCH 3/4] fix: notarize completed macOS DMG --- .github/workflows/release-macos.yml | 19 ++++++++++++++++++- docs/macos-releases.md | 2 +- 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/.github/workflows/release-macos.yml b/.github/workflows/release-macos.yml index 46abe87..68e5c5f 100644 --- a/.github/workflows/release-macos.yml +++ b/.github/workflows/release-macos.yml @@ -151,9 +151,26 @@ jobs: chmod 600 "$api_key_path" echo "APPLE_API_KEY_PATH=$api_key_path" >> "$GITHUB_ENV" - - name: Build signed and notarized app and DMG + - name: Build signed and notarized app and signed DMG run: npm run tauri build -- --bundles app,dmg --target aarch64-apple-darwin + - name: Notarize and staple DMG + shell: bash + run: | + set -euo pipefail + dmg_path="$(find src-tauri/target/aarch64-apple-darwin/release/bundle/dmg -maxdepth 1 -type f -name '*.dmg' -print -quit)" + if [[ -z "$dmg_path" ]]; then + echo 'No signed DMG was produced for notarization.' >&2 + exit 1 + fi + xcrun notarytool submit "$dmg_path" \ + --key "$APPLE_API_KEY_PATH" \ + --key-id "$APPLE_API_KEY" \ + --issuer "$APPLE_API_ISSUER" \ + --wait \ + --timeout 20m + xcrun stapler staple "$dmg_path" + - name: Verify signed release shell: bash run: ./scripts/verify-macos-release.sh "$RELEASE_TAG" "$APPLE_SIGNING_IDENTITY" "$APPLE_TEAM_ID" diff --git a/docs/macos-releases.md b/docs/macos-releases.md index 039092a..f064047 100644 --- a/docs/macos-releases.md +++ b/docs/macos-releases.md @@ -45,7 +45,7 @@ The workflow writes credentials only beneath the ephemeral runner directory, imp ## Publish a release -Keep `package.json` and `src-tauri/tauri.conf.json` versions identical, then create and push the corresponding tag, for example `v1.0.0-beta.1`. The release workflow checks out that exact tag, builds on an Apple Silicon runner, signs and notarizes the app and DMG, validates Gatekeeper and stapled tickets, and creates or updates the matching GitHub Release. +Keep `package.json` and `src-tauri/tauri.conf.json` versions identical, then create and push the corresponding tag, for example `v1.0.0-beta.1`. The release workflow checks out that exact tag, builds on an Apple Silicon runner, lets Tauri sign and notarize the app, then separately submits and staples the finished signed DMG. It validates Gatekeeper and both stapled tickets before creating or updating the matching GitHub Release. The workflow can be manually dispatched with an existing tag to recover or replace a macOS asset. It never modifies earlier tags or release assets. The published DMG is accompanied by `SHA256SUMS-macos.txt`. From 5dbb2ec3186c8bc2a87b626c99eb4275fdf628d5 Mon Sep 17 00:00:00 2001 From: Owen McGirr Date: Sun, 9 Aug 2026 20:13:36 +0100 Subject: [PATCH 4/4] security: pin production release actions --- .github/workflows/release-macos.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/release-macos.yml b/.github/workflows/release-macos.yml index 68e5c5f..244de37 100644 --- a/.github/workflows/release-macos.yml +++ b/.github/workflows/release-macos.yml @@ -52,19 +52,19 @@ jobs: echo "RELEASE_TAG=$release_tag" >> "$GITHUB_ENV" - name: Checkout release tag - uses: actions/checkout@v6 + uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6 with: ref: ${{ github.event_name == 'workflow_dispatch' && inputs.tag || github.ref }} - name: Set up Node.js - uses: actions/setup-node@v6 + uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6 with: node-version: 24 cache: npm cache-dependency-path: package-lock.json - name: Set up Rust - uses: dtolnay/rust-toolchain@stable + uses: dtolnay/rust-toolchain@4360b52568e2003a75bf9bc1d59f33a8e3fc893c # stable with: toolchain: 1.97.1 targets: aarch64-apple-darwin @@ -212,7 +212,7 @@ jobs: fi - name: Upload workflow artifacts - uses: actions/upload-artifact@v7 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: name: switchify-pc-macos-${{ env.RELEASE_TAG }} path: dist/*