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@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 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@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 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_output="$(xcodebuild -version)" IFS= read -r xcode_version <<< "$xcode_version_output" 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@v7 with: node-version: 22 cache: npm - name: Install dependencies run: npm ci --legacy-peer-deps - name: Verify stable API and Capacitor iOS CORS shell: bash run: | set -euo pipefail api_base="${IOS_DEBUG_API_URL%/}" curl --fail --silent --show-error --location \ --connect-timeout 10 --max-time 20 \ --header 'Accept: application/json' \ --output /dev/null \ "$api_base/ping" cors_headers="$RUNNER_TEMP/ios-debug-cors-headers.txt" cors_body="$RUNNER_TEMP/ios-debug-cors-body.txt" cors_status="$(curl --silent --show-error \ --connect-timeout 10 --max-time 20 \ --request OPTIONS \ --header 'Origin: capacitor://localhost' \ --header 'Access-Control-Request-Method: POST' \ --header 'Access-Control-Request-Headers: authorization,content-type' \ --dump-header "$cors_headers" \ --output "$cors_body" \ --write-out '%{http_code}' \ "$api_base/ping")" if [[ ! "$cors_status" =~ ^2[0-9][0-9]$ ]]; then echo "Stable API rejected the Capacitor iOS CORS preflight with HTTP $cors_status" >&2 sed -n '1,20p' "$cors_body" >&2 exit 1 fi if ! grep -Eiq '^access-control-allow-origin:[[:space:]]*capacitor://localhost[[:space:]]*$' "$cors_headers"; then echo "Stable API did not allow the exact capacitor://localhost origin" >&2 exit 1 fi if ! grep -Eiq '^access-control-allow-credentials:[[:space:]]*true[[:space:]]*$' "$cors_headers"; then echo "Stable API did not allow credentialed Capacitor requests" >&2 exit 1 fi if ! grep -Eiq '^access-control-allow-methods:.*[[:space:],]POST([[:space:],]|$)' "$cors_headers"; then echo "Stable API did not allow POST from the Capacitor origin" >&2 exit 1 fi if ! grep -Eiq '^access-control-allow-headers:.*[[:space:],]Authorization([[:space:],]|$)' "$cors_headers"; then echo "Stable API did not allow the Authorization header from the Capacitor origin" >&2 exit 1 fi if ! grep -Eiq '^access-control-allow-headers:.*[[:space:],]Content-Type([[:space:],]|$)' "$cors_headers"; then echo "Stable API did not allow the Content-Type header from the Capacitor origin" >&2 exit 1 fi - 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@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 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"