Skip to content

feat(release): add macOS TestFlight pipeline and target #1

feat(release): add macOS TestFlight pipeline and target

feat(release): add macOS TestFlight pipeline and target #1

---
name: Native macOS TestFlight
'on':
push:
tags:
- 'macos-v*'
workflow_dispatch:
inputs:
git_ref:
description: "Git ref to build (branch, tag, or SHA)"
required: false
default: ""
upload_to_testflight:
description: "Upload exported package to TestFlight"
required: true
type: boolean
default: true
permissions:
contents: read
concurrency:
group: native-macos-testflight-${{ github.ref }}
cancel-in-progress: false
jobs:
build-and-upload:
name: Build signed macOS package
runs-on: macos-15
env:
APPLE_TEAM_ID: MM5YXC7T6E
BUNDLE_ID: com.shinycomputers.everycodecompanion.macos
SCHEME: CodeNativeMac
XCODE_PROJECT: native/CodeNativeMac/CodeNativeMac.xcodeproj
ARCHIVE_PATH: /tmp/ecc-macos/EveryCodeCompanion.xcarchive
EXPORT_PATH: /tmp/ecc-macos/export
EXPORT_OPTIONS_PATH: /tmp/ecc-macos/ExportOptions.plist
PROFILE_PATH: /tmp/ecc-macos/every-code-companion-macos.provisionprofile
steps:
- name: Resolve checkout ref
id: resolve_ref
run: |
set -euo pipefail
ref="$GITHUB_REF"
if [ "${{ github.event_name }}" = "workflow_dispatch" ] \
&& [ -n "${{ github.event.inputs.git_ref }}" ]; then
ref="${{ github.event.inputs.git_ref }}"
fi
echo "value=$ref" >> "$GITHUB_OUTPUT"
- name: Prepare build directories
run: |
set -euo pipefail
mkdir -p "$(dirname "$ARCHIVE_PATH")"
mkdir -p "$EXPORT_PATH"
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
ref: ${{ steps.resolve_ref.outputs.value }}
- name: Generate macOS project
run: |
set -euo pipefail
cd native/CodeNativeMac
xcodegen generate
- name: Select Xcode
uses: maxim-lobanov/setup-xcode@v1
with:
xcode-version: latest-stable
- name: Validate required secrets
env:
IOS_DIST_CERT_P12_BASE64: ${{ secrets.IOS_DIST_CERT_P12_BASE64 }}
IOS_DIST_CERT_PASSWORD: ${{ secrets.IOS_DIST_CERT_PASSWORD }}
MACOS_APPSTORE_PROFILE_BASE64:
${{ secrets.MACOS_APPSTORE_PROFILE_BASE64 }}
MACOS_APPSTORE_PROFILE_NAME:
${{ secrets.MACOS_APPSTORE_PROFILE_NAME }}
APP_STORE_CONNECT_KEY_ID: ${{ secrets.APP_STORE_CONNECT_KEY_ID }}
APP_STORE_CONNECT_ISSUER_ID:
${{ secrets.APP_STORE_CONNECT_ISSUER_ID }}
APP_STORE_CONNECT_PRIVATE_KEY:
${{ secrets.APP_STORE_CONNECT_PRIVATE_KEY }}
run: |
set -euo pipefail
missing=()
for name in \
IOS_DIST_CERT_P12_BASE64 \
IOS_DIST_CERT_PASSWORD \
MACOS_APPSTORE_PROFILE_BASE64 \
MACOS_APPSTORE_PROFILE_NAME \
APP_STORE_CONNECT_KEY_ID \
APP_STORE_CONNECT_ISSUER_ID \
APP_STORE_CONNECT_PRIVATE_KEY; do
if [ -z "${!name:-}" ]; then
missing+=("$name")
fi
done
if [ "${#missing[@]}" -gt 0 ]; then
printf 'Missing required secrets:\n' >&2
printf ' - %s\n' "${missing[@]}" >&2
exit 1
fi
- name: Import Apple Distribution certificate
uses: apple-actions/import-codesign-certs@v3
with:
p12-file-base64: ${{ secrets.IOS_DIST_CERT_P12_BASE64 }}
p12-password: ${{ secrets.IOS_DIST_CERT_PASSWORD }}
- name: Install Mac App Store provisioning profile
env:
MACOS_APPSTORE_PROFILE_BASE64:
${{ secrets.MACOS_APPSTORE_PROFILE_BASE64 }}
run: |
set -euo pipefail
mkdir -p "$HOME/Library/MobileDevice/Provisioning Profiles"
echo "$MACOS_APPSTORE_PROFILE_BASE64" | base64 --decode \
> "$PROFILE_PATH"
profile_plist=$(security cms -D -i "$PROFILE_PATH")
profile_uuid=$(
/usr/libexec/PlistBuddy -c 'Print UUID' /dev/stdin \
<<<"$profile_plist"
)
profiles_dir="$HOME/Library/MobileDevice/Provisioning Profiles"
cp "$PROFILE_PATH" "$profiles_dir/$profile_uuid.provisionprofile"
- name: Write export options
env:
MACOS_APPSTORE_PROFILE_NAME:
${{ secrets.MACOS_APPSTORE_PROFILE_NAME }}
run: |
set -euo pipefail
cat > "$EXPORT_OPTIONS_PATH" <<EOF
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>destination</key>
<string>export</string>
<key>method</key>
<string>app-store-connect</string>
<key>signingStyle</key>
<string>manual</string>
<key>stripSwiftSymbols</key>
<true/>
<key>teamID</key>
<string>${APPLE_TEAM_ID}</string>
<key>provisioningProfiles</key>
<dict>
<key>${BUNDLE_ID}</key>
<string>${MACOS_APPSTORE_PROFILE_NAME}</string>
</dict>
</dict>
</plist>
EOF
- name: Archive macOS app
env:
MACOS_APPSTORE_PROFILE_NAME:
${{ secrets.MACOS_APPSTORE_PROFILE_NAME }}
run: |
set -euo pipefail
xcodebuild \
-project "$XCODE_PROJECT" \
-scheme "$SCHEME" \
-configuration Release \
-destination "generic/platform=macOS" \
-archivePath "$ARCHIVE_PATH" \
DEVELOPMENT_TEAM="$APPLE_TEAM_ID" \
PRODUCT_BUNDLE_IDENTIFIER="$BUNDLE_ID" \
CODE_SIGN_STYLE=Manual \
CODE_SIGN_IDENTITY="Apple Distribution" \
PROVISIONING_PROFILE_SPECIFIER="$MACOS_APPSTORE_PROFILE_NAME" \
clean archive
- name: Export package
run: |
set -euo pipefail
xcodebuild \
-exportArchive \
-archivePath "$ARCHIVE_PATH" \
-exportPath "$EXPORT_PATH" \
-exportOptionsPlist "$EXPORT_OPTIONS_PATH"
pkg_path=$(find "$EXPORT_PATH" -maxdepth 1 -name '*.pkg' -print -quit)
if [ -z "$pkg_path" ]; then
echo "No PKG produced by export" >&2
exit 1
fi
mv "$pkg_path" "$EXPORT_PATH/EveryCodeCompanion.pkg"
- name: Install App Store Connect API key
env:
APP_STORE_CONNECT_KEY_ID: ${{ secrets.APP_STORE_CONNECT_KEY_ID }}
APP_STORE_CONNECT_PRIVATE_KEY:
${{ secrets.APP_STORE_CONNECT_PRIVATE_KEY }}
run: |
set -euo pipefail
key_dir="$HOME/.appstoreconnect/private_keys"
key_path="$key_dir/AuthKey_${APP_STORE_CONNECT_KEY_ID}.p8"
mkdir -p "$key_dir"
printf '%s' "$APP_STORE_CONNECT_PRIVATE_KEY" > "$key_path"
chmod 600 "$key_path"
- name: Upload package artifact
uses: actions/upload-artifact@v4
with:
name: EveryCodeCompanion-macos-pkg
path: |
${{ env.EXPORT_PATH }}/EveryCodeCompanion.pkg
${{ env.ARCHIVE_PATH }}/dSYMs
if-no-files-found: error
- name: Upload to TestFlight
if: >-
${{ github.event_name == 'push'
|| github.event.inputs.upload_to_testflight == 'true' }}
env:
APP_STORE_CONNECT_KEY_ID: ${{ secrets.APP_STORE_CONNECT_KEY_ID }}
APP_STORE_CONNECT_ISSUER_ID:
${{ secrets.APP_STORE_CONNECT_ISSUER_ID }}
run: |
set -euo pipefail
log_path=/tmp/ecc-macos-upload.log
xcrun altool --output-format xml \
--upload-app \
--file "$EXPORT_PATH/EveryCodeCompanion.pkg" \
--type macos \
--apiKey "$APP_STORE_CONNECT_KEY_ID" \
--apiIssuer "$APP_STORE_CONNECT_ISSUER_ID" \
| tee "$log_path"
if rg -q "UPLOAD FAILED|Validation failed| ERROR:" "$log_path"; then
echo "Detected upload failure markers in altool output" >&2
exit 1
fi
if ! rg -q "UPLOAD SUCCEEDED with no errors" "$log_path"; then
echo "Upload result did not include success marker" >&2
exit 1
fi