From b9d4d9486227ec748d7f77ce1c8ca162f725679d Mon Sep 17 00:00:00 2001 From: Nicola Corti Date: Mon, 21 Sep 2026 14:24:22 +0100 Subject: [PATCH] Retry artifact uploads in CI --- .github/actions/build-android/action.yml | 8 +- .../actions/build-fantom-runner/action.yml | 2 +- .github/actions/build-npm-package/action.yml | 4 +- .github/actions/maestro-android/action.yml | 4 +- .github/actions/maestro-ios/action.yml | 4 +- .github/actions/run-fantom-tests/action.yml | 2 +- .github/actions/test-ios-rntester/action.yml | 6 +- .github/actions/test-js/action.yml | 2 +- .github/actions/upload-artifact/action.yml | 100 ++++++++++++++++++ .../workflows/build-android-templateapp.yml | 2 +- .github/workflows/e2e-android-rntester.yml | 2 +- .github/workflows/e2e-android-templateapp.yml | 2 +- .github/workflows/e2e-ios-templateapp.yml | 2 +- .../workflows/expo-code-review-command.yml | 2 +- .github/workflows/expo-code-review.yml | 2 +- .github/workflows/format.yml | 2 +- .github/workflows/prebuild-ios-core.yml | 10 +- .../workflows/prebuild-ios-dependencies.yml | 10 +- .github/workflows/test-all.yml | 2 +- .../workflows/validate-cxx-api-snapshots.yml | 2 +- 20 files changed, 135 insertions(+), 35 deletions(-) create mode 100644 .github/actions/upload-artifact/action.yml diff --git a/.github/actions/build-android/action.yml b/.github/actions/build-android/action.yml index 27ddd7b6373..528b0edf664 100644 --- a/.github/actions/build-android/action.yml +++ b/.github/actions/build-android/action.yml @@ -85,13 +85,13 @@ runs: shell: bash run: ccache -s -v - name: Upload Maven Artifacts - uses: actions/upload-artifact@v6 + uses: ./.github/actions/upload-artifact with: name: maven-local path: /tmp/maven-local - name: Upload test results if: ${{ always() }} - uses: actions/upload-artifact@v6 + uses: ./.github/actions/upload-artifact with: name: build-android-results compression-level: 1 @@ -101,14 +101,14 @@ runs: packages/react-native/ReactAndroid/build/reports - name: Upload RNTester APK - hermes-debug if: ${{ always() }} - uses: actions/upload-artifact@v6 + uses: ./.github/actions/upload-artifact with: name: rntester-debug path: packages/rn-tester/android/app/build/outputs/apk/debug/ compression-level: 0 - name: Upload RNTester APK - hermes-release if: ${{ always() }} - uses: actions/upload-artifact@v6 + uses: ./.github/actions/upload-artifact with: name: rntester-release path: packages/rn-tester/android/app/build/outputs/apk/release/ diff --git a/.github/actions/build-fantom-runner/action.yml b/.github/actions/build-fantom-runner/action.yml index c4b34c32fbe..5a10c9e0695 100644 --- a/.github/actions/build-fantom-runner/action.yml +++ b/.github/actions/build-fantom-runner/action.yml @@ -77,7 +77,7 @@ runs: shell: bash run: cp packages/react-native/ReactAndroid/hermes-engine/build/hermes/lib/libhermesvm.so private/react-native-fantom/build/tester/ - name: Upload Fantom Runner binary - uses: actions/upload-artifact@v6 + uses: ./.github/actions/upload-artifact with: name: fantom-runner-binary compression-level: 1 diff --git a/.github/actions/build-npm-package/action.yml b/.github/actions/build-npm-package/action.yml index e3771ce3d51..383adedcb37 100644 --- a/.github/actions/build-npm-package/action.yml +++ b/.github/actions/build-npm-package/action.yml @@ -77,7 +77,7 @@ runs: fi node ./scripts/releases-ci/publish-npm.js -t ${{ inputs.release-type }} - name: Upload npm logs - uses: actions/upload-artifact@v6 + uses: ./.github/actions/upload-artifact with: name: npm-logs path: ~/.npm/_logs @@ -92,7 +92,7 @@ runs: echo "$FILENAME" > build/react-native-package-version - name: Upload release package - uses: actions/upload-artifact@v6 + uses: ./.github/actions/upload-artifact if: ${{ inputs.release-type == 'dry-run' }} with: name: react-native-package diff --git a/.github/actions/maestro-android/action.yml b/.github/actions/maestro-android/action.yml index 7f760573c94..94a3be4c252 100644 --- a/.github/actions/maestro-android/action.yml +++ b/.github/actions/maestro-android/action.yml @@ -90,7 +90,7 @@ runs: NORM_APP_ID=$(echo "${{ inputs.app-id }}" | tr '.' '-') echo "app-id=$NORM_APP_ID" >> $GITHUB_OUTPUT - name: Store tests result - uses: actions/upload-artifact@v6 + uses: ./.github/actions/upload-artifact if: always() with: name: e2e_android_${{ steps.normalize-app-id.outputs.app-id }}_report_${{ inputs.flavor }}_${{ inputs.emulator-arch }}_NewArch @@ -100,7 +100,7 @@ runs: screen.mp4 - name: Store Logs if: always() - uses: actions/upload-artifact@v6 + uses: ./.github/actions/upload-artifact with: name: maestro-logs-android-${{ steps.normalize-app-id.outputs.app-id }}-${{ inputs.flavor }}-${{ inputs.emulator-arch }}-NewArch overwrite: true diff --git a/.github/actions/maestro-ios/action.yml b/.github/actions/maestro-ios/action.yml index def69fd7809..b5e4036d628 100644 --- a/.github/actions/maestro-ios/action.yml +++ b/.github/actions/maestro-ios/action.yml @@ -76,7 +76,7 @@ runs: "${{ inputs.device-os }}" - name: Store video record if: always() - uses: actions/upload-artifact@v6 + uses: ./.github/actions/upload-artifact with: name: e2e_ios_${{ inputs.app-id }}_report_${{ inputs.flavor }}_NewArch overwrite: true @@ -89,7 +89,7 @@ runs: report.xml - name: Store Logs if: failure() && steps.run-tests.outcome == 'failure' - uses: actions/upload-artifact@v6 + uses: ./.github/actions/upload-artifact with: name: maestro-logs-${{ inputs.app-id }}-${{ inputs.flavor }}-NewArch overwrite: true diff --git a/.github/actions/run-fantom-tests/action.yml b/.github/actions/run-fantom-tests/action.yml index 8a6fdcd832a..c2b4e231a66 100644 --- a/.github/actions/run-fantom-tests/action.yml +++ b/.github/actions/run-fantom-tests/action.yml @@ -46,7 +46,7 @@ runs: LD_LIBRARY_PATH: ${{ github.workspace }}/private/react-native-fantom/build/tester - name: Upload test results if: ${{ always() }} - uses: actions/upload-artifact@v6 + uses: ./.github/actions/upload-artifact with: name: run-fantom-tests-results compression-level: 1 diff --git a/.github/actions/test-ios-rntester/action.yml b/.github/actions/test-ios-rntester/action.yml index 2b62f554daa..7f5d83855a8 100644 --- a/.github/actions/test-ios-rntester/action.yml +++ b/.github/actions/test-ios-rntester/action.yml @@ -134,20 +134,20 @@ runs: cd /Users/runner/Library/Developer/Xcode tar -zcvf xcresults.tar.gz "/tmp/RNTesterTestResults" - name: Upload artifact - uses: actions/upload-artifact@v6 + uses: ./.github/actions/upload-artifact if: ${{ inputs.run-unit-tests == 'true' }} with: name: xcresults-${{ inputs.flavor }}-${{ inputs.use-frameworks == 'true' && 'dynamic-frameworks' || 'static-libraries' }}-Ruby${{ inputs.ruby-version }} path: /Users/runner/Library/Developer/Xcode/xcresults.tar.gz - name: Upload RNTester App if: ${{ inputs.use-frameworks == 'false' && inputs.ruby-version == '2.6.10' }} # This is needed to avoid conflicts with the artifacts - uses: actions/upload-artifact@v6 + uses: ./.github/actions/upload-artifact with: name: RNTesterApp-NewArch-${{ inputs.flavor }} path: ${{ env.app-path }} - name: Store test results if: ${{ inputs.run-unit-tests == 'true' }} - uses: actions/upload-artifact@v6 + uses: ./.github/actions/upload-artifact with: name: test-results-${{ inputs.flavor }}-${{ inputs.use-frameworks == 'true' && 'dynamic-frameworks' || 'static-libraries' }}-Ruby${{ inputs.ruby-version }} path: ./reports/junit diff --git a/.github/actions/test-js/action.yml b/.github/actions/test-js/action.yml index 653a7ee9592..6d2c4e0f093 100644 --- a/.github/actions/test-js/action.yml +++ b/.github/actions/test-js/action.yml @@ -19,7 +19,7 @@ runs: run: node ./scripts/run-ci-javascript-tests.js --maxWorkers 2 - name: Upload test results if: ${{ always() }} - uses: actions/upload-artifact@v6 + uses: ./.github/actions/upload-artifact with: name: test-js-results compression-level: 1 diff --git a/.github/actions/upload-artifact/action.yml b/.github/actions/upload-artifact/action.yml new file mode 100644 index 00000000000..9d1b6a39781 --- /dev/null +++ b/.github/actions/upload-artifact/action.yml @@ -0,0 +1,100 @@ +name: Upload artifact with retries +description: Uploads an artifact, retrying transient failures up to two times + +inputs: + name: + description: Artifact name + default: artifact + path: + description: A file, directory, or wildcard pattern to upload + required: true + if-no-files-found: + description: Behavior when no files are found (warn, error, or ignore) + default: warn + retention-days: + description: Number of days to retain the artifact + compression-level: + description: Zlib compression level (0-9) + default: '6' + overwrite: + description: Whether to replace an artifact with the same name + default: 'false' + include-hidden-files: + description: Whether to include hidden files + default: 'false' + archive: + description: Whether to archive the artifact before uploading + default: 'true' + +outputs: + artifact-id: + description: The uploaded artifact's ID + value: ${{ steps.upload-3.outputs.artifact-id || steps.upload-2.outputs.artifact-id || steps.upload-1.outputs.artifact-id }} + artifact-url: + description: The uploaded artifact's URL + value: ${{ steps.upload-3.outputs.artifact-url || steps.upload-2.outputs.artifact-url || steps.upload-1.outputs.artifact-url }} + artifact-digest: + description: The uploaded artifact's SHA-256 digest + value: ${{ steps.upload-3.outputs.artifact-digest || steps.upload-2.outputs.artifact-digest || steps.upload-1.outputs.artifact-digest }} + +runs: + using: composite + steps: + - name: Upload artifact (attempt 1 of 3) + id: upload-1 + if: ${{ always() }} + continue-on-error: true + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: ${{ inputs.name }} + path: ${{ inputs.path }} + if-no-files-found: ${{ inputs.if-no-files-found }} + retention-days: ${{ inputs.retention-days }} + compression-level: ${{ inputs.compression-level }} + overwrite: ${{ inputs.overwrite }} + include-hidden-files: ${{ inputs.include-hidden-files }} + archive: ${{ inputs.archive }} + + - name: Wait before attempt 2 + if: ${{ always() && steps.upload-1.outcome == 'failure' }} + shell: bash + run: | + echo "::warning::Artifact upload failed; retrying in 10 seconds" + sleep 10 + + - name: Upload artifact (attempt 2 of 3) + id: upload-2 + if: ${{ always() && steps.upload-1.outcome == 'failure' }} + continue-on-error: true + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: ${{ inputs.name }} + path: ${{ inputs.path }} + if-no-files-found: ${{ inputs.if-no-files-found }} + retention-days: ${{ inputs.retention-days }} + compression-level: ${{ inputs.compression-level }} + overwrite: ${{ inputs.overwrite }} + include-hidden-files: ${{ inputs.include-hidden-files }} + archive: ${{ inputs.archive }} + + - name: Wait before attempt 3 + if: ${{ always() && steps.upload-2.outcome == 'failure' }} + shell: bash + run: | + echo "::warning::Artifact upload failed again; retrying in 20 seconds" + sleep 20 + + - name: Upload artifact (attempt 3 of 3) + id: upload-3 + if: ${{ always() && steps.upload-2.outcome == 'failure' }} + # Deliberately do not continue on error: the last failure must fail the caller. + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: ${{ inputs.name }} + path: ${{ inputs.path }} + if-no-files-found: ${{ inputs.if-no-files-found }} + retention-days: ${{ inputs.retention-days }} + compression-level: ${{ inputs.compression-level }} + overwrite: ${{ inputs.overwrite }} + include-hidden-files: ${{ inputs.include-hidden-files }} + archive: ${{ inputs.archive }} diff --git a/.github/workflows/build-android-templateapp.yml b/.github/workflows/build-android-templateapp.yml index 7e9156f9b8c..41d5af9d94a 100644 --- a/.github/workflows/build-android-templateapp.yml +++ b/.github/workflows/build-android-templateapp.yml @@ -66,7 +66,7 @@ jobs: assemble$CAPITALIZED_FLAVOR \ -PreactNativeArchitectures=arm64-v8a - name: Upload template app APK - uses: actions/upload-artifact@v6 + uses: ./.github/actions/upload-artifact with: name: android-templateapp-${{ matrix.flavor }} path: /tmp/RNTestProject/android/app/build/outputs/apk/${{ matrix.flavor }}/app-${{ matrix.flavor }}.apk diff --git a/.github/workflows/e2e-android-rntester.yml b/.github/workflows/e2e-android-rntester.yml index 7b98d00fed2..bee20a3dc8b 100644 --- a/.github/workflows/e2e-android-rntester.yml +++ b/.github/workflows/e2e-android-rntester.yml @@ -77,7 +77,7 @@ jobs: exclude-tags: ${{ matrix.flavor == 'debug' && 'android-release-only' || '' }} - name: Store per-flow test state if: always() - uses: actions/upload-artifact@v6 + uses: ./.github/actions/upload-artifact with: name: e2e_android_rntester_state_${{ matrix.flavor }}_arm64-v8a_NewArch overwrite: true diff --git a/.github/workflows/e2e-android-templateapp.yml b/.github/workflows/e2e-android-templateapp.yml index efe5f563b17..dfc0132d74e 100644 --- a/.github/workflows/e2e-android-templateapp.yml +++ b/.github/workflows/e2e-android-templateapp.yml @@ -96,7 +96,7 @@ jobs: emulator-arch: x86_64 - name: Store per-flow test state if: always() - uses: actions/upload-artifact@v6 + uses: ./.github/actions/upload-artifact with: name: e2e_android_templateapp_state_${{ matrix.flavor }}_arm64-v8a_NewArch overwrite: true diff --git a/.github/workflows/e2e-ios-templateapp.yml b/.github/workflows/e2e-ios-templateapp.yml index 7ef8845069c..dbaaaa2b566 100644 --- a/.github/workflows/e2e-ios-templateapp.yml +++ b/.github/workflows/e2e-ios-templateapp.yml @@ -91,7 +91,7 @@ jobs: -destination "generic/platform=iOS Simulator" \ -derivedDataPath "/tmp/RNTestProject" - name: Upload app - uses: actions/upload-artifact@v6 + uses: ./.github/actions/upload-artifact with: name: RNTestProject-${{ matrix.flavor }} overwrite: true diff --git a/.github/workflows/expo-code-review-command.yml b/.github/workflows/expo-code-review-command.yml index fe33163a156..d74e120c5b3 100644 --- a/.github/workflows/expo-code-review-command.yml +++ b/.github/workflows/expo-code-review-command.yml @@ -171,7 +171,7 @@ jobs: # (issue_comment context has no pull_request.number). - name: Upload review run log if: always() && steps.cmd.outputs.run == 'true' - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + uses: ./.github/actions/upload-artifact with: name: review-run-log-pr${{ github.event.issue.number }} path: .expo-code-review/.runs/reviews.jsonl diff --git a/.github/workflows/expo-code-review.yml b/.github/workflows/expo-code-review.yml index a14aa69321b..f165f76a64b 100644 --- a/.github/workflows/expo-code-review.yml +++ b/.github/workflows/expo-code-review.yml @@ -119,7 +119,7 @@ jobs: # that failed before writing the log (or a no-op skip) legitimately has no file. - name: Upload review run log if: always() - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + uses: ./.github/actions/upload-artifact with: name: review-run-log-pr${{ github.event.pull_request.number }} path: .expo-code-review/.runs/reviews.jsonl diff --git a/.github/workflows/format.yml b/.github/workflows/format.yml index 07537e4b284..5e7be2490cc 100644 --- a/.github/workflows/format.yml +++ b/.github/workflows/format.yml @@ -69,7 +69,7 @@ jobs: fi - name: Upload formatting report if: always() && github.event_name == 'pull_request' - uses: actions/upload-artifact@v6 + uses: ./.github/actions/upload-artifact with: name: format-results path: .format-results/ diff --git a/.github/workflows/prebuild-ios-core.yml b/.github/workflows/prebuild-ios-core.yml index 9c6bad03a43..e8eef406461 100644 --- a/.github/workflows/prebuild-ios-core.yml +++ b/.github/workflows/prebuild-ios-core.yml @@ -96,12 +96,12 @@ jobs: cd packages/react-native node scripts/ios-prebuild -b -f "${{ matrix.flavor }}" -p "${{ matrix.slice }}" - name: Upload headers - uses: actions/upload-artifact@v6 + uses: ./.github/actions/upload-artifact with: name: prebuild-ios-core-headers-${{ matrix.flavor }}-${{ matrix.slice }} path: packages/react-native/.build/headers - name: Upload artifacts - uses: actions/upload-artifact@v6 + uses: ./.github/actions/upload-artifact with: name: prebuild-ios-core-slice-${{ matrix.flavor }}-${{ matrix.slice }} path: | @@ -245,17 +245,17 @@ jobs: cp packages/react-native/.build/output/xcframeworks/${{matrix.flavor}}/ReactNativeHeaders.xcframework.tar.gz \ packages/react-native/.build/output/xcframeworks/ReactNativeHeaders${{matrix.flavor}}.xcframework.tar.gz - name: Upload XCFramework Artifact - uses: actions/upload-artifact@v6 + uses: ./.github/actions/upload-artifact with: name: ReactCore${{ matrix.flavor }}.xcframework.tar.gz path: packages/react-native/.build/output/xcframeworks/ReactCore${{matrix.flavor}}.xcframework.tar.gz - name: Upload dSYM Artifact - uses: actions/upload-artifact@v6 + uses: ./.github/actions/upload-artifact with: name: ReactCore${{ matrix.flavor }}.framework.dSYM.tar.gz path: packages/react-native/.build/output/xcframeworks/ReactCore${{matrix.flavor}}.framework.dSYM.tar.gz - name: Upload ReactNativeHeaders XCFramework Artifact - uses: actions/upload-artifact@v6 + uses: ./.github/actions/upload-artifact with: name: ReactNativeHeaders${{ matrix.flavor }}.xcframework.tar.gz path: packages/react-native/.build/output/xcframeworks/ReactNativeHeaders${{matrix.flavor}}.xcframework.tar.gz diff --git a/.github/workflows/prebuild-ios-dependencies.yml b/.github/workflows/prebuild-ios-dependencies.yml index b34fcf203eb..8d02b3ec149 100644 --- a/.github/workflows/prebuild-ios-dependencies.yml +++ b/.github/workflows/prebuild-ios-dependencies.yml @@ -31,7 +31,7 @@ jobs: run: | node scripts/releases/prepare-ios-prebuilds.js -w - name: Upload Artifacts - uses: actions/upload-artifact@v6 + uses: ./.github/actions/upload-artifact with: name: ios-prebuilds-workspace path: packages/react-native/third-party/ @@ -92,7 +92,7 @@ jobs: if: steps.restore-slice-folder.outputs.cache-hit != 'true' run: node scripts/releases/prepare-ios-prebuilds.js -b -p ${{ matrix.slice }} -r ${{ matrix.flavor }} - name: Upload Artifacts - uses: actions/upload-artifact@v6 + uses: ./.github/actions/upload-artifact with: name: prebuild-slice-${{ matrix.flavor }}-${{ matrix.slice }} path: | @@ -181,17 +181,17 @@ jobs: tar -cz -f ../ReactNativeDependencies${{ matrix.flavor }}.framework.dSYM.tar.gz . mv ../ReactNativeDependencies${{ matrix.flavor }}.framework.dSYM.tar.gz ./ReactNativeDependencies${{ matrix.flavor }}.framework.dSYM.tar.gz - name: Upload XCFramework Artifact - uses: actions/upload-artifact@v6 + uses: ./.github/actions/upload-artifact with: name: ReactNativeDependencies${{ matrix.flavor }}.xcframework.tar.gz path: packages/react-native/third-party/ReactNativeDependencies${{ matrix.flavor }}.xcframework.tar.gz - name: Upload Headers Sidecar XCFramework Artifact - uses: actions/upload-artifact@v6 + uses: ./.github/actions/upload-artifact with: name: ReactNativeDependenciesHeaders${{ matrix.flavor }}.xcframework.tar.gz path: packages/react-native/third-party/ReactNativeDependenciesHeaders${{ matrix.flavor }}.xcframework.tar.gz - name: Upload dSYM Artifact - uses: actions/upload-artifact@v6 + uses: ./.github/actions/upload-artifact with: name: ReactNativeDependencies${{ matrix.flavor }}.framework.dSYM.tar.gz path: | diff --git a/.github/workflows/test-all.yml b/.github/workflows/test-all.yml index c138cd5730f..2f066913a80 100644 --- a/.github/workflows/test-all.yml +++ b/.github/workflows/test-all.yml @@ -502,7 +502,7 @@ jobs: fi yarn build android "${args[@]}" -P reactNativeArchitectures="$TARGET_ARCHITECTURE" -P react.internal.mavenLocalRepo="/tmp/maven-local" - name: Upload artifact - uses: actions/upload-artifact@v6 + uses: ./.github/actions/upload-artifact with: name: helloworld-apk-${{ matrix.flavor }}-NewArch-hermes path: ./private/helloworld/android/app/build/outputs/apk/ diff --git a/.github/workflows/validate-cxx-api-snapshots.yml b/.github/workflows/validate-cxx-api-snapshots.yml index e0c7caa2f5d..3fe834b522c 100644 --- a/.github/workflows/validate-cxx-api-snapshots.yml +++ b/.github/workflows/validate-cxx-api-snapshots.yml @@ -79,7 +79,7 @@ jobs: run: yarn cxx-api-validate --output-dir /tmp/cxx-api-snapshots - name: Upload C++ API snapshots if: always() - uses: actions/upload-artifact@v6 + uses: ./.github/actions/upload-artifact with: name: cxx-api-snapshots path: /tmp/cxx-api-snapshots/