From 8ad4ca0332280ba2f5c18014c94ec043a9dada9f Mon Sep 17 00:00:00 2001 From: Lauri Piisang Date: Fri, 17 Apr 2026 18:05:43 +0400 Subject: [PATCH 1/6] infra: add suite filtering and PR-triggered e2e test workflows for SDK - Add workflow_call trigger + suite/exclude-suite inputs to test-sdk.yml - Thread suite/exclude-suite through desktop, android, and iOS reusable workflows - Create on-pr-test-sdk.yml for label-based and release-branch triggers - Add suite choice dropdown (full/smoke/custom) for manual dispatch --- .github/workflows/on-pr-test-sdk.yml | 105 +++++++++++++++++++++++++ .github/workflows/test-android-sdk.yml | 24 +++++- .github/workflows/test-desktop-sdk.yml | 16 +++- .github/workflows/test-ios-sdk.yml | 24 +++++- .github/workflows/test-sdk.yml | 97 ++++++++++++++++++----- 5 files changed, 245 insertions(+), 21 deletions(-) create mode 100644 .github/workflows/on-pr-test-sdk.yml diff --git a/.github/workflows/on-pr-test-sdk.yml b/.github/workflows/on-pr-test-sdk.yml new file mode 100644 index 0000000000..0bb5f6f3e4 --- /dev/null +++ b/.github/workflows/on-pr-test-sdk.yml @@ -0,0 +1,105 @@ +# QVAC SDK E2E Tests - PR Trigger +# +# Triggers SDK e2e tests via test-sdk.yml when: +# - "test-e2e-smoke" label applied: runs smoke suite on all platforms +# - "test-e2e-full" label applied: runs full suite on all platforms +# - PR opened/updated targeting release-* branch with packages/sdk/ changes: runs full suite +# +# On success, applies "e2e-tested" label to the PR. +# +# Uses pull_request_target for secrets access (Device Farm, MQTT). +# Authorization gate prevents untrusted fork PRs from running. + +name: QVAC Tests (sdk) - PR + +on: + pull_request_target: + types: [labeled, opened, synchronize] + paths: + - "packages/sdk/**" + +permissions: + contents: read + pull-requests: write + packages: read + +jobs: + resolve-config: + runs-on: ubuntu-latest + outputs: + should-run: ${{ steps.check.outputs.should-run }} + suite: ${{ steps.check.outputs.suite }} + steps: + - name: Checkout + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # 6.0.2 + with: + fetch-depth: 0 + + - name: Authorize PR + id: auth + uses: ./.github/actions/authorize-pr + with: + label: safe-to-test + github-token: ${{ github.token }} + + - name: Determine test config + if: steps.auth.outputs.allowed == 'true' + id: check + shell: bash + run: | + EVENT="${{ github.event.action }}" + LABEL="${{ github.event.label.name }}" + TARGET="${{ github.base_ref }}" + + if [ "$EVENT" = "labeled" ]; then + if [ "$LABEL" = "test-e2e-smoke" ]; then + echo "should-run=true" >> "$GITHUB_OUTPUT" + echo "suite=smoke" >> "$GITHUB_OUTPUT" + elif [ "$LABEL" = "test-e2e-full" ]; then + echo "should-run=true" >> "$GITHUB_OUTPUT" + echo "suite=" >> "$GITHUB_OUTPUT" + else + echo "::notice::Ignoring unrelated label: $LABEL" + echo "should-run=false" >> "$GITHUB_OUTPUT" + fi + elif echo "$TARGET" | grep -q '^release-'; then + git fetch origin "refs/pull/${{ github.event.pull_request.number }}/head" + CHANGED=$(git diff --name-only "${{ github.event.pull_request.base.sha }}...${{ github.event.pull_request.head.sha }}" -- packages/sdk/) + if [ -n "$CHANGED" ]; then + echo "::notice::Release branch PR with SDK changes — running full suite" + echo "should-run=true" >> "$GITHUB_OUTPUT" + echo "suite=" >> "$GITHUB_OUTPUT" + else + echo "::notice::Release branch PR but no changes in packages/sdk/ — skipping" + echo "should-run=false" >> "$GITHUB_OUTPUT" + fi + else + echo "should-run=false" >> "$GITHUB_OUTPUT" + fi + + run-tests: + needs: resolve-config + if: needs.resolve-config.outputs.should-run == 'true' + uses: ./.github/workflows/test-sdk.yml + with: + targets: all + suite: ${{ needs.resolve-config.outputs.suite }} + secrets: inherit + + apply-label: + needs: run-tests + if: success() + runs-on: ubuntu-latest + permissions: + pull-requests: write + steps: + - name: Apply e2e-tested label + uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # 7.0.1 + with: + script: | + await github.rest.issues.addLabels({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.payload.pull_request.number, + labels: ['e2e-tested'] + }); diff --git a/.github/workflows/test-android-sdk.yml b/.github/workflows/test-android-sdk.yml index 213f86734e..d7f5f7cdf3 100644 --- a/.github/workflows/test-android-sdk.yml +++ b/.github/workflows/test-android-sdk.yml @@ -23,6 +23,14 @@ on: description: "Filter tests by category or testId prefix (comma-separated)" required: false type: string + suite: + description: "Suite filter (e.g. 'smoke'). Empty = all tests." + required: false + type: string + exclude-suite: + description: "Exclude suites (comma-separated)" + required: false + type: string device-farm-timeout: description: "Maximum time in minutes to keep Device Farm session alive." required: false @@ -517,17 +525,31 @@ jobs: echo " Run ID: ${{ needs.build.outputs.runId }}" echo " Consumer timeout: ${{ inputs.consumer-timeout }}s" echo " Filter: ${{ inputs.filter || '(none)' }}" + echo " Suite: ${{ inputs.suite || '(all)' }}" + echo " Exclude suite: ${{ inputs.exclude-suite || '(none)' }}" FILTER_ARG="" if [ -n "${{ inputs.filter }}" ]; then FILTER_ARG="--filter=${{ inputs.filter }}" fi + SUITE_ARG="" + if [ -n "${{ inputs.suite }}" ]; then + SUITE_ARG="--suite=${{ inputs.suite }}" + fi + + EXCLUDE_SUITE_ARG="" + if [ -n "${{ inputs.exclude-suite }}" ]; then + EXCLUDE_SUITE_ARG="--exclude-suite=${{ inputs.exclude-suite }}" + fi + npx qvac-test run:producer \ --runId "${{ needs.build.outputs.runId }}" \ --consumer-timeout "${{ inputs.consumer-timeout }}" \ --config . \ - $FILTER_ARG + $FILTER_ARG \ + $SUITE_ARG \ + $EXCLUDE_SUITE_ARG - name: Upload results if: always() diff --git a/.github/workflows/test-desktop-sdk.yml b/.github/workflows/test-desktop-sdk.yml index 8268e5f23b..db50653a7d 100644 --- a/.github/workflows/test-desktop-sdk.yml +++ b/.github/workflows/test-desktop-sdk.yml @@ -27,6 +27,14 @@ on: description: 'Filter tests by category or testId prefix (comma-separated, e.g., "model,completion")' required: false type: string + suite: + description: "Suite filter (e.g. 'smoke'). Empty = all tests." + required: false + type: string + exclude-suite: + description: "Exclude suites (comma-separated)" + required: false + type: string test-version: description: "Git ref to checkout (branch, tag, or SHA). Defaults to trigger ref." required: false @@ -261,6 +269,10 @@ jobs: const filter = '${{ inputs.filter }}'; const filterArgs = filter ? ['--filter=' + filter] : []; + const suite = '${{ inputs.suite }}'; + const suiteArgs = suite ? ['--suite=' + suite] : []; + const excludeSuite = '${{ inputs.exclude-suite }}'; + const excludeSuiteArgs = excludeSuite ? ['--exclude-suite=' + excludeSuite] : []; const consumer = spawn('npx', [ 'qvac-test', 'run:consumer:desktop', @@ -309,7 +321,9 @@ jobs: '--runId=${{ steps.runid.outputs.runId }}', '--consumer-timeout=${{ inputs.consumer-timeout }}', '--config=.', - ...filterArgs + ...filterArgs, + ...suiteArgs, + ...excludeSuiteArgs ]; console.log('Producer command: npx ' + producerArgs.join(' ')); diff --git a/.github/workflows/test-ios-sdk.yml b/.github/workflows/test-ios-sdk.yml index 0e246e9863..e6a27de039 100644 --- a/.github/workflows/test-ios-sdk.yml +++ b/.github/workflows/test-ios-sdk.yml @@ -22,6 +22,14 @@ on: description: "Filter tests by category or testId prefix (comma-separated)" required: false type: string + suite: + description: "Suite filter (e.g. 'smoke'). Empty = all tests." + required: false + type: string + exclude-suite: + description: "Exclude suites (comma-separated)" + required: false + type: string device-farm-timeout: description: "Maximum time in minutes to keep Device Farm session alive." required: false @@ -633,17 +641,31 @@ jobs: echo " Run ID: ${{ needs.build.outputs.runId }}" echo " Consumer timeout: ${{ inputs.consumer-timeout }}s" echo " Filter: ${{ inputs.filter || '(none)' }}" + echo " Suite: ${{ inputs.suite || '(all)' }}" + echo " Exclude suite: ${{ inputs.exclude-suite || '(none)' }}" FILTER_ARG="" if [ -n "${{ inputs.filter }}" ]; then FILTER_ARG="--filter=${{ inputs.filter }}" fi + SUITE_ARG="" + if [ -n "${{ inputs.suite }}" ]; then + SUITE_ARG="--suite=${{ inputs.suite }}" + fi + + EXCLUDE_SUITE_ARG="" + if [ -n "${{ inputs.exclude-suite }}" ]; then + EXCLUDE_SUITE_ARG="--exclude-suite=${{ inputs.exclude-suite }}" + fi + npx qvac-test run:producer \ --runId "${{ needs.build.outputs.runId }}" \ --consumer-timeout "${{ inputs.consumer-timeout }}" \ --config . \ - $FILTER_ARG + $FILTER_ARG \ + $SUITE_ARG \ + $EXCLUDE_SUITE_ARG - name: Upload results if: always() diff --git a/.github/workflows/test-sdk.yml b/.github/workflows/test-sdk.yml index ad0df73800..f6657e56a4 100644 --- a/.github/workflows/test-sdk.yml +++ b/.github/workflows/test-sdk.yml @@ -1,5 +1,9 @@ # QVAC SDK Tests # Runs desktop and/or mobile (Android/iOS) e2e tests for the SDK package +# +# Triggered via: +# - workflow_dispatch: manual runs with full customizability +# - workflow_call: called by on-pr-test-sdk.yml for label/release-branch triggers name: QVAC Tests (sdk) @@ -15,7 +19,21 @@ on: - mobile - android - ios - default: desktop + default: all + suite: + description: "Suite selection (full = all tests, smoke = smoke suite, custom = use suite-custom value)" + type: choice + options: + - full + - smoke + - custom + default: full + suite-custom: + description: "Custom suite value (only used when suite=custom). Comma-separated suite names." + type: string + exclude-suite: + description: "Exclude suites (comma-separated, e.g. 'slow,flaky'). Tests tagged with these suites are skipped." + type: string filter: description: "Test filter (comma-separated categories/prefixes, empty = all tests)" type: string @@ -42,20 +60,39 @@ on: description: "Cache downloaded models between runs" type: boolean default: true - # pull_request: - # paths: - # - "packages/sdk/**" - # branches: - # - main - # - release-* - # push: - # branches: - # - main - # - release-* - # - feature-* - # - tmp-* - # paths: - # - "packages/sdk/**" + + workflow_call: + inputs: + targets: + description: "Which platforms to test" + type: string + default: "all" + suite: + description: "Resolved suite value: empty string = full, 'smoke', or custom comma-separated" + type: string + exclude-suite: + description: "Exclude suites (comma-separated)" + type: string + filter: + description: "Test filter (comma-separated categories/prefixes)" + type: string + desktop-consumer-timeout: + type: number + default: 60 + mobile-consumer-timeout: + type: number + default: 600 + device-farm-timeout: + type: number + default: 30 + desktop-platforms: + type: string + default: '["ai-run-windows11-gpu", "ai-run-linux-gpu", "mac-mini-m4-gpu"]' + test-version: + type: string + cache-models: + type: boolean + default: true permissions: contents: read @@ -63,8 +100,26 @@ permissions: packages: read jobs: + resolve: + runs-on: ubuntu-latest + outputs: + suite: ${{ steps.resolve.outputs.suite }} + steps: + - name: Resolve suite + id: resolve + shell: bash + run: | + SUITE="${{ inputs.suite }}" + if [ "$SUITE" = "full" ] || [ -z "$SUITE" ]; then + echo "suite=" >> "$GITHUB_OUTPUT" + elif [ "$SUITE" = "custom" ]; then + echo "suite=${{ inputs.suite-custom }}" >> "$GITHUB_OUTPUT" + else + echo "suite=$SUITE" >> "$GITHUB_OUTPUT" + fi + desktop-tests: - # Runs for: all, desktop, empty (future PR/push) + needs: resolve if: inputs.targets != 'mobile' && inputs.targets != 'android' && inputs.targets != 'ios' uses: ./.github/workflows/test-desktop-sdk.yml with: @@ -73,17 +128,19 @@ jobs: platforms: ${{ inputs.desktop-platforms || '["ai-run-windows11-gpu", "ai-run-linux-gpu", "mac-mini-m4-gpu"]' }} consumer-timeout: ${{ fromJSON(inputs.desktop-consumer-timeout || '60') }} filter: ${{ inputs.filter }} + suite: ${{ needs.resolve.outputs.suite }} + exclude-suite: ${{ inputs.exclude-suite }} test-version: ${{ inputs.test-version || '' }} cache-models: ${{ inputs.cache-models }} secrets: inherit android-tests: + needs: resolve permissions: id-token: write contents: read pull-requests: write packages: read - # Runs for: all, mobile, android, empty (future PR/push) if: inputs.targets != 'desktop' && inputs.targets != 'ios' uses: ./.github/workflows/test-android-sdk.yml with: @@ -91,6 +148,8 @@ jobs: working-directory: "packages/sdk/tests-qvac" consumer-timeout: ${{ fromJSON(inputs.mobile-consumer-timeout || '600') }} filter: ${{ inputs.filter }} + suite: ${{ needs.resolve.outputs.suite }} + exclude-suite: ${{ inputs.exclude-suite }} device-farm-timeout: ${{ fromJSON(inputs.device-farm-timeout || '90') }} device-farm-project-arn: ${{ vars.SDK_DEVICE_FARM_PROJECT_ARN }} device-pools: ${{ vars.SDK_DEVICE_FARM_POOLS }} @@ -98,12 +157,12 @@ jobs: secrets: inherit ios-tests: + needs: resolve permissions: id-token: write contents: read pull-requests: write packages: read - # Runs for: all, mobile, ios, empty (future PR/push) if: inputs.targets != 'desktop' && inputs.targets != 'android' uses: ./.github/workflows/test-ios-sdk.yml with: @@ -111,6 +170,8 @@ jobs: working-directory: "packages/sdk/tests-qvac" consumer-timeout: ${{ fromJSON(inputs.mobile-consumer-timeout || '600') }} filter: ${{ inputs.filter }} + suite: ${{ needs.resolve.outputs.suite }} + exclude-suite: ${{ inputs.exclude-suite }} device-farm-timeout: ${{ fromJSON(inputs.device-farm-timeout || '90') }} device-farm-project-arn: ${{ vars.SDK_DEVICE_FARM_PROJECT_ARN }} ios-device-pools: ${{ vars.SDK_IOS_DEVICE_FARM_POOLS }} From 6143f5010c596436770fbba6b4f840ccfe52eb12 Mon Sep 17 00:00:00 2001 From: Lauri Piisang Date: Fri, 17 Apr 2026 18:10:11 +0400 Subject: [PATCH 2/6] infra: add Device Farm artifact download and upload to Android SDK test workflow --- .github/workflows/test-android-sdk.yml | 56 +++++++++++++++++++++++++- 1 file changed, 55 insertions(+), 1 deletion(-) diff --git a/.github/workflows/test-android-sdk.yml b/.github/workflows/test-android-sdk.yml index d7f5f7cdf3..722d1d4138 100644 --- a/.github/workflows/test-android-sdk.yml +++ b/.github/workflows/test-android-sdk.yml @@ -573,7 +573,7 @@ jobs: if: always() runs-on: ubuntu-latest environment: release - timeout-minutes: 5 + timeout-minutes: 10 steps: - name: Configure AWS credentials uses: aws-actions/configure-aws-credentials@8df5847569e6427dd6c4fb1cf565c83acfa8afa7 # 6.0.0 @@ -609,6 +609,60 @@ jobs: fi done + - name: Download Device Farm artifacts + run: | + mkdir -p ./device-farm-logs + + for arn_file in run-arns/*.txt; do + if [ -f "$arn_file" ]; then + RUN_ARN=$(cat "$arn_file") + DEVICE_NAME=$(basename "$arn_file" .txt) + + echo "Waiting for run to finish: $DEVICE_NAME" + for attempt in $(seq 1 30); do + STATUS=$(aws devicefarm get-run --arn "$RUN_ARN" --query 'run.status' --output text 2>/dev/null || echo "UNKNOWN") + if [ "$STATUS" = "COMPLETED" ] || [ "$STATUS" = "STOPPED" ] || [ "$STATUS" = "ERRORED" ]; then + echo "Run $DEVICE_NAME finished with status: $STATUS" + break + fi + echo "Run $DEVICE_NAME still $STATUS (attempt $attempt/30)..." + sleep 10 + done + + DEVICE_DIR="./device-farm-logs/$DEVICE_NAME" + mkdir -p "$DEVICE_DIR" + + echo "Downloading Customer Artifacts for $DEVICE_NAME..." + JOBS=$(aws devicefarm list-jobs --arn "$RUN_ARN" --query 'jobs[*].arn' --output text 2>/dev/null || echo "") + for JOB_ARN in $JOBS; do + aws devicefarm list-artifacts \ + --arn "$JOB_ARN" \ + --type FILE \ + --query "artifacts[?contains(name,'Customer')].[name,extension,url]" \ + --output text 2>/dev/null \ + | while IFS=$'\t' read -r NAME EXT URL; do + if [ -z "$URL" ] || [ "$URL" = "None" ]; then + continue + fi + SAFE_NAME=$(echo "$NAME" | tr ' /' '__') + echo " Downloading ${SAFE_NAME}.${EXT}" + curl -sS -o "$DEVICE_DIR/${SAFE_NAME}.${EXT}" "$URL" || echo " Failed: ${SAFE_NAME}.${EXT}" + done + done + + echo "Artifacts for $DEVICE_NAME:" + ls -lh "$DEVICE_DIR" 2>/dev/null || echo " (empty)" + fi + done + + - name: Upload Device Farm logs + if: always() + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # 7.0.0 + with: + name: device-farm-logs-android-${{ needs.build.outputs.runId }} + path: ./device-farm-logs/ + retention-days: 30 + compare-results: needs: [build, run-producer] if: github.event_name == 'pull_request' From 7f00ea8a970840f2f07d535ac271df397b59e085 Mon Sep 17 00:00:00 2001 From: Lauri Piisang Date: Fri, 17 Apr 2026 18:15:54 +0400 Subject: [PATCH 3/6] infra: fix CodeQL alert in on-pr-test-sdk.yml Remove git fetch/diff of PR head refs that triggered "checkout of untrusted code in trusted context" alert. Use sparse checkout (only authorize-pr action) and rely on the trigger-level paths filter for SDK change detection on release branch PRs. --- .github/workflows/on-pr-test-sdk.yml | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/.github/workflows/on-pr-test-sdk.yml b/.github/workflows/on-pr-test-sdk.yml index 0bb5f6f3e4..1c4f3bdaba 100644 --- a/.github/workflows/on-pr-test-sdk.yml +++ b/.github/workflows/on-pr-test-sdk.yml @@ -33,7 +33,8 @@ jobs: - name: Checkout uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # 6.0.2 with: - fetch-depth: 0 + sparse-checkout: .github/actions/authorize-pr + sparse-checkout-cone-mode: false - name: Authorize PR id: auth @@ -63,16 +64,10 @@ jobs: echo "should-run=false" >> "$GITHUB_OUTPUT" fi elif echo "$TARGET" | grep -q '^release-'; then - git fetch origin "refs/pull/${{ github.event.pull_request.number }}/head" - CHANGED=$(git diff --name-only "${{ github.event.pull_request.base.sha }}...${{ github.event.pull_request.head.sha }}" -- packages/sdk/) - if [ -n "$CHANGED" ]; then - echo "::notice::Release branch PR with SDK changes — running full suite" - echo "should-run=true" >> "$GITHUB_OUTPUT" - echo "suite=" >> "$GITHUB_OUTPUT" - else - echo "::notice::Release branch PR but no changes in packages/sdk/ — skipping" - echo "should-run=false" >> "$GITHUB_OUTPUT" - fi + # Path filter on the trigger already ensures packages/sdk/ changes exist + echo "::notice::Release branch PR with SDK changes — running full suite" + echo "should-run=true" >> "$GITHUB_OUTPUT" + echo "suite=" >> "$GITHUB_OUTPUT" else echo "should-run=false" >> "$GITHUB_OUTPUT" fi From 2e76a4e35e59262f9b9499b41b8df1ef0d7e874b Mon Sep 17 00:00:00 2001 From: Lauri Piisang Date: Fri, 17 Apr 2026 19:17:08 +0400 Subject: [PATCH 4/6] infra: improve Android Device Farm logging with continuous background capture Replace inline logcat consumption with background capture to a persistent log file, matching the iOS pymobiledevice3 pattern. Adds full unfiltered logcat dump and React Native log extraction in post_test. --- .../device-farm/test-spec-android.yml | 40 ++++++++++++++++--- 1 file changed, 34 insertions(+), 6 deletions(-) diff --git a/packages/sdk/tests-qvac/device-farm/test-spec-android.yml b/packages/sdk/tests-qvac/device-farm/test-spec-android.yml index b9781370ab..bb06e9eebe 100644 --- a/packages/sdk/tests-qvac/device-farm/test-spec-android.yml +++ b/packages/sdk/tests-qvac/device-farm/test-spec-android.yml @@ -13,6 +13,15 @@ phases: sleep 5 echo "App launched" + # Clear logcat buffer and start continuous background capture (unfiltered) + # Captures everything; filtering happens post-test when extracting subsets + - |- + adb logcat -c + adb logcat -v threadtime \ + >> "$DEVICEFARM_LOG_DIR/testconsumer.log" 2>&1 & + echo $! > "$DEVICEFARM_LOG_DIR/logcat-pid.txt" + echo "Background logcat capture started (pid: $(cat $DEVICEFARM_LOG_DIR/logcat-pid.txt))" + # Periodic screenshots every 30s in the background - |- (while true; do @@ -24,7 +33,7 @@ phases: test: commands: - # Stream logcat, track test progress in real-time, detect completion + # Poll the background log file for test progress and completion - |- TIMEOUT=${DEVICEFARM_TIMEOUT:-5400} START=$(date +%s) @@ -33,13 +42,13 @@ phases: COMPLETED=0 EXPECTED="?" RESULTS_FILE="$DEVICEFARM_LOG_DIR/test-results.txt" + LOGFILE="$DEVICEFARM_LOG_DIR/testconsumer.log" touch "$RESULTS_FILE" - adb logcat -c - echo "Monitoring app logcat for test results (timeout: ${TIMEOUT}s)..." + echo "Monitoring app logs for test results (timeout: ${TIMEOUT}s)..." - adb logcat ReactNativeJS:I *:S | while IFS= read -r line; do + tail -f "$LOGFILE" | while IFS= read -r line; do case "$line" in *"Registration ack"*"tests in queue"*) EXPECTED=$(echo "$line" | grep -oP '\d+(?= tests in queue)') @@ -81,8 +90,26 @@ phases: post_test: commands: - # Capture full logcat - - adb logcat -d ReactNativeJS:I *:S > "$DEVICEFARM_LOG_DIR/app-console.log" + # Stop background logcat capture + - |- + if [ -f "$DEVICEFARM_LOG_DIR/logcat-pid.txt" ]; then + kill $(cat "$DEVICEFARM_LOG_DIR/logcat-pid.txt") 2>/dev/null || true + echo "Background logcat stopped" + fi + + # Extract React Native JS logs into a separate file + - |- + grep "ReactNativeJS" "$DEVICEFARM_LOG_DIR/testconsumer.log" \ + > "$DEVICEFARM_LOG_DIR/react-native-logs.log" 2>/dev/null || true + RN_LINES=$(wc -l < "$DEVICEFARM_LOG_DIR/react-native-logs.log" 2>/dev/null || echo 0) + echo "=== React Native logs: ${RN_LINES} lines ===" + tail -50 "$DEVICEFARM_LOG_DIR/react-native-logs.log" 2>/dev/null || echo "(none)" + + - echo "=== Full logcat summary ===" + - wc -l "$DEVICEFARM_LOG_DIR/testconsumer.log" 2>/dev/null || echo "No logs captured" + + - echo "Available log files:" + - ls -lh $DEVICEFARM_LOG_DIR/ || true # Final screenshot - adb shell screencap -p > "$DEVICEFARM_SCREENSHOT_PATH/final.png" 2>/dev/null @@ -92,3 +119,4 @@ phases: artifacts: - $DEVICEFARM_LOG_DIR + From 0e8a23e38e6ead48b0c9f1c7b44516242b79d3e0 Mon Sep 17 00:00:00 2001 From: Lauri Piisang Date: Fri, 17 Apr 2026 19:25:49 +0400 Subject: [PATCH 5/6] infra: increase Device Farm cleanup wait to 30 min for artifact download The STOPPING state can take 10+ minutes on Device Farm. Previous 5-min wait (30x10s) was insufficient, resulting in empty artifact downloads. Now waits up to 30 min (120x15s) and merges stop+wait+download into a single step. Applied to both iOS and Android cleanup jobs. --- .github/workflows/test-android-sdk.yml | 28 +++++++++----------------- .github/workflows/test-ios-sdk.yml | 28 +++++++++----------------- 2 files changed, 18 insertions(+), 38 deletions(-) diff --git a/.github/workflows/test-android-sdk.yml b/.github/workflows/test-android-sdk.yml index 722d1d4138..c14d4d09c6 100644 --- a/.github/workflows/test-android-sdk.yml +++ b/.github/workflows/test-android-sdk.yml @@ -573,7 +573,7 @@ jobs: if: always() runs-on: ubuntu-latest environment: release - timeout-minutes: 10 + timeout-minutes: 35 steps: - name: Configure AWS credentials uses: aws-actions/configure-aws-credentials@8df5847569e6427dd6c4fb1cf565c83acfa8afa7 # 6.0.0 @@ -588,9 +588,10 @@ jobs: path: ./run-arns merge-multiple: true - - name: Stop Device Farm runs + - name: Stop Device Farm runs and download artifacts run: | echo "Cleaning up Device Farm runs for runId: ${{ needs.build.outputs.runId }}" + mkdir -p ./device-farm-logs for arn_file in run-arns/*.txt; do if [ -f "$arn_file" ]; then @@ -603,30 +604,19 @@ jobs: if [ "$STATUS" = "RUNNING" ] || [ "$STATUS" = "PENDING" ]; then echo "Stopping run: $RUN_ARN" aws devicefarm stop-run --arn "$RUN_ARN" || true - else - echo "Run already finished ($STATUS), skipping" fi - fi - done - - - name: Download Device Farm artifacts - run: | - mkdir -p ./device-farm-logs - for arn_file in run-arns/*.txt; do - if [ -f "$arn_file" ]; then - RUN_ARN=$(cat "$arn_file") - DEVICE_NAME=$(basename "$arn_file" .txt) - - echo "Waiting for run to finish: $DEVICE_NAME" - for attempt in $(seq 1 30); do + echo "Waiting for run to finish: $DEVICE_NAME (up to 30 min)..." + for attempt in $(seq 1 120); do STATUS=$(aws devicefarm get-run --arn "$RUN_ARN" --query 'run.status' --output text 2>/dev/null || echo "UNKNOWN") if [ "$STATUS" = "COMPLETED" ] || [ "$STATUS" = "STOPPED" ] || [ "$STATUS" = "ERRORED" ]; then echo "Run $DEVICE_NAME finished with status: $STATUS" break fi - echo "Run $DEVICE_NAME still $STATUS (attempt $attempt/30)..." - sleep 10 + if [ $((attempt % 6)) -eq 0 ]; then + echo "Run $DEVICE_NAME still $STATUS (${attempt}/120, $((attempt * 15))s elapsed)..." + fi + sleep 15 done DEVICE_DIR="./device-farm-logs/$DEVICE_NAME" diff --git a/.github/workflows/test-ios-sdk.yml b/.github/workflows/test-ios-sdk.yml index e6a27de039..b44670ddba 100644 --- a/.github/workflows/test-ios-sdk.yml +++ b/.github/workflows/test-ios-sdk.yml @@ -689,7 +689,7 @@ jobs: if: always() runs-on: ubuntu-latest environment: release - timeout-minutes: 10 + timeout-minutes: 35 steps: - name: Configure AWS credentials uses: aws-actions/configure-aws-credentials@8df5847569e6427dd6c4fb1cf565c83acfa8afa7 # 6.0.0 @@ -704,9 +704,10 @@ jobs: path: ./run-arns merge-multiple: true - - name: Stop Device Farm runs + - name: Stop Device Farm runs and download artifacts run: | echo "Cleaning up Device Farm runs for runId: ${{ needs.build.outputs.runId }}" + mkdir -p ./device-farm-logs for arn_file in run-arns/*.txt; do if [ -f "$arn_file" ]; then @@ -719,30 +720,19 @@ jobs: if [ "$STATUS" = "RUNNING" ] || [ "$STATUS" = "PENDING" ]; then echo "Stopping run: $RUN_ARN" aws devicefarm stop-run --arn "$RUN_ARN" || true - else - echo "Run already finished ($STATUS), skipping" fi - fi - done - - - name: Download Device Farm artifacts - run: | - mkdir -p ./device-farm-logs - for arn_file in run-arns/*.txt; do - if [ -f "$arn_file" ]; then - RUN_ARN=$(cat "$arn_file") - DEVICE_NAME=$(basename "$arn_file" .txt) - - echo "Waiting for run to finish: $DEVICE_NAME" - for attempt in $(seq 1 30); do + echo "Waiting for run to finish: $DEVICE_NAME (up to 30 min)..." + for attempt in $(seq 1 120); do STATUS=$(aws devicefarm get-run --arn "$RUN_ARN" --query 'run.status' --output text 2>/dev/null || echo "UNKNOWN") if [ "$STATUS" = "COMPLETED" ] || [ "$STATUS" = "STOPPED" ] || [ "$STATUS" = "ERRORED" ]; then echo "Run $DEVICE_NAME finished with status: $STATUS" break fi - echo "Run $DEVICE_NAME still $STATUS (attempt $attempt/30)..." - sleep 10 + if [ $((attempt % 6)) -eq 0 ]; then + echo "Run $DEVICE_NAME still $STATUS (${attempt}/120, $((attempt * 15))s elapsed)..." + fi + sleep 15 done DEVICE_DIR="./device-farm-logs/$DEVICE_NAME" From 81d7655e01101cb66a53d29686860b5516f96ad3 Mon Sep 17 00:00:00 2001 From: Lauri Piisang Date: Mon, 20 Apr 2026 22:21:32 +0400 Subject: [PATCH 6/6] fix: fix security issue caused by allowing out-of-date labeled prs --- .github/actions/authorize-pr/action.yml | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/.github/actions/authorize-pr/action.yml b/.github/actions/authorize-pr/action.yml index 646c19e639..6832860013 100644 --- a/.github/actions/authorize-pr/action.yml +++ b/.github/actions/authorize-pr/action.yml @@ -86,6 +86,16 @@ runs: HAS_LABEL=$(echo "$LABELS_JSON" | jq -r --arg l "$LABEL_NAME" \ 'if . == null then "false" elif any(. == $l) then "true" else "false" end') + # On synchronize from a non-writer, reject even if the label is still + # present. The label was granted for the previous commit; new unreviewed + # code must not run with secrets. The strip step below removes the label + # so that a re-review + re-label is required for the next run. + if [ "$ACTION" = "synchronize" ]; then + echo "::warning::External fork synchronize from non-writer — blocking until re-review" + echo "allowed=false" >> "$GITHUB_OUTPUT" + exit 0 + fi + if [ "$HAS_LABEL" = "true" ]; then echo "::notice::Fork PR with '$LABEL_NAME' label — authorized" echo "allowed=true" >> "$GITHUB_OUTPUT"