mirror of
https://github.com/priyanshujain/margin.git
synced 2026-10-02 11:07:04 +00:00
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
131 lines
4.7 KiB
YAML
131 lines
4.7 KiB
YAML
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
|