feat(appstore): add a Mac App Store build track

Tauri has no App Store target, so mas-package.sh covers the distance between the .app and something
App Store Connect will take. The order is load-bearing: the provisioning profile goes in before
codesign runs because the signature covers it, which is also why Tauri's own signing is switched
off for this build.

The sandbox costs three things, all Apple's rules rather than choices. The updater is gone, and
falls out for free because the plugin was already conditional on the config declaring it; only the
release overlay does. The Check for Updates menu item is gated on the same condition, since a menu
item that errors when clicked is its own rejection risk. The library moves into the container, and
the system spelling dictionary becomes unreadable.

Two things the first upload taught us. The profile is kept owner-only where it lives next to the
signing keys, and cp carried that mode into the bundle; Apple rejects a package containing anything
a non-root user cannot read. And altool exits 0 after printing UPLOAD FAILED, so its exit status
cannot be trusted and the transcript is the only reliable signal.

Claude-Session: https://claude.ai/code/session_018HwEWvoE1NkkUjJCMLSnup
This commit is contained in:
pj committed 2026-08-31 17:26:03 +05:30
1 parent 39d4097773
commit 92b446973f
7 files changed
+339 -6

No files matched your search

+130
View File
@@ -0,0 +1,130 @@
name: App Store
on:
workflow_dispatch:
inputs:
tag:
description: "Tag to build, e.g. v0.1.18. Defaults to the latest release."
required: false
type: string
upload:
description: "Upload to App Store Connect. Off means build and sign only."
required: false
default: true
type: boolean
permissions:
contents: read
jobs:
build:
runs-on: macos-26
steps:
- name: Resolve the tag
id: tag
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
REPO: ${{ github.repository }}
run: |
TAG="${{ inputs.tag }}"
if [ -z "$TAG" ]; then
TAG=$(gh release view --repo "$REPO" --json tagName --jq .tagName)
fi
echo "tag=$TAG" >> "$GITHUB_OUTPUT"
echo "Building $TAG for the App Store"
- uses: actions/checkout@v7
with:
ref: ${{ steps.tag.outputs.tag }}
- uses: actions/setup-node@v6
with:
node-version: 26
- uses: pnpm/action-setup@v6
with:
version: 10
- name: Install Rust
uses: dtolnay/rust-toolchain@stable
with:
targets: aarch64-apple-darwin,x86_64-apple-darwin
- uses: swatinem/rust-cache@v2
with:
workspaces: src-tauri -> target
- name: Install frontend dependencies
run: pnpm install --frozen-lockfile
- name: Provision Google credentials
env:
GOOGLE_CREDENTIALS: ${{ secrets.GOOGLE_CREDENTIALS }}
run: |
if [ -n "$GOOGLE_CREDENTIALS" ]; then
printf '%s' "$GOOGLE_CREDENTIALS" > google-credentials.json
else
echo "::error::GOOGLE_CREDENTIALS is not set; an App Store build with placeholder credentials would ship a broken backup feature."
exit 1
fi
- name: Import the App Store certificates
env:
APP_CERT: ${{ secrets.MAS_APP_CERTIFICATE }}
APP_CERT_PASSWORD: ${{ secrets.MAS_APP_CERTIFICATE_PASSWORD }}
INSTALLER_CERT: ${{ secrets.MAS_INSTALLER_CERTIFICATE }}
INSTALLER_CERT_PASSWORD: ${{ secrets.MAS_INSTALLER_CERTIFICATE_PASSWORD }}
PROFILE: ${{ secrets.MAS_PROVISION_PROFILE }}
run: |
keychain="$RUNNER_TEMP/appstore.keychain-db"
password=$(uuidgen)
security create-keychain -p "$password" "$keychain"
security set-keychain-settings -lut 3600 "$keychain"
security unlock-keychain -p "$password" "$keychain"
import_p12() {
printf '%s' "$1" | base64 --decode > "$RUNNER_TEMP/cert.p12"
security import "$RUNNER_TEMP/cert.p12" -k "$keychain" -P "$2" \
-T /usr/bin/codesign -T /usr/bin/productbuild
rm -f "$RUNNER_TEMP/cert.p12"
}
import_p12 "$APP_CERT" "$APP_CERT_PASSWORD"
import_p12 "$INSTALLER_CERT" "$INSTALLER_CERT_PASSWORD"
# Without this, codesign on a headless runner blocks on a keychain prompt nobody can
# answer and the job hangs until it times out.
security set-key-partition-list -S apple-tool:,apple: -k "$password" "$keychain" > /dev/null
security list-keychains -d user -s "$keychain" login.keychain-db
printf '%s' "$PROFILE" | base64 --decode > "$RUNNER_TEMP/margin.provisionprofile"
security find-identity -v "$keychain"
- name: Build the sandboxed bundle
run: |
# No APPLE_SIGNING_IDENTITY here on purpose: mas-package.sh signs, because the
# provisioning profile has to be inside the bundle before codesign runs.
pnpm tauri build --target universal-apple-darwin --config src-tauri/tauri.appstore.conf.json --bundles app
- name: Sign, package and upload
env:
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
MAS_APP_IDENTITY: ${{ secrets.MAS_APP_IDENTITY }}
MAS_INSTALLER_IDENTITY: ${{ secrets.MAS_INSTALLER_IDENTITY }}
MAS_PROVISION_PROFILE: ${{ runner.temp }}/margin.provisionprofile
MAS_BUILD_NUMBER: ${{ github.run_number }}
APPLE_API_KEY_ID: ${{ secrets.APPLE_API_KEY_ID }}
APPLE_API_ISSUER: ${{ secrets.APPLE_API_ISSUER }}
APPLE_API_KEY_P8: ${{ secrets.APPLE_API_KEY_P8 }}
MAS_UPLOAD: ${{ inputs.upload && '1' || '' }}
run: |
mkdir -p ~/private_keys
printf '%s' "$APPLE_API_KEY_P8" | base64 --decode > ~/private_keys/AuthKey_$APPLE_API_KEY_ID.p8
chmod 600 ~/private_keys/AuthKey_$APPLE_API_KEY_ID.p8
./scripts/mas-package.sh
- uses: actions/upload-artifact@v4
if: always()
with:
name: margin-appstore-pkg
path: target-mas/*.pkg
if-no-files-found: warn