feat: add macOS ARM64 build workflow and script

- Introduced a new GitHub Actions workflow for building Tauri applications on macOS Apple Silicon (aarch64).
- Created a script to facilitate local execution of the macOS ARM64 build process, including handling secrets and environment variables.
- Updated existing workflows to replace the APPLE_APP_SPECIFIC_PASSWORD with a more generic APPLE_PASSWORD for consistency.
- Modified example secrets file to reflect the new password structure.
This commit is contained in:
Steven Enamakel
2026-03-26 17:33:08 -07:00
parent 94f0575a44
commit 52ba6b9614
5 changed files with 321 additions and 28 deletions
+108
View File
@@ -0,0 +1,108 @@
# Standalone macOS Apple Silicon (aarch64) Tauri build — no prepare-release / tagging.
#
# GitHub: workflow_dispatch checks out the ref. Local act: ACT=true skips checkout (use -b bind + run
# ./scripts/run-macos-arm64-build.sh so submodules are initialized before act copies the tree).
name: macOS ARM64 build
on:
workflow_dispatch:
permissions:
contents: read
jobs:
build-macos-arm64:
name: Build macOS aarch64 (signed)
runs-on: macos-latest
env:
GITHUB_TOKEN: ${{ secrets.XGH_TOKEN || github.token }}
steps:
- name: Checkout repository
if: ${{ env.ACT != 'true' }}
uses: actions/checkout@v4
- name: Set Xcode version
uses: maxim-lobanov/setup-xcode@v1
with:
xcode-version: latest-stable
- name: Setup Node.js 24.x
uses: actions/setup-node@v4
with:
node-version: 24.x
cache: yarn
- name: Install Rust stable
uses: dtolnay/rust-toolchain@stable
with:
targets: aarch64-apple-darwin
- name: Cargo.lock fingerprint (deps only)
id: cargo-lock-fingerprint
shell: bash
run: |
echo "hash=$(tail -n +8 src-tauri/Cargo.lock | openssl dgst -sha256 | awk '{print $2}')" >> "$GITHUB_OUTPUT"
- name: Cache Cargo registry and git sources
uses: actions/cache@v4
with:
path: |
~/.cargo/registry
~/.cargo/git
key: ${{ runner.os }}-cargo-registry-${{ steps.cargo-lock-fingerprint.outputs.hash }}
restore-keys: |
${{ runner.os }}-cargo-registry-
- name: Install dependencies
run: yarn install --frozen-lockfile
- name: Install skills dependencies
run: cd skills && yarn install --frozen-lockfile
- name: Build skills
run: cd skills && yarn build
- name: Define Tauri configuration overrides
id: config-overrides
uses: actions/github-script@v7
env:
BASE_URL: ${{ vars.BASE_URL }}
UPDATER_PUBLIC_KEY: ${{ secrets.UPDATER_PUBLIC_KEY }}
WITH_UPDATER: 'true'
with:
github-token: ${{ secrets.XGH_TOKEN || github.token }}
script: |
const workspacePath = process.env.GITHUB_WORKSPACE.replace(/\\/g, '/');
const prefix = workspacePath.startsWith('/') ? 'file://' : 'file:///';
const moduleUrl = `${prefix}${workspacePath}/scripts/prepareTauriConfig.js`;
const { default: prepareTauriConfig } = await import(moduleUrl);
const config = prepareTauriConfig();
core.setOutput('json', JSON.stringify(config));
- name: Build frontend
run: yarn build
env:
NODE_ENV: production
VITE_BACKEND_URL: ${{ vars.VITE_BACKEND_URL }}
VITE_SENTRY_DSN: ${{ vars.VITE_SENTRY_DSN }}
VITE_DEBUG: ${{ vars.VITE_DEBUG }}
- name: Build, package (no GitHub release upload)
uses: tauri-apps/tauri-action@v0.6.2
env:
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE_BASE64 }}
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }}
APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
BASE_URL: ${{ vars.BASE_URL }}
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
WITH_UPDATER: 'true'
MACOSX_DEPLOYMENT_TARGET: '10.15'
with:
args: -c ${{ steps.config-overrides.outputs.json }} --target aarch64-apple-darwin
includeDebug: false
includeRelease: true
includeUpdaterJson: false
+1 -1
View File
@@ -305,7 +305,7 @@ jobs:
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }}
APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }}
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
BASE_URL: ${{ vars.BASE_URL }}
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
+3 -6
View File
@@ -1,17 +1,14 @@
{
"secrets": {
"APPLE_APP_SPECIFIC_PASSWORD": "",
"APPLE_CERTIFICATE_BASE64": "",
"APPLE_CERTIFICATE_PASSWORD": "",
"APPLE_ID": "",
"APPLE_PASSWORD": "",
"APPLE_SIGNING_IDENTITY": "",
"APPLE_TEAM_ID": "",
"GITHUB_TOKEN": "",
"UPDATER_GIST_ID": "",
"UPDATER_GIST_URL": "",
"UPDATER_PRIVATE_KEY_PASSWORD": "",
"UPDATER_PRIVATE_KEY": "",
"UPDATER_PUBLIC_KEY": "",
"TAURI_SIGNING_PRIVATE_KEY_PASSWORD": "",
"TAURI_SIGNING_PRIVATE_KEY": "",
"XGH_TOKEN": "",
"XGITHUB_APP_ID": "",
"XGITHUB_APP_PRIVATE_KEY": ""
+143
View File
@@ -0,0 +1,143 @@
#!/usr/bin/env bash
# Run the standalone macOS ARM64 Tauri build via nektos/act (self-hosted = your Mac).
# No prepare-release, no tagging, no GitHub release upload (includeUpdaterJson: false).
#
# Usage:
# ./scripts/run-macos-arm64-build.sh # dry-run
# ./scripts/run-macos-arm64-build.sh --run # full signed build on this machine
#
# Requires: act, jq, scripts/ci-secrets.json (copy from ci-secrets.example.json)
set -euo pipefail
cd "$(git rev-parse --show-toplevel)"
WORKFLOW=".github/workflows/macos-arm64-build.yml"
SECRETS_JSON="scripts/ci-secrets.json"
RUN_MODE="dryrun"
while [[ $# -gt 0 ]]; do
case "$1" in
--run)
RUN_MODE="run"
shift
;;
--dryrun|-n)
RUN_MODE="dryrun"
shift
;;
--secrets-json)
SECRETS_JSON="${2:-}"
shift 2
;;
*)
echo "Unknown argument: $1" >&2
exit 1
;;
esac
done
if [[ ! -f "$SECRETS_JSON" ]]; then
echo "Secrets JSON not found: $SECRETS_JSON" >&2
exit 1
fi
if ! command -v act >/dev/null 2>&1; then
echo "act is required. Install with: brew install act" >&2
exit 1
fi
if ! command -v jq >/dev/null 2>&1; then
echo "jq is required. Install with: brew install jq" >&2
exit 1
fi
SECRETS_FILE="$(mktemp)"
VARS_FILE="$(mktemp)"
EVENT_JSON="$(mktemp)"
MERGED_SECRETS="$(mktemp)"
trap 'rm -f "$SECRETS_FILE" "$VARS_FILE" "$EVENT_JSON" "$MERGED_SECRETS"' EXIT
jq '
.secrets |= (
. + {
APPLE_APP_SPECIFIC_PASSWORD: (
if (.APPLE_APP_SPECIFIC_PASSWORD // "") | length > 0 then .APPLE_APP_SPECIFIC_PASSWORD
else (.APPLE_PASSWORD // "") end
)
}
)
' "$SECRETS_JSON" > "$MERGED_SECRETS"
jq -r '
def dotenv_escape:
gsub("\""; "\\\"") | gsub("\r"; "\\r") | gsub("\n"; "\\n");
(.secrets // {}) | to_entries[] | select(.key != "GITHUB_TOKEN") | "\(.key)=\"\(.value | dotenv_escape)\""
' "$MERGED_SECRETS" > "$SECRETS_FILE"
jq -r '
def dotenv_escape:
gsub("\""; "\\\"") | gsub("\r"; "\\r") | gsub("\n"; "\\n");
(.vars // {}) | to_entries[] | "\(.key)=\"\(.value | dotenv_escape)\""
' "$SECRETS_JSON" > "$VARS_FILE"
REPO_FULL="${GITHUB_REPOSITORY:-}"
if [[ -z "$REPO_FULL" ]]; then
REPO_FULL="$(git remote get-url origin 2>/dev/null | sed -E 's#^git@github\.com:([^/]+)/([^/.]+)(\.git)?$#\1/\2#; s#^https://github\.com/([^/]+)/([^/.]+)(\.git)?$#\1/\2#')"
fi
if [[ -z "$REPO_FULL" || "$REPO_FULL" != */* ]]; then
echo "Could not resolve GitHub owner/repo (set GITHUB_REPOSITORY or fix git remote origin)" >&2
exit 1
fi
OWNER="${REPO_FULL%%/*}"
REPO_NAME="${REPO_FULL##*/}"
REF="$(git symbolic-ref -q HEAD || true)"
if [[ -z "$REF" ]]; then
REF="refs/heads/main"
fi
jq -n \
--arg ref "$REF" \
--arg full "$REPO_FULL" \
--arg owner "$OWNER" \
--arg name "$REPO_NAME" \
'{
ref: $ref,
inputs: {},
repository: {
full_name: $full,
default_branch: "main",
name: $name,
owner: { login: $owner }
},
sender: { login: "local-dev" }
}' > "$EVENT_JSON"
echo "Workflow: $WORKFLOW"
echo "Secrets: $SECRETS_JSON"
echo "Ref: $REF"
echo "Mode: $RUN_MODE"
echo
# act -b copies the tree without .git — submodules must be materialized here first.
if [[ -d .git ]]; then
echo "Syncing git submodules (required for skills/, etc.)..."
git submodule update --init --recursive
fi
echo
ACT_ARGS=(
workflow_dispatch
-W "$WORKFLOW"
--eventpath "$EVENT_JSON"
--secret-file "$SECRETS_FILE"
--var-file "$VARS_FILE"
-b
-P macos-latest=-self-hosted
)
if [[ "$RUN_MODE" == "dryrun" ]]; then
echo "Dry-run only. Use --run for the full macOS ARM64 build."
act "${ACT_ARGS[@]}" -n
else
act "${ACT_ARGS[@]}"
fi
+66 -21
View File
@@ -5,9 +5,10 @@
# - Uses scripts/ci-secrets.example.json for secrets/vars.
# - Runs in dry-run mode unless --run is passed.
#
# For --run: set XGH_TOKEN in scripts/ci-secrets.json (PAT with repo scope). nektos/act sets
# ACT=true and uses a fake repository name, so the GitHub App token step is skipped and the
# workflow uses XGH_TOKEN instead (see "Resolve release git token" in release.yml).
# For --run: set XGH_TOKEN in scripts/ci-secrets.json (PAT with repo scope). prepare-release uses
# XGH_TOKEN for checkout/push. Do not put a bad GITHUB_TOKEN in ci-secrets.json — act uses it to
# clone action repos and an invalid PAT breaks even public clones. github-script steps use
# secrets.XGH_TOKEN (see release.yml).
#
# Usage:
# ./scripts/test-release-act.sh
@@ -16,6 +17,9 @@
# ./scripts/test-release-act.sh --job prepare-release
# ./scripts/test-release-act.sh --release-type minor
# ./scripts/test-release-act.sh --secrets-json scripts/ci-secrets.json --run
# # Single macOS (Apple Silicon) build for signing — pass through to act --matrix:
# ./scripts/test-release-act.sh --run --job build-artifacts \
# --matrix 'settings.platform:macos-latest' --matrix 'settings.args:--target aarch64-apple-darwin'
set -euo pipefail
cd "$(git rev-parse --show-toplevel)"
@@ -25,6 +29,7 @@ SECRETS_JSON="scripts/ci-secrets.json"
RELEASE_TYPE="patch"
RUN_MODE="dryrun"
JOB_NAME=""
MATRIX_ARGS=()
while [[ $# -gt 0 ]]; do
case "$1" in
@@ -52,6 +57,10 @@ while [[ $# -gt 0 ]]; do
SECRETS_JSON="${2:-}"
shift 2
;;
--matrix)
MATRIX_ARGS+=(--matrix "${2:-}")
shift 2
;;
*)
echo "Unknown argument: $1" >&2
exit 1
@@ -90,36 +99,64 @@ fi
SECRETS_FILE="$(mktemp)"
VARS_FILE="$(mktemp)"
EVENT_JSON="$(mktemp)"
trap 'rm -f "$SECRETS_FILE" "$VARS_FILE" "$EVENT_JSON"' EXIT
MERGED_SECRETS="$(mktemp)"
trap 'rm -f "$SECRETS_FILE" "$VARS_FILE" "$EVENT_JSON" "$MERGED_SECRETS"' EXIT
# Merge defaults: APPLE_APP_SPECIFIC_PASSWORD (APPLE_PASSWORD is a common alias).
# Do not put GITHUB_TOKEN in the act secret file — an invalid PAT breaks act's clone of public actions.
jq '
.secrets |= (
. + {
APPLE_APP_SPECIFIC_PASSWORD: (
if (.APPLE_APP_SPECIFIC_PASSWORD // "") | length > 0 then .APPLE_APP_SPECIFIC_PASSWORD
else (.APPLE_PASSWORD // "") end
)
}
)
' "$SECRETS_JSON" > "$MERGED_SECRETS"
# act --secret-file/--var-file expect dotenv format. Unquoted multiline values break the
# parser (PEM/private keys look like extra KEY= lines and trigger errors on '/' etc.).
jq -r '
def dotenv_escape:
gsub("\""; "\\\"") | gsub("\r"; "\\r") | gsub("\n"; "\\n");
(.secrets // {}) | to_entries[] | "\(.key)=\"\(.value | dotenv_escape)\""
' "$SECRETS_JSON" > "$SECRETS_FILE"
(.secrets // {}) | to_entries[] | select(.key != "GITHUB_TOKEN") | "\(.key)=\"\(.value | dotenv_escape)\""
' "$MERGED_SECRETS" > "$SECRETS_FILE"
jq -r '
def dotenv_escape:
gsub("\""; "\\\"") | gsub("\r"; "\\r") | gsub("\n"; "\\n");
(.vars // {}) | to_entries[] | "\(.key)=\"\(.value | dotenv_escape)\""
' "$SECRETS_JSON" > "$VARS_FILE"
cat > "$EVENT_JSON" <<EOF
{
"ref": "refs/heads/main",
"inputs": {
"release_type": "$RELEASE_TYPE"
},
"repository": {
"full_name": "local/openhuman",
"default_branch": "main",
"name": "openhuman",
"owner": { "login": "local" }
},
"sender": { "login": "local-dev" }
}
EOF
# Use real owner/repo from git so context.repo and tauri-action match your fork (not local/openhuman).
REPO_FULL="${GITHUB_REPOSITORY:-}"
if [[ -z "$REPO_FULL" ]]; then
REPO_FULL="$(git remote get-url origin 2>/dev/null | sed -E 's#^git@github\.com:([^/]+)/([^/.]+)(\.git)?$#\1/\2#; s#^https://github\.com/([^/]+)/([^/.]+)(\.git)?$#\1/\2#')"
fi
if [[ -z "$REPO_FULL" || "$REPO_FULL" != */* ]]; then
echo "Could not resolve GitHub owner/repo (set GITHUB_REPOSITORY or fix git remote origin)" >&2
exit 1
fi
OWNER="${REPO_FULL%%/*}"
REPO_NAME="${REPO_FULL##*/}"
jq -n \
--arg ref "refs/heads/main" \
--arg rt "$RELEASE_TYPE" \
--arg full "$REPO_FULL" \
--arg owner "$OWNER" \
--arg name "$REPO_NAME" \
'{
ref: $ref,
inputs: { release_type: $rt },
repository: {
full_name: $full,
default_branch: "main",
name: $name,
owner: { login: $owner }
},
sender: { login: "local-dev" }
}' > "$EVENT_JSON"
echo "Workflow: $WORKFLOW"
echo "Secrets: $SECRETS_JSON"
@@ -128,6 +165,9 @@ echo "Mode: $RUN_MODE"
if [[ -n "$JOB_NAME" ]]; then
echo "Job: $JOB_NAME"
fi
if [[ ${#MATRIX_ARGS[@]} -gt 0 ]]; then
echo "Matrix: ${MATRIX_ARGS[*]}"
fi
echo
ACT_ARGS=(
@@ -136,6 +176,7 @@ ACT_ARGS=(
--eventpath "$EVENT_JSON"
--secret-file "$SECRETS_FILE"
--var-file "$VARS_FILE"
--container-architecture linux/amd64
-P ubuntu-latest=catthehacker/ubuntu:act-latest
-P ubuntu-22.04=catthehacker/ubuntu:act-22.04
-P macos-latest=-self-hosted
@@ -145,6 +186,10 @@ if [[ -n "$JOB_NAME" ]]; then
ACT_ARGS+=(-j "$JOB_NAME")
fi
if [[ ${#MATRIX_ARGS[@]} -gt 0 ]]; then
ACT_ARGS+=("${MATRIX_ARGS[@]}")
fi
if [[ "$RUN_MODE" == "dryrun" ]]; then
echo "Dry-run only. Use --run to execute."
act "${ACT_ARGS[@]}" -n