Skip to content

Merge remote-tracking branch 'origin' into feat/migration-data-receive #45

Merge remote-tracking branch 'origin' into feat/migration-data-receive

Merge remote-tracking branch 'origin' into feat/migration-data-receive #45

name: Build iOS
on:
push:
branches:
- "**"
paths:
- "apps/mobile/**"
- "packages/app-zum-doc-utils/**"
- "scripts/mobile-sync.sh"
- "apps/mobile/build-metadata.json"
- ".github/workflows/app-zum-doc-mobile-ios-build.yaml"
workflow_call:
outputs:
app_version:
description: Resolved iOS version name
value: ${{ jobs.build.outputs.app_version }}
build_number:
description: Resolved iOS build number
value: ${{ jobs.build.outputs.build_number }}
workflow_dispatch:
jobs:
verify:
runs-on: "ubuntu-24.04" # @sync android.runner
outputs:
app_version: ${{ steps.version.outputs.app_version }}
code: ${{ steps.version.outputs.code }}
steps:
- uses: actions/checkout@v5
- id: version
run: |
set -euo pipefail
chmod +x scripts/mobile-sync.sh
scripts/mobile-sync.sh check
if [[ "${GITHUB_REF_TYPE}" == "tag" ]]; then
scripts/mobile-sync.sh check-tag "${GITHUB_REF_NAME}"
else
echo "not a tag (${GITHUB_REF_TYPE} ${GITHUB_REF_NAME}); skipping tag check"
fi
echo "app_version=$(scripts/mobile-sync.sh store-version)" >> "${GITHUB_OUTPUT}"
echo "code=$(scripts/mobile-sync.sh get code)" >> "${GITHUB_OUTPUT}"
build:
needs: verify
runs-on: "macos-15" # @sync ios.runner
outputs:
app_version: ${{ steps.version.outputs.app_version }}
build_number: ${{ steps.version.outputs.build_number }}
steps:
- name: Checkout
uses: actions/checkout@v5
- name: Select Xcode
env:
XCODE: "26.3" # @sync ios.xcode
run: |
set -euo pipefail
XCODE_APP="/Applications/Xcode_${XCODE}.app"
if [[ ! -d "${XCODE_APP}" ]]; then
echo "Xcode ${XCODE} is not installed on this runner image. Available:"
ls -d /Applications/Xcode*.app
echo "Update ios.xcode or ios.runner in apps/mobile/build-metadata.json."
exit 1
fi
sudo xcode-select -s "${XCODE_APP}/Contents/Developer"
xcodebuild -version
- uses: pnpm/action-setup@v5
with:
version: "11.13.0" # @sync pnpm
- uses: actions/setup-node@v5
with:
node-version: "24" # @sync node
cache: pnpm
- name: Resolve version
id: version
env:
APP_VERSION: ${{ needs.verify.outputs.app_version }}
BUILD_NUMBER: ${{ needs.verify.outputs.code }}
run: |
echo "version=${APP_VERSION} build_number=${BUILD_NUMBER}"
echo "app_version=${APP_VERSION}" >> "${GITHUB_OUTPUT}"
echo "build_number=${BUILD_NUMBER}" >> "${GITHUB_OUTPUT}"
- name: Install Apple certificate and provisioning profile
id: signing
env:
IOS_DIST_CERTIFICATE_BASE64: ${{ secrets.IOS_DIST_CERTIFICATE_BASE64 }}
IOS_DIST_CERTIFICATE_PASSWORD: ${{ secrets.IOS_DIST_CERTIFICATE_PASSWORD }}
IOS_PROVISIONING_PROFILE_BASE64: ${{ secrets.IOS_PROVISIONING_PROFILE_BASE64 }}
run: |
set -euo pipefail
if [[ -z "${IOS_DIST_CERTIFICATE_BASE64}" || -z "${IOS_PROVISIONING_PROFILE_BASE64}" ]]; then
echo "iOS distribution certificate and provisioning profile secrets are required."
exit 1
fi
CERTIFICATE_PATH="${RUNNER_TEMP}/dist_certificate.p12"
PP_PATH="${RUNNER_TEMP}/appstore.mobileprovision"
KEYCHAIN_PATH="${RUNNER_TEMP}/app-signing.keychain-db"
KEYCHAIN_PASSWORD="$(openssl rand -base64 32)"
PROFILE_PLIST="${RUNNER_TEMP}/profile.plist"
echo -n "${IOS_DIST_CERTIFICATE_BASE64}" | base64 --decode -o "${CERTIFICATE_PATH}"
echo -n "${IOS_PROVISIONING_PROFILE_BASE64}" | base64 --decode -o "${PP_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 "${IOS_DIST_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-keychain -d user -s "${KEYCHAIN_PATH}" login.keychain-db
security default-keychain -s "${KEYCHAIN_PATH}"
security find-identity -v -p codesigning "${KEYCHAIN_PATH}" | grep -q "Apple Distribution" || {
echo "No 'Apple Distribution' identity found in the imported .p12."
security find-identity -v -p codesigning "${KEYCHAIN_PATH}"
exit 1
}
security cms -D -i "${PP_PATH}" > "${PROFILE_PLIST}"
UUID="$(plutil -extract UUID raw "${PROFILE_PLIST}")"
PROFILE_NAME="$(plutil -extract Name raw "${PROFILE_PLIST}")"
TEAM_ID="$(plutil -extract TeamIdentifier.0 raw "${PROFILE_PLIST}")"
APP_ID="$(plutil -extract Entitlements.application-identifier raw "${PROFILE_PLIST}")"
BUNDLE_ID="${APP_ID#*.}"
mkdir -p "${HOME}/Library/MobileDevice/Provisioning Profiles"
mkdir -p "${HOME}/Library/Developer/Xcode/UserData/Provisioning Profiles"
cp "${PP_PATH}" "${HOME}/Library/MobileDevice/Provisioning Profiles/${UUID}.mobileprovision"
cp "${PP_PATH}" "${HOME}/Library/Developer/Xcode/UserData/Provisioning Profiles/${UUID}.mobileprovision"
echo "Profile: ${PROFILE_NAME} | Team: ${TEAM_ID} | Bundle: ${BUNDLE_ID}"
echo "profile_name=${PROFILE_NAME}" >> "${GITHUB_OUTPUT}"
echo "profile_uuid=${UUID}" >> "${GITHUB_OUTPUT}"
echo "team_id=${TEAM_ID}" >> "${GITHUB_OUTPUT}"
echo "bundle_id=${BUNDLE_ID}" >> "${GITHUB_OUTPUT}"
- name: Install JavaScript dependencies
run: pnpm install --frozen-lockfile
- name: Prepare packages
env:
EXPO_NO_GIT_STATUS: "1"
LANG: en_US.UTF-8
APP_VERSION: ${{ steps.version.outputs.app_version }}
IOS_BUILD_NUMBER: ${{ steps.version.outputs.build_number }}
IOS_TEAM_ID: ${{ steps.signing.outputs.team_id }}
run: |
pnpm --filter @app-zum-doc/utils run init
pnpm --filter @app-zum-doc/mobile run prebuild:ios
- name: Build signed IPA
env:
LANG: en_US.UTF-8
RCT_NO_LAUNCH_PACKAGER: "1"
PROFILE_NAME: ${{ steps.signing.outputs.profile_name }}
TEAM_ID: ${{ steps.signing.outputs.team_id }}
BUNDLE_ID: ${{ steps.signing.outputs.bundle_id }}
run: |
set -euo pipefail
IOS_DIR="${GITHUB_WORKSPACE}/apps/mobile/ios"
cd "${IOS_DIR}"
find . -maxdepth 2 -name project.pbxproj -not -path './Pods/*' -print0 | xargs -0 sed -i '' \
-e 's/CODE_SIGN_STYLE = Automatic;/CODE_SIGN_STYLE = Manual;/g'
WORKSPACE="$(find . -maxdepth 1 -name '*.xcworkspace' | head -n 1)"
if [[ -z "${WORKSPACE}" ]]; then
echo "No Xcode workspace found after prebuild."
exit 1
fi
SCHEME="$(xcodebuild -list -json -workspace "${WORKSPACE}" | python3 -c '
import json, sys
data = json.load(sys.stdin)
schemes = data.get("workspace", {}).get("schemes", [])
for scheme in schemes:
if scheme.lower() != "pods":
print(scheme)
break
')"
if [[ -z "${SCHEME}" ]]; then
echo "No Xcode scheme found after prebuild."
exit 1
fi
ARCHIVE_PATH="${RUNNER_TEMP}/AppZumDoc.xcarchive"
EXPORT_PATH="${RUNNER_TEMP}/ios-export"
EXPORT_OPTIONS="${RUNNER_TEMP}/ExportOptions.plist"
rm -rf "${ARCHIVE_PATH}" "${EXPORT_PATH}"
mkdir -p "${EXPORT_PATH}"
cat > "${EXPORT_OPTIONS}" <<PLIST
<?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>method</key>
<string>app-store-connect</string>
<key>destination</key>
<string>export</string>
<key>signingStyle</key>
<string>manual</string>
<key>teamID</key>
<string>${TEAM_ID}</string>
<key>compileBitcode</key>
<false/>
<key>stripSwiftSymbols</key>
<true/>
<key>uploadSymbols</key>
<true/>
<key>provisioningProfiles</key>
<dict>
<key>${BUNDLE_ID}</key>
<string>${PROFILE_NAME}</string>
</dict>
</dict>
</plist>
PLIST
xcodebuild archive \
-workspace "${WORKSPACE}" \
-scheme "${SCHEME}" \
-configuration Release \
-destination "generic/platform=iOS" \
-archivePath "${ARCHIVE_PATH}" \
CODE_SIGN_STYLE=Manual \
CODE_SIGN_IDENTITY="Apple Distribution" \
DEVELOPMENT_TEAM="${TEAM_ID}" \
PROVISIONING_PROFILE_SPECIFIER="${PROFILE_NAME}"
xcodebuild -exportArchive \
-archivePath "${ARCHIVE_PATH}" \
-exportOptionsPlist "${EXPORT_OPTIONS}" \
-exportPath "${EXPORT_PATH}"
IPA="$(find "${EXPORT_PATH}" -name '*.ipa' | head -n 1)"
if [[ -z "${IPA}" ]]; then
echo "Export produced no IPA."
exit 1
fi
mv "${IPA}" "${EXPORT_PATH}/AppZumDoc.ipa"
codesign -dv --verbose=2 "${EXPORT_PATH}/AppZumDoc.ipa" 2>&1 || true
- name: Verify IPA version
env:
EXPECTED_VERSION: ${{ steps.version.outputs.app_version }}
EXPECTED_BUILD: ${{ steps.version.outputs.build_number }}
run: |
set -euo pipefail
unzip -p "${RUNNER_TEMP}/ios-export/AppZumDoc.ipa" 'Payload/*.app/Info.plist' > "${RUNNER_TEMP}/Info.plist"
GOT_VERSION="$(plutil -extract CFBundleShortVersionString raw "${RUNNER_TEMP}/Info.plist")"
GOT_BUILD="$(plutil -extract CFBundleVersion raw "${RUNNER_TEMP}/Info.plist")"
echo "IPA: ${GOT_VERSION} (${GOT_BUILD}) — expected ${EXPECTED_VERSION} (${EXPECTED_BUILD})"
[[ "${GOT_VERSION}" == "${EXPECTED_VERSION}" ]] || { echo "CFBundleShortVersionString mismatch"; exit 1; }
[[ "${GOT_BUILD}" == "${EXPECTED_BUILD}" ]] || { echo "CFBundleVersion mismatch"; exit 1; }
- name: Clean up keychain and provisioning profile
if: ${{ always() }}
run: |
security delete-keychain "${RUNNER_TEMP}/app-signing.keychain-db" || true
rm -f "${HOME}/Library/MobileDevice/Provisioning Profiles/"*.mobileprovision || true
- name: Upload IPA
uses: actions/upload-artifact@v6
with:
name: ios-ipa
path: ${{ runner.temp }}/ios-export/AppZumDoc.ipa
if-no-files-found: error