diff --git a/.github/workflows/exact-package-candidate.yml b/.github/workflows/exact-package-candidate.yml index ffa11d5..2b99ba3 100644 --- a/.github/workflows/exact-package-candidate.yml +++ b/.github/workflows/exact-package-candidate.yml @@ -3,10 +3,6 @@ name: Exact package candidate on: push: branches: [main] - pull_request: - branches: [main] - merge_group: - types: [checks_requested] schedule: - cron: '17 3 * * *' workflow_dispatch: @@ -23,8 +19,9 @@ on: concurrency: # A nightly or manually dispatched matrix must never cancel the push run that - # authorizes a release for the same main commit. - group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.ref }} + # authorizes a release for the same main commit, and consecutive pushes must + # not cancel each other's runs: every main SHA stays release-eligible. + group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.event_name == 'push' && github.sha || github.ref }} cancel-in-progress: true jobs: @@ -47,7 +44,7 @@ jobs: - name: Inspect metadata, contents, entrypoints, and type surfaces env: - CANDIDATE_CHANNEL: ${{ inputs.channel || (github.event_name == 'schedule' && 'nightly') || (github.event_name == 'push' && 'release') || 'pull-request' }} + CANDIDATE_CHANNEL: ${{ inputs.channel || (github.event_name == 'schedule' && 'nightly') || 'release' }} run: | node scripts/verify-package-artifact.mjs \ artifacts/candidate/react-native-reorderable.tgz \ @@ -94,118 +91,6 @@ jobs: - name: Verify Legend List alone run: node scripts/verify-legend-list-package.mjs artifacts/candidate/react-native-reorderable.tgz - quality: - name: Required quality and performance gates - needs: candidate - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - - - name: Setup - uses: ./.github/actions/setup - - - name: Download inspected candidate - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4 - with: - name: release-candidate - path: artifacts/candidate - - - name: Verify source and packed API stay aligned - run: | - yarn lint - yarn typecheck - yarn test --maxWorkers=2 --coverage - yarn test:release - yarn docs:api:check - node scripts/verify-package-artifact.mjs artifacts/candidate/react-native-reorderable.tgz - - - name: Enforce production geometry budgets - run: | - mkdir -p "$RUNNER_TEMP/candidate-package" - tar -xzf artifacts/candidate/react-native-reorderable.tgz -C "$RUNNER_TEMP/candidate-package" - test ! -e "$GITHUB_WORKSPACE/lib" - cp -R "$RUNNER_TEMP/candidate-package/package/lib" "$GITHUB_WORKSPACE/lib" - node benchmarks/issue34-geometry.mjs - node benchmarks/issue35-section-geometry.mjs - - render-regressions: - name: Candidate Reassure render gates - needs: candidate - runs-on: ubuntu-latest - steps: - - name: Checkout candidate with baseline history - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - with: - fetch-depth: 0 - - - name: Setup candidate - uses: ./.github/actions/setup - - - name: Download inspected candidate - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4 - with: - name: release-candidate - path: artifacts/candidate - - - name: Resolve baseline commit - id: baseline - env: - EVENT_NAME: ${{ github.event_name }} - PR_BASE_SHA: ${{ github.event.pull_request.base.sha }} - PUSH_BEFORE_SHA: ${{ github.event.before }} - run: | - if [[ "$EVENT_NAME" == "pull_request" ]]; then - echo "sha=$PR_BASE_SHA" >> "$GITHUB_OUTPUT" - elif [[ "$EVENT_NAME" == "push" ]]; then - echo "sha=$PUSH_BEFORE_SHA" >> "$GITHUB_OUTPUT" - else - baseline_sha="$(git merge-base HEAD origin/main)" - if [[ "$baseline_sha" == "$(git rev-parse HEAD)" ]]; then - baseline_sha="$(git rev-parse HEAD^)" - fi - echo "sha=$baseline_sha" >> "$GITHUB_OUTPUT" - fi - - - name: Measure baseline in an isolated worktree - env: - BASELINE_SHA: ${{ steps.baseline.outputs.sha }} - BASELINE_ROOT: ${{ runner.temp }}/reassure-baseline - run: | - git worktree add --detach "$BASELINE_ROOT" "$BASELINE_SHA" - cp "$GITHUB_WORKSPACE"/src/__perf__/*.perf-test.tsx "$BASELINE_ROOT/src/__perf__/" - cd "$BASELINE_ROOT" - corepack enable - yarn install --immutable - yarn performance:reassure:baseline - mkdir -p "$GITHUB_WORKSPACE/.reassure" - cp .reassure/baseline.perf "$GITHUB_WORKSPACE/.reassure/baseline.perf" - - - name: Replace candidate source with the source carried by the tarball - run: | - mkdir -p "$RUNNER_TEMP/candidate-package" - tar -xzf artifacts/candidate/react-native-reorderable.tgz -C "$RUNNER_TEMP/candidate-package" - mv "$GITHUB_WORKSPACE/src" "$RUNNER_TEMP/repository-source" - cp -R "$RUNNER_TEMP/candidate-package/package/src" "$GITHUB_WORKSPACE/src" - mkdir -p "$GITHUB_WORKSPACE/src/__perf__" - cp "$RUNNER_TEMP/repository-source"/__perf__/*.perf-test.tsx "$GITHUB_WORKSPACE/src/__perf__/" - - - name: Measure and enforce candidate regressions - run: | - yarn performance:reassure - yarn performance:reassure:verify - - - name: Upload Reassure report - if: always() - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 - with: - name: candidate-reassure-performance-report - if-no-files-found: error - retention-days: 30 - path: | - .reassure/output.json - .reassure/output.md - clean-native-consumers: name: ${{ matrix.kind }} / RN ${{ matrix.versions.reactNative }} / ${{ matrix.platform }} needs: candidate @@ -386,8 +271,7 @@ jobs: build minimum-android-runtime: - name: Nightly minimum Android API 24 - if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' + name: Minimum Android API 24 runtime floor needs: candidate runs-on: ubuntu-latest timeout-minutes: 120 @@ -445,8 +329,7 @@ jobs: adb shell am start -W -n com.reorderableconsumer/.MainActivity minimum-ios-runtime: - name: Nightly iOS 15.1 deployment target on hosted iOS 18.5 - if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' + name: Minimum iOS 15.1 deployment target on hosted iOS 18.5 needs: candidate runs-on: macos-15 timeout-minutes: 120 @@ -522,6 +405,60 @@ jobs: "$SIMULATOR_ID" \ org.reactjs.native.example.ReorderableConsumer + peer-canary: + name: Nightly peer-version canary (non-gating) + # Drift canary only: deliberately absent from approve-candidate's needs so + # a breaking peer release alerts on the nightly run without blocking any + # push-to-main candidate or publication. + if: github.event_name == 'schedule' + needs: candidate + runs-on: ubuntu-latest + timeout-minutes: 60 + steps: + - name: Checkout + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + + - name: Setup Node.js + uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 + with: + node-version-file: .nvmrc + + - name: Download inspected candidate + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4 + with: + name: release-candidate + path: artifacts/candidate + + - name: Install the candidate against the latest peer releases + env: + FIXTURE: ${{ runner.temp }}/ReorderableConsumer + run: | + REACT="$(npm view react version)" + REACT_NATIVE="$(npm view react-native version)" + GESTURE_HANDLER="$(npm view react-native-gesture-handler version)" + REANIMATED="$(npm view react-native-reanimated version)" + WORKLETS="$(npm view react-native-worklets version)" + echo "Latest peers: react=$REACT react-native=$REACT_NATIVE" \ + "gesture-handler=$GESTURE_HANDLER reanimated=$REANIMATED worklets=$WORKLETS" + # Unpinned on purpose: this canary exists to surface drift against + # whatever the ecosystem ships today, so the CLI floats with it. + npx --yes @react-native-community/cli@latest init ReorderableConsumer \ + --version "$REACT_NATIVE" --directory "$FIXTURE" --skip-install --pm npm + node scripts/configure-clean-consumer.mjs \ + "$FIXTURE" \ + "$GITHUB_WORKSPACE/artifacts/candidate/react-native-reorderable.tgz" \ + bare \ + "{\"react\":\"$REACT\",\"reactNative\":\"$REACT_NATIVE\",\"reactNativeGestureHandler\":\"$GESTURE_HANDLER\",\"reactNativeReanimated\":\"$REANIMATED\",\"reactNativeWorklets\":\"$WORKLETS\"}" + npm install --prefix "$FIXTURE" --legacy-peer-deps --no-audit --no-fund + + - name: Prove TypeScript, Babel, and autolinking against latest peers + working-directory: ${{ runner.temp }}/ReorderableConsumer + run: | + npx tsc --noEmit + npx react-native config | node "$GITHUB_WORKSPACE/scripts/verify-consumer-autolinking.mjs" \ + --platform android + node -e "const output=require('@babel/core').transformFileSync('worklet-smoke.ts',{configFile:require('node:path').resolve('babel.config.js')}).code;if(!output.includes('__workletHash'))process.exit(1)" + contract: name: ${{ matrix.configuration }} / RN 0.85 needs: candidate @@ -566,18 +503,32 @@ jobs: - name: Verify designated React Native baseline run: node -e "if(require('./package.json').devDependencies['react-native']!=='0.85.0')process.exit(1)" + - name: Cache cocoapods + if: runner.os == 'macOS' + id: cocoapods-cache + uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3 + with: + path: example/ios/Pods + key: ${{ runner.os }}-cocoapods-${{ hashFiles('example/ios/Podfile.lock') }} + restore-keys: | + ${{ runner.os }}-cocoapods- + - name: Verify Apple runtime and prepare native app if: runner.os == 'macOS' + env: + COCOAPODS_CACHE_HIT: ${{ steps.cocoapods-cache.outputs.cache-hit }} run: | xcrun simctl list runtimes | grep -F '${{ matrix.runtime }}' brew tap wix/brew brew trust --formula wix/brew/applesimutils brew install wix/brew/applesimutils - ( - cd example - bundle install - bundle exec pod install --project-directory=ios - ) + if [[ "$COCOAPODS_CACHE_HIT" != 'true' ]]; then + ( + cd example + bundle install + bundle exec pod install --project-directory=ios + ) + fi # build-framework-cache must run before detox build: without a # prebuilt framework cache, Detox compiles it lazily mid-build and # the Apple contract build fails on hosted runners. @@ -655,23 +606,14 @@ jobs: always() && needs.candidate.result == 'success' && needs.clean-package-consumers.result == 'success' && - needs.quality.result == 'success' && - needs.render-regressions.result == 'success' && needs.clean-native-consumers.result == 'success' && + needs.minimum-android-runtime.result == 'success' && + needs.minimum-ios-runtime.result == 'success' && needs.contract.result == 'success' && - needs.parity.result == 'success' && - ( - (github.event_name != 'schedule' && github.event_name != 'workflow_dispatch' && - needs.minimum-android-runtime.result == 'skipped' && - needs.minimum-ios-runtime.result == 'skipped') || - (needs.minimum-android-runtime.result == 'success' && - needs.minimum-ios-runtime.result == 'success') - ) + needs.parity.result == 'success' needs: - candidate - clean-package-consumers - - quality - - render-regressions - clean-native-consumers - minimum-android-runtime - minimum-ios-runtime @@ -698,9 +640,7 @@ jobs: - name: Recheck immutable candidate identity env: EXPECTED_SHA: ${{ needs.candidate.outputs.sha256 }} - run: | - test "$(sha256sum artifacts/candidate/react-native-reorderable.tgz | cut -d' ' -f1)" = "$EXPECTED_SHA" - test "$(node -p "require('./artifacts/candidate/manifest.json').sha256")" = "$EXPECTED_SHA" + run: test "$(sha256sum artifacts/candidate/react-native-reorderable.tgz | cut -d' ' -f1)" = "$EXPECTED_SHA" - name: Expose the verified bytes to release automation uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 diff --git a/docs/releasing.md b/docs/releasing.md index bcaff13..7c768ea 100644 --- a/docs/releasing.md +++ b/docs/releasing.md @@ -87,7 +87,7 @@ The release workflow intentionally has no npm token. Its publish job receives a Every user-visible package change carries a file from `yarn changeset`. After a fully green push to `main`, Changesets creates or updates `changeset-release/main`. This pull request is the on-demand release boundary: leave it open while accumulating changes and merge it when the release should happen. -Merging the release pull request causes the exact-package workflow to build one tarball and run that same tarball through package inspection, clean consumers, the supported React Native compatibility matrix, performance checks, and the four-engine device contract. Publication starts only after that workflow succeeds; the `main` ruleset's required pull-request checks are what guarantee the commit already passed the fast set. Immediately before npm, the publisher rechecks the tarball bytes, SHA-256, source commit, package name, and version against its manifest. +Merging the release pull request causes the exact-package workflow to build one tarball and run that same tarball through package inspection, clean consumers, the supported React Native compatibility matrix, the minimum Android and iOS runtime floors, and the four-engine device contract. Publication starts only after that workflow succeeds; the `main` ruleset's required pull-request checks are what guarantee the commit already passed the fast set. Immediately before npm, the publisher rechecks the tarball bytes, SHA-256, source commit, package name, and version against its manifest. Changesets then creates the package tag and GitHub release. The same release workflow builds and deploys the documentation from that tagged commit. Stable versions use the npm `latest` tag.