diff --git a/.github/actions/ios-install-cert/action.yml b/.github/actions/ios-install-cert/action.yml index 8ebf5d1..eb4355b 100644 --- a/.github/actions/ios-install-cert/action.yml +++ b/.github/actions/ios-install-cert/action.yml @@ -1,11 +1,23 @@ name: Install iOS Signing Certificate description: Installs an iOS signing certificate (.p12) into a temporary keychain for code signing +inputs: + p12Base64: + description: Base64-encoded .p12 certificate + required: true + p12Password: + description: Password for the .p12 certificate + required: true + keychainPath: + description: Path for the temporary keychain. Defaults to $RUNNER_TEMP/build.keychain-db. + required: false + default: "" runs: using: composite steps: - name: Announce certificate installation shell: bash run: | + set -euo pipefail echo "::notice title=🔏 [SIGN] Install iOS Signing Certificate::Importing .p12 into temporary keychain — value suppressed" echo "CHECKPOINT_1_STATUS=⏳ Pending" >> "$GITHUB_ENV" @@ -14,15 +26,21 @@ runs: env: IOS_P12_BASE64: ${{ inputs.p12Base64 }} IOS_P12_PASSWORD: ${{ inputs.p12Password }} + KEYCHAIN_PATH_INPUT: ${{ inputs.keychainPath }} run: | - echo "::group::🔏 [CHECKPOINT 1/1] Install Signing Certificate" set -euo pipefail + echo "::group::🔏 [CHECKPOINT 1/1] Install Signing Certificate" echo "::add-mask::${IOS_P12_PASSWORD}" - KEYCHAIN_PATH="$RUNNER_TEMP/build.keychain-db" + KEYCHAIN_PATH="${KEYCHAIN_PATH_INPUT:-"$RUNNER_TEMP/build.keychain-db"}" KEYCHAIN_PASS="ci-pass" - echo "$IOS_P12_BASE64" | base64 -D > "$RUNNER_TEMP/cert.p12" + printf '%s' "${IOS_P12_BASE64}" | base64 -D > "$RUNNER_TEMP/cert.p12" + + if [[ ! -s "$RUNNER_TEMP/cert.p12" ]]; then + echo "ERROR: base64 decoding produced an empty file. Check that p12Base64 is a valid base64-encoded .p12 file." + exit 1 + fi security create-keychain -p "$KEYCHAIN_PASS" "$KEYCHAIN_PATH" security set-keychain-settings -lut 21600 "$KEYCHAIN_PATH" @@ -44,19 +62,15 @@ runs: echo "=== Available code signing identities ===" security find-identity -v -p codesigning + + echo "KEYCHAIN_PATH=${KEYCHAIN_PATH}" >> "$GITHUB_ENV" echo "CHECKPOINT_1_STATUS=✅ PASSED" >> "$GITHUB_ENV" - echo "::notice title=✅ [SIGN] Certificate installed::Signing identity imported into keychain at $KEYCHAIN_PATH" + echo "::notice title=✅ [SIGN] Certificate installed::Signing identity imported into keychain at ${KEYCHAIN_PATH}" echo "::endgroup::" - name: Report failure if: failure() shell: bash run: | - echo "::error title=❌ [SIGN] Certificate installation failed::Failed to import .p12 into keychain. Check that p12Base64 is valid base64 and p12Password is correct." -inputs: - p12Base64: - description: Base64-encoded .p12 certificate - required: true - p12Password: - description: Password for the .p12 certificate - required: true + set -euo pipefail + echo "::error title=❌ [SIGN] Certificate installation failed::Failed to import .p12 into keychain. Check that p12Base64 is valid base64 and p12Password is correct." \ No newline at end of file diff --git a/.github/actions/ios-install-profile/action.yml b/.github/actions/ios-install-profile/action.yml index 0466216..d4a5723 100644 --- a/.github/actions/ios-install-profile/action.yml +++ b/.github/actions/ios-install-profile/action.yml @@ -1,11 +1,16 @@ name: Install iOS Provisioning Profile description: Installs an iOS provisioning profile from a base64-encoded string +inputs: + profileBase64: + description: Base64-encoded provisioning profile + required: true runs: using: composite steps: - name: Announce provisioning profile installation shell: bash run: | + set -euo pipefail echo "::notice title=🔏 [SIGN] Install iOS Provisioning Profile::Installing .mobileprovision from base64 input" echo "CHECKPOINT_1_STATUS=⏳ Pending" >> "$GITHUB_ENV" @@ -14,24 +19,35 @@ runs: env: IOS_MOBILEPROVISION_BASE64: ${{ inputs.profileBase64 }} run: | - echo "::group::🔏 [CHECKPOINT 1/1] Install Provisioning Profile" set -euo pipefail + echo "::group::🔏 [CHECKPOINT 1/1] Install Provisioning Profile" mkdir -p "$HOME/Library/MobileDevice/Provisioning Profiles" printf '%s' "${IOS_MOBILEPROVISION_BASE64}" | base64 -D > "$RUNNER_TEMP/profile.mobileprovision" + + if [[ ! -s "$RUNNER_TEMP/profile.mobileprovision" ]]; then + echo "ERROR: base64 decoding produced an empty file. Check that profileBase64 is a valid base64-encoded .mobileprovision file." + exit 1 + fi + security cms -D -i "$RUNNER_TEMP/profile.mobileprovision" > "$RUNNER_TEMP/profile.plist" UUID=$(/usr/libexec/PlistBuddy -c 'Print :UUID' "$RUNNER_TEMP/profile.plist") NAME=$(/usr/libexec/PlistBuddy -c 'Print :Name' "$RUNNER_TEMP/profile.plist") - echo "Profile UUID: $UUID" - echo "Profile Name: $NAME" + if [[ -z "${UUID}" ]]; then + echo "ERROR: Could not extract UUID from provisioning profile. The profile may be malformed." + exit 1 + fi + + echo "Profile UUID: ${UUID}" + echo "Profile Name: ${NAME}" cp "$RUNNER_TEMP/profile.mobileprovision" \ - "$HOME/Library/MobileDevice/Provisioning Profiles/$UUID.mobileprovision" + "$HOME/Library/MobileDevice/Provisioning Profiles/${UUID}.mobileprovision" - echo "IOS_PROFILE_UUID=$UUID" >> "$GITHUB_ENV" - echo "IOS_PROFILE_NAME=$NAME" >> "$GITHUB_ENV" + echo "IOS_PROFILE_UUID=${UUID}" >> "$GITHUB_ENV" + echo "IOS_PROFILE_NAME=${NAME}" >> "$GITHUB_ENV" echo "CHECKPOINT_1_STATUS=✅ PASSED" >> "$GITHUB_ENV" echo "::notice title=✅ [SIGN] Profile installed::Name: ${NAME}, UUID: ${UUID}" echo "::endgroup::" @@ -40,8 +56,5 @@ runs: if: failure() shell: bash run: | - echo "::error title=❌ [SIGN] Provisioning profile installation failed::Failed to decode or install provisioning profile. Check that profileBase64 is a valid base64-encoded .mobileprovision file." -inputs: - profileBase64: - description: Base64-encoded provisioning profile - required: true + set -euo pipefail + echo "::error title=❌ [SIGN] Provisioning profile installation failed::Failed to decode or install provisioning profile. Check that profileBase64 is a valid base64-encoded .mobileprovision file." \ No newline at end of file diff --git a/.github/actions/xcode-build/action.yml b/.github/actions/xcode-build/action.yml index b274b56..3610649 100644 --- a/.github/actions/xcode-build/action.yml +++ b/.github/actions/xcode-build/action.yml @@ -19,37 +19,29 @@ inputs: required: false description: Xcode destination (e.g., generic/platform=iOS). default: generic/platform=iOS - signingStyle: required: false description: "manual or automatic" default: manual - developmentTeam: required: false - description: Apple Developer Team ID (e.g., ABCDE12345). Required for manual signing. - + description: Apple Developer Team ID. Required for manual signing. codeSignIdentity: required: false - description: Code signing identity for manual signing, e.g. "Apple Distribution" or "Apple Development". - + description: Code signing identity for manual signing. provisioningProfileUuid: required: false - description: Provisioning profile UUID (CI-only, binds profile to app target like Azure DevOps). - + description: Provisioning profile UUID. bundleId: required: false description: Bundle identifier override (PRODUCT_BUNDLE_IDENTIFIER), optional. - keychainPath: required: false - description: Path to keychain that contains signing identity (e.g. $RUNNER_TEMP/build.keychain-db). Strongly recommended in CI. - + description: Path to keychain that contains signing identity. derivedDataPath: required: false description: Optional derived data path. default: "" - debugBuildSettings: required: false description: "true/false: print build settings before archive" @@ -60,89 +52,118 @@ runs: steps: - name: Announce Xcode build shell: bash + env: + SCHEME: ${{ inputs.scheme }} + CONFIGURATION: ${{ inputs.configuration }} + SIGNING_STYLE: ${{ inputs.signingStyle }} + WORKSPACE: ${{ inputs.workspace }} + ARCHIVE_PATH: ${{ inputs.archivePath }} run: | - echo "::notice title=🍎 [IOS] Build and Archive::scheme: ${{ inputs.scheme }}, config: ${{ inputs.configuration }}, signing: ${{ inputs.signingStyle }}" - echo "🔨 [IOS] Workspace: ${{ inputs.workspace }}, archive: ${{ inputs.archivePath }}" + set -euo pipefail + echo "::notice title=🍎 [IOS] Build and Archive::scheme: ${SCHEME}, config: ${CONFIGURATION}, signing: ${SIGNING_STYLE}" + echo "🔨 [IOS] Workspace: ${WORKSPACE}, archive: ${ARCHIVE_PATH}" echo "CHECKPOINT_1_STATUS=⏳ Pending" >> "$GITHUB_ENV" - name: Build and archive shell: bash + env: + WORKSPACE: ${{ inputs.workspace }} + SCHEME: ${{ inputs.scheme }} + CONFIGURATION: ${{ inputs.configuration }} + DESTINATION: ${{ inputs.destination }} + ARCHIVE_PATH: ${{ inputs.archivePath }} + SIGNING_STYLE: ${{ inputs.signingStyle }} + DEVELOPMENT_TEAM: ${{ inputs.developmentTeam }} + CODE_SIGN_IDENTITY_INPUT: ${{ inputs.codeSignIdentity }} + PROVISIONING_PROFILE_UUID: ${{ inputs.provisioningProfileUuid }} + BUNDLE_ID: ${{ inputs.bundleId }} + KEYCHAIN_PATH_INPUT: ${{ inputs.keychainPath }} + DERIVED_DATA_PATH: ${{ inputs.derivedDataPath }} + DEBUG_BUILD_SETTINGS: ${{ inputs.debugBuildSettings }} run: | - echo "::group::🍎 [CHECKPOINT 1/1] Build and Archive" set -euo pipefail + echo "::group::🍎 [CHECKPOINT 1/1] Build and Archive" ARGS=( - -workspace "${{ inputs.workspace }}" - -scheme "${{ inputs.scheme }}" - -configuration "${{ inputs.configuration }}" + -workspace "${WORKSPACE}" + -scheme "${SCHEME}" + -configuration "${CONFIGURATION}" -sdk iphoneos - -destination "${{ inputs.destination }}" - -archivePath "${{ inputs.archivePath }}" + -destination "${DESTINATION}" + -archivePath "${ARCHIVE_PATH}" archive + -parallelizeTargets + COMPILER_INDEX_STORE_ENABLE=NO + -hideShellScriptEnvironment -showBuildTimingSummary ) - if [[ -n "${{ inputs.derivedDataPath }}" ]]; then - ARGS+=(-derivedDataPath "${{ inputs.derivedDataPath }}") + if [[ -n "${DERIVED_DATA_PATH}" ]]; then + ARGS+=(-derivedDataPath "${DERIVED_DATA_PATH}") fi - if [[ "${{ inputs.signingStyle }}" == "automatic" ]]; then + if [[ "${SIGNING_STYLE}" == "automatic" ]]; then ARGS+=(-allowProvisioningUpdates) else - if [[ -z "${{ inputs.developmentTeam }}" ]]; then - echo "ERROR: inputs.developmentTeam is required for manual signing" + if [[ -z "${DEVELOPMENT_TEAM}" ]]; then + echo "ERROR: developmentTeam is required for manual signing" exit 1 fi ARGS+=( CODE_SIGN_STYLE=Manual - DEVELOPMENT_TEAM="${{ inputs.developmentTeam }}" + DEVELOPMENT_TEAM="${DEVELOPMENT_TEAM}" ) - if [[ -n "${{ inputs.codeSignIdentity }}" ]]; then - ARGS+=(CODE_SIGN_IDENTITY="${{ inputs.codeSignIdentity }}") + if [[ -n "${CODE_SIGN_IDENTITY_INPUT}" ]]; then + ARGS+=(CODE_SIGN_IDENTITY="${CODE_SIGN_IDENTITY_INPUT}") fi - # 🔑 THIS IS THE AZURE DEVOPS BEHAVIOR - # UUID-based profile binding is safe for Pods - if [[ -n "${{ inputs.provisioningProfileUuid }}" ]]; then - ARGS+=(PROVISIONING_PROFILE="${{ inputs.provisioningProfileUuid }}") + if [[ -n "${PROVISIONING_PROFILE_UUID}" ]]; then + ARGS+=(PROVISIONING_PROFILE="${PROVISIONING_PROFILE_UUID}") fi - if [[ -n "${{ inputs.bundleId }}" ]]; then - ARGS+=(PRODUCT_BUNDLE_IDENTIFIER="${{ inputs.bundleId }}") + if [[ -n "${BUNDLE_ID}" ]]; then + ARGS+=(PRODUCT_BUNDLE_IDENTIFIER="${BUNDLE_ID}") fi - if [[ -n "${{ inputs.keychainPath }}" ]]; then - ARGS+=(OTHER_CODE_SIGN_FLAGS="--keychain ${{ inputs.keychainPath }}") + if [[ -n "${KEYCHAIN_PATH_INPUT}" ]]; then + ARGS+=(OTHER_CODE_SIGN_FLAGS="--keychain ${KEYCHAIN_PATH_INPUT}") fi fi - if [[ "${{ inputs.debugBuildSettings }}" == "true" ]]; then + if [[ "${DEBUG_BUILD_SETTINGS}" == "true" ]]; then echo "=== Build settings (summary) ===" xcodebuild \ - -workspace "${{ inputs.workspace }}" \ - -scheme "${{ inputs.scheme }}" \ - -configuration "${{ inputs.configuration }}" \ + -workspace "${WORKSPACE}" \ + -scheme "${SCHEME}" \ + -configuration "${CONFIGURATION}" \ -sdk iphoneos \ - -showBuildSettings | egrep -i \ + -showBuildSettings | grep -Ei \ 'CODE_SIGN|DEVELOPMENT_TEAM|PROVISION|PRODUCT_BUNDLE_IDENTIFIER' || true fi - echo "🔨 Starting xcodebuild archive for scheme '${{ inputs.scheme }}' (configuration: ${{ inputs.configuration }})..." + echo "🔨 Starting xcodebuild archive for scheme '${SCHEME}' (configuration: ${CONFIGURATION})..." if command -v xcpretty >/dev/null 2>&1; then xcodebuild "${ARGS[@]}" | xcpretty --color + exit "${PIPESTATUS[0]}" else echo "WARN: xcpretty not found; falling back to raw xcodebuild output" xcodebuild "${ARGS[@]}" fi + echo "CHECKPOINT_1_STATUS=✅ PASSED" >> "$GITHUB_ENV" - echo "::notice title=✅ [IOS] Archive created::${{ inputs.archivePath }}" + echo "::notice title=✅ [IOS] Archive created::${ARCHIVE_PATH}" echo "::endgroup::" - name: Report failure if: failure() shell: bash + env: + SCHEME: ${{ inputs.scheme }} + CONFIGURATION: ${{ inputs.configuration }} + SIGNING_STYLE: ${{ inputs.signingStyle }} run: | - echo "::error title=❌ [IOS] Build failed::scheme: ${{ inputs.scheme }}, config: ${{ inputs.configuration }}, signing: ${{ inputs.signingStyle }}. Checkpoints — 1) Archive: ${CHECKPOINT_1_STATUS:-⏭️ Not reached}. Check provisioning profile UUID and keychain path." + set -euo pipefail + echo "::error title=❌ [IOS] Build failed::scheme: ${SCHEME}, config: ${CONFIGURATION}, signing: ${SIGNING_STYLE}. Checkpoints — 1) Archive: ${CHECKPOINT_1_STATUS:-⏭️ Not reached}. Check provisioning profile UUID and keychain path." \ No newline at end of file diff --git a/.github/actions/xcode-export/action.yml b/.github/actions/xcode-export/action.yml index c737fa1..6ae7d3e 100644 --- a/.github/actions/xcode-export/action.yml +++ b/.github/actions/xcode-export/action.yml @@ -16,27 +16,44 @@ runs: steps: - name: Announce IPA export shell: bash + env: + ARCHIVE_PATH: ${{ inputs.archivePath }} + EXPORT_PLIST: ${{ inputs.exportOptionsPlist }} + EXPORT_PATH: ${{ inputs.exportPath }} run: | - echo "::notice title=🍎 [IOS] Export IPA::Exporting archive to IPA — archive: ${{ inputs.archivePath }}, output: ${{ inputs.exportPath }}" - echo "📋 [IOS] Export options: ${{ inputs.exportOptionsPlist }}" + set -euo pipefail + echo "::notice title=🍎 [IOS] Export IPA::Exporting archive to IPA — archive: ${ARCHIVE_PATH}, output: ${EXPORT_PATH}" + echo "📋 [IOS] Export options: ${EXPORT_PLIST}" echo "CHECKPOINT_1_STATUS=⏳ Pending" >> "$GITHUB_ENV" - name: Export IPA shell: bash + env: + ARCHIVE_PATH: ${{ inputs.archivePath }} + EXPORT_PLIST: ${{ inputs.exportOptionsPlist }} + EXPORT_PATH: ${{ inputs.exportPath }} run: | + set -euo pipefail echo "::group::🍎 [CHECKPOINT 1/1] Export IPA" - mkdir -p "${{ inputs.exportPath }}" + if [[ ! -d "${ARCHIVE_PATH}" ]]; then + echo "ERROR: Archive not found at '${ARCHIVE_PATH}'" + exit 1 + fi + mkdir -p "${EXPORT_PATH}" xcodebuild \ -exportArchive \ - -archivePath "${{ inputs.archivePath }}" \ - -exportOptionsPlist "${{ inputs.exportOptionsPlist }}" \ - -exportPath "${{ inputs.exportPath }}" + -archivePath "${ARCHIVE_PATH}" \ + -exportOptionsPlist "${EXPORT_PLIST}" \ + -exportPath "${EXPORT_PATH}" echo "CHECKPOINT_1_STATUS=✅ PASSED" >> "$GITHUB_ENV" - echo "::notice title=✅ [IOS] IPA exported::Output: ${{ inputs.exportPath }}" + echo "::notice title=✅ [IOS] IPA exported::Output: ${EXPORT_PATH}" echo "::endgroup::" - name: Report failure if: failure() shell: bash + env: + ARCHIVE_PATH: ${{ inputs.archivePath }} run: | - echo "::error title=❌ [IOS] IPA export failed::xcodebuild -exportArchive failed for archive '${{ inputs.archivePath }}'. Check exportOptionsPlist path and signing configuration." + set -euo pipefail + echo "::error title=❌ [IOS] IPA export failed::xcodebuild -exportArchive failed for archive '${ARCHIVE_PATH}'. Check exportOptionsPlist path and signing configuration." \ No newline at end of file diff --git a/.github/actions/xcode-setup/action.yml b/.github/actions/xcode-setup/action.yml index 61e4189..2dcd67f 100644 --- a/.github/actions/xcode-setup/action.yml +++ b/.github/actions/xcode-setup/action.yml @@ -1,5 +1,5 @@ -name: Simplify9 Xcode Setup -description: Prepare Xcode environment and CocoaPods for iOS build. +name: Simplify9 Xcode Setup (Legacy) +description: (Legacy) Prepare Xcode environment and CocoaPods for iOS build. author: simplify9 inputs: xcodeVersion: diff --git a/.github/workflows/generic-ios-testflight.yml b/.github/workflows/generic-ios-testflight.yml index 762a524..4451b99 100644 --- a/.github/workflows/generic-ios-testflight.yml +++ b/.github/workflows/generic-ios-testflight.yml @@ -824,24 +824,13 @@ jobs: OTHER_CODE_SIGN_FLAGS="--keychain ${KEYCHAIN_PATH}" \ -parallelizeTargets \ COMPILER_INDEX_STORE_ENABLE=NO \ + CLANG_ENABLE_EXPLICIT_MODULES_WITH_COMPILER_LAUNCHER=YES \ -quiet \ -hideShellScriptEnvironment \ -showBuildTimingSummary echo "CHECKPOINT_2_STATUS=✅ PASSED" >> "$GITHUB_ENV" echo "✅ [CHECKPOINT 2/3] Archive created (manual xcodebuild) — PASSED: ${{ inputs.archive-path }}" - - - name: Show ccache stats after archive - if: ${{ always() && inputs.enable-ccache && inputs.use-manual-inline-archive }} - shell: bash - run: | - set -euo pipefail - - echo "## ccache stats" >> "$GITHUB_STEP_SUMMARY" - echo "" >> "$GITHUB_STEP_SUMMARY" - echo '```text' >> "$GITHUB_STEP_SUMMARY" - ccache --show-stats | tee -a "$GITHUB_STEP_SUMMARY" || true - echo '```' >> "$GITHUB_STEP_SUMMARY" - name: Generate exportOptions.plist if: ${{ inputs.generate-export-options }} @@ -958,6 +947,8 @@ jobs: NODE_VERSION: ${{ inputs.node-version }} REF_NAME: ${{ github.ref_name }} ACTOR: ${{ github.actor }} + ENABLE_CCACHE: ${{ inputs.enable-ccache }} + USE_MANUAL_INLINE_ARCHIVE: ${{ inputs.use-manual-inline-archive }} run: | STATUS=$( [ "${JOB_STATUS}" = "success" ] \ && echo "✅ SUCCESS" || echo "❌ FAILED" ) @@ -983,7 +974,16 @@ jobs: echo "| 2 | Archive created | ${CHECKPOINT_2_STATUS:-⏭️ Not reached} |" echo "| 3 | IPA renamed | ${CHECKPOINT_3_STATUS:-⏭️ Not reached} |" } >> "$GITHUB_STEP_SUMMARY" - + if [[ "${ENABLE_CCACHE}" == "true" && "${USE_MANUAL_INLINE_ARCHIVE}" == "true" ]]; then + { + echo "" + echo "## ⚡ ccache Stats" + echo "" + echo '```text' + ccache --show-stats || true + echo '```' + } >> "$GITHUB_STEP_SUMMARY" + fi release: name: Upload to TestFlight runs-on: ${{ inputs.macos-runner }} @@ -1107,12 +1107,34 @@ jobs: exit 1 fi + IPA_BYTES=$(stat -f%z "${IPA}") + IPA_SIZE_MB=$(awk "BEGIN { printf \"%.2f\", ${IPA_BYTES} / 1048576 }") + echo "→ IPA: ${IPA} (${IPA_SIZE_MB} MB)" + echo "→ Upload started at $(date -u +'%H:%M:%S UTC')" + UPLOAD_START=$(date +%s) + xcrun iTMSTransporter \ -m upload \ -assetFile "${IPA}" \ -apiKey "${{ secrets.appstore-api-key-id }}" \ -apiIssuer "${{ secrets.appstore-issuer-id }}" \ - -v informational + -v informational 2>&1 | while IFS= read -r line; do + if [[ "${line}" =~ ^DBG-X:[[:space:]]+([0-9]+(\.[0-9]+)?)[[:space:]]*%[[:space:]]+of[[:space:]]+([0-9]+(\.[0-9]+)?)[[:space:]]*MB ]]; then + PCT=$(printf '%.0f' "${BASH_REMATCH[1]}") + TOTAL="${BASH_REMATCH[3]}" + DONE_MB=$(awk "BEGIN { printf \"%.2f\", ${PCT} * ${TOTAL} / 100 }") + filled=$(( PCT * 30 / 100 )) + empty=$(( 30 - filled )) + bar='' + for (( i=0; i