diff --git a/.github/workflows/android.preview.build.yml b/.github/workflows/android.preview.build.yml new file mode 100644 index 000000000..31af497d9 --- /dev/null +++ b/.github/workflows/android.preview.build.yml @@ -0,0 +1,104 @@ +name: Notesnook Android Preview Build + +# UNTRUSTED stage. Runs on `pull_request`, so fork code is checked out and +# compiled with a read-only GITHUB_TOKEN and NO repository secrets. The release +# APK is built without any signing secret (the same as before). Firebase +# distribution and the PR comment happen in android.preview.publish.yml, which +# runs in the trusted `workflow_run` context and never executes fork code. +# Because no secrets are exposed here, the build runs automatically for every +# PR (including forks) with no authorization gate. + +on: + pull_request: + types: [opened, reopened, synchronize] + branches: [master, beta] + paths: + - "apps/mobile/**" + - "packages/**" + - ".github/workflows/android.preview.build.yml" + - ".github/workflows/android.preview.publish.yml" + +concurrency: + group: android-preview-${{ github.event.pull_request.number }} + cancel-in-progress: true + +jobs: + build: + runs-on: ubuntu-22.04 + env: + STAGING_BUILD: true + + steps: + - name: Checkout PR code + uses: actions/checkout@v5 + with: + ref: ${{ github.event.pull_request.head.sha }} + + - name: Setup Node + uses: ./.github/actions/setup-node-with-cache + + - name: Use specific Java version for the builds + uses: joschi/setup-jdk@v2 + with: + java-version: "17" + architecture: "x64" + + - name: Free Disk Space (Ubuntu) + uses: jlumbroso/free-disk-space@main + with: + tool-cache: false + android: false + dotnet: true + haskell: true + large-packages: true + docker-images: true + swap-storage: true + + - name: Install node modules + run: | + npm ci --ignore-scripts --prefer-offline --no-audit + npm run bootstrap -- --scope=mobile + + - name: Make Gradlew Executable + run: cd apps/mobile/android && chmod +x ./gradlew + + - name: Get build number + id: build-number + run: echo "timestamp=$(($(date +%s) - 1774851180 ))" >> $GITHUB_OUTPUT + + - name: Check for typescript errors + run: | + npm run tx mobile:build + cd apps/mobile + npx tsc --noEmit + + - name: Build arm64-v8a apk + run: | + cd apps/mobile/android + ./gradlew assembleRelease -PreactNativeArchitectures=arm64-v8a -PstagingReleaseBuild=true -PprBuildNumber=${{ steps.build-number.outputs.timestamp }} + + - name: Stage build artifact + run: | + set -euo pipefail + mkdir -p "$RUNNER_TEMP/artifact" + cp apps/mobile/android/app/build/outputs/apk/release/app-arm64-v8a-release.apk \ + "$RUNNER_TEMP/artifact/app-preview.apk" + { + echo "PR_NUMBER=${{ github.event.pull_request.number }}" + echo "HEAD_SHA=${{ github.event.pull_request.head.sha }}" + } > "$RUNNER_TEMP/artifact/pr-meta.env" + + - name: Upload build artifact + uses: actions/upload-artifact@v4 + with: + name: android-preview-build + path: ${{ runner.temp }}/artifact + if-no-files-found: error + retention-days: 1 + + - name: Upload sourcemaps + uses: actions/upload-artifact@v4 + with: + name: sourcemaps + path: | + apps/mobile/android/app/build/generated/sourcemaps/**/*.map diff --git a/.github/workflows/android.preview.firebase.yml b/.github/workflows/android.preview.firebase.yml deleted file mode 100644 index 33ce24907..000000000 --- a/.github/workflows/android.preview.firebase.yml +++ /dev/null @@ -1,135 +0,0 @@ -name: Notesnook Android Preview - -on: - pull_request_target: - types: [opened, reopened, synchronize] - branches: [master, beta] - paths: - - "apps/mobile/**" - - "packages/**" - - ".github/workflows/android.preview.firebase.yml" - -jobs: - authorize: - environment: ${{ github.event_name == 'pull_request_target' && - github.event.pull_request.head.repo.full_name != github.repository && - 'external' || 'internal' }} - - runs-on: ubuntu-latest - steps: - - run: echo true - - build: - needs: authorize - runs-on: ubuntu-22.04 - env: - STAGING_BUILD: true - - steps: - - name: Checkout - uses: actions/checkout@v5 - with: - ref: ${{ github.event.pull_request.head.sha || github.ref }} - - - name: Setup Node - uses: ./.github/actions/setup-node-with-cache - - - name: Use specific Java version for the builds - uses: joschi/setup-jdk@v2 - with: - java-version: "17" - architecture: "x64" - - - name: Free Disk Space (Ubuntu) - uses: jlumbroso/free-disk-space@main - with: - # this might remove tools that are actually needed, - # if set to "true" but frees about 6 GB - tool-cache: false - - # all of these default to true, but feel free to set to - # "false" if necessary for your workflow - android: false - dotnet: true - haskell: true - large-packages: true - docker-images: true - swap-storage: true - - - name: Install node modules - run: | - npm ci --ignore-scripts --prefer-offline --no-audit - npm run bootstrap -- --scope=mobile - - - name: Make Gradlew Executable - run: cd apps/mobile/android && chmod +x ./gradlew - - - name: Get build number - id: build-number - run: echo "timestamp=$(($(date +%s) - 1774851180 ))" >> $GITHUB_OUTPUT - - - name: Check for typescript errors - run: | - npm run tx mobile:build - cd apps/mobile - npx tsc --noEmit - - - name: Build arm64-v8a apk - run: | - cd apps/mobile/android - ./gradlew assembleRelease -PreactNativeArchitectures=arm64-v8a -PstagingReleaseBuild=true -PprBuildNumber=${{ steps.build-number.outputs.timestamp }} - - - name: Get app version - id: package-version - uses: saionaro/extract-package-version@master - with: - path: apps/mobile - - - name: Publish to firebase CLI - id: firebase-output - uses: wzieba/Firebase-Distribution-Github-Action@v1 - with: - appId: ${{secrets.FIREBASE_APP_ID}} - serviceCredentialsFileContent: ${{ secrets.QA_SERVICE_ACCOUNT }} - groups: testers - file: apps/mobile/android/app/build/outputs/apk/release/app-arm64-v8a-release.apk - releaseNotes: Preview for https://github.com/streetwriters/notesnook/pull/${{github.event.number}} - - - name: Post or update PR comment - uses: actions/github-script@v6 - env: - preview_url: ${{ steps.firebase-output.outputs.TESTING_URI }} - with: - script: | - const marker = ''; - const prNumber = context.issue.number; - const previewUrl = process.env.preview_url || ''; - const body = `${marker}\n**Android App Preview**\n\n${previewUrl || 'Preview URL unavailable — check workflow logs.'}\n\nCommit: ${process.env.GITHUB_SHA}\n`; - const { data: comments } = await github.rest.issues.listComments({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: prNumber, - }); - const existing = comments.find(c => c.body && c.body.includes(marker)); - if (existing) { - await github.rest.issues.updateComment({ - owner: context.repo.owner, - repo: context.repo.repo, - comment_id: existing.id, - body, - }); - } else { - await github.rest.issues.createComment({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: prNumber, - body, - }); - } - - - name: Upload sourcemaps - uses: actions/upload-artifact@v4 - with: - name: sourcemaps - path: | - apps/mobile/android/app/build/generated/sourcemaps/**/*.map diff --git a/.github/workflows/android.preview.publish.yml b/.github/workflows/android.preview.publish.yml new file mode 100644 index 000000000..6bd3d920b --- /dev/null +++ b/.github/workflows/android.preview.publish.yml @@ -0,0 +1,72 @@ +name: Notesnook Android Preview Publish + +# TRUSTED stage. Runs via `workflow_run` after the build workflow finishes, so +# it has the base repo's secrets and a write-scoped token. It downloads the APK +# the build produced and distributes it + posts the PR comment. It never checks +# out or executes fork code. + +on: + workflow_run: + workflows: ["Notesnook Android Preview Build"] + types: [completed] + +jobs: + publish: + if: github.event.workflow_run.conclusion == 'success' + runs-on: ubuntu-latest + timeout-minutes: 20 + + steps: + - name: Download build artifact + uses: actions/download-artifact@v4 + with: + name: android-preview-build + path: ${{ runner.temp }}/artifact + run-id: ${{ github.event.workflow_run.id }} + github-token: ${{ secrets.GITHUB_TOKEN }} + + - name: Load PR metadata + run: cat "$RUNNER_TEMP/artifact/pr-meta.env" >> "$GITHUB_ENV" + + - name: Publish to Firebase + id: firebase-output + uses: wzieba/Firebase-Distribution-Github-Action@v1 + with: + appId: ${{ secrets.FIREBASE_APP_ID }} + serviceCredentialsFileContent: ${{ secrets.QA_SERVICE_ACCOUNT }} + groups: testers + file: ${{ runner.temp }}/artifact/app-preview.apk + releaseNotes: Preview for https://github.com/${{ github.repository }}/pull/${{ env.PR_NUMBER }} + + - name: Post or update PR comment + uses: actions/github-script@v7 + env: + preview_url: ${{ steps.firebase-output.outputs.TESTING_URI }} + with: + script: | + const marker = ''; + const prNumber = Number(process.env.PR_NUMBER); + if (!prNumber) return; + const previewUrl = process.env.preview_url || ''; + const body = `${marker}\n**Android App Preview**\n\n${previewUrl || 'Preview URL unavailable — check workflow logs.'}\n\nCommit: ${process.env.HEAD_SHA}\n`; + const { data: comments } = await github.rest.issues.listComments({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: prNumber, + }); + const existing = comments.find(c => c.body && c.body.includes(marker)); + if (existing) { + await github.rest.issues.updateComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: existing.id, + body, + }); + } else { + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: prNumber, + body, + }); + } diff --git a/.github/workflows/ios.preview.build.yml b/.github/workflows/ios.preview.build.yml new file mode 100644 index 000000000..4228b5a63 --- /dev/null +++ b/.github/workflows/ios.preview.build.yml @@ -0,0 +1,127 @@ +name: Notesnook iOS Preview Build + +# UNTRUSTED stage. Runs on `pull_request`, so fork code is checked out and +# compiled with a read-only GITHUB_TOKEN and NO repository secrets. It only +# produces an *unsigned* archive. Signing, Firebase distribution and PR +# comments happen in ios.preview.publish.yml, which runs in the trusted +# `workflow_run` context and never executes fork code. Because no secrets are +# exposed here, the build runs automatically for every PR (including forks) +# with no authorization gate. + +on: + pull_request: + types: [opened, reopened, synchronize] + branches: [master, beta] + paths: + - "apps/mobile/**" + - "packages/**" + - ".github/workflows/ios.preview.build.yml" + - ".github/workflows/ios.preview.publish.yml" + +# A fork that spams pushes shouldn't queue up macOS builds. +concurrency: + group: ios-preview-${{ github.event.pull_request.number }} + cancel-in-progress: true + +jobs: + build: + runs-on: macos-26 + timeout-minutes: 60 + + steps: + - name: Checkout PR code + uses: actions/checkout@v5 + with: + ref: ${{ github.event.pull_request.head.sha }} + + - name: Setup Node + uses: ./.github/actions/setup-node-with-cache + + - name: Setup Xcode + uses: maxim-lobanov/setup-xcode@v1 + with: + xcode-version: "26.1.1" + + - name: Setup iOS Platform + run: | + xcodebuild -downloadPlatform iOS -exportPath ~/Downloads + xcodebuild -importPlatform ~/Downloads/iphonesimulator_26.1_23B86.dmg + + - name: Install node modules + run: | + npm ci --ignore-scripts --prefer-offline --no-audit + npm run bootstrap -- --scope=mobile + + - name: Build packages + run: npm run tx @notesnook/mobile:build + + - name: Cache Pods + uses: actions/cache@v4 + id: pods-cache + with: + path: apps/mobile/ios/Pods + key: ${{ runner.os }}-pods-${{ hashFiles('apps/mobile/ios/Podfile.lock') }} + + - name: Install Pods + run: | + cd apps/mobile/ios + pod install + + - name: Check for typescript errors + run: | + npm run tx mobile:build + cd apps/mobile + npx tsc --noEmit + + - name: Get marketing version + id: marketing-version + run: echo "version=$(grep "IOS_MARKETING_VERSION" apps/mobile/ios/build-configs/ios-build.staging.xcconfig | awk -F'=' '{print $2}' | xargs)" >> $GITHUB_OUTPUT + + - name: Get build number + id: build-number + run: echo "timestamp=$(($(date +%s) - 1774851180 ))" >> $GITHUB_OUTPUT + + - name: Make staging xcconfig active + run: | + cd apps/mobile/ios/build-configs + ./use-ios-build-config.sh staging --marketing-version ${{steps.marketing-version.outputs.version}} --build-number ${{steps.build-number.outputs.timestamp}} + + # Archive WITHOUT signing. No certificates or secrets are present in this + # job. The trusted publish workflow re-signs and exports this archive; + # `-exportArchive` only packages/signs and does NOT re-run the app's build + # phases, so fork code never runs alongside secrets. + - name: Archive (unsigned) + run: | + set -euo pipefail + xcodebuild \ + -workspace apps/mobile/ios/Notesnook.xcworkspace \ + -scheme Notesnook \ + -configuration Release \ + -sdk iphoneos \ + -destination 'generic/platform=iOS' \ + -archivePath "$RUNNER_TEMP/Notesnook.xcarchive" \ + CODE_SIGNING_ALLOWED=NO \ + CODE_SIGNING_REQUIRED=NO \ + CODE_SIGN_IDENTITY="" \ + CODE_SIGN_ENTITLEMENTS="" \ + archive + + - name: Stage build artifact + run: | + set -euo pipefail + mkdir -p "$RUNNER_TEMP/artifact" + tar -czf "$RUNNER_TEMP/artifact/Notesnook.xcarchive.tar.gz" \ + -C "$RUNNER_TEMP" Notesnook.xcarchive + # Carry the PR context forward; workflow_run cannot see it reliably for forks. + { + echo "PR_NUMBER=${{ github.event.pull_request.number }}" + echo "HEAD_SHA=${{ github.event.pull_request.head.sha }}" + } > "$RUNNER_TEMP/artifact/pr-meta.env" + + - name: Upload build artifact + uses: actions/upload-artifact@v4 + with: + name: ios-preview-build + path: ${{ runner.temp }}/artifact + if-no-files-found: error + retention-days: 1 diff --git a/.github/workflows/ios.preview.firebase.yml b/.github/workflows/ios.preview.firebase.yml deleted file mode 100644 index dc1411003..000000000 --- a/.github/workflows/ios.preview.firebase.yml +++ /dev/null @@ -1,173 +0,0 @@ -name: Notesnook iOS Preview - -on: - pull_request_target: - types: [opened, reopened, synchronize] - branches: [master, beta] - paths: - - "apps/mobile/**" - - "packages/**" - - ".github/workflows/ios.preview.firebase.yml" - -jobs: - authorize: - environment: ${{ github.event_name == 'pull_request_target' && - github.event.pull_request.head.repo.full_name != github.repository && - 'external' || 'internal' }} - - runs-on: ubuntu-latest - steps: - - run: echo true - - build: - needs: authorize - runs-on: macos-26 - timeout-minutes: 60 - - steps: - - name: Checkout - uses: actions/checkout@v5 - with: - ref: ${{ github.event.pull_request.head.sha || github.ref }} - - - name: Setup Node - uses: ./.github/actions/setup-node-with-cache - - - name: Setup Xcode - uses: maxim-lobanov/setup-xcode@v1 - with: - xcode-version: "26.1.1" - - - name: Setup iOS Platform - run: | - xcodebuild -downloadPlatform iOS -exportPath ~/Downloads - xcodebuild -importPlatform ~/Downloads/iphonesimulator_26.1_23B86.dmg - - - name: Install node modules - run: | - npm ci --ignore-scripts --prefer-offline --no-audit - npm run bootstrap -- --scope=mobile - - - name: Build packages - run: npm run tx @notesnook/mobile:build - - - name: Cache Pods - uses: actions/cache@v3 - id: pods-cache - with: - path: apps/mobile/ios/Pods - key: ${{ runner.os }}-pods-${{ hashFiles('apps/mobile/ios/Podfile.lock') }} - - - name: Install Pods - run: | - cd apps/mobile/ios - pod install - - - name: Check for typescript errors - run: | - npm run tx mobile:build - cd apps/mobile - npx tsc --noEmit - - - name: Get marketing version - id: marketing-version - run: echo "version=$(grep "IOS_MARKETING_VERSION" apps/mobile/ios/build-configs/ios-build.staging.xcconfig | awk -F'=' '{print $2}' | xargs)" >> $GITHUB_OUTPUT - - - name: Get build number - id: build-number - run: echo "timestamp=$(($(date +%s) - 1774851180 ))" >> $GITHUB_OUTPUT - - - name: Make staging xcconfig active - run: | - cd apps/mobile/ios/build-configs - ./use-ios-build-config.sh staging --marketing-version ${{steps.marketing-version.outputs.version}} --build-number ${{steps.build-number.outputs.timestamp}} - - - name: Build iOS App - uses: ammarahm-ed/ios-build-action@master - with: - bundle-identifier: org.streetwriters.notesnook - scheme: Notesnook - configuration: "Release" - export-options: apps/mobile/ios/ExportOptionsStaging.plist - export-method: "ad-hoc" - project-path: apps/mobile/ios/Notesnook.xcodeproj - workspace-path: apps/mobile/ios/Notesnook.xcworkspace - update-targets: | - Notesnook - Make Note - NotesWidgetExtension - disable-targets: Notesnook-tvOS,Notesnook-tvOSTests,NotesnookTests - code-signing-identity: Apple Distribution - team-id: ${{ secrets.APPLE_TEAM_ID }} - p12-base64: ${{ secrets.APPLE_CERTIFICATE_P12 }} - certificate-password: ${{ secrets.APPLE_CERTIFICATE_P12_PASSWORD }} - app-store-connect-api-key-issuer-id: ${{ secrets.API_KEY_ISSUER_ID }} - app-store-connect-api-key-id: ${{ secrets.APPSTORE_KEY_ID }} - app-store-connect-api-key-base64: ${{ secrets.APPSTORE_CONNECT_API_KEY_BASE64 }} - output-path: Notesnook.ipa - mobileprovision-base64: | - ${{ secrets.APPLE_MOBILE_PROVISION_ADHOC_APP }} - ${{ secrets.APPLE_MOBILE_PROVISION_ADHOC_SHARE }} - ${{ secrets.APPLE_MOBILE_PROVISION_ADHOC_WIDGET }} - - - name: Upload IPA artifact - uses: actions/upload-artifact@v4 - with: - name: ios-preview-ipa - path: Notesnook.ipa - if-no-files-found: error - retention-days: 1 - - publish: - runs-on: ubuntu-latest - needs: build - timeout-minutes: 20 - - steps: - - name: Download IPA artifact - uses: actions/download-artifact@v4 - with: - name: ios-preview-ipa - path: . - - - name: Publish to firebase CLI - id: firebase-output - uses: wzieba/Firebase-Distribution-Github-Action@v1 - with: - appId: ${{secrets.FIREBASE_IOS_APP_ID}} - serviceCredentialsFileContent: ${{ secrets.QA_SERVICE_ACCOUNT }} - groups: testers - file: Notesnook.ipa - releaseNotes: Preview for https://github.com/streetwriters/notesnook/pull/${{github.event.number}} - - - name: Post or update PR comment - uses: actions/github-script@v6 - env: - preview_url: ${{ steps.firebase-output.outputs.TESTING_URI }} - with: - script: | - const marker = ''; - const prNumber = context.issue.number; - const previewUrl = process.env.preview_url || ''; - const body = `${marker}\n**iOS App Preview**\n\n${previewUrl || 'Preview URL unavailable — check workflow logs.'}\n\nCommit: ${process.env.GITHUB_SHA}\n`; - const { data: comments } = await github.rest.issues.listComments({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: prNumber, - }); - const existing = comments.find(c => c.body && c.body.includes(marker)); - if (existing) { - await github.rest.issues.updateComment({ - owner: context.repo.owner, - repo: context.repo.repo, - comment_id: existing.id, - body, - }); - } else { - await github.rest.issues.createComment({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: prNumber, - body, - }); - } diff --git a/.github/workflows/ios.preview.publish.yml b/.github/workflows/ios.preview.publish.yml new file mode 100644 index 000000000..b9f6b8739 --- /dev/null +++ b/.github/workflows/ios.preview.publish.yml @@ -0,0 +1,132 @@ +name: Notesnook iOS Preview Publish + +# TRUSTED stage. Runs via `workflow_run` after the build workflow finishes, so +# it has the base repo's secrets and a write-scoped token. It checks out the +# BASE repository (never fork code), downloads the unsigned archive the build +# produced, then signs + exports + distributes it and posts the PR comment. +# `xcodebuild -exportArchive` only packages and signs a prebuilt archive; it +# does not run the app's build phases, so fork code is never executed here. + +on: + workflow_run: + workflows: ["Notesnook iOS Preview Build"] + types: [completed] + +jobs: + publish: + # Only publish previews for builds that actually succeeded. + if: github.event.workflow_run.conclusion == 'success' + runs-on: macos-26 + timeout-minutes: 30 + + steps: + - name: Checkout base repo (trusted) + uses: actions/checkout@v5 + # No `ref` -> checks out the default branch, i.e. trusted base code. + + - name: Setup Xcode + uses: maxim-lobanov/setup-xcode@v1 + with: + xcode-version: "26.1.1" + + - name: Download build artifact + uses: actions/download-artifact@v4 + with: + name: ios-preview-build + path: ${{ runner.temp }}/artifact + run-id: ${{ github.event.workflow_run.id }} + github-token: ${{ secrets.GITHUB_TOKEN }} + + - name: Load PR metadata & extract archive + run: | + set -euo pipefail + cat "$RUNNER_TEMP/artifact/pr-meta.env" >> "$GITHUB_ENV" + tar -xzf "$RUNNER_TEMP/artifact/Notesnook.xcarchive.tar.gz" -C "$RUNNER_TEMP" + + - name: Import signing certificate + uses: apple-actions/import-codesign-certs@v3 + with: + p12-file-base64: ${{ secrets.APPLE_CERTIFICATE_P12 }} + p12-password: ${{ secrets.APPLE_CERTIFICATE_P12_PASSWORD }} + + - name: Install provisioning profiles + env: + PROFILE_APP: ${{ secrets.APPLE_MOBILE_PROVISION_ADHOC_APP }} + PROFILE_SHARE: ${{ secrets.APPLE_MOBILE_PROVISION_ADHOC_SHARE }} + PROFILE_WIDGET: ${{ secrets.APPLE_MOBILE_PROVISION_ADHOC_WIDGET }} + run: | + set -euo pipefail + PROFILE_DIR="$HOME/Library/MobileDevice/Provisioning Profiles" + mkdir -p "$PROFILE_DIR" + i=0 + for p in "$PROFILE_APP" "$PROFILE_SHARE" "$PROFILE_WIDGET"; do + echo "$p" | base64 --decode > "$PROFILE_DIR/preview-$i.mobileprovision" + i=$((i + 1)) + done + + - name: Export signed IPA + env: + APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} + run: | + set -euo pipefail + EXPORT_PLIST="$RUNNER_TEMP/ExportOptions.plist" + cp apps/mobile/ios/ExportOptionsStaging.plist "$EXPORT_PLIST" + # Augment the checked-in profile map with the fields exportArchive needs. + /usr/libexec/PlistBuddy -c "Add :method string ad-hoc" "$EXPORT_PLIST" 2>/dev/null \ + || /usr/libexec/PlistBuddy -c "Set :method ad-hoc" "$EXPORT_PLIST" + /usr/libexec/PlistBuddy -c "Add :signingStyle string manual" "$EXPORT_PLIST" 2>/dev/null || true + /usr/libexec/PlistBuddy -c "Add :signingCertificate string Apple Distribution" "$EXPORT_PLIST" 2>/dev/null || true + /usr/libexec/PlistBuddy -c "Add :teamID string $APPLE_TEAM_ID" "$EXPORT_PLIST" 2>/dev/null \ + || /usr/libexec/PlistBuddy -c "Set :teamID $APPLE_TEAM_ID" "$EXPORT_PLIST" + + xcodebuild -exportArchive \ + -archivePath "$RUNNER_TEMP/Notesnook.xcarchive" \ + -exportOptionsPlist "$EXPORT_PLIST" \ + -exportPath "$RUNNER_TEMP/export" + + # Normalize the IPA name for the distribution step. + IPA="$(find "$RUNNER_TEMP/export" -name '*.ipa' | head -n1)" + cp "$IPA" "$RUNNER_TEMP/Notesnook.ipa" + + - name: Publish to Firebase + id: firebase-output + uses: wzieba/Firebase-Distribution-Github-Action@v1 + with: + appId: ${{ secrets.FIREBASE_IOS_APP_ID }} + serviceCredentialsFileContent: ${{ secrets.QA_SERVICE_ACCOUNT }} + groups: testers + file: ${{ runner.temp }}/Notesnook.ipa + releaseNotes: Preview for https://github.com/${{ github.repository }}/pull/${{ env.PR_NUMBER }} + + - name: Post or update PR comment + uses: actions/github-script@v7 + env: + preview_url: ${{ steps.firebase-output.outputs.TESTING_URI }} + with: + script: | + const marker = ''; + const prNumber = Number(process.env.PR_NUMBER); + if (!prNumber) return; + const previewUrl = process.env.preview_url || ''; + const body = `${marker}\n**iOS App Preview**\n\n${previewUrl || 'Preview URL unavailable — check workflow logs.'}\n\nCommit: ${process.env.HEAD_SHA}\n`; + const { data: comments } = await github.rest.issues.listComments({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: prNumber, + }); + const existing = comments.find(c => c.body && c.body.includes(marker)); + if (existing) { + await github.rest.issues.updateComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: existing.id, + body, + }); + } else { + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: prNumber, + body, + }); + }