Add signed iOS device debug workflow (#179)
Add a protected development-signing workflow, isolated debug app identity, Linux USB device tooling, documentation, and focused validation coverage.
This commit is contained in:
@@ -0,0 +1,574 @@
|
||||
name: iOS Device Debug IPA
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
source_ref:
|
||||
description: Same-repository branch, tag, or commit to build
|
||||
required: true
|
||||
default: master
|
||||
type: string
|
||||
expected_sha:
|
||||
description: Full 40-character SHA that source_ref must resolve to
|
||||
required: true
|
||||
type: string
|
||||
confirmation:
|
||||
description: Type SIGN IOS DEBUG IPA
|
||||
required: true
|
||||
type: string
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: ios-device-debug-${{ github.run_id }}
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
resolve:
|
||||
name: Resolve and verify source
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 10
|
||||
outputs:
|
||||
source_sha: ${{ steps.resolve.outputs.source_sha }}
|
||||
steps:
|
||||
- name: Validate dispatch confirmation
|
||||
shell: bash
|
||||
env:
|
||||
CONFIRMATION: ${{ inputs.confirmation }}
|
||||
EXPECTED_SHA: ${{ inputs.expected_sha }}
|
||||
SOURCE_REF: ${{ inputs.source_ref }}
|
||||
WORKFLOW_REF: ${{ github.ref }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
if [[ "$WORKFLOW_REF" != "refs/heads/master" ]]; then
|
||||
echo "The signing workflow must be dispatched from the master workflow ref" >&2
|
||||
exit 1
|
||||
fi
|
||||
if [[ "$CONFIRMATION" != "SIGN IOS DEBUG IPA" ]]; then
|
||||
echo "confirmation must exactly match SIGN IOS DEBUG IPA" >&2
|
||||
exit 1
|
||||
fi
|
||||
if [[ ! "$EXPECTED_SHA" =~ ^[0-9a-fA-F]{40}$ ]]; then
|
||||
echo "expected_sha must be a full 40-character commit SHA" >&2
|
||||
exit 1
|
||||
fi
|
||||
if [[ -z "$SOURCE_REF" || "$SOURCE_REF" =~ [[:space:]] ]]; then
|
||||
echo "source_ref must be non-empty and contain no whitespace" >&2
|
||||
exit 1
|
||||
fi
|
||||
if [[ "$SOURCE_REF" == refs/pull/* || "$SOURCE_REF" == pull/* ]]; then
|
||||
echo "Pull-request refs are not eligible for device-debug signing" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Checkout same-repository history
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
|
||||
- name: Resolve immutable commit
|
||||
id: resolve
|
||||
shell: bash
|
||||
env:
|
||||
EXPECTED_SHA: ${{ inputs.expected_sha }}
|
||||
SOURCE_REF: ${{ inputs.source_ref }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
expected_sha="${EXPECTED_SHA,,}"
|
||||
|
||||
if [[ "$SOURCE_REF" =~ ^[0-9a-fA-F]{7,40}$ ]]; then
|
||||
candidate="$SOURCE_REF"
|
||||
elif [[ "$SOURCE_REF" == refs/heads/* ]]; then
|
||||
candidate="refs/remotes/origin/${SOURCE_REF#refs/heads/}"
|
||||
elif [[ "$SOURCE_REF" == refs/tags/* ]]; then
|
||||
candidate="$SOURCE_REF"
|
||||
elif git show-ref --verify --quiet "refs/remotes/origin/$SOURCE_REF"; then
|
||||
candidate="refs/remotes/origin/$SOURCE_REF"
|
||||
elif git show-ref --verify --quiet "refs/tags/$SOURCE_REF"; then
|
||||
candidate="refs/tags/$SOURCE_REF"
|
||||
else
|
||||
echo "source_ref does not identify a same-repository branch, tag, or fetched commit" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
source_sha="$(git rev-parse --verify "${candidate}^{commit}" 2>/dev/null || true)"
|
||||
source_sha="${source_sha,,}"
|
||||
if [[ ! "$source_sha" =~ ^[0-9a-f]{40}$ ]]; then
|
||||
echo "source_ref could not be resolved to a commit" >&2
|
||||
exit 1
|
||||
fi
|
||||
if [[ "$source_sha" != "$expected_sha" ]]; then
|
||||
echo "source_ref resolved to a SHA different from expected_sha" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
reachable=false
|
||||
while IFS= read -r repository_ref; do
|
||||
if git merge-base --is-ancestor "$source_sha" "$repository_ref" 2>/dev/null; then
|
||||
reachable=true
|
||||
break
|
||||
fi
|
||||
done < <(git for-each-ref --format='%(refname)' refs/remotes/origin refs/tags)
|
||||
if [[ "$reachable" != true ]]; then
|
||||
echo "The requested commit is not reachable from a same-repository branch or tag" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "source_sha=$source_sha" >> "$GITHUB_OUTPUT"
|
||||
echo "Resolved source_ref to $source_sha" >> "$GITHUB_STEP_SUMMARY"
|
||||
|
||||
build:
|
||||
name: Build development-signed IPA
|
||||
needs: resolve
|
||||
runs-on: macos-26
|
||||
timeout-minutes: 90
|
||||
environment:
|
||||
name: mobile-device-debug
|
||||
env:
|
||||
IOS_PROJECT_PATH: ios/App/App.xcodeproj
|
||||
IOS_SCHEME: App
|
||||
IOS_DEBUG_BUNDLE_ID: ${{ vars.IOS_DEBUG_BUNDLE_ID || 'io.truckwash.app.debug' }}
|
||||
IOS_DEBUG_API_URL: ${{ vars.IOS_DEBUG_API_URL || 'https://api-v2.truckwash.io/master/api' }}
|
||||
APPLE_TEAM_ID: ${{ vars.APPLE_TEAM_ID }}
|
||||
MOBILE_VERSION_NAME: 0.0.${{ github.run_number }}
|
||||
RESOLVED_SOURCE_SHA: ${{ needs.resolve.outputs.source_sha }}
|
||||
steps:
|
||||
- name: Checkout resolved source
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
ref: ${{ needs.resolve.outputs.source_sha }}
|
||||
fetch-depth: 1
|
||||
persist-credentials: false
|
||||
|
||||
- name: Verify runner and resolve build number
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
xcode_version="$(xcodebuild -version | head -n 1)"
|
||||
xcode_major="$(awk '{split($2, version, "."); print version[1]}' <<< "$xcode_version")"
|
||||
if [[ ! "$xcode_major" =~ ^[0-9]+$ ]] || (( xcode_major < 26 )); then
|
||||
echo "Xcode 26 or newer is required; found $xcode_version" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
build_number="$((10#$GITHUB_RUN_NUMBER * 100 + 10#$GITHUB_RUN_ATTEMPT))"
|
||||
if [[ ! "$build_number" =~ ^[1-9][0-9]{0,17}$ ]]; then
|
||||
echo "Derived build number is outside Apple's supported integer format" >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "MOBILE_VERSION_CODE=$build_number" >> "$GITHUB_ENV"
|
||||
echo "XCODE_VERSION=$xcode_version" >> "$GITHUB_ENV"
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v5
|
||||
with:
|
||||
node-version: 22
|
||||
cache: npm
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci --legacy-peer-deps
|
||||
|
||||
- name: Build stable production web payload
|
||||
env:
|
||||
RELEASE_COMMIT_SHA: ${{ env.RESOLVED_SOURCE_SHA }}
|
||||
VITE_API_URL: ${{ env.IOS_DEBUG_API_URL }}
|
||||
VITE_RELEASE_MANAGER_CONTROL_API_URL: ${{ env.IOS_DEBUG_API_URL }}
|
||||
VITE_RELEASE_PUBLIC_GATEWAY_API_URL: https://api-v2.truckwash.io
|
||||
run: |
|
||||
npm run build
|
||||
node -e "const manifest = require('./dist/release-manifest.json'); if (manifest.commit_sha !== process.env.RESOLVED_SOURCE_SHA) { throw new Error('Web release manifest source SHA mismatch'); }"
|
||||
|
||||
- name: Sync and validate iOS shell
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
npx cap sync ios
|
||||
npm run mobile:permissions:check
|
||||
if grep -q 'isa = PBXShellScriptBuildPhase;' "$IOS_PROJECT_PATH/project.pbxproj"; then
|
||||
echo "Unexpected Xcode shell-script build phase detected" >&2
|
||||
exit 1
|
||||
fi
|
||||
xcodebuild -resolvePackageDependencies -project "$IOS_PROJECT_PATH" -scheme "$IOS_SCHEME"
|
||||
|
||||
- name: Validate native Debug and Release settings
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
debug_settings="$RUNNER_TEMP/ios-debug-build-settings.txt"
|
||||
release_settings="$RUNNER_TEMP/ios-release-build-settings.txt"
|
||||
xcodebuild -showBuildSettings \
|
||||
-project "$IOS_PROJECT_PATH" \
|
||||
-scheme "$IOS_SCHEME" \
|
||||
-configuration Debug \
|
||||
CODE_SIGNING_ALLOWED=NO > "$debug_settings"
|
||||
xcodebuild -showBuildSettings \
|
||||
-project "$IOS_PROJECT_PATH" \
|
||||
-scheme "$IOS_SCHEME" \
|
||||
-configuration Release \
|
||||
CODE_SIGNING_ALLOWED=NO > "$release_settings"
|
||||
|
||||
grep -Eq '^[[:space:]]*PRODUCT_BUNDLE_IDENTIFIER = io\.truckwash\.app\.debug$' "$debug_settings"
|
||||
grep -Eq '^[[:space:]]*APP_DISPLAY_NAME = Truck Wash Debug$' "$debug_settings"
|
||||
grep -Eq '^[[:space:]]*PRODUCT_NAME = TruckWashDebug$' "$debug_settings"
|
||||
grep -Eq '^[[:space:]]*CAPACITOR_DEBUG = true$' "$debug_settings"
|
||||
grep -Eq '^[[:space:]]*DEBUG_INFORMATION_FORMAT = dwarf-with-dsym$' "$debug_settings"
|
||||
grep -Eq '^[[:space:]]*IPHONEOS_DEPLOYMENT_TARGET = 15\.0$' "$debug_settings"
|
||||
grep -Eq '^[[:space:]]*PRODUCT_BUNDLE_IDENTIFIER = io\.truckwash\.app$' "$release_settings"
|
||||
grep -Eq '^[[:space:]]*APP_DISPLAY_NAME = Truck Wash$' "$release_settings"
|
||||
grep -Eq '^[[:space:]]*PRODUCT_NAME = App$' "$release_settings"
|
||||
|
||||
- name: Install and validate Apple development signing assets
|
||||
shell: bash
|
||||
env:
|
||||
IOS_DEBUG_CERTIFICATE_BASE64: ${{ secrets.IOS_DEBUG_CERTIFICATE_BASE64 }}
|
||||
IOS_DEBUG_CERTIFICATE_PASSWORD: ${{ secrets.IOS_DEBUG_CERTIFICATE_PASSWORD }}
|
||||
IOS_DEBUG_PROVISION_PROFILE_BASE64: ${{ secrets.IOS_DEBUG_PROVISION_PROFILE_BASE64 }}
|
||||
IOS_DEBUG_ALLOWED_UDIDS: ${{ secrets.IOS_DEBUG_ALLOWED_UDIDS }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
node scripts/mobile/check-ios-debug-signing-env.mjs
|
||||
|
||||
certificate_path="$RUNNER_TEMP/ios-debug-development.p12"
|
||||
profile_path="$RUNNER_TEMP/ios-debug-development.mobileprovision"
|
||||
profile_plist="$RUNNER_TEMP/ios-debug-development-profile.plist"
|
||||
keychain_path="$RUNNER_TEMP/ios-debug-signing.keychain-db"
|
||||
keychain_password="$(openssl rand -base64 48 | tr -d '\n')"
|
||||
echo "::add-mask::$keychain_password"
|
||||
|
||||
node -e "const fs = require('fs'); fs.writeFileSync(process.argv[1], Buffer.from(process.env.IOS_DEBUG_CERTIFICATE_BASE64.replace(/\\s/g, ''), 'base64'))" "$certificate_path"
|
||||
node -e "const fs = require('fs'); fs.writeFileSync(process.argv[1], Buffer.from(process.env.IOS_DEBUG_PROVISION_PROFILE_BASE64.replace(/\\s/g, ''), 'base64'))" "$profile_path"
|
||||
chmod 600 "$certificate_path" "$profile_path"
|
||||
security cms -D -i "$profile_path" > "$profile_plist"
|
||||
|
||||
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_DEBUG_CERTIFICATE_PASSWORD" \
|
||||
-A \
|
||||
-t cert \
|
||||
-f pkcs12 \
|
||||
-k "$keychain_path"
|
||||
security list-keychains -d user -s "$keychain_path" $(security list-keychains -d user | tr -d '"')
|
||||
security set-key-partition-list \
|
||||
-S apple-tool:,apple: \
|
||||
-s \
|
||||
-k "$keychain_password" \
|
||||
"$keychain_path"
|
||||
|
||||
signing_identity_sha="$(security find-identity -v -p codesigning "$keychain_path" | awk '/Apple Development/ {print $2; exit}')"
|
||||
if [[ ! "$signing_identity_sha" =~ ^[0-9A-Fa-f]{40}$ ]]; then
|
||||
echo "The PKCS#12 file does not contain a valid Apple Development signing identity" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
IOS_SIGNING_IDENTITY_SHA="$signing_identity_sha" PROFILE_PLIST="$profile_plist" python3 <<'PY'
|
||||
import datetime
|
||||
import hashlib
|
||||
import os
|
||||
import plistlib
|
||||
import re
|
||||
import sys
|
||||
|
||||
with open(os.environ["PROFILE_PLIST"], "rb") as handle:
|
||||
profile = plistlib.load(handle)
|
||||
|
||||
team_id = os.environ["APPLE_TEAM_ID"]
|
||||
bundle_id = os.environ["IOS_DEBUG_BUNDLE_ID"]
|
||||
entitlements = profile.get("Entitlements", {})
|
||||
allowed = {line.strip() for line in os.environ["IOS_DEBUG_ALLOWED_UDIDS"].splitlines() if line.strip()}
|
||||
provisioned = set(profile.get("ProvisionedDevices", []))
|
||||
expiration = profile.get("ExpirationDate")
|
||||
now = datetime.datetime.now(datetime.timezone.utc)
|
||||
if expiration and expiration.tzinfo is None:
|
||||
expiration = expiration.replace(tzinfo=datetime.timezone.utc)
|
||||
|
||||
checks = {
|
||||
"profile team identifier": team_id in profile.get("TeamIdentifier", []),
|
||||
"application identifier": entitlements.get("application-identifier") == f"{team_id}.{bundle_id}",
|
||||
"entitlement team identifier": entitlements.get("com.apple.developer.team-identifier") == team_id,
|
||||
"development entitlement": entitlements.get("get-task-allow") is True,
|
||||
"profile expiration": expiration is not None and expiration > now,
|
||||
"registered devices": bool(allowed) and allowed <= provisioned,
|
||||
"non-enterprise profile": profile.get("ProvisionsAllDevices") is not True,
|
||||
"developer certificate": bool(profile.get("DeveloperCertificates")),
|
||||
"profile UUID": isinstance(profile.get("UUID"), str) and re.fullmatch(r"[0-9A-Fa-f-]{36}", profile["UUID"]) is not None,
|
||||
"safe profile name": isinstance(profile.get("Name"), str) and not any(char in profile["Name"] for char in "\r\n"),
|
||||
}
|
||||
identity_sha = os.environ["IOS_SIGNING_IDENTITY_SHA"].upper()
|
||||
certificate_hashes = {hashlib.sha1(value).hexdigest().upper() for value in profile.get("DeveloperCertificates", [])}
|
||||
checks["certificate belongs to profile"] = identity_sha in certificate_hashes
|
||||
|
||||
failures = [label for label, passed in checks.items() if not passed]
|
||||
if failures:
|
||||
print("Development provisioning profile validation failed:", file=sys.stderr)
|
||||
for failure in failures:
|
||||
print(f"- {failure}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
PY
|
||||
|
||||
profile_uuid="$(/usr/libexec/PlistBuddy -c 'Print :UUID' "$profile_plist")"
|
||||
profile_name="$(/usr/libexec/PlistBuddy -c 'Print :Name' "$profile_plist")"
|
||||
profile_expiration="$(PROFILE_PLIST="$profile_plist" python3 - <<'PY'
|
||||
import datetime
|
||||
import os
|
||||
import plistlib
|
||||
|
||||
with open(os.environ["PROFILE_PLIST"], "rb") as handle:
|
||||
expiration = plistlib.load(handle)["ExpirationDate"]
|
||||
if expiration.tzinfo is None:
|
||||
expiration = expiration.replace(tzinfo=datetime.timezone.utc)
|
||||
print(expiration.astimezone(datetime.timezone.utc).isoformat().replace("+00:00", "Z"))
|
||||
PY
|
||||
)"
|
||||
profile_install_dir="$HOME/Library/MobileDevice/Provisioning Profiles"
|
||||
profile_install_path="$profile_install_dir/$profile_uuid.mobileprovision"
|
||||
mkdir -p "$profile_install_dir"
|
||||
cp "$profile_path" "$profile_install_path"
|
||||
|
||||
echo "IOS_KEYCHAIN_PATH=$keychain_path" >> "$GITHUB_ENV"
|
||||
echo "IOS_PROFILE_INSTALL_PATH=$profile_install_path" >> "$GITHUB_ENV"
|
||||
echo "IOS_PROFILE_NAME=$profile_name" >> "$GITHUB_ENV"
|
||||
echo "IOS_PROFILE_UUID=$profile_uuid" >> "$GITHUB_ENV"
|
||||
echo "IOS_PROFILE_EXPIRATION=$profile_expiration" >> "$GITHUB_ENV"
|
||||
|
||||
- name: Archive Debug app with Apple Development signing
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
archive_path="$RUNNER_TEMP/TruckWashDebug.xcarchive"
|
||||
xcodebuild \
|
||||
-project "$IOS_PROJECT_PATH" \
|
||||
-scheme "$IOS_SCHEME" \
|
||||
-configuration Debug \
|
||||
-destination "generic/platform=iOS" \
|
||||
-archivePath "$archive_path" \
|
||||
archive \
|
||||
DEVELOPMENT_TEAM="$APPLE_TEAM_ID" \
|
||||
CODE_SIGN_STYLE=Manual \
|
||||
CODE_SIGN_IDENTITY="Apple Development" \
|
||||
PROVISIONING_PROFILE_SPECIFIER="$IOS_PROFILE_NAME" \
|
||||
PRODUCT_BUNDLE_IDENTIFIER="$IOS_DEBUG_BUNDLE_ID" \
|
||||
MARKETING_VERSION="$MOBILE_VERSION_NAME" \
|
||||
CURRENT_PROJECT_VERSION="$MOBILE_VERSION_CODE" \
|
||||
DEBUG_INFORMATION_FORMAT="dwarf-with-dsym" \
|
||||
ONLY_ACTIVE_ARCH=NO
|
||||
echo "IOS_ARCHIVE_PATH=$archive_path" >> "$GITHUB_ENV"
|
||||
|
||||
- name: Export development IPA
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
export_options="$RUNNER_TEMP/ios-debug-ExportOptions.plist"
|
||||
export_path="$RUNNER_TEMP/ios-debug-export"
|
||||
EXPORT_OPTIONS="$export_options" python3 <<'PY'
|
||||
import os
|
||||
import plistlib
|
||||
|
||||
options = {
|
||||
"method": "development",
|
||||
"signingStyle": "manual",
|
||||
"teamID": os.environ["APPLE_TEAM_ID"],
|
||||
"provisioningProfiles": {
|
||||
os.environ["IOS_DEBUG_BUNDLE_ID"]: os.environ["IOS_PROFILE_NAME"],
|
||||
},
|
||||
"stripSwiftSymbols": True,
|
||||
"manageAppVersionAndBuildNumber": False,
|
||||
}
|
||||
with open(os.environ["EXPORT_OPTIONS"], "wb") as handle:
|
||||
plistlib.dump(options, handle)
|
||||
PY
|
||||
xcodebuild \
|
||||
-exportArchive \
|
||||
-archivePath "$IOS_ARCHIVE_PATH" \
|
||||
-exportPath "$export_path" \
|
||||
-exportOptionsPlist "$export_options"
|
||||
|
||||
shopt -s nullglob
|
||||
ipa_files=("$export_path"/*.ipa)
|
||||
if [[ ${#ipa_files[@]} -ne 1 ]]; then
|
||||
echo "Expected exactly one exported IPA; found ${#ipa_files[@]}" >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "IOS_EXPORTED_IPA=${ipa_files[0]}" >> "$GITHUB_ENV"
|
||||
|
||||
- name: Validate exported IPA and embedded signature
|
||||
shell: bash
|
||||
env:
|
||||
IOS_DEBUG_ALLOWED_UDIDS: ${{ secrets.IOS_DEBUG_ALLOWED_UDIDS }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
inspect_dir="$RUNNER_TEMP/ios-debug-inspect"
|
||||
mkdir -p "$inspect_dir"
|
||||
unzip -q "$IOS_EXPORTED_IPA" -d "$inspect_dir"
|
||||
shopt -s nullglob
|
||||
app_bundles=("$inspect_dir"/Payload/*.app)
|
||||
if [[ ${#app_bundles[@]} -ne 1 ]]; then
|
||||
echo "Expected exactly one Payload app; found ${#app_bundles[@]}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
app_path="${app_bundles[0]}"
|
||||
app_info="$app_path/Info.plist"
|
||||
embedded_profile="$RUNNER_TEMP/ios-debug-embedded-profile.plist"
|
||||
signature_entitlements="$RUNNER_TEMP/ios-debug-signature-entitlements.plist"
|
||||
signature_details="$RUNNER_TEMP/ios-debug-signature-details.txt"
|
||||
security cms -D -i "$app_path/embedded.mobileprovision" > "$embedded_profile"
|
||||
codesign --verify --deep --strict "$app_path"
|
||||
codesign -d --entitlements :- "$app_path" > "$signature_entitlements"
|
||||
codesign -dvv "$app_path" > /dev/null 2> "$signature_details"
|
||||
grep -Fq "TeamIdentifier=$APPLE_TEAM_ID" "$signature_details"
|
||||
grep -Eq '^Authority=Apple Development:' "$signature_details"
|
||||
|
||||
executable_name="$(/usr/libexec/PlistBuddy -c 'Print :CFBundleExecutable' "$app_info")"
|
||||
architectures="$(lipo -archs "$app_path/$executable_name")"
|
||||
if [[ " $architectures " != *" arm64 "* ]]; then
|
||||
echo "Exported executable does not contain arm64" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
APP_INFO="$app_info" PROFILE_PLIST="$embedded_profile" SIGNATURE_ENTITLEMENTS="$signature_entitlements" python3 <<'PY'
|
||||
import datetime
|
||||
import os
|
||||
import plistlib
|
||||
import sys
|
||||
|
||||
def load(path):
|
||||
with open(path, "rb") as handle:
|
||||
return plistlib.load(handle)
|
||||
|
||||
info = load(os.environ["APP_INFO"])
|
||||
profile = load(os.environ["PROFILE_PLIST"])
|
||||
signature = load(os.environ["SIGNATURE_ENTITLEMENTS"])
|
||||
profile_entitlements = profile.get("Entitlements", {})
|
||||
team_id = os.environ["APPLE_TEAM_ID"]
|
||||
bundle_id = os.environ["IOS_DEBUG_BUNDLE_ID"]
|
||||
allowed = {line.strip() for line in os.environ["IOS_DEBUG_ALLOWED_UDIDS"].splitlines() if line.strip()}
|
||||
provisioned = set(profile.get("ProvisionedDevices", []))
|
||||
expiration = profile.get("ExpirationDate")
|
||||
now = datetime.datetime.now(datetime.timezone.utc)
|
||||
if expiration and expiration.tzinfo is None:
|
||||
expiration = expiration.replace(tzinfo=datetime.timezone.utc)
|
||||
|
||||
checks = {
|
||||
"bundle identifier": info.get("CFBundleIdentifier") == bundle_id,
|
||||
"display name": info.get("CFBundleDisplayName") == "Truck Wash Debug",
|
||||
"debug executable": info.get("CFBundleExecutable") == "TruckWashDebug",
|
||||
"marketing version": info.get("CFBundleShortVersionString") == os.environ["MOBILE_VERSION_NAME"],
|
||||
"build number": info.get("CFBundleVersion") == os.environ["MOBILE_VERSION_CODE"],
|
||||
"minimum iOS": info.get("MinimumOSVersion") == "15.0",
|
||||
"profile UUID": profile.get("UUID") == os.environ["IOS_PROFILE_UUID"],
|
||||
"profile team": team_id in profile.get("TeamIdentifier", []),
|
||||
"profile application identifier": profile_entitlements.get("application-identifier") == f"{team_id}.{bundle_id}",
|
||||
"development profile": profile_entitlements.get("get-task-allow") is True,
|
||||
"signature application identifier": signature.get("application-identifier") == f"{team_id}.{bundle_id}",
|
||||
"signature team identifier": signature.get("com.apple.developer.team-identifier") == team_id,
|
||||
"debuggable signature": signature.get("get-task-allow") is True,
|
||||
"profile expiration": expiration is not None and expiration > now,
|
||||
"registered devices": bool(allowed) and allowed <= provisioned,
|
||||
"non-enterprise profile": profile.get("ProvisionsAllDevices") is not True,
|
||||
}
|
||||
failures = [label for label, passed in checks.items() if not passed]
|
||||
if failures:
|
||||
print("Exported development IPA validation failed:", file=sys.stderr)
|
||||
for failure in failures:
|
||||
print(f"- {failure}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
PY
|
||||
|
||||
- name: Assemble debug artifact
|
||||
shell: bash
|
||||
env:
|
||||
SOURCE_REF: ${{ inputs.source_ref }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
short_sha="${RESOLVED_SOURCE_SHA:0:12}"
|
||||
artifact_name="truck-wash-debug-${MOBILE_VERSION_NAME}-${short_sha}"
|
||||
artifact_dir="$RUNNER_TEMP/device-debug-artifact"
|
||||
ipa_filename="$artifact_name.ipa"
|
||||
dsym_filename="$artifact_name.dSYM.zip"
|
||||
mkdir -p "$artifact_dir"
|
||||
cp "$IOS_EXPORTED_IPA" "$artifact_dir/$ipa_filename"
|
||||
|
||||
shopt -s nullglob
|
||||
dsym_bundles=("$IOS_ARCHIVE_PATH"/dSYMs/*.dSYM)
|
||||
if [[ ${#dsym_bundles[@]} -eq 0 ]]; then
|
||||
echo "The Debug archive did not contain any dSYM bundles" >&2
|
||||
exit 1
|
||||
fi
|
||||
ditto -c -k --sequesterRsrc --keepParent "$IOS_ARCHIVE_PATH/dSYMs" "$artifact_dir/$dsym_filename"
|
||||
|
||||
capacitor_version="$(node -p "require('./node_modules/@capacitor/core/package.json').version")"
|
||||
BUILT_AT_UTC="$(date -u '+%Y-%m-%dT%H:%M:%SZ')" \
|
||||
CAPACITOR_VERSION="$capacitor_version" \
|
||||
DSYM_FILENAME="$dsym_filename" \
|
||||
IPA_FILENAME="$ipa_filename" \
|
||||
MANIFEST_PATH="$artifact_dir/manifest.json" \
|
||||
python3 <<'PY'
|
||||
import json
|
||||
import os
|
||||
|
||||
manifest = {
|
||||
"schema_version": 1,
|
||||
"repository": os.environ["GITHUB_REPOSITORY"],
|
||||
"source_ref": os.environ["SOURCE_REF"],
|
||||
"source_sha": os.environ["RESOLVED_SOURCE_SHA"],
|
||||
"workflow_run": int(os.environ["GITHUB_RUN_NUMBER"]),
|
||||
"workflow_attempt": int(os.environ["GITHUB_RUN_ATTEMPT"]),
|
||||
"built_at_utc": os.environ["BUILT_AT_UTC"],
|
||||
"api_url": os.environ["IOS_DEBUG_API_URL"],
|
||||
"release_manager_control_api_url": os.environ["IOS_DEBUG_API_URL"],
|
||||
"bundle_id": os.environ["IOS_DEBUG_BUNDLE_ID"],
|
||||
"display_name": "Truck Wash Debug",
|
||||
"executable_name": "TruckWashDebug",
|
||||
"version": os.environ["MOBILE_VERSION_NAME"],
|
||||
"build": os.environ["MOBILE_VERSION_CODE"],
|
||||
"minimum_ios": "15.0",
|
||||
"capacitor_version": os.environ["CAPACITOR_VERSION"],
|
||||
"xcode_version": os.environ["XCODE_VERSION"],
|
||||
"signing_method": "development",
|
||||
"profile_expiration_utc": os.environ["IOS_PROFILE_EXPIRATION"],
|
||||
"ipa_filename": os.environ["IPA_FILENAME"],
|
||||
"dsym_filename": os.environ["DSYM_FILENAME"],
|
||||
}
|
||||
with open(os.environ["MANIFEST_PATH"], "w", encoding="utf-8") as handle:
|
||||
json.dump(manifest, handle, indent=2, sort_keys=True)
|
||||
handle.write("\n")
|
||||
PY
|
||||
|
||||
(
|
||||
cd "$artifact_dir"
|
||||
shasum -a 256 "$ipa_filename" "$dsym_filename" manifest.json > SHA256SUMS
|
||||
)
|
||||
echo "IOS_DEBUG_ARTIFACT_DIR=$artifact_dir" >> "$GITHUB_ENV"
|
||||
echo "IOS_DEBUG_ARTIFACT_NAME=$artifact_name" >> "$GITHUB_ENV"
|
||||
|
||||
- name: Upload device-debug artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: ${{ env.IOS_DEBUG_ARTIFACT_NAME }}
|
||||
path: ${{ env.IOS_DEBUG_ARTIFACT_DIR }}
|
||||
if-no-files-found: error
|
||||
retention-days: 7
|
||||
|
||||
- name: Clean up Apple signing assets
|
||||
if: always()
|
||||
shell: bash
|
||||
run: |
|
||||
if [[ -n "${IOS_KEYCHAIN_PATH:-}" ]]; then
|
||||
security delete-keychain "$IOS_KEYCHAIN_PATH" || true
|
||||
else
|
||||
security delete-keychain "$RUNNER_TEMP/ios-debug-signing.keychain-db" || true
|
||||
fi
|
||||
if [[ -n "${IOS_PROFILE_INSTALL_PATH:-}" ]]; then
|
||||
rm -f "$IOS_PROFILE_INSTALL_PATH"
|
||||
fi
|
||||
rm -f \
|
||||
"$RUNNER_TEMP/ios-debug-development.p12" \
|
||||
"$RUNNER_TEMP/ios-debug-development.mobileprovision" \
|
||||
"$RUNNER_TEMP/ios-debug-development-profile.plist" \
|
||||
"$RUNNER_TEMP/ios-debug-embedded-profile.plist" \
|
||||
"$RUNNER_TEMP/ios-debug-signature-entitlements.plist" \
|
||||
"$RUNNER_TEMP/ios-debug-signature-details.txt"
|
||||
Reference in New Issue
Block a user