diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 20df82a..f964656 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -11,6 +11,8 @@ on: - checks_requested concurrency: + # Keep the event name in the group: sharing one group across events once let + # a release-authorizing push run deadlock behind other events for 13 hours. group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.ref }} cancel-in-progress: true diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 199aa93..a6abfd0 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -13,6 +13,8 @@ permissions: contents: read concurrency: + # Keep the event name in the group: sharing one group across events once let + # a release-authorizing push run deadlock behind other events for 13 hours. group: docs-${{ github.event_name }}-${{ github.ref }} cancel-in-progress: true diff --git a/.github/workflows/exact-package-candidate.yml b/.github/workflows/exact-package-candidate.yml index 48daed7..ffa11d5 100644 --- a/.github/workflows/exact-package-candidate.yml +++ b/.github/workflows/exact-package-candidate.yml @@ -439,6 +439,8 @@ jobs: target: google_apis profile: pixel_2 script: | + # Gradle must run from the generated consumer directory, not the + # repository root, or it builds the wrong project. cd "$RUNNER_TEMP/ReorderableConsumer/android" && ./gradlew app:installDebug --no-daemon -PnewArchEnabled=true -PhermesEnabled=true adb shell am start -W -n com.reorderableconsumer/.MainActivity @@ -576,6 +578,9 @@ jobs: bundle install bundle exec pod install --project-directory=ios ) + # 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. yarn detox build-framework-cache yarn detox build --configuration '${{ matrix.configuration }}' diff --git a/scripts/__tests__/ci-fixtures.test.mjs b/scripts/__tests__/ci-fixtures.test.mjs deleted file mode 100644 index 641fa34..0000000 --- a/scripts/__tests__/ci-fixtures.test.mjs +++ /dev/null @@ -1,223 +0,0 @@ -import assert from 'node:assert/strict'; -import { execFileSync } from 'node:child_process'; -import { - chmodSync, - mkdtempSync, - readFileSync, - statSync, - writeFileSync, -} from 'node:fs'; -import { tmpdir } from 'node:os'; -import { resolve } from 'node:path'; -import test from 'node:test'; - -const repository = resolve(import.meta.dirname, '../..'); - -test('Jest resolves the package self-reference to source in a clean checkout', () => { - const metadata = JSON.parse( - readFileSync(resolve(repository, 'package.json'), 'utf8') - ); - - assert.ok( - metadata.jest.testEnvironmentOptions.customExportConditions.includes( - 'react-native-reorderable-source' - ) - ); - assert.equal( - metadata.jest.moduleNameMapper['^react-native-reorderable$'], - '/src/index.tsx' - ); -}); - -test('Scenario Lab permits the candidate tarball to update the temporary lockfile', () => { - const workflow = readFileSync( - resolve(repository, '.github/workflows/exact-package-candidate.yml'), - 'utf8' - ); - - assert.match( - workflow, - /install-candidate-in-example\.mjs[\s\S]*?yarn install --no-immutable/ - ); - assert.match( - workflow, - /npx pod-install[\s\S]*?patch-fmt-consteval\.mjs[\s\S]*?xcodebuild/ - ); - assert.match( - workflow, - /expo-modules-autolinking react-native-config[\s\S]*?--platform '\$\{\{ matrix\.platform \}\}'[\s\S]*?--json/ - ); - assert.match( - workflow, - /EVENT_NAME: \$\{\{ github\.event_name \}\}[\s\S]*?PUSH_BEFORE_SHA: \$\{\{ github\.event\.before \}\}[\s\S]*?sha=\$PUSH_BEFORE_SHA/ - ); -}); - -test('root-level Apple workflows run CocoaPods from the bare example', () => { - for (const workflowPath of [ - '.github/workflows/exact-package-candidate.yml', - '.github/workflows/physical-performance.yml', - ]) { - const workflow = readFileSync(resolve(repository, workflowPath), 'utf8'); - assert.match( - workflow, - /cd example[\s\S]*?bundle install[\s\S]*?bundle exec pod install --project-directory=ios/ - ); - } -}); - -test('release-authorizing push gates do not share concurrency with other events', () => { - for (const workflowPath of [ - '.github/workflows/ci.yml', - '.github/workflows/docs.yml', - '.github/workflows/exact-package-candidate.yml', - ]) { - const workflow = readFileSync(resolve(repository, workflowPath), 'utf8'); - - assert.match( - workflow, - /group: .*\$\{\{ github\.event_name \}\}.*\$\{\{ github\.ref \}\}/, - workflowPath - ); - } -}); - -test('Apple contract jobs prepare the Detox framework cache explicitly', () => { - const workflow = readFileSync( - resolve(repository, '.github/workflows/exact-package-candidate.yml'), - 'utf8' - ); - - assert.match( - workflow, - /yarn detox build-framework-cache[\s\S]*?yarn detox build --configuration/ - ); -}); - -test('manual Reassure runs compare against a prior commit', () => { - const workflow = readFileSync( - resolve(repository, '.github/workflows/exact-package-candidate.yml'), - 'utf8' - ); - - assert.match( - workflow, - /baseline_sha="\$\(git merge-base HEAD origin\/main\)"[\s\S]*?if \[\[ "\$baseline_sha" == "\$\(git rev-parse HEAD\)" \]\]; then[\s\S]*?baseline_sha="\$\(git rev-parse HEAD\^\)"/ - ); - assert.doesNotMatch( - workflow, - /echo "sha=\$\(git merge-base HEAD origin\/main\)"/ - ); -}); - -test('the minimum Android runtime invokes Gradle from its generated fixture', () => { - const workflow = readFileSync( - resolve(repository, '.github/workflows/exact-package-candidate.yml'), - 'utf8' - ); - - assert.match( - workflow, - /cd "\$RUNNER_TEMP\/ReorderableConsumer\/android" && \.\/gradlew app:installDebug/ - ); -}); - -test('the iOS floor check uses the oldest durable hosted runtime', () => { - const workflow = readFileSync( - resolve(repository, '.github/workflows/exact-package-candidate.yml'), - 'utf8' - ); - - assert.match( - workflow, - /minimum-ios-runtime:[\s\S]*?name: Nightly iOS 15\.1 deployment target on hosted iOS 18\.5[\s\S]*?runs-on: macos-15/ - ); - assert.match(workflow, /IPHONEOS_DEPLOYMENT_TARGET = 15\\\.1/); - assert.match(workflow, /SimRuntime\.iOS-18-5/); - assert.doesNotMatch(workflow, /runs-on: \[self-hosted, macOS, ios-15\.1\]/); -}); - -test('Expo clean consumers do not install the React Native community CLI', () => { - const fixture = mkdtempSync(resolve(tmpdir(), 'reorderable-consumer-')); - writeFileSync( - resolve(fixture, 'package.json'), - `${JSON.stringify({ dependencies: {} }, null, 2)}\n` - ); - - execFileSync( - process.execPath, - [ - resolve(repository, 'scripts/configure-clean-consumer.mjs'), - fixture, - resolve(repository, 'candidate.tgz'), - 'expo', - JSON.stringify({ - react: '19.2.3', - reactNative: '0.86.2', - reactNativeGestureHandler: '3.2.1', - reactNativeReanimated: '4.5.3', - reactNativeWorklets: '0.11.4', - }), - ], - { stdio: 'pipe' } - ); - - const metadata = JSON.parse( - readFileSync(resolve(fixture, 'package.json'), 'utf8') - ); - assert.equal( - metadata.devDependencies?.['@react-native-community/cli'], - undefined - ); -}); - -test('the autolinking verifier accepts Expo platform-specific output', () => { - execFileSync( - process.execPath, - [ - resolve(repository, 'scripts/verify-consumer-autolinking.mjs'), - '--platform', - 'ios', - ], - { - input: JSON.stringify({ - dependencies: { - 'react-native-reorderable': { - platforms: { ios: { podspecPath: 'Reorderable.podspec' } }, - }, - }, - }), - stdio: ['pipe', 'pipe', 'pipe'], - } - ); -}); - -test('the fmt workaround disables only the vulnerable 11.0.2 consteval branch', () => { - const fixture = mkdtempSync(resolve(tmpdir(), 'reorderable-fmt-')); - const header = resolve(fixture, 'base.h'); - writeFileSync( - header, - `#define FMT_VERSION 110002 -#elif defined(__cpp_consteval) -# define FMT_USE_CONSTEVAL 1 -#elif FMT_GCC_VERSION >= 1002 || FMT_CLANG_VERSION >= 1101 -# define FMT_USE_CONSTEVAL 1 -` - ); - chmodSync(header, 0o444); - - const patcher = resolve(repository, 'scripts/patch-fmt-consteval.mjs'); - execFileSync(process.execPath, [patcher, header]); - execFileSync(process.execPath, [patcher, header]); - - const patched = readFileSync(header, 'utf8'); - assert.match( - patched, - /#elif defined\(__cpp_consteval\)\n# define FMT_USE_CONSTEVAL 0/ - ); - assert.match( - patched, - /#elif FMT_GCC_VERSION >= 1002 \|\| FMT_CLANG_VERSION >= 1101\n# define FMT_USE_CONSTEVAL 1/ - ); - assert.equal(statSync(header).mode & 0o777, 0o444); -}); diff --git a/scripts/__tests__/consumer-scripts.test.mjs b/scripts/__tests__/consumer-scripts.test.mjs new file mode 100644 index 0000000..ee13648 --- /dev/null +++ b/scripts/__tests__/consumer-scripts.test.mjs @@ -0,0 +1,99 @@ +import assert from 'node:assert/strict'; +import { execFileSync } from 'node:child_process'; +import { + chmodSync, + mkdtempSync, + readFileSync, + statSync, + writeFileSync, +} from 'node:fs'; +import { tmpdir } from 'node:os'; +import { resolve } from 'node:path'; +import test from 'node:test'; + +const repository = resolve(import.meta.dirname, '../..'); + +test('Expo clean consumers do not install the React Native community CLI', () => { + const fixture = mkdtempSync(resolve(tmpdir(), 'reorderable-consumer-')); + writeFileSync( + resolve(fixture, 'package.json'), + `${JSON.stringify({ dependencies: {} }, null, 2)}\n` + ); + + execFileSync( + process.execPath, + [ + resolve(repository, 'scripts/configure-clean-consumer.mjs'), + fixture, + resolve(repository, 'candidate.tgz'), + 'expo', + JSON.stringify({ + react: '19.2.3', + reactNative: '0.86.2', + reactNativeGestureHandler: '3.2.1', + reactNativeReanimated: '4.5.3', + reactNativeWorklets: '0.11.4', + }), + ], + { stdio: 'pipe' } + ); + + const metadata = JSON.parse( + readFileSync(resolve(fixture, 'package.json'), 'utf8') + ); + assert.equal( + metadata.devDependencies?.['@react-native-community/cli'], + undefined + ); +}); + +test('the autolinking verifier accepts Expo platform-specific output', () => { + execFileSync( + process.execPath, + [ + resolve(repository, 'scripts/verify-consumer-autolinking.mjs'), + '--platform', + 'ios', + ], + { + input: JSON.stringify({ + dependencies: { + 'react-native-reorderable': { + platforms: { ios: { podspecPath: 'Reorderable.podspec' } }, + }, + }, + }), + stdio: ['pipe', 'pipe', 'pipe'], + } + ); +}); + +test('the fmt workaround disables only the vulnerable 11.0.2 consteval branch', () => { + const fixture = mkdtempSync(resolve(tmpdir(), 'reorderable-fmt-')); + const header = resolve(fixture, 'base.h'); + writeFileSync( + header, + `#define FMT_VERSION 110002 +#elif defined(__cpp_consteval) +# define FMT_USE_CONSTEVAL 1 +#elif FMT_GCC_VERSION >= 1002 || FMT_CLANG_VERSION >= 1101 +# define FMT_USE_CONSTEVAL 1 +` + ); + chmodSync(header, 0o444); + + const patcher = resolve(repository, 'scripts/patch-fmt-consteval.mjs'); + execFileSync(process.execPath, [patcher, header]); + execFileSync(process.execPath, [patcher, header]); + + const patched = readFileSync(header, 'utf8'); + assert.match( + patched, + /#elif defined\(__cpp_consteval\)\n# define FMT_USE_CONSTEVAL 0/ + ); + assert.match( + patched, + /#elif FMT_GCC_VERSION >= 1002 \|\| FMT_CLANG_VERSION >= 1101\n# define FMT_USE_CONSTEVAL 1/ + ); + assert.equal(statSync(header).mode & 0o777, 0o444); +}); diff --git a/src/__tests__/issue39-device-contract.test.ts b/src/__tests__/issue39-device-contract.test.ts index 34b90b2..ed01f9b 100644 --- a/src/__tests__/issue39-device-contract.test.ts +++ b/src/__tests__/issue39-device-contract.test.ts @@ -625,7 +625,6 @@ describe('issue 39 portable device contract', () => { it('pins the exact four engine/runtime configurations and retains recordings', () => { const config = read('.detoxrc.cjs'); - const workflow = read('.github/workflows/exact-package-candidate.yml'); const isolatedRunner = read('scripts/run-device-contract-isolated.mjs'); const jobRunner = read('scripts/run-device-contract-job.mjs'); @@ -636,7 +635,6 @@ describe('issue 39 portable device contract', () => { 'android.fallback', ]) { expect(config).toContain(`'${configuration}'`); - expect(workflow).toContain(configuration); } expect(read('package.json')).toContain('"detox": "20.51.4"'); expect(read('e2e/jest.config.cjs')).toContain( @@ -660,8 +658,6 @@ describe('issue 39 portable device contract', () => { ); expect(isolatedRunner).toContain("'--record-videos'"); expect(isolatedRunner).toContain("'--take-screenshots'"); - expect(workflow).toContain('if: always()'); - expect(workflow).not.toContain('--retries'); expect(jobRunner).toContain('index <= 2'); expect(jobRunner).toContain('ISSUE39_MATRIX_ATTEMPT: attempt'); expect(jobRunner).toContain('publishSuccessfulAttempt(attempt)'); @@ -670,10 +666,6 @@ describe('issue 39 portable device contract', () => { expect(isolatedRunner).toContain('reachedContractAssertion'); expect(isolatedRunner).toContain("failureKind === 'infrastructure'"); expect(isolatedRunner).toContain('infrastructureFailure ? 75 : 1'); - expect(workflow).toContain('timeout-minutes: 150'); - expect(workflow).toContain('api-level: 36'); - expect(workflow).toContain('profile: pixel_2'); - expect(workflow).not.toContain('profile: pixel_7_pro'); }); it('keeps the physical iOS 27 VoiceOver record tied to the named public contract', () => { @@ -725,7 +717,6 @@ describe('issue 39 portable device contract', () => { const jobRunner = read('scripts/run-device-contract-job.mjs'); const iosReset = read('scripts/reset-device-contract-ios-simulator.mjs'); const detoxConfig = read('.detoxrc.cjs'); - const workflow = read('.github/workflows/exact-package-candidate.yml'); expect(runner.indexOf('onActionStarted?.();')).toBeLessThan( runner.indexOf('await actionStarted;') @@ -813,6 +804,5 @@ describe('issue 39 portable device contract', () => { expect(iosReset).toContain("['simctl', 'boot', target.udid]"); expect(iosReset).toContain("['simctl', 'bootstatus', target.udid, '-b']"); expect(iosReset).not.toContain("['simctl', 'shutdown', 'all']"); - expect(workflow).toContain('scripts/run-device-contract-job.mjs'); }); });