From d17081665b589496f015d699c74a5b5587bfa280 Mon Sep 17 00:00:00 2001 From: Shahroz Khan Date: Sat, 29 Aug 2026 11:59:04 -0400 Subject: [PATCH 01/15] ci: add fast and full PR validation lanes --- .github/workflows/api-validation.yml | 11 ++- .github/workflows/build-sample-app.yml | 44 ++++++++++-- .github/workflows/build-test-sample-apps.yml | 71 ++++++++++++-------- .github/workflows/react-native-scene-e2e.yml | 11 ++- .github/workflows/test.yml | 11 ++- 5 files changed, 109 insertions(+), 39 deletions(-) diff --git a/.github/workflows/api-validation.yml b/.github/workflows/api-validation.yml index 7f023d44..1e56e81c 100644 --- a/.github/workflows/api-validation.yml +++ b/.github/workflows/api-validation.yml @@ -1,6 +1,13 @@ name: Public API change Validation -on: [pull_request] +on: + pull_request: + paths-ignore: + - '**/*.md' + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true jobs: validate-api: @@ -18,4 +25,4 @@ jobs: run: npm run prepare - name: Validate API - run: npx api-extractor run \ No newline at end of file + run: npx api-extractor run diff --git a/.github/workflows/build-sample-app.yml b/.github/workflows/build-sample-app.yml index b175274b..e5e4eb2f 100644 --- a/.github/workflows/build-sample-app.yml +++ b/.github/workflows/build-sample-app.yml @@ -32,6 +32,11 @@ on: description: "Name of the platform in all upper case (IOS or ANDROID)" required: true type: string + should_distribute: + description: "Whether to sign and publish the sample app" + required: false + default: true + type: boolean outputs: sdk_version_name: @@ -302,6 +307,7 @@ jobs: uses: customerio/mobile-ci-tools/github-actions/ios/setup-ios/v1@main - name: Build and upload ${{ inputs.platform_name }} ${{ inputs.app_name }} sample app via Fastlane + if: ${{ inputs.should_distribute }} id: build_app uses: maierj/fastlane-action@5a3b971aaa26776459bb26894d6c1a1a84a311a7 # v3.1.0 with: @@ -314,8 +320,27 @@ jobs: FIREBASE_APP_DISTRIBUTION_SERVICE_ACCOUNT_CREDS_B64: ${{ secrets.FIREBASE_APP_DISTRIBUTION_SERVICE_ACCOUNT_CREDS_B64 }} continue-on-error: true + - name: Compile ${{ inputs.platform_name }} ${{ inputs.app_name }} sample app (verification only) + if: ${{ !inputs.should_distribute }} + id: verify_build + shell: bash + run: | + set -euo pipefail + if [[ "${{ inputs.platform }}" == 'android' ]]; then + ./android/gradlew -p android :app:assembleRelease + else + xcodebuild \ + -workspace ios/SampleApp.xcworkspace \ + -scheme SampleApp \ + -configuration Release \ + -sdk iphonesimulator \ + -destination 'generic/platform=iOS Simulator' \ + CODE_SIGNING_ALLOWED=NO \ + build + fi + - name: Send slack notification for ${{ inputs.platform_name }} ${{ inputs.app_name }} sample app builds - if: ${{ always() && env.IS_PRIMARY_APP == 'true' }} + if: ${{ always() && inputs.should_distribute && env.IS_PRIMARY_APP == 'true' }} uses: customerio/mobile-ci-tools/github-actions/slack-notify-sample-app/v1@main with: build_status: ${{ steps.build_app.outcome }} @@ -332,11 +357,18 @@ jobs: slack_webhook_url: ${{ secrets.SLACK_WEBHOOK_URL }} - name: Check build statuses and mark failure + if: ${{ always() }} + env: + SHOULD_DISTRIBUTE: ${{ inputs.should_distribute }} + DISTRIBUTION_OUTCOME: ${{ steps.build_app.outcome }} + VERIFICATION_OUTCOME: ${{ steps.verify_build.outcome }} run: | - FAILED_BUILDS=() - [ "${{ steps.build_app.outcome }}" != "success" ] && FAILED_BUILDS+=("(${{ inputs.platform_name }})") - - if [ ${#FAILED_BUILDS[@]} -ne 0 ]; then - echo "Build failed for: ${FAILED_BUILDS[*]}" + if [[ "$SHOULD_DISTRIBUTE" == 'true' ]]; then + outcome="$DISTRIBUTION_OUTCOME" + else + outcome="$VERIFICATION_OUTCOME" + fi + if [[ "$outcome" != 'success' ]]; then + echo "${{ inputs.platform_name }} build failed with outcome: $outcome" exit 1 fi diff --git a/.github/workflows/build-test-sample-apps.yml b/.github/workflows/build-test-sample-apps.yml index 77a4ae22..4bd2100a 100644 --- a/.github/workflows/build-test-sample-apps.yml +++ b/.github/workflows/build-test-sample-apps.yml @@ -1,18 +1,44 @@ name: Publish Test Sample Apps on: - pull_request: # build sample apps for every commit pushed to an open pull request (including drafts) + pull_request: + types: [opened, synchronize, reopened, labeled] + paths-ignore: + - '**/*.md' workflow_dispatch: push: - branches: [main, feature/*] + branches: [main] concurrency: # cancel previous workflow run if one exists. - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true + group: ${{ github.workflow }}-${{ github.ref }}-${{ (github.event.action == 'labeled' && github.event.label.name != 'ci:full') && github.run_id || 'validation' }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} jobs: + prepare: + name: Select sample app lane + runs-on: ubuntu-latest + outputs: + matrix: ${{ steps.select.outputs.matrix }} + steps: + - name: Select verification or distribution matrix + id: select + shell: bash + env: + EVENT_NAME: ${{ github.event_name }} + FULL_PR: ${{ contains(github.event.pull_request.labels.*.name, 'ci:full') && github.event.pull_request.head.repo.full_name == github.repository }} + run: | + if [[ "$EVENT_NAME" == 'pull_request' && "$FULL_PR" != 'true' ]]; then + echo 'matrix=[{"name":"APN","cio-workspace-name":"Mobile: React Native","platform":"ios","platform_name":"iOS","platform_name_upper":"IOS","should_distribute":false},{"name":"APN","cio-workspace-name":"Mobile: React Native","platform":"android","platform_name":"Android","platform_name_upper":"ANDROID","should_distribute":false}]' >> "$GITHUB_OUTPUT" + else + echo 'matrix=[{"name":"APN","cio-workspace-name":"Mobile: React Native","platform":"ios","platform_name":"iOS","platform_name_upper":"IOS","should_distribute":true},{"name":"FCM","cio-workspace-name":"Mobile: xReact Native FCM workspace","platform":"ios","platform_name":"iOS","platform_name_upper":"IOS","should_distribute":true},{"name":"APN","cio-workspace-name":"Mobile: React Native","platform":"android","platform_name":"Android","platform_name_upper":"ANDROID","should_distribute":true}]' >> "$GITHUB_OUTPUT" + fi + update-pr-comment: - if: ${{ github.event_name == 'pull_request' }} + if: >- + (github.event.action != 'labeled' || github.event.label.name == 'ci:full') && + github.event_name == 'pull_request' && + contains(github.event.pull_request.labels.*.name, 'ci:full') && + github.event.pull_request.head.repo.full_name == github.repository runs-on: ubuntu-latest permissions: pull-requests: write # to be able to comment on PR @@ -40,33 +66,19 @@ jobs: Below you will find the list of the latest versions of the sample apps. It's recommended to always download the latest builds of the sample apps to accurately test the pull request. --- - ${{ steps.build.outputs.build-log }} + Builds are in progress. This comment will be updated when they finish. edit-mode: replace # replace the existing comment with new content since we are creating new builds build-sample-apps: - if: ${{ always() }} # do not skip running this step if update-pr-comment does not run - needs: [update-pr-comment] # wait for PR comment to be created saying new builds are being made. + if: >- + always() && + needs.prepare.result == 'success' && + (github.event.action != 'labeled' || github.event.label.name == 'ci:full') + needs: [prepare, update-pr-comment] strategy: fail-fast: false # if one sample app fails to build, let the other sample apps continue to build and not cancel them. matrix: # Use a matrix allowing us to build multiple apps in parallel. Just add an entry to the matrix and it will build! - sample-app: - # List all sample apps you want to have compiled. - # List item is name of directory inside of "Apps" directory for the corresponding app to compile. - - name: 'APN' - cio-workspace-name: 'Mobile: React Native' - platform: 'ios' - platform_name: 'iOS' - platform_name_upper: 'IOS' - - name: 'FCM' - cio-workspace-name: 'Mobile: xReact Native FCM workspace' - platform: 'ios' - platform_name: 'iOS' - platform_name_upper: 'IOS' - - name: 'APN' - cio-workspace-name: 'Mobile: React Native' - platform: 'android' - platform_name: 'Android' - platform_name_upper: 'ANDROID' + sample-app: ${{ fromJSON(needs.prepare.outputs.matrix) }} name: Building the ${{ matrix.sample-app.platform_name }} ${{ matrix.sample-app.name }} sample app uses: ./.github/workflows/build-sample-app.yml @@ -76,12 +88,17 @@ jobs: platform: ${{ matrix.sample-app.platform }} platform_name: ${{ matrix.sample-app.platform_name }} platform_name_upper: ${{ matrix.sample-app.platform_name_upper }} + should_distribute: ${{ matrix.sample-app.should_distribute }} secrets: inherit # Update PR comment with build information update-pr-comment-with-status: - if: ${{ github.event_name == 'pull_request' }} + if: >- + (github.event.action != 'labeled' || github.event.label.name == 'ci:full') && + github.event_name == 'pull_request' && + contains(github.event.pull_request.labels.*.name, 'ci:full') && + github.event.pull_request.head.repo.full_name == github.repository needs: [build-sample-apps, update-pr-comment] runs-on: ubuntu-latest permissions: diff --git a/.github/workflows/react-native-scene-e2e.yml b/.github/workflows/react-native-scene-e2e.yml index 6bf4db17..2c409d0f 100644 --- a/.github/workflows/react-native-scene-e2e.yml +++ b/.github/workflows/react-native-scene-e2e.yml @@ -2,6 +2,7 @@ name: React Native scene routing E2E on: pull_request: + types: [opened, synchronize, reopened, labeled] paths: - '.github/workflows/react-native-scene-e2e.yml' - '.maestro/fixtures/customerio_scene_cold.apns' @@ -48,13 +49,17 @@ permissions: contents: read concurrency: - group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.ref }} - cancel-in-progress: true + group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.ref }}-${{ (github.event.action == 'labeled' && github.event.label.name != 'ci:full') && github.run_id || 'validation' }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} jobs: route-notifications: name: RN scene notification routing on Xcode 27 - if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository + if: >- + (github.event.action != 'labeled' || github.event.label.name == 'ci:full') && + (github.event_name != 'pull_request' || + (contains(github.event.pull_request.labels.*.name, 'ci:full') && + github.event.pull_request.head.repo.full_name == github.repository)) runs-on: xcode-27 timeout-minutes: 60 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 5a1df8c0..229a22be 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -1,6 +1,15 @@ name: Test -on: [push, pull_request] +on: + push: + branches: [main] + pull_request: + paths-ignore: + - '**/*.md' + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} jobs: test-deploy: From c09cbd41494aa4f34fae8e702c9a712b72fa4d60 Mon Sep 17 00:00:00 2001 From: Shahroz Khan Date: Sat, 29 Aug 2026 17:49:01 -0400 Subject: [PATCH 02/15] ci: tighten and streamline PR validation --- .github/workflows/build-sample-app.yml | 9 +++-- .github/workflows/pr-helper.yml | 2 +- .github/workflows/react-native-scene-e2e.yml | 38 -------------------- .github/workflows/test.yml | 1 + 4 files changed, 8 insertions(+), 42 deletions(-) diff --git a/.github/workflows/build-sample-app.yml b/.github/workflows/build-sample-app.yml index e5e4eb2f..cc9ae0c4 100644 --- a/.github/workflows/build-sample-app.yml +++ b/.github/workflows/build-sample-app.yml @@ -227,10 +227,13 @@ jobs: if: ${{ inputs.platform == 'ios' }} uses: actions/cache@v4 with: - path: example/Pods - key: ${{ runner.os }}-${{ inputs.app_name }}-Pods-${{ github.ref }} + path: | + ~/Library/Caches/CocoaPods + ~/.cocoapods + example/ios/Pods + key: ${{ runner.os }}-${{ inputs.app_name }}-Pods-${{ hashFiles('package.json', 'package-lock.json', 'example/package.json', 'example/package-lock.json', 'example/ios/Podfile', 'example/Gemfile.lock', 'customerio-reactnative.podspec', 'customerio-reactnative-richpush.podspec') }} restore-keys: | - ${{ runner.os }}-${{ inputs.app_name }}-Pods + ${{ runner.os }}-${{ inputs.app_name }}-Pods- - name: Install dependencies to build SDK run: npm ci diff --git a/.github/workflows/pr-helper.yml b/.github/workflows/pr-helper.yml index 114ecdc7..ed43e73a 100644 --- a/.github/workflows/pr-helper.yml +++ b/.github/workflows/pr-helper.yml @@ -2,7 +2,7 @@ name: Semantic PR helper on: pull_request: - types: [opened, reopened, edited, synchronize, labeled] + types: [opened, reopened, edited, synchronize] jobs: lint-pr-title: diff --git a/.github/workflows/react-native-scene-e2e.yml b/.github/workflows/react-native-scene-e2e.yml index 2c409d0f..62400dec 100644 --- a/.github/workflows/react-native-scene-e2e.yml +++ b/.github/workflows/react-native-scene-e2e.yml @@ -3,44 +3,6 @@ name: React Native scene routing E2E on: pull_request: types: [opened, synchronize, reopened, labeled] - paths: - - '.github/workflows/react-native-scene-e2e.yml' - - '.maestro/fixtures/customerio_scene_cold.apns' - - '.maestro/fixtures/customerio_scene_declined.apns' - - '.maestro/fixtures/customerio_scene_warm.apns' - - '.maestro/fixtures/react-native-scene/**' - - '.maestro/run_scene_push.sh' - - '.maestro/scene_push_open.yaml' - - '.maestro/scene_push_declined.yaml' - - '.maestro/scene_push_prepare.yaml' - - '.maestro/scene_push_warm.yaml' - - '.maestro/scene_url_open.yaml' - - 'src/customerio-cdp.ts' - - 'src/customerio-push.ts' - - 'src/index.ts' - - 'src/native-logger-listener.ts' - - 'src/specs/modules/NativeCustomerIO.ts' - - 'src/specs/modules/NativeCustomerIOMessagingPush.ts' - - 'src/types/data-pipelines.ts' - - 'src/types/index.ts' - - 'src/types/internal.ts' - - 'src/types/push.ts' - - 'src/utils/native-bridge.ts' - - 'src/utils/param-validation.ts' - - 'ios/wrappers/CustomerIOReactNativeDeepLinkRouter.swift' - - 'ios/wrappers/CustomerIOReactNativeDeepLinkRequestStore.swift' - - 'ios/wrappers/NativeCustomerIO.swift' - - 'ios/wrappers/NativeCustomerIO.mm' - - 'ios/wrappers/liveactivities/NativeLiveActivities.swift' - - 'ios/wrappers/push/**' - - 'ios/wrappers/utils/CioConfigUtils.swift' - - 'ios/wrappers/utils/CioConstants.swift' - - 'ios/wrappers/CustomerioReactnative-Bridging-Header.h' - - 'ios/cocoapods_deployment_target.rb' - - 'customerio-reactnative.podspec' - - 'scripts/test_ios_deep_link_request_store.swift' - - 'package.json' - - 'package-lock.json' schedule: - cron: '47 7 * * *' workflow_dispatch: diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 229a22be..c4b77cb3 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -20,6 +20,7 @@ jobs: - uses: actions/setup-node@v4 with: node-version: '24' + cache: 'npm' - run: npm ci - name: Compile From c33b4edce994de6fa6ab7a4c5a70ec4bf2e2067d Mon Sep 17 00:00:00 2001 From: Shahroz Khan Date: Sun, 30 Aug 2026 21:05:24 -0400 Subject: [PATCH 03/15] ci: address adversarial reliability review --- .github/workflows/build-sample-app.yml | 9 ++---- .github/workflows/build-test-sample-apps.yml | 5 ++-- .../workflows/react-native-scene-e2e-full.yml | 18 ++++++++++++ .github/workflows/react-native-scene-e2e.yml | 28 +++++++++++++++---- 4 files changed, 46 insertions(+), 14 deletions(-) create mode 100644 .github/workflows/react-native-scene-e2e-full.yml diff --git a/.github/workflows/build-sample-app.yml b/.github/workflows/build-sample-app.yml index cc9ae0c4..e5e4eb2f 100644 --- a/.github/workflows/build-sample-app.yml +++ b/.github/workflows/build-sample-app.yml @@ -227,13 +227,10 @@ jobs: if: ${{ inputs.platform == 'ios' }} uses: actions/cache@v4 with: - path: | - ~/Library/Caches/CocoaPods - ~/.cocoapods - example/ios/Pods - key: ${{ runner.os }}-${{ inputs.app_name }}-Pods-${{ hashFiles('package.json', 'package-lock.json', 'example/package.json', 'example/package-lock.json', 'example/ios/Podfile', 'example/Gemfile.lock', 'customerio-reactnative.podspec', 'customerio-reactnative-richpush.podspec') }} + path: example/Pods + key: ${{ runner.os }}-${{ inputs.app_name }}-Pods-${{ github.ref }} restore-keys: | - ${{ runner.os }}-${{ inputs.app_name }}-Pods- + ${{ runner.os }}-${{ inputs.app_name }}-Pods - name: Install dependencies to build SDK run: npm ci diff --git a/.github/workflows/build-test-sample-apps.yml b/.github/workflows/build-test-sample-apps.yml index 4bd2100a..aae51ed4 100644 --- a/.github/workflows/build-test-sample-apps.yml +++ b/.github/workflows/build-test-sample-apps.yml @@ -7,15 +7,16 @@ on: - '**/*.md' workflow_dispatch: push: - branches: [main] + branches: [main, feature/*] concurrency: # cancel previous workflow run if one exists. group: ${{ github.workflow }}-${{ github.ref }}-${{ (github.event.action == 'labeled' && github.event.label.name != 'ci:full') && github.run_id || 'validation' }} - cancel-in-progress: ${{ github.event_name == 'pull_request' }} + cancel-in-progress: true jobs: prepare: name: Select sample app lane + if: github.event.action != 'labeled' || github.event.label.name == 'ci:full' runs-on: ubuntu-latest outputs: matrix: ${{ steps.select.outputs.matrix }} diff --git a/.github/workflows/react-native-scene-e2e-full.yml b/.github/workflows/react-native-scene-e2e-full.yml new file mode 100644 index 00000000..c244f3e6 --- /dev/null +++ b/.github/workflows/react-native-scene-e2e-full.yml @@ -0,0 +1,18 @@ +name: Full React Native scene routing E2E + +on: + pull_request: + types: [opened, synchronize, reopened, labeled] + +permissions: + contents: read + +jobs: + full-scene-e2e: + if: >- + ((github.event.action == 'labeled' && github.event.label.name == 'ci:full') || + (github.event.action != 'labeled' && contains(github.event.pull_request.labels.*.name, 'ci:full'))) && + github.event.pull_request.head.repo.full_name == github.repository + uses: ./.github/workflows/react-native-scene-e2e.yml + with: + full: true diff --git a/.github/workflows/react-native-scene-e2e.yml b/.github/workflows/react-native-scene-e2e.yml index 62400dec..1984fd23 100644 --- a/.github/workflows/react-native-scene-e2e.yml +++ b/.github/workflows/react-native-scene-e2e.yml @@ -2,26 +2,42 @@ name: React Native scene routing E2E on: pull_request: - types: [opened, synchronize, reopened, labeled] + paths: + - '.github/workflows/react-native-scene-e2e.yml' + - '.github/workflows/react-native-scene-e2e-full.yml' + - '.maestro/**' + - 'src/**' + - 'ios/wrappers/**' + - 'ios/cocoapods_deployment_target.rb' + - 'customerio-reactnative.podspec' + - 'customerio-reactnative-richpush.podspec' + - 'scripts/test_ios_deep_link_request_store.swift' + - 'package.json' + - 'package-lock.json' schedule: - cron: '47 7 * * *' workflow_dispatch: + workflow_call: + inputs: + full: + description: Run the label-triggered full lane. + type: boolean + required: false + default: false permissions: contents: read concurrency: - group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.ref }}-${{ (github.event.action == 'labeled' && github.event.label.name != 'ci:full') && github.run_id || 'validation' }} + group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.ref }}-${{ inputs.full && 'full' || 'direct' }} cancel-in-progress: ${{ github.event_name == 'pull_request' }} jobs: route-notifications: name: RN scene notification routing on Xcode 27 if: >- - (github.event.action != 'labeled' || github.event.label.name == 'ci:full') && - (github.event_name != 'pull_request' || - (contains(github.event.pull_request.labels.*.name, 'ci:full') && - github.event.pull_request.head.repo.full_name == github.repository)) + (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) && + (inputs.full == true || !contains(github.event.pull_request.labels.*.name, 'ci:full')) runs-on: xcode-27 timeout-minutes: 60 From 26f5f43b5528c6df7c7bf20cd718e4e5b66c18d4 Mon Sep 17 00:00:00 2001 From: Shahroz Khan Date: Mon, 31 Aug 2026 00:19:19 -0400 Subject: [PATCH 04/15] ci: validate release-bearing PRs automatically --- .github/scripts/validate-release-policy.sh | 68 +++++++++++++++++ .github/workflows/build-test-sample-apps.yml | 74 ++++++++++++++++--- .github/workflows/pr-helper.yml | 23 +++++- .../workflows/react-native-scene-e2e-full.yml | 25 ++++++- .github/workflows/react-native-scene-e2e.yml | 19 ++++- 5 files changed, 190 insertions(+), 19 deletions(-) create mode 100644 .github/scripts/validate-release-policy.sh diff --git a/.github/scripts/validate-release-policy.sh b/.github/scripts/validate-release-policy.sh new file mode 100644 index 00000000..5e8cb254 --- /dev/null +++ b/.github/scripts/validate-release-policy.sh @@ -0,0 +1,68 @@ +#!/usr/bin/env bash +set -euo pipefail + +: "${PR_TITLE:?PR_TITLE is required}" +: "${BASE_SHA:?BASE_SHA is required}" +: "${HEAD_SHA:?HEAD_SHA is required}" +: "${HEAD_REPOSITORY:?HEAD_REPOSITORY is required}" +: "${TARGET_REPOSITORY:?TARGET_REPOSITORY is required}" +: "${PR_AUTHOR:?PR_AUTHOR is required}" + +is_release_message() { + local message="$1" + local header + header=$(sed -n '/[^[:space:]]/{p;q;}' <<< "$message") + + case "$header" in + fix:*|fix\(*|feat:*|feat\(*|perf:*|perf\(*|revert:*|revert\(*|Revert\ \"*) + return 0 + ;; + esac + + if grep -Eq '^[[:alpha:]]+(\([^)]*\))?!:' <<< "$header"; then + return 0 + fi + + if grep -Eq '(^|[[:space:]])(BREAKING CHANGES?:|BREAKING-CHANGE:)' <<< "$message"; then + return 0 + fi + + if grep -Eq '(^|[[:space:]])This reverts commit[[:space:]]' <<< "$message"; then + return 0 + fi + + return 1 +} + +pr_message="$PR_TITLE"$'\n\n'"${PR_BODY:-}" +pr_release=false +commit_release=false + +if is_release_message "$pr_message"; then + pr_release=true +fi + +git cat-file -e "$BASE_SHA^{commit}" +git cat-file -e "$HEAD_SHA^{commit}" +commit_messages_file=$(mktemp) +trap 'rm -f "$commit_messages_file"' EXIT +git log --format='%B%x00' "$BASE_SHA..$HEAD_SHA" > "$commit_messages_file" + +while IFS= read -r -d '' commit_message; do + if is_release_message "$commit_message"; then + commit_release=true + break + fi +done < "$commit_messages_file" + +if [[ "$commit_release" == 'true' && "$pr_release" != 'true' && "${CI_FULL:-false}" != 'true' ]]; then + echo "::error::This PR contains a release-bearing commit, but its PR title/body is non-release. Retitle it with fix:, feat:, perf:, revert:, or a breaking-change marker, or apply ci:full, so release validation runs before merge." + exit 1 +fi + +if [[ ("$pr_release" == 'true' || "$commit_release" == 'true') && ("$HEAD_REPOSITORY" != "$TARGET_REPOSITORY" || "$PR_AUTHOR" == 'dependabot[bot]') ]]; then + echo "::error::Release-bearing PRs require secret-dependent validation from a trusted repository branch. Recreate or update this change on an internal branch before merge." + exit 1 +fi + +echo "Release validation policy satisfied (pr_release=$pr_release, commit_release=$commit_release)." diff --git a/.github/workflows/build-test-sample-apps.yml b/.github/workflows/build-test-sample-apps.yml index aae51ed4..68bce6f7 100644 --- a/.github/workflows/build-test-sample-apps.yml +++ b/.github/workflows/build-test-sample-apps.yml @@ -2,7 +2,7 @@ name: Publish Test Sample Apps on: pull_request: - types: [opened, synchronize, reopened, labeled] + types: [opened, synchronize, reopened, edited, labeled] paths-ignore: - '**/*.md' workflow_dispatch: @@ -10,13 +10,13 @@ on: branches: [main, feature/*] concurrency: # cancel previous workflow run if one exists. - group: ${{ github.workflow }}-${{ github.ref }}-${{ (github.event.action == 'labeled' && github.event.label.name != 'ci:full') && github.run_id || 'validation' }} + group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true jobs: + # Metadata edits rerun full so a newer skipped check cannot replace a failure. prepare: name: Select sample app lane - if: github.event.action != 'labeled' || github.event.label.name == 'ci:full' runs-on: ubuntu-latest outputs: matrix: ${{ steps.select.outputs.matrix }} @@ -26,7 +26,26 @@ jobs: shell: bash env: EVENT_NAME: ${{ github.event_name }} - FULL_PR: ${{ contains(github.event.pull_request.labels.*.name, 'ci:full') && github.event.pull_request.head.repo.full_name == github.repository }} + FULL_PR: >- + ${{ (github.event.action == 'edited' || + github.event.action == 'labeled' || + contains(github.event.pull_request.labels.*.name, 'ci:full') || + startsWith(github.event.pull_request.title, 'fix:') || + startsWith(github.event.pull_request.title, 'fix(') || + startsWith(github.event.pull_request.title, 'feat:') || + startsWith(github.event.pull_request.title, 'feat(') || + startsWith(github.event.pull_request.title, 'perf:') || + startsWith(github.event.pull_request.title, 'perf(') || + startsWith(github.event.pull_request.title, 'revert:') || + startsWith(github.event.pull_request.title, 'revert(') || + startsWith(github.event.pull_request.title, 'Revert "') || + contains(github.event.pull_request.body, 'This reverts commit') || + contains(github.event.pull_request.title, '!:') || + contains(github.event.pull_request.body, 'BREAKING CHANGE:') || + contains(github.event.pull_request.body, 'BREAKING CHANGES:') || + contains(github.event.pull_request.body, 'BREAKING-CHANGE:')) && + github.event.pull_request.head.repo.full_name == github.repository && + github.event.pull_request.user.login != 'dependabot[bot]' }} run: | if [[ "$EVENT_NAME" == 'pull_request' && "$FULL_PR" != 'true' ]]; then echo 'matrix=[{"name":"APN","cio-workspace-name":"Mobile: React Native","platform":"ios","platform_name":"iOS","platform_name_upper":"IOS","should_distribute":false},{"name":"APN","cio-workspace-name":"Mobile: React Native","platform":"android","platform_name":"Android","platform_name_upper":"ANDROID","should_distribute":false}]' >> "$GITHUB_OUTPUT" @@ -36,10 +55,26 @@ jobs: update-pr-comment: if: >- - (github.event.action != 'labeled' || github.event.label.name == 'ci:full') && github.event_name == 'pull_request' && - contains(github.event.pull_request.labels.*.name, 'ci:full') && - github.event.pull_request.head.repo.full_name == github.repository + (github.event.action == 'edited' || + github.event.action == 'labeled' || + contains(github.event.pull_request.labels.*.name, 'ci:full') || + startsWith(github.event.pull_request.title, 'fix:') || + startsWith(github.event.pull_request.title, 'fix(') || + startsWith(github.event.pull_request.title, 'feat:') || + startsWith(github.event.pull_request.title, 'feat(') || + startsWith(github.event.pull_request.title, 'perf:') || + startsWith(github.event.pull_request.title, 'perf(') || + startsWith(github.event.pull_request.title, 'revert:') || + startsWith(github.event.pull_request.title, 'revert(') || + startsWith(github.event.pull_request.title, 'Revert "') || + contains(github.event.pull_request.body, 'This reverts commit') || + contains(github.event.pull_request.title, '!:') || + contains(github.event.pull_request.body, 'BREAKING CHANGE:') || + contains(github.event.pull_request.body, 'BREAKING CHANGES:') || + contains(github.event.pull_request.body, 'BREAKING-CHANGE:')) && + github.event.pull_request.head.repo.full_name == github.repository && + github.event.pull_request.user.login != 'dependabot[bot]' runs-on: ubuntu-latest permissions: pull-requests: write # to be able to comment on PR @@ -73,8 +108,7 @@ jobs: build-sample-apps: if: >- always() && - needs.prepare.result == 'success' && - (github.event.action != 'labeled' || github.event.label.name == 'ci:full') + needs.prepare.result == 'success' needs: [prepare, update-pr-comment] strategy: fail-fast: false # if one sample app fails to build, let the other sample apps continue to build and not cancel them. @@ -96,10 +130,26 @@ jobs: # Update PR comment with build information update-pr-comment-with-status: if: >- - (github.event.action != 'labeled' || github.event.label.name == 'ci:full') && github.event_name == 'pull_request' && - contains(github.event.pull_request.labels.*.name, 'ci:full') && - github.event.pull_request.head.repo.full_name == github.repository + (github.event.action == 'edited' || + github.event.action == 'labeled' || + contains(github.event.pull_request.labels.*.name, 'ci:full') || + startsWith(github.event.pull_request.title, 'fix:') || + startsWith(github.event.pull_request.title, 'fix(') || + startsWith(github.event.pull_request.title, 'feat:') || + startsWith(github.event.pull_request.title, 'feat(') || + startsWith(github.event.pull_request.title, 'perf:') || + startsWith(github.event.pull_request.title, 'perf(') || + startsWith(github.event.pull_request.title, 'revert:') || + startsWith(github.event.pull_request.title, 'revert(') || + startsWith(github.event.pull_request.title, 'Revert "') || + contains(github.event.pull_request.body, 'This reverts commit') || + contains(github.event.pull_request.title, '!:') || + contains(github.event.pull_request.body, 'BREAKING CHANGE:') || + contains(github.event.pull_request.body, 'BREAKING CHANGES:') || + contains(github.event.pull_request.body, 'BREAKING-CHANGE:')) && + github.event.pull_request.head.repo.full_name == github.repository && + github.event.pull_request.user.login != 'dependabot[bot]' needs: [build-sample-apps, update-pr-comment] runs-on: ubuntu-latest permissions: diff --git a/.github/workflows/pr-helper.yml b/.github/workflows/pr-helper.yml index ed43e73a..311f7bfe 100644 --- a/.github/workflows/pr-helper.yml +++ b/.github/workflows/pr-helper.yml @@ -2,12 +2,31 @@ name: Semantic PR helper on: pull_request: - types: [opened, reopened, edited, synchronize] + types: [opened, reopened, edited, synchronize, labeled, unlabeled] + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true jobs: lint-pr-title: runs-on: ubuntu-latest permissions: + contents: read pull-requests: write # to comment on PRs - steps: + steps: - uses: levibostian/action-conventional-pr-linter@acd7e6035a4c70ae2e6aab469c791cc5ca2a989d # v4.0.1 + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: Verify release validation policy + env: + PR_TITLE: ${{ github.event.pull_request.title }} + PR_BODY: ${{ github.event.pull_request.body }} + BASE_SHA: ${{ github.event.pull_request.base.sha }} + HEAD_SHA: ${{ github.event.pull_request.head.sha }} + HEAD_REPOSITORY: ${{ github.event.pull_request.head.repo.full_name }} + TARGET_REPOSITORY: ${{ github.repository }} + PR_AUTHOR: ${{ github.event.pull_request.user.login }} + CI_FULL: ${{ contains(github.event.pull_request.labels.*.name, 'ci:full') }} + run: bash .github/scripts/validate-release-policy.sh diff --git a/.github/workflows/react-native-scene-e2e-full.yml b/.github/workflows/react-native-scene-e2e-full.yml index c244f3e6..b2adf1e9 100644 --- a/.github/workflows/react-native-scene-e2e-full.yml +++ b/.github/workflows/react-native-scene-e2e-full.yml @@ -2,17 +2,34 @@ name: Full React Native scene routing E2E on: pull_request: - types: [opened, synchronize, reopened, labeled] + types: [opened, synchronize, reopened, edited, labeled] permissions: contents: read jobs: + # Metadata edits rerun full so a newer skipped check cannot replace a failure. full-scene-e2e: if: >- - ((github.event.action == 'labeled' && github.event.label.name == 'ci:full') || - (github.event.action != 'labeled' && contains(github.event.pull_request.labels.*.name, 'ci:full'))) && - github.event.pull_request.head.repo.full_name == github.repository + (github.event.action == 'edited' || + github.event.action == 'labeled' || + contains(github.event.pull_request.labels.*.name, 'ci:full') || + startsWith(github.event.pull_request.title, 'fix:') || + startsWith(github.event.pull_request.title, 'fix(') || + startsWith(github.event.pull_request.title, 'feat:') || + startsWith(github.event.pull_request.title, 'feat(') || + startsWith(github.event.pull_request.title, 'perf:') || + startsWith(github.event.pull_request.title, 'perf(') || + startsWith(github.event.pull_request.title, 'revert:') || + startsWith(github.event.pull_request.title, 'revert(') || + startsWith(github.event.pull_request.title, 'Revert "') || + contains(github.event.pull_request.body, 'This reverts commit') || + contains(github.event.pull_request.title, '!:') || + contains(github.event.pull_request.body, 'BREAKING CHANGE:') || + contains(github.event.pull_request.body, 'BREAKING CHANGES:') || + contains(github.event.pull_request.body, 'BREAKING-CHANGE:')) && + github.event.pull_request.head.repo.full_name == github.repository && + github.event.pull_request.user.login != 'dependabot[bot]' uses: ./.github/workflows/react-native-scene-e2e.yml with: full: true diff --git a/.github/workflows/react-native-scene-e2e.yml b/.github/workflows/react-native-scene-e2e.yml index 1984fd23..5c761bb2 100644 --- a/.github/workflows/react-native-scene-e2e.yml +++ b/.github/workflows/react-native-scene-e2e.yml @@ -2,6 +2,7 @@ name: React Native scene routing E2E on: pull_request: + types: [opened, synchronize, reopened, edited] paths: - '.github/workflows/react-native-scene-e2e.yml' - '.github/workflows/react-native-scene-e2e-full.yml' @@ -37,7 +38,23 @@ jobs: name: RN scene notification routing on Xcode 27 if: >- (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) && - (inputs.full == true || !contains(github.event.pull_request.labels.*.name, 'ci:full')) + (inputs.full == true || + (github.event.action != 'edited' && + !contains(github.event.pull_request.labels.*.name, 'ci:full') && + !startsWith(github.event.pull_request.title, 'fix:') && + !startsWith(github.event.pull_request.title, 'fix(') && + !startsWith(github.event.pull_request.title, 'feat:') && + !startsWith(github.event.pull_request.title, 'feat(') && + !startsWith(github.event.pull_request.title, 'perf:') && + !startsWith(github.event.pull_request.title, 'perf(') && + !startsWith(github.event.pull_request.title, 'revert:') && + !startsWith(github.event.pull_request.title, 'revert(') && + !startsWith(github.event.pull_request.title, 'Revert "') && + !contains(github.event.pull_request.body, 'This reverts commit') && + !contains(github.event.pull_request.title, '!:') && + !contains(github.event.pull_request.body, 'BREAKING CHANGE:') && + !contains(github.event.pull_request.body, 'BREAKING CHANGES:') && + !contains(github.event.pull_request.body, 'BREAKING-CHANGE:'))) runs-on: xcode-27 timeout-minutes: 60 From 46913ebf2b3e69629ab9d1242b85028f002fcf35 Mon Sep 17 00:00:00 2001 From: Shahroz Khan Date: Mon, 31 Aug 2026 01:27:25 -0400 Subject: [PATCH 05/15] ci: auto-trigger full validation from commits --- .github/scripts/validate-release-policy.sh | 25 +++++++++++++++++++--- .github/workflows/pr-helper.yml | 12 +++++++++++ 2 files changed, 34 insertions(+), 3 deletions(-) diff --git a/.github/scripts/validate-release-policy.sh b/.github/scripts/validate-release-policy.sh index 5e8cb254..b97fc4fd 100644 --- a/.github/scripts/validate-release-policy.sh +++ b/.github/scripts/validate-release-policy.sh @@ -55,9 +55,15 @@ while IFS= read -r -d '' commit_message; do fi done < "$commit_messages_file" +full_validation=false +requires_full_label=false + +if [[ "$pr_release" == 'true' || "$commit_release" == 'true' || "${CI_FULL:-false}" == 'true' ]]; then + full_validation=true +fi + if [[ "$commit_release" == 'true' && "$pr_release" != 'true' && "${CI_FULL:-false}" != 'true' ]]; then - echo "::error::This PR contains a release-bearing commit, but its PR title/body is non-release. Retitle it with fix:, feat:, perf:, revert:, or a breaking-change marker, or apply ci:full, so release validation runs before merge." - exit 1 + requires_full_label=true fi if [[ ("$pr_release" == 'true' || "$commit_release" == 'true') && ("$HEAD_REPOSITORY" != "$TARGET_REPOSITORY" || "$PR_AUTHOR" == 'dependabot[bot]') ]]; then @@ -65,4 +71,17 @@ if [[ ("$pr_release" == 'true' || "$commit_release" == 'true') && ("$HEAD_REPOSI exit 1 fi -echo "Release validation policy satisfied (pr_release=$pr_release, commit_release=$commit_release)." +if [[ -n "${GITHUB_OUTPUT:-}" ]]; then + { + echo "pr_release=$pr_release" + echo "commit_release=$commit_release" + echo "full_validation=$full_validation" + echo "requires_full_label=$requires_full_label" + } >> "$GITHUB_OUTPUT" +fi + +if [[ "$requires_full_label" == 'true' ]]; then + echo "::notice::A release-bearing commit was detected behind a non-release PR title. The semantic PR helper will apply ci:full automatically." +fi + +echo "Release validation policy satisfied (pr_release=$pr_release, commit_release=$commit_release, full_validation=$full_validation)." diff --git a/.github/workflows/pr-helper.yml b/.github/workflows/pr-helper.yml index 311f7bfe..3597c97f 100644 --- a/.github/workflows/pr-helper.yml +++ b/.github/workflows/pr-helper.yml @@ -13,6 +13,7 @@ jobs: runs-on: ubuntu-latest permissions: contents: read + issues: write pull-requests: write # to comment on PRs steps: - uses: levibostian/action-conventional-pr-linter@acd7e6035a4c70ae2e6aab469c791cc5ca2a989d # v4.0.1 @@ -20,6 +21,7 @@ jobs: with: fetch-depth: 0 - name: Verify release validation policy + id: release-policy env: PR_TITLE: ${{ github.event.pull_request.title }} PR_BODY: ${{ github.event.pull_request.body }} @@ -30,3 +32,13 @@ jobs: PR_AUTHOR: ${{ github.event.pull_request.user.login }} CI_FULL: ${{ contains(github.event.pull_request.labels.*.name, 'ci:full') }} run: bash .github/scripts/validate-release-policy.sh + - name: Automatically request full validation for release-bearing commits + if: steps.release-policy.outputs.requires_full_label == 'true' + env: + GH_TOKEN: ${{ github.token }} + PR_NUMBER: ${{ github.event.pull_request.number }} + REPOSITORY: ${{ github.repository }} + run: >- + gh api "repos/$REPOSITORY/issues/$PR_NUMBER/labels" + --method POST + --raw-field 'labels[]=ci:full' From 88f4501bcfdede47e124e0018c1006f9345304f3 Mon Sep 17 00:00:00 2001 From: Shahroz Khan Date: Mon, 31 Aug 2026 01:48:39 -0400 Subject: [PATCH 06/15] ci: classify release commits inside gated workflows --- .github/scripts/validate-release-policy.sh | 4 +- .github/workflows/build-test-sample-apps.yml | 69 +++++++------------ .github/workflows/pr-helper.yml | 4 +- .../workflows/react-native-scene-e2e-full.yml | 32 +++++---- .github/workflows/react-native-scene-e2e.yml | 43 +++++++----- .github/workflows/reusable-release-policy.yml | 62 +++++++++++++++++ 6 files changed, 134 insertions(+), 80 deletions(-) create mode 100644 .github/workflows/reusable-release-policy.yml diff --git a/.github/scripts/validate-release-policy.sh b/.github/scripts/validate-release-policy.sh index b97fc4fd..50bd5937 100644 --- a/.github/scripts/validate-release-policy.sh +++ b/.github/scripts/validate-release-policy.sh @@ -66,7 +66,9 @@ if [[ "$commit_release" == 'true' && "$pr_release" != 'true' && "${CI_FULL:-fals requires_full_label=true fi -if [[ ("$pr_release" == 'true' || "$commit_release" == 'true') && ("$HEAD_REPOSITORY" != "$TARGET_REPOSITORY" || "$PR_AUTHOR" == 'dependabot[bot]') ]]; then +if [[ "${ENFORCE_TRUSTED_RELEASE:-true}" == 'true' && + ("$pr_release" == 'true' || "$commit_release" == 'true') && + ("$HEAD_REPOSITORY" != "$TARGET_REPOSITORY" || "$PR_AUTHOR" == 'dependabot[bot]') ]]; then echo "::error::Release-bearing PRs require secret-dependent validation from a trusted repository branch. Recreate or update this change on an internal branch before merge." exit 1 fi diff --git a/.github/workflows/build-test-sample-apps.yml b/.github/workflows/build-test-sample-apps.yml index 68bce6f7..058d0445 100644 --- a/.github/workflows/build-test-sample-apps.yml +++ b/.github/workflows/build-test-sample-apps.yml @@ -14,9 +14,26 @@ concurrency: # cancel previous workflow run if one exists. cancel-in-progress: true jobs: + release_policy: + if: github.event_name == 'pull_request' + uses: ./.github/workflows/reusable-release-policy.yml + permissions: + contents: read + with: + pr_title: ${{ github.event.pull_request.title }} + pr_body: ${{ github.event.pull_request.body || '' }} + base_sha: ${{ github.event.pull_request.base.sha }} + head_sha: ${{ github.event.pull_request.head.sha }} + head_repository: ${{ github.event.pull_request.head.repo.full_name }} + target_repository: ${{ github.repository }} + pr_author: ${{ github.event.pull_request.user.login }} + ci_full: ${{ contains(github.event.pull_request.labels.*.name, 'ci:full') }} + # Metadata edits rerun full so a newer skipped check cannot replace a failure. prepare: name: Select sample app lane + if: always() + needs: release_policy runs-on: ubuntu-latest outputs: matrix: ${{ steps.select.outputs.matrix }} @@ -29,21 +46,7 @@ jobs: FULL_PR: >- ${{ (github.event.action == 'edited' || github.event.action == 'labeled' || - contains(github.event.pull_request.labels.*.name, 'ci:full') || - startsWith(github.event.pull_request.title, 'fix:') || - startsWith(github.event.pull_request.title, 'fix(') || - startsWith(github.event.pull_request.title, 'feat:') || - startsWith(github.event.pull_request.title, 'feat(') || - startsWith(github.event.pull_request.title, 'perf:') || - startsWith(github.event.pull_request.title, 'perf(') || - startsWith(github.event.pull_request.title, 'revert:') || - startsWith(github.event.pull_request.title, 'revert(') || - startsWith(github.event.pull_request.title, 'Revert "') || - contains(github.event.pull_request.body, 'This reverts commit') || - contains(github.event.pull_request.title, '!:') || - contains(github.event.pull_request.body, 'BREAKING CHANGE:') || - contains(github.event.pull_request.body, 'BREAKING CHANGES:') || - contains(github.event.pull_request.body, 'BREAKING-CHANGE:')) && + needs.release_policy.outputs.full_validation == 'true') && github.event.pull_request.head.repo.full_name == github.repository && github.event.pull_request.user.login != 'dependabot[bot]' }} run: | @@ -55,26 +58,14 @@ jobs: update-pr-comment: if: >- + always() && github.event_name == 'pull_request' && (github.event.action == 'edited' || github.event.action == 'labeled' || - contains(github.event.pull_request.labels.*.name, 'ci:full') || - startsWith(github.event.pull_request.title, 'fix:') || - startsWith(github.event.pull_request.title, 'fix(') || - startsWith(github.event.pull_request.title, 'feat:') || - startsWith(github.event.pull_request.title, 'feat(') || - startsWith(github.event.pull_request.title, 'perf:') || - startsWith(github.event.pull_request.title, 'perf(') || - startsWith(github.event.pull_request.title, 'revert:') || - startsWith(github.event.pull_request.title, 'revert(') || - startsWith(github.event.pull_request.title, 'Revert "') || - contains(github.event.pull_request.body, 'This reverts commit') || - contains(github.event.pull_request.title, '!:') || - contains(github.event.pull_request.body, 'BREAKING CHANGE:') || - contains(github.event.pull_request.body, 'BREAKING CHANGES:') || - contains(github.event.pull_request.body, 'BREAKING-CHANGE:')) && + needs.release_policy.outputs.full_validation == 'true') && github.event.pull_request.head.repo.full_name == github.repository && github.event.pull_request.user.login != 'dependabot[bot]' + needs: release_policy runs-on: ubuntu-latest permissions: pull-requests: write # to be able to comment on PR @@ -133,24 +124,10 @@ jobs: github.event_name == 'pull_request' && (github.event.action == 'edited' || github.event.action == 'labeled' || - contains(github.event.pull_request.labels.*.name, 'ci:full') || - startsWith(github.event.pull_request.title, 'fix:') || - startsWith(github.event.pull_request.title, 'fix(') || - startsWith(github.event.pull_request.title, 'feat:') || - startsWith(github.event.pull_request.title, 'feat(') || - startsWith(github.event.pull_request.title, 'perf:') || - startsWith(github.event.pull_request.title, 'perf(') || - startsWith(github.event.pull_request.title, 'revert:') || - startsWith(github.event.pull_request.title, 'revert(') || - startsWith(github.event.pull_request.title, 'Revert "') || - contains(github.event.pull_request.body, 'This reverts commit') || - contains(github.event.pull_request.title, '!:') || - contains(github.event.pull_request.body, 'BREAKING CHANGE:') || - contains(github.event.pull_request.body, 'BREAKING CHANGES:') || - contains(github.event.pull_request.body, 'BREAKING-CHANGE:')) && + needs.release_policy.outputs.full_validation == 'true') && github.event.pull_request.head.repo.full_name == github.repository && github.event.pull_request.user.login != 'dependabot[bot]' - needs: [build-sample-apps, update-pr-comment] + needs: [release_policy, build-sample-apps, update-pr-comment] runs-on: ubuntu-latest permissions: pull-requests: write # to be able to comment on PR diff --git a/.github/workflows/pr-helper.yml b/.github/workflows/pr-helper.yml index 3597c97f..61756f2e 100644 --- a/.github/workflows/pr-helper.yml +++ b/.github/workflows/pr-helper.yml @@ -32,7 +32,9 @@ jobs: PR_AUTHOR: ${{ github.event.pull_request.user.login }} CI_FULL: ${{ contains(github.event.pull_request.labels.*.name, 'ci:full') }} run: bash .github/scripts/validate-release-policy.sh - - name: Automatically request full validation for release-bearing commits + # GITHUB_TOKEN label events do not start workflows. Gated workflows scan + # commits independently; this label is a durable, visible explanation. + - name: Mark automatically detected full validation if: steps.release-policy.outputs.requires_full_label == 'true' env: GH_TOKEN: ${{ github.token }} diff --git a/.github/workflows/react-native-scene-e2e-full.yml b/.github/workflows/react-native-scene-e2e-full.yml index b2adf1e9..7a0b9c10 100644 --- a/.github/workflows/react-native-scene-e2e-full.yml +++ b/.github/workflows/react-native-scene-e2e-full.yml @@ -8,26 +8,28 @@ permissions: contents: read jobs: + release_policy: + uses: ./.github/workflows/reusable-release-policy.yml + permissions: + contents: read + with: + pr_title: ${{ github.event.pull_request.title }} + pr_body: ${{ github.event.pull_request.body || '' }} + base_sha: ${{ github.event.pull_request.base.sha }} + head_sha: ${{ github.event.pull_request.head.sha }} + head_repository: ${{ github.event.pull_request.head.repo.full_name }} + target_repository: ${{ github.repository }} + pr_author: ${{ github.event.pull_request.user.login }} + ci_full: ${{ contains(github.event.pull_request.labels.*.name, 'ci:full') }} + # Metadata edits rerun full so a newer skipped check cannot replace a failure. full-scene-e2e: + needs: release_policy if: >- + always() && (github.event.action == 'edited' || github.event.action == 'labeled' || - contains(github.event.pull_request.labels.*.name, 'ci:full') || - startsWith(github.event.pull_request.title, 'fix:') || - startsWith(github.event.pull_request.title, 'fix(') || - startsWith(github.event.pull_request.title, 'feat:') || - startsWith(github.event.pull_request.title, 'feat(') || - startsWith(github.event.pull_request.title, 'perf:') || - startsWith(github.event.pull_request.title, 'perf(') || - startsWith(github.event.pull_request.title, 'revert:') || - startsWith(github.event.pull_request.title, 'revert(') || - startsWith(github.event.pull_request.title, 'Revert "') || - contains(github.event.pull_request.body, 'This reverts commit') || - contains(github.event.pull_request.title, '!:') || - contains(github.event.pull_request.body, 'BREAKING CHANGE:') || - contains(github.event.pull_request.body, 'BREAKING CHANGES:') || - contains(github.event.pull_request.body, 'BREAKING-CHANGE:')) && + needs.release_policy.outputs.full_validation == 'true') && github.event.pull_request.head.repo.full_name == github.repository && github.event.pull_request.user.login != 'dependabot[bot]' uses: ./.github/workflows/react-native-scene-e2e.yml diff --git a/.github/workflows/react-native-scene-e2e.yml b/.github/workflows/react-native-scene-e2e.yml index 5c761bb2..aa3d80d2 100644 --- a/.github/workflows/react-native-scene-e2e.yml +++ b/.github/workflows/react-native-scene-e2e.yml @@ -34,27 +34,36 @@ concurrency: cancel-in-progress: ${{ github.event_name == 'pull_request' }} jobs: + release_policy: + if: >- + github.event_name == 'pull_request' && + inputs.full != true + uses: ./.github/workflows/reusable-release-policy.yml + permissions: + contents: read + with: + pr_title: ${{ github.event.pull_request.title }} + pr_body: ${{ github.event.pull_request.body || '' }} + base_sha: ${{ github.event.pull_request.base.sha }} + head_sha: ${{ github.event.pull_request.head.sha }} + head_repository: ${{ github.event.pull_request.head.repo.full_name }} + target_repository: ${{ github.repository }} + pr_author: ${{ github.event.pull_request.user.login }} + ci_full: ${{ contains(github.event.pull_request.labels.*.name, 'ci:full') }} + route-notifications: name: RN scene notification routing on Xcode 27 + needs: release_policy if: >- - (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) && + always() && + (github.event_name != 'pull_request' || + (github.event.pull_request.head.repo.full_name == github.repository && + github.event.pull_request.user.login != 'dependabot[bot]')) && (inputs.full == true || - (github.event.action != 'edited' && - !contains(github.event.pull_request.labels.*.name, 'ci:full') && - !startsWith(github.event.pull_request.title, 'fix:') && - !startsWith(github.event.pull_request.title, 'fix(') && - !startsWith(github.event.pull_request.title, 'feat:') && - !startsWith(github.event.pull_request.title, 'feat(') && - !startsWith(github.event.pull_request.title, 'perf:') && - !startsWith(github.event.pull_request.title, 'perf(') && - !startsWith(github.event.pull_request.title, 'revert:') && - !startsWith(github.event.pull_request.title, 'revert(') && - !startsWith(github.event.pull_request.title, 'Revert "') && - !contains(github.event.pull_request.body, 'This reverts commit') && - !contains(github.event.pull_request.title, '!:') && - !contains(github.event.pull_request.body, 'BREAKING CHANGE:') && - !contains(github.event.pull_request.body, 'BREAKING CHANGES:') && - !contains(github.event.pull_request.body, 'BREAKING-CHANGE:'))) + github.event_name != 'pull_request' || + (needs.release_policy.result == 'success' && + github.event.action != 'edited' && + needs.release_policy.outputs.full_validation != 'true')) runs-on: xcode-27 timeout-minutes: 60 diff --git a/.github/workflows/reusable-release-policy.yml b/.github/workflows/reusable-release-policy.yml new file mode 100644 index 00000000..d3c493a4 --- /dev/null +++ b/.github/workflows/reusable-release-policy.yml @@ -0,0 +1,62 @@ +name: Reusable release policy + +on: + workflow_call: + inputs: + pr_title: + type: string + required: true + pr_body: + type: string + required: false + default: '' + base_sha: + type: string + required: true + head_sha: + type: string + required: true + head_repository: + type: string + required: true + target_repository: + type: string + required: true + pr_author: + type: string + required: true + ci_full: + type: boolean + required: true + outputs: + full_validation: + description: Whether this PR needs the full validation lane. + value: ${{ jobs.classify.outputs.full_validation }} + +jobs: + classify: + runs-on: ubuntu-latest + permissions: + contents: read + outputs: + full_validation: ${{ steps.policy.outputs.full_validation }} + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: Classify release validation + id: policy + env: + PR_TITLE: ${{ inputs.pr_title }} + PR_BODY: ${{ inputs.pr_body }} + BASE_SHA: ${{ inputs.base_sha }} + HEAD_SHA: ${{ inputs.head_sha }} + HEAD_REPOSITORY: ${{ inputs.head_repository }} + TARGET_REPOSITORY: ${{ inputs.target_repository }} + PR_AUTHOR: ${{ inputs.pr_author }} + CI_FULL: ${{ inputs.ci_full }} + # The semantic PR helper owns the trust-policy failure. Callers use + # this reusable workflow only to select safe full or light lanes. + ENFORCE_TRUSTED_RELEASE: 'false' + run: bash .github/scripts/validate-release-policy.sh + From a6c1c2c46ff6dbbd292991babf6573647169d67d Mon Sep 17 00:00:00 2001 From: Shahroz Khan Date: Mon, 31 Aug 2026 10:32:14 -0400 Subject: [PATCH 07/15] ci: keep PR correctness coverage automatic --- .github/actionlint.yaml | 5 ++ .github/scripts/validate-release-policy.sh | 26 +--------- .github/workflows/build-sample-app.yml | 38 ++++++++++---- .github/workflows/build-test-sample-apps.yml | 52 +++++++++++++------ .github/workflows/pr-helper.yml | 31 +---------- .../workflows/react-native-scene-e2e-full.yml | 35 +++++-------- .github/workflows/react-native-scene-e2e.yml | 51 ------------------ .github/workflows/reusable-release-policy.yml | 23 +------- 8 files changed, 87 insertions(+), 174 deletions(-) create mode 100644 .github/actionlint.yaml diff --git a/.github/actionlint.yaml b/.github/actionlint.yaml new file mode 100644 index 00000000..835a58af --- /dev/null +++ b/.github/actionlint.yaml @@ -0,0 +1,5 @@ +self-hosted-runner: + labels: + # GitHub-hosted public preview label announced in actions/runner-images#14404. + # Remove this override after actionlint recognizes the hosted label. + - xcode-27 diff --git a/.github/scripts/validate-release-policy.sh b/.github/scripts/validate-release-policy.sh index 50bd5937..27ec7b59 100644 --- a/.github/scripts/validate-release-policy.sh +++ b/.github/scripts/validate-release-policy.sh @@ -4,10 +4,6 @@ set -euo pipefail : "${PR_TITLE:?PR_TITLE is required}" : "${BASE_SHA:?BASE_SHA is required}" : "${HEAD_SHA:?HEAD_SHA is required}" -: "${HEAD_REPOSITORY:?HEAD_REPOSITORY is required}" -: "${TARGET_REPOSITORY:?TARGET_REPOSITORY is required}" -: "${PR_AUTHOR:?PR_AUTHOR is required}" - is_release_message() { local message="$1" local header @@ -34,11 +30,10 @@ is_release_message() { return 1 } -pr_message="$PR_TITLE"$'\n\n'"${PR_BODY:-}" pr_release=false commit_release=false -if is_release_message "$pr_message"; then +if is_release_message "$PR_TITLE"; then pr_release=true fi @@ -56,34 +51,17 @@ while IFS= read -r -d '' commit_message; do done < "$commit_messages_file" full_validation=false -requires_full_label=false if [[ "$pr_release" == 'true' || "$commit_release" == 'true' || "${CI_FULL:-false}" == 'true' ]]; then full_validation=true fi -if [[ "$commit_release" == 'true' && "$pr_release" != 'true' && "${CI_FULL:-false}" != 'true' ]]; then - requires_full_label=true -fi - -if [[ "${ENFORCE_TRUSTED_RELEASE:-true}" == 'true' && - ("$pr_release" == 'true' || "$commit_release" == 'true') && - ("$HEAD_REPOSITORY" != "$TARGET_REPOSITORY" || "$PR_AUTHOR" == 'dependabot[bot]') ]]; then - echo "::error::Release-bearing PRs require secret-dependent validation from a trusted repository branch. Recreate or update this change on an internal branch before merge." - exit 1 -fi - if [[ -n "${GITHUB_OUTPUT:-}" ]]; then { echo "pr_release=$pr_release" echo "commit_release=$commit_release" echo "full_validation=$full_validation" - echo "requires_full_label=$requires_full_label" } >> "$GITHUB_OUTPUT" fi -if [[ "$requires_full_label" == 'true' ]]; then - echo "::notice::A release-bearing commit was detected behind a non-release PR title. The semantic PR helper will apply ci:full automatically." -fi - -echo "Release validation policy satisfied (pr_release=$pr_release, commit_release=$commit_release, full_validation=$full_validation)." +echo "Distribution policy classified (pr_release=$pr_release, commit_release=$commit_release, full_validation=$full_validation)." diff --git a/.github/workflows/build-sample-app.yml b/.github/workflows/build-sample-app.yml index e5e4eb2f..f7319040 100644 --- a/.github/workflows/build-sample-app.yml +++ b/.github/workflows/build-sample-app.yml @@ -81,15 +81,26 @@ jobs: # Copy the tracked project file to the root of the repository cp -R SampleApp.xcodeproj.tracked SampleApp.xcodeproj + - name: Test deep-link acknowledgement state + if: ${{ inputs.platform == 'ios' && inputs.app_name == 'APN' }} + shell: bash + working-directory: . + run: | + xcrun swiftc \ + ios/wrappers/CustomerIOReactNativeDeepLinkRequestStore.swift \ + scripts/test_ios_deep_link_request_store.swift \ + -o "$RUNNER_TEMP/test-ios-deep-link-request-store" + "$RUNNER_TEMP/test-ios-deep-link-request-store" + # Install CLI tools, Ruby, and Ruby dependencies for Fastlane - name: Set IS_PRIMARY_APP run: | if [[ "${{ inputs.app_name }}" == "APN" ]]; then - echo "IS_PRIMARY_APP=true" >> $GITHUB_ENV - echo "PUSH_PROVIDER=apn" >> $GITHUB_ENV + echo "IS_PRIMARY_APP=true" >> "$GITHUB_ENV" + echo "PUSH_PROVIDER=apn" >> "$GITHUB_ENV" else - echo "IS_PRIMARY_APP=false" >> $GITHUB_ENV - echo "PUSH_PROVIDER=fcm" >> $GITHUB_ENV + echo "IS_PRIMARY_APP=false" >> "$GITHUB_ENV" + echo "PUSH_PROVIDER=fcm" >> "$GITHUB_ENV" fi - name: Set Default Firebase Distribution Groups @@ -114,7 +125,8 @@ jobs: fi # Export the groups as an environment variable - echo "firebase_distribution_groups=$(IFS=','; echo "${distribution_groups[*]}")" >> $GITHUB_ENV + groups=$(IFS=','; echo "${distribution_groups[*]}") + echo "firebase_distribution_groups=$groups" >> "$GITHUB_ENV" - name: Install sd CLI for Android builds if: ${{ inputs.platform == 'android' }} @@ -152,9 +164,11 @@ jobs: id: export-vars run: | echo "Exporting SDK and App Version..." - echo "sdk_version_name=${SDK_VERSION_NAME}" >> $GITHUB_OUTPUT - echo "app_version_name=${APP_VERSION_NAME}" >> $GITHUB_OUTPUT - echo "app_version_code=${APP_VERSION_CODE}" >> $GITHUB_OUTPUT + { + echo "sdk_version_name=${SDK_VERSION_NAME}" + echo "app_version_name=${APP_VERSION_NAME}" + echo "app_version_code=${APP_VERSION_CODE}" + } >> "$GITHUB_OUTPUT" env: SDK_VERSION_NAME: ${{ env.SDK_VERSION_NAME }} APP_VERSION_NAME: ${{ env.APP_VERSION_NAME }} @@ -185,10 +199,12 @@ jobs: APP_VERSION_CODE: ${{ env.APP_VERSION_CODE }} - name: Set Git Context Variables + env: + BRANCH_NAME_RAW: ${{ github.head_ref || github.ref_name }} + COMMIT_HASH_RAW: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }} run: | - echo "BRANCH_NAME=${{ github.head_ref || github.ref_name }}" >> $GITHUB_ENV - COMMIT_HASH="${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }}" - echo "COMMIT_HASH=${COMMIT_HASH:0:7}" >> $GITHUB_ENV + echo "BRANCH_NAME=$BRANCH_NAME_RAW" >> "$GITHUB_ENV" + echo "COMMIT_HASH=${COMMIT_HASH_RAW:0:7}" >> "$GITHUB_ENV" - name: Setup workspace credentials in React Native environment files run: | diff --git a/.github/workflows/build-test-sample-apps.yml b/.github/workflows/build-test-sample-apps.yml index 058d0445..93e829d0 100644 --- a/.github/workflows/build-test-sample-apps.yml +++ b/.github/workflows/build-test-sample-apps.yml @@ -21,37 +21,59 @@ jobs: contents: read with: pr_title: ${{ github.event.pull_request.title }} - pr_body: ${{ github.event.pull_request.body || '' }} base_sha: ${{ github.event.pull_request.base.sha }} head_sha: ${{ github.event.pull_request.head.sha }} - head_repository: ${{ github.event.pull_request.head.repo.full_name }} - target_repository: ${{ github.repository }} - pr_author: ${{ github.event.pull_request.user.login }} ci_full: ${{ contains(github.event.pull_request.labels.*.name, 'ci:full') }} - # Metadata edits rerun full so a newer skipped check cannot replace a failure. prepare: name: Select sample app lane + # Fail closed on metadata events: an edited or unrelated-label run repeats + # unsigned verification instead of replacing a failed check with a skip. if: always() needs: release_policy runs-on: ubuntu-latest outputs: matrix: ${{ steps.select.outputs.matrix }} steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: Select verification or distribution matrix id: select shell: bash env: EVENT_NAME: ${{ github.event_name }} + BASE_SHA: ${{ github.event.pull_request.base.sha }} + HEAD_SHA: ${{ github.event.pull_request.head.sha }} FULL_PR: >- - ${{ (github.event.action == 'edited' || - github.event.action == 'labeled' || - needs.release_policy.outputs.full_validation == 'true') && + ${{ needs.release_policy.outputs.full_validation == 'true' && + (github.event.action != 'edited' || github.event.changes.title.from != '') && + (github.event.action != 'labeled' || github.event.label.name == 'ci:full') && github.event.pull_request.head.repo.full_name == github.repository && github.event.pull_request.user.login != 'dependabot[bot]' }} run: | + set -euo pipefail if [[ "$EVENT_NAME" == 'pull_request' && "$FULL_PR" != 'true' ]]; then - echo 'matrix=[{"name":"APN","cio-workspace-name":"Mobile: React Native","platform":"ios","platform_name":"iOS","platform_name_upper":"IOS","should_distribute":false},{"name":"APN","cio-workspace-name":"Mobile: React Native","platform":"android","platform_name":"Android","platform_name_upper":"ANDROID","should_distribute":false}]' >> "$GITHUB_OUTPUT" + matrix='[{"name":"APN","cio-workspace-name":"Mobile: React Native","platform":"ios","platform_name":"iOS","platform_name_upper":"IOS","should_distribute":false},{"name":"APN","cio-workspace-name":"Mobile: React Native","platform":"android","platform_name":"Android","platform_name_upper":"ANDROID","should_distribute":false}]' + fcm_required=false + changed_paths="$RUNNER_TEMP/react-native-changed-paths" + git cat-file -e "$BASE_SHA^{commit}" + git cat-file -e "$HEAD_SHA^{commit}" + git diff --name-only -z "$BASE_SHA...$HEAD_SHA" > "$changed_paths" + + while IFS= read -r -d '' path; do + case "$path" in + ios/*|example/ios/*|customerio-reactnative*.podspec|package.json|package-lock.json|scripts/*|.github/workflows/build-sample-app.yml|.github/workflows/build-test-sample-apps.yml) + fcm_required=true + ;; + esac + done < "$changed_paths" + + if [[ "$fcm_required" == 'true' ]]; then + matrix=$(jq -c '. + [{"name":"FCM","cio-workspace-name":"Mobile: xReact Native FCM workspace","platform":"ios","platform_name":"iOS","platform_name_upper":"IOS","should_distribute":false}]' <<< "$matrix") + fi + echo "matrix=$matrix" >> "$GITHUB_OUTPUT" else echo 'matrix=[{"name":"APN","cio-workspace-name":"Mobile: React Native","platform":"ios","platform_name":"iOS","platform_name_upper":"IOS","should_distribute":true},{"name":"FCM","cio-workspace-name":"Mobile: xReact Native FCM workspace","platform":"ios","platform_name":"iOS","platform_name_upper":"IOS","should_distribute":true},{"name":"APN","cio-workspace-name":"Mobile: React Native","platform":"android","platform_name":"Android","platform_name_upper":"ANDROID","should_distribute":true}]' >> "$GITHUB_OUTPUT" fi @@ -60,9 +82,9 @@ jobs: if: >- always() && github.event_name == 'pull_request' && - (github.event.action == 'edited' || - github.event.action == 'labeled' || - needs.release_policy.outputs.full_validation == 'true') && + needs.release_policy.outputs.full_validation == 'true' && + (github.event.action != 'edited' || github.event.changes.title.from != '') && + (github.event.action != 'labeled' || github.event.label.name == 'ci:full') && github.event.pull_request.head.repo.full_name == github.repository && github.event.pull_request.user.login != 'dependabot[bot]' needs: release_policy @@ -122,9 +144,9 @@ jobs: update-pr-comment-with-status: if: >- github.event_name == 'pull_request' && - (github.event.action == 'edited' || - github.event.action == 'labeled' || - needs.release_policy.outputs.full_validation == 'true') && + needs.release_policy.outputs.full_validation == 'true' && + (github.event.action != 'edited' || github.event.changes.title.from != '') && + (github.event.action != 'labeled' || github.event.label.name == 'ci:full') && github.event.pull_request.head.repo.full_name == github.repository && github.event.pull_request.user.login != 'dependabot[bot]' needs: [release_policy, build-sample-apps, update-pr-comment] diff --git a/.github/workflows/pr-helper.yml b/.github/workflows/pr-helper.yml index 61756f2e..bd4d6aa0 100644 --- a/.github/workflows/pr-helper.yml +++ b/.github/workflows/pr-helper.yml @@ -2,7 +2,7 @@ name: Semantic PR helper on: pull_request: - types: [opened, reopened, edited, synchronize, labeled, unlabeled] + types: [opened, reopened, edited, synchronize] concurrency: group: ${{ github.workflow }}-${{ github.ref }} @@ -12,35 +12,6 @@ jobs: lint-pr-title: runs-on: ubuntu-latest permissions: - contents: read - issues: write pull-requests: write # to comment on PRs steps: - uses: levibostian/action-conventional-pr-linter@acd7e6035a4c70ae2e6aab469c791cc5ca2a989d # v4.0.1 - - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - name: Verify release validation policy - id: release-policy - env: - PR_TITLE: ${{ github.event.pull_request.title }} - PR_BODY: ${{ github.event.pull_request.body }} - BASE_SHA: ${{ github.event.pull_request.base.sha }} - HEAD_SHA: ${{ github.event.pull_request.head.sha }} - HEAD_REPOSITORY: ${{ github.event.pull_request.head.repo.full_name }} - TARGET_REPOSITORY: ${{ github.repository }} - PR_AUTHOR: ${{ github.event.pull_request.user.login }} - CI_FULL: ${{ contains(github.event.pull_request.labels.*.name, 'ci:full') }} - run: bash .github/scripts/validate-release-policy.sh - # GITHUB_TOKEN label events do not start workflows. Gated workflows scan - # commits independently; this label is a durable, visible explanation. - - name: Mark automatically detected full validation - if: steps.release-policy.outputs.requires_full_label == 'true' - env: - GH_TOKEN: ${{ github.token }} - PR_NUMBER: ${{ github.event.pull_request.number }} - REPOSITORY: ${{ github.repository }} - run: >- - gh api "repos/$REPOSITORY/issues/$PR_NUMBER/labels" - --method POST - --raw-field 'labels[]=ci:full' diff --git a/.github/workflows/react-native-scene-e2e-full.yml b/.github/workflows/react-native-scene-e2e-full.yml index 7a0b9c10..efbced35 100644 --- a/.github/workflows/react-native-scene-e2e-full.yml +++ b/.github/workflows/react-native-scene-e2e-full.yml @@ -2,34 +2,27 @@ name: Full React Native scene routing E2E on: pull_request: - types: [opened, synchronize, reopened, edited, labeled] + types: [opened, synchronize, reopened, labeled] + paths: + - '.github/workflows/react-native-scene-e2e.yml' + - '.github/workflows/react-native-scene-e2e-full.yml' + - '.maestro/**' + - 'src/**' + - 'ios/wrappers/**' + - 'ios/cocoapods_deployment_target.rb' + - 'customerio-reactnative.podspec' + - 'customerio-reactnative-richpush.podspec' + - 'package.json' + - 'package-lock.json' permissions: contents: read jobs: - release_policy: - uses: ./.github/workflows/reusable-release-policy.yml - permissions: - contents: read - with: - pr_title: ${{ github.event.pull_request.title }} - pr_body: ${{ github.event.pull_request.body || '' }} - base_sha: ${{ github.event.pull_request.base.sha }} - head_sha: ${{ github.event.pull_request.head.sha }} - head_repository: ${{ github.event.pull_request.head.repo.full_name }} - target_repository: ${{ github.repository }} - pr_author: ${{ github.event.pull_request.user.login }} - ci_full: ${{ contains(github.event.pull_request.labels.*.name, 'ci:full') }} - - # Metadata edits rerun full so a newer skipped check cannot replace a failure. full-scene-e2e: - needs: release_policy if: >- - always() && - (github.event.action == 'edited' || - github.event.action == 'labeled' || - needs.release_policy.outputs.full_validation == 'true') && + contains(github.event.pull_request.labels.*.name, 'ci:full') && + (github.event.action != 'labeled' || github.event.label.name == 'ci:full') && github.event.pull_request.head.repo.full_name == github.repository && github.event.pull_request.user.login != 'dependabot[bot]' uses: ./.github/workflows/react-native-scene-e2e.yml diff --git a/.github/workflows/react-native-scene-e2e.yml b/.github/workflows/react-native-scene-e2e.yml index aa3d80d2..41dd5663 100644 --- a/.github/workflows/react-native-scene-e2e.yml +++ b/.github/workflows/react-native-scene-e2e.yml @@ -1,20 +1,6 @@ name: React Native scene routing E2E on: - pull_request: - types: [opened, synchronize, reopened, edited] - paths: - - '.github/workflows/react-native-scene-e2e.yml' - - '.github/workflows/react-native-scene-e2e-full.yml' - - '.maestro/**' - - 'src/**' - - 'ios/wrappers/**' - - 'ios/cocoapods_deployment_target.rb' - - 'customerio-reactnative.podspec' - - 'customerio-reactnative-richpush.podspec' - - 'scripts/test_ios_deep_link_request_store.swift' - - 'package.json' - - 'package-lock.json' schedule: - cron: '47 7 * * *' workflow_dispatch: @@ -34,36 +20,8 @@ concurrency: cancel-in-progress: ${{ github.event_name == 'pull_request' }} jobs: - release_policy: - if: >- - github.event_name == 'pull_request' && - inputs.full != true - uses: ./.github/workflows/reusable-release-policy.yml - permissions: - contents: read - with: - pr_title: ${{ github.event.pull_request.title }} - pr_body: ${{ github.event.pull_request.body || '' }} - base_sha: ${{ github.event.pull_request.base.sha }} - head_sha: ${{ github.event.pull_request.head.sha }} - head_repository: ${{ github.event.pull_request.head.repo.full_name }} - target_repository: ${{ github.repository }} - pr_author: ${{ github.event.pull_request.user.login }} - ci_full: ${{ contains(github.event.pull_request.labels.*.name, 'ci:full') }} - route-notifications: name: RN scene notification routing on Xcode 27 - needs: release_policy - if: >- - always() && - (github.event_name != 'pull_request' || - (github.event.pull_request.head.repo.full_name == github.repository && - github.event.pull_request.user.login != 'dependabot[bot]')) && - (inputs.full == true || - github.event_name != 'pull_request' || - (needs.release_policy.result == 'success' && - github.event.action != 'edited' && - needs.release_policy.outputs.full_validation != 'true')) runs-on: xcode-27 timeout-minutes: 60 @@ -102,15 +60,6 @@ jobs: unzip -q "$maestro_zip" -d "$maestro_dir" echo "$maestro_dir/maestro/bin" >> "$GITHUB_PATH" - - name: Test deep-link acknowledgement state - shell: bash - run: | - xcrun swiftc \ - ios/wrappers/CustomerIOReactNativeDeepLinkRequestStore.swift \ - scripts/test_ios_deep_link_request_store.swift \ - -o "$RUNNER_TEMP/test-ios-deep-link-request-store" - "$RUNNER_TEMP/test-ios-deep-link-request-store" - - name: Test warm and terminated notification routing shell: bash env: diff --git a/.github/workflows/reusable-release-policy.yml b/.github/workflows/reusable-release-policy.yml index d3c493a4..07f12233 100644 --- a/.github/workflows/reusable-release-policy.yml +++ b/.github/workflows/reusable-release-policy.yml @@ -6,31 +6,18 @@ on: pr_title: type: string required: true - pr_body: - type: string - required: false - default: '' base_sha: type: string required: true head_sha: type: string required: true - head_repository: - type: string - required: true - target_repository: - type: string - required: true - pr_author: - type: string - required: true ci_full: type: boolean required: true outputs: full_validation: - description: Whether this PR needs the full validation lane. + description: Whether release intent or ci:full requests distribution validation. value: ${{ jobs.classify.outputs.full_validation }} jobs: @@ -48,15 +35,7 @@ jobs: id: policy env: PR_TITLE: ${{ inputs.pr_title }} - PR_BODY: ${{ inputs.pr_body }} BASE_SHA: ${{ inputs.base_sha }} HEAD_SHA: ${{ inputs.head_sha }} - HEAD_REPOSITORY: ${{ inputs.head_repository }} - TARGET_REPOSITORY: ${{ inputs.target_repository }} - PR_AUTHOR: ${{ inputs.pr_author }} CI_FULL: ${{ inputs.ci_full }} - # The semantic PR helper owns the trust-policy failure. Callers use - # this reusable workflow only to select safe full or light lanes. - ENFORCE_TRUSTED_RELEASE: 'false' run: bash .github/scripts/validate-release-policy.sh - From e6da6548b807f4bfc9b8558c34ac0131a5cf88a0 Mon Sep 17 00:00:00 2001 From: Shahroz Khan Date: Mon, 31 Aug 2026 13:17:02 -0400 Subject: [PATCH 08/15] fix: preserve full CI runs across metadata events --- .github/workflows/build-test-sample-apps.yml | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build-test-sample-apps.yml b/.github/workflows/build-test-sample-apps.yml index 93e829d0..feef7115 100644 --- a/.github/workflows/build-test-sample-apps.yml +++ b/.github/workflows/build-test-sample-apps.yml @@ -10,7 +10,17 @@ on: branches: [main, feature/*] concurrency: # cancel previous workflow run if one exists. - group: ${{ github.workflow }}-${{ github.ref }} + # Unrelated labels and body-only edits must not cancel an in-flight release or + # ci:full run. They share a separate group, where unsigned validation repeats. + group: >- + ${{ github.workflow }}-${{ github.ref }}-${{ + github.event_name == 'pull_request' && + ((github.event.action == 'labeled' && github.event.label.name != 'ci:full') || + (github.event.action == 'edited' && + github.event.changes.title.from == '' && + github.event.changes.base.ref.from == '')) && + 'metadata' || 'validation' + }} cancel-in-progress: true jobs: From e323e8946fd0c06f50644a7ed9651661ed92f366 Mon Sep 17 00:00:00 2001 From: Shahroz Khan Date: Mon, 31 Aug 2026 13:33:52 -0400 Subject: [PATCH 09/15] ci: make metadata event classification explicit --- .github/workflows/build-test-sample-apps.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build-test-sample-apps.yml b/.github/workflows/build-test-sample-apps.yml index feef7115..193eb9fb 100644 --- a/.github/workflows/build-test-sample-apps.yml +++ b/.github/workflows/build-test-sample-apps.yml @@ -17,8 +17,8 @@ concurrency: # cancel previous workflow run if one exists. github.event_name == 'pull_request' && ((github.event.action == 'labeled' && github.event.label.name != 'ci:full') || (github.event.action == 'edited' && - github.event.changes.title.from == '' && - github.event.changes.base.ref.from == '')) && + !github.event.changes.title && + !github.event.changes.base)) && 'metadata' || 'validation' }} cancel-in-progress: true From 7f2f3f09a2a5dc64cafc2073ba3623a7393d53e7 Mon Sep 17 00:00:00 2001 From: Shahroz Khan Date: Mon, 31 Aug 2026 14:17:37 -0400 Subject: [PATCH 10/15] ci: align edited-event full-lane guards --- .github/workflows/build-test-sample-apps.yml | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/.github/workflows/build-test-sample-apps.yml b/.github/workflows/build-test-sample-apps.yml index 193eb9fb..3c00da32 100644 --- a/.github/workflows/build-test-sample-apps.yml +++ b/.github/workflows/build-test-sample-apps.yml @@ -17,8 +17,8 @@ concurrency: # cancel previous workflow run if one exists. github.event_name == 'pull_request' && ((github.event.action == 'labeled' && github.event.label.name != 'ci:full') || (github.event.action == 'edited' && - !github.event.changes.title && - !github.event.changes.base)) && + github.event.changes.title == null && + github.event.changes.base == null)) && 'metadata' || 'validation' }} cancel-in-progress: true @@ -58,7 +58,9 @@ jobs: HEAD_SHA: ${{ github.event.pull_request.head.sha }} FULL_PR: >- ${{ needs.release_policy.outputs.full_validation == 'true' && - (github.event.action != 'edited' || github.event.changes.title.from != '') && + (github.event.action != 'edited' || + github.event.changes.title != null || + github.event.changes.base != null) && (github.event.action != 'labeled' || github.event.label.name == 'ci:full') && github.event.pull_request.head.repo.full_name == github.repository && github.event.pull_request.user.login != 'dependabot[bot]' }} @@ -93,7 +95,9 @@ jobs: always() && github.event_name == 'pull_request' && needs.release_policy.outputs.full_validation == 'true' && - (github.event.action != 'edited' || github.event.changes.title.from != '') && + (github.event.action != 'edited' || + github.event.changes.title != null || + github.event.changes.base != null) && (github.event.action != 'labeled' || github.event.label.name == 'ci:full') && github.event.pull_request.head.repo.full_name == github.repository && github.event.pull_request.user.login != 'dependabot[bot]' @@ -155,7 +159,9 @@ jobs: if: >- github.event_name == 'pull_request' && needs.release_policy.outputs.full_validation == 'true' && - (github.event.action != 'edited' || github.event.changes.title.from != '') && + (github.event.action != 'edited' || + github.event.changes.title != null || + github.event.changes.base != null) && (github.event.action != 'labeled' || github.event.label.name == 'ci:full') && github.event.pull_request.head.repo.full_name == github.repository && github.event.pull_request.user.login != 'dependabot[bot]' From 1e26031d67fecebdf7bebd49cc500d8ad84039aa Mon Sep 17 00:00:00 2001 From: Shahroz Khan Date: Mon, 31 Aug 2026 17:25:08 -0400 Subject: [PATCH 11/15] ci: include sample changes in FCM verification --- .github/workflows/build-test-sample-apps.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build-test-sample-apps.yml b/.github/workflows/build-test-sample-apps.yml index 3c00da32..d3408dae 100644 --- a/.github/workflows/build-test-sample-apps.yml +++ b/.github/workflows/build-test-sample-apps.yml @@ -76,7 +76,7 @@ jobs: while IFS= read -r -d '' path; do case "$path" in - ios/*|example/ios/*|customerio-reactnative*.podspec|package.json|package-lock.json|scripts/*|.github/workflows/build-sample-app.yml|.github/workflows/build-test-sample-apps.yml) + ios/*|example/*|customerio-reactnative*.podspec|package.json|package-lock.json|scripts/*|.github/workflows/build-sample-app.yml|.github/workflows/build-test-sample-apps.yml) fcm_required=true ;; esac From 8ffa41a20710a3bf6749fd7243c50a054eda6b1a Mon Sep 17 00:00:00 2001 From: Shahroz Khan Date: Mon, 31 Aug 2026 17:55:17 -0400 Subject: [PATCH 12/15] ci: preserve rename source paths in sample selector --- .github/workflows/build-test-sample-apps.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build-test-sample-apps.yml b/.github/workflows/build-test-sample-apps.yml index d3408dae..3a40661c 100644 --- a/.github/workflows/build-test-sample-apps.yml +++ b/.github/workflows/build-test-sample-apps.yml @@ -72,7 +72,7 @@ jobs: changed_paths="$RUNNER_TEMP/react-native-changed-paths" git cat-file -e "$BASE_SHA^{commit}" git cat-file -e "$HEAD_SHA^{commit}" - git diff --name-only -z "$BASE_SHA...$HEAD_SHA" > "$changed_paths" + git diff --no-renames --name-only -z "$BASE_SHA...$HEAD_SHA" > "$changed_paths" while IFS= read -r -d '' path; do case "$path" in From 78264a4a4a252fa5153793cb2c080ed52021ba3b Mon Sep 17 00:00:00 2001 From: Shahroz Khan Date: Mon, 31 Aug 2026 18:04:59 -0400 Subject: [PATCH 13/15] ci: run scene E2E for affected paths --- .github/workflows/build-test-sample-apps.yml | 4 +- .../workflows/react-native-scene-e2e-full.yml | 71 +++++++++++++++---- 2 files changed, 61 insertions(+), 14 deletions(-) diff --git a/.github/workflows/build-test-sample-apps.yml b/.github/workflows/build-test-sample-apps.yml index 3a40661c..5af990fc 100644 --- a/.github/workflows/build-test-sample-apps.yml +++ b/.github/workflows/build-test-sample-apps.yml @@ -21,7 +21,8 @@ concurrency: # cancel previous workflow run if one exists. github.event.changes.base == null)) && 'metadata' || 'validation' }} - cancel-in-progress: true + # Never cancel a main-branch distribution after a newer merge starts. + cancel-in-progress: ${{ github.event_name == 'pull_request' || startsWith(github.ref, 'refs/heads/feature/') }} jobs: release_policy: @@ -157,6 +158,7 @@ jobs: # Update PR comment with build information update-pr-comment-with-status: if: >- + always() && github.event_name == 'pull_request' && needs.release_policy.outputs.full_validation == 'true' && (github.event.action != 'edited' || diff --git a/.github/workflows/react-native-scene-e2e-full.yml b/.github/workflows/react-native-scene-e2e-full.yml index efbced35..2a4e4b37 100644 --- a/.github/workflows/react-native-scene-e2e-full.yml +++ b/.github/workflows/react-native-scene-e2e-full.yml @@ -3,26 +3,71 @@ name: Full React Native scene routing E2E on: pull_request: types: [opened, synchronize, reopened, labeled] - paths: - - '.github/workflows/react-native-scene-e2e.yml' - - '.github/workflows/react-native-scene-e2e-full.yml' - - '.maestro/**' - - 'src/**' - - 'ios/wrappers/**' - - 'ios/cocoapods_deployment_target.rb' - - 'customerio-reactnative.podspec' - - 'customerio-reactnative-richpush.podspec' - - 'package.json' - - 'package-lock.json' + paths-ignore: + - '**/*.md' permissions: contents: read jobs: + select: + name: Select scene-routing E2E lane + runs-on: ubuntu-latest + outputs: + run: ${{ steps.paths.outputs.run }} + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Select manual or affected-path validation + id: paths + shell: bash + env: + ACTION: ${{ github.event.action }} + LABEL: ${{ github.event.label.name }} + BASE_SHA: ${{ github.event.pull_request.base.sha }} + HEAD_SHA: ${{ github.event.pull_request.head.sha }} + run: | + set -euo pipefail + run=false + + if [[ "$ACTION" == 'labeled' ]]; then + if [[ "$LABEL" == 'ci:full' ]]; then + run=true + fi + else + changed_paths="$RUNNER_TEMP/react-native-scene-e2e-paths" + git cat-file -e "$BASE_SHA^{commit}" + git cat-file -e "$HEAD_SHA^{commit}" + git diff --no-renames --name-only -z "$BASE_SHA...$HEAD_SHA" > "$changed_paths" + + while IFS= read -r -d '' path; do + case "$path" in + .github/workflows/react-native-scene-e2e.yml|.github/workflows/react-native-scene-e2e-full.yml|.maestro/*|scripts/test_ios_deep_link_request_store.swift) + run=true + break + ;; + ios/wrappers/CustomerIOReactNativeDeepLinkRouter.swift|ios/wrappers/CustomerIOReactNativeDeepLinkRequestStore.swift|ios/wrappers/NativeCustomerIO.swift|ios/wrappers/NativeCustomerIO.mm|ios/wrappers/liveactivities/NativeLiveActivities.*|ios/wrappers/push/*) + run=true + break + ;; + src/customerio-cdp.ts|src/customerio-push.ts|src/customerio-liveactivities.ts|src/specs/modules/NativeCustomerIO.ts|src/specs/modules/NativeCustomerIOMessagingPush.ts|src/specs/modules/NativeCustomerIOLiveActivities.ts) + run=true + break + ;; + esac + done < "$changed_paths" + fi + + echo "run=$run" >> "$GITHUB_OUTPUT" + full-scene-e2e: + needs: select + # Direct scene-routing paths run automatically. ci:full remains an explicit + # escape hatch for any other non-documentation PR. if: >- - contains(github.event.pull_request.labels.*.name, 'ci:full') && - (github.event.action != 'labeled' || github.event.label.name == 'ci:full') && + needs.select.outputs.run == 'true' && github.event.pull_request.head.repo.full_name == github.repository && github.event.pull_request.user.login != 'dependabot[bot]' uses: ./.github/workflows/react-native-scene-e2e.yml From 90d177580014b51259256b78419d7273bd1f968b Mon Sep 17 00:00:00 2001 From: Shahroz Khan Date: Mon, 31 Aug 2026 18:09:54 -0400 Subject: [PATCH 14/15] ci: keep full validation current --- .github/workflows/build-test-sample-apps.yml | 4 ++-- .github/workflows/react-native-scene-e2e-full.yml | 8 ++++++++ 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build-test-sample-apps.yml b/.github/workflows/build-test-sample-apps.yml index 5af990fc..2d14ff76 100644 --- a/.github/workflows/build-test-sample-apps.yml +++ b/.github/workflows/build-test-sample-apps.yml @@ -182,7 +182,7 @@ jobs: platform_name: 'iOS' steps: - name: Update sample builds PR comment with build information - if: ${{ github.event_name == 'pull_request' }} + if: needs.build-sample-apps.result == 'success' uses: peter-evans/create-or-update-comment@e8674b075228eee787fea43ef493e45ece1004c9 # v5.0.0 with: comment-id: ${{ needs.update-pr-comment.outputs.comment-id }} @@ -192,7 +192,7 @@ jobs: edit-mode: append # append new line to the existing PR comment to build a list of all sample app builds. - name: Update sample builds PR comment with build failure message - if: ${{ github.event_name == 'pull_request' && failure() }} + if: needs.build-sample-apps.result != 'success' uses: peter-evans/create-or-update-comment@e8674b075228eee787fea43ef493e45ece1004c9 # v5.0.0 with: comment-id: ${{ needs.update-pr-comment.outputs.comment-id }} diff --git a/.github/workflows/react-native-scene-e2e-full.yml b/.github/workflows/react-native-scene-e2e-full.yml index 2a4e4b37..b8cb10fa 100644 --- a/.github/workflows/react-native-scene-e2e-full.yml +++ b/.github/workflows/react-native-scene-e2e-full.yml @@ -26,6 +26,7 @@ jobs: env: ACTION: ${{ github.event.action }} LABEL: ${{ github.event.label.name }} + CI_FULL: ${{ contains(github.event.pull_request.labels.*.name, 'ci:full') }} BASE_SHA: ${{ github.event.pull_request.base.sha }} HEAD_SHA: ${{ github.event.pull_request.head.sha }} run: | @@ -36,6 +37,9 @@ jobs: if [[ "$LABEL" == 'ci:full' ]]; then run=true fi + elif [[ "$CI_FULL" == 'true' ]]; then + # Keep validating new commits after ci:full has been applied. + run=true else changed_paths="$RUNNER_TEMP/react-native-scene-e2e-paths" git cat-file -e "$BASE_SHA^{commit}" @@ -52,6 +56,10 @@ jobs: run=true break ;; + ios/wrappers/utils/CioConfigUtils.swift|src/utils/native-bridge.ts|src/utils/param-validation.ts) + run=true + break + ;; src/customerio-cdp.ts|src/customerio-push.ts|src/customerio-liveactivities.ts|src/specs/modules/NativeCustomerIO.ts|src/specs/modules/NativeCustomerIOMessagingPush.ts|src/specs/modules/NativeCustomerIOLiveActivities.ts) run=true break From d88be542a455d223876dc761588e239617599395 Mon Sep 17 00:00:00 2001 From: Shahroz Khan Date: Tue, 1 Sep 2026 17:36:51 -0400 Subject: [PATCH 15/15] ci: add explicit unsigned sample verification --- .github/workflows/build-sample-app.yml | 2 ++ .github/workflows/build-test-sample-apps.yml | 28 +++++++++++++++++--- 2 files changed, 26 insertions(+), 4 deletions(-) diff --git a/.github/workflows/build-sample-app.yml b/.github/workflows/build-sample-app.yml index f7319040..6e2deff6 100644 --- a/.github/workflows/build-sample-app.yml +++ b/.github/workflows/build-sample-app.yml @@ -319,6 +319,8 @@ jobs: uses: customerio/customerio-android/.github/actions/setup-android@main - name: Setup ${{ inputs.platform_name }} environment for the ${{ inputs.platform }} sample app + # This action only selects the repository's supported Xcode version. + # Signing credentials are scoped to the distribution-only Fastlane step. if: ${{ inputs.platform == 'ios' }} uses: customerio/mobile-ci-tools/github-actions/ios/setup-ios/v1@main diff --git a/.github/workflows/build-test-sample-apps.yml b/.github/workflows/build-test-sample-apps.yml index 2d14ff76..7209a065 100644 --- a/.github/workflows/build-test-sample-apps.yml +++ b/.github/workflows/build-test-sample-apps.yml @@ -6,6 +6,12 @@ on: paths-ignore: - '**/*.md' workflow_dispatch: + inputs: + verification_only: + description: Run unsigned compatibility builds without distribution + type: boolean + required: false + default: true push: branches: [main, feature/*] @@ -14,6 +20,8 @@ concurrency: # cancel previous workflow run if one exists. # ci:full run. They share a separate group, where unsigned validation repeats. group: >- ${{ github.workflow }}-${{ github.ref }}-${{ + github.event_name == 'workflow_dispatch' && inputs.verification_only == true && + 'manual-verification' || github.event_name == 'pull_request' && ((github.event.action == 'labeled' && github.event.label.name != 'ci:full') || (github.event.action == 'edited' && @@ -21,8 +29,12 @@ concurrency: # cancel previous workflow run if one exists. github.event.changes.base == null)) && 'metadata' || 'validation' }} - # Never cancel a main-branch distribution after a newer merge starts. - cancel-in-progress: ${{ github.event_name == 'pull_request' || startsWith(github.ref, 'refs/heads/feature/') }} + # Manual verification has its own group, so it can be superseded without + # canceling or delaying a signed main-branch distribution. + cancel-in-progress: >- + ${{ github.event_name == 'pull_request' || + (github.event_name == 'workflow_dispatch' && inputs.verification_only == true) || + startsWith(github.ref, 'refs/heads/feature/') }} jobs: release_policy: @@ -57,6 +69,7 @@ jobs: EVENT_NAME: ${{ github.event_name }} BASE_SHA: ${{ github.event.pull_request.base.sha }} HEAD_SHA: ${{ github.event.pull_request.head.sha }} + VERIFICATION_ONLY: ${{ github.event_name == 'workflow_dispatch' && inputs.verification_only == true }} FULL_PR: >- ${{ needs.release_policy.outputs.full_validation == 'true' && (github.event.action != 'edited' || @@ -67,7 +80,12 @@ jobs: github.event.pull_request.user.login != 'dependabot[bot]' }} run: | set -euo pipefail - if [[ "$EVENT_NAME" == 'pull_request' && "$FULL_PR" != 'true' ]]; then + distribution_matrix='[{"name":"APN","cio-workspace-name":"Mobile: React Native","platform":"ios","platform_name":"iOS","platform_name_upper":"IOS","should_distribute":true},{"name":"FCM","cio-workspace-name":"Mobile: xReact Native FCM workspace","platform":"ios","platform_name":"iOS","platform_name_upper":"IOS","should_distribute":true},{"name":"APN","cio-workspace-name":"Mobile: React Native","platform":"android","platform_name":"Android","platform_name_upper":"ANDROID","should_distribute":true}]' + + if [[ "$EVENT_NAME" == 'workflow_dispatch' && "$VERIFICATION_ONLY" == 'true' ]]; then + verification_matrix=$(jq -c 'map(.should_distribute = false)' <<< "$distribution_matrix") + echo "matrix=$verification_matrix" >> "$GITHUB_OUTPUT" + elif [[ "$EVENT_NAME" == 'pull_request' && "$FULL_PR" != 'true' ]]; then matrix='[{"name":"APN","cio-workspace-name":"Mobile: React Native","platform":"ios","platform_name":"iOS","platform_name_upper":"IOS","should_distribute":false},{"name":"APN","cio-workspace-name":"Mobile: React Native","platform":"android","platform_name":"Android","platform_name_upper":"ANDROID","should_distribute":false}]' fcm_required=false changed_paths="$RUNNER_TEMP/react-native-changed-paths" @@ -88,7 +106,7 @@ jobs: fi echo "matrix=$matrix" >> "$GITHUB_OUTPUT" else - echo 'matrix=[{"name":"APN","cio-workspace-name":"Mobile: React Native","platform":"ios","platform_name":"iOS","platform_name_upper":"IOS","should_distribute":true},{"name":"FCM","cio-workspace-name":"Mobile: xReact Native FCM workspace","platform":"ios","platform_name":"iOS","platform_name_upper":"IOS","should_distribute":true},{"name":"APN","cio-workspace-name":"Mobile: React Native","platform":"android","platform_name":"Android","platform_name_upper":"ANDROID","should_distribute":true}]' >> "$GITHUB_OUTPUT" + echo "matrix=$distribution_matrix" >> "$GITHUB_OUTPUT" fi update-pr-comment: @@ -134,6 +152,8 @@ jobs: edit-mode: replace # replace the existing comment with new content since we are creating new builds build-sample-apps: + # Forks and Dependabot deliberately reach this job through prepare's + # unsigned matrix. Only signing and distribution are restricted. if: >- always() && needs.prepare.result == 'success'