Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions .github/workflows/app-versions.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
name: App Version Alignment

on:
pull_request:
paths:
- '.github/workflows/app-versions.yml'
- 'scripts/check-app-versions*.mjs'
- 'package.json'
- 'apps/*/package.json'
push:
branches: [main]
paths:
- '.github/workflows/app-versions.yml'
- 'scripts/check-app-versions*.mjs'
- 'package.json'
- 'apps/*/package.json'

permissions:
contents: read

jobs:
app-versions:
runs-on: ubuntu-24.04
timeout-minutes: 5
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
- uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2
with:
bun-version: 1.3.14
- name: Test the version checker
run: bun test scripts/check-app-versions.test.mjs
- name: Check app versions
run: bun run check:app-versions
7 changes: 5 additions & 2 deletions apps/benchmark/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,5 +108,8 @@ Same-revision scheduled/manual runs show baseline variation. Performance remains
report-only.

The example's former benchmark screen and TurboModule control have moved here.
No public Nitro API changes are needed. App dependency versions initially match
the example; automated version-alignment enforcement is a separate follow-up.
No public Nitro API changes are needed. Run `bun run check:app-versions` to check
app versions and shared dependency declarations against the example and root
workspace. The lightweight App Version Alignment workflow runs this check
without installing dependencies or building either app. App-specific dependencies
such as Harness are allowed; shared version declarations must match exactly.
6 changes: 3 additions & 3 deletions apps/benchmark/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,9 @@
"@babel/core": "^7.29.7",
"@babel/preset-env": "^7.29.7",
"@babel/runtime": "^7.29.7",
"@react-native-community/cli": "20.1.3",
"@react-native-community/cli-platform-android": "20.1.3",
"@react-native-community/cli-platform-ios": "20.1.3",
"@react-native-community/cli": "20.2.0",
"@react-native-community/cli-platform-android": "20.2.0",
"@react-native-community/cli-platform-ios": "20.2.0",
"@react-native/babel-preset": "0.85.3",
"@react-native/eslint-config": "0.85.3",
"@react-native/metro-config": "0.85.3",
Expand Down
240 changes: 18 additions & 222 deletions bun.lock

Large diffs are not rendered by default.

5 changes: 5 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
"build": "bun nitro build && bun nitrogen build && bun nitro-test-external build && bun nitro-test build",
"bootstrap": "bun i && bun run build && cd apps/example && bundle install && bun pods",
"typecheck": "bun --filter=\"**\" typecheck",
"check:app-versions": "bun scripts/check-app-versions.mjs",
"lint": "bun nitro lint && bun nitrogen lint && bun nitro-test lint && bun example lint",
"lint-cpp": "./scripts/clang-format.sh",
"lint-swift": "./scripts/swift-format.sh",
Expand Down Expand Up @@ -92,6 +93,10 @@
{
"file": "apps/example/package.json",
"path": "version"
},
{
"file": "apps/benchmark/package.json",
"path": "version"
}
]
},
Expand Down
71 changes: 71 additions & 0 deletions scripts/check-app-versions.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { readFile } from 'node:fs/promises'

const appDirectories = ['apps/example', 'apps/benchmark']

/** Compare declared versions; app-specific dependencies are intentionally allowed. */
export function findAppVersionMismatches(manifests) {
const problems = []
const dependencies = new Map()
for (const [directory, manifest] of Object.entries(manifests)) {
for (const [name, version] of Object.entries({
...manifest.devDependencies,
...manifest.dependencies,
})) {
const declarations = dependencies.get(name) ?? []
declarations.push({ directory, version })
dependencies.set(name, declarations)
}
}

for (const directory of appDirectories) {
const manifest = manifests[directory]
if (manifest == null) {
problems.push(`${directory}/package.json is missing.`)
continue
}
if (manifest.version !== manifests['.']?.version) {
problems.push(
`${directory}: app version ${manifest.version} must match root ${manifests['.']?.version}.`
)
}
for (const name of ['react', 'react-native']) {
if (manifest.dependencies?.[name] == null) {
problems.push(`${directory}: ${name} must be a direct dependency.`)
}
}
}

for (const [name, declarations] of dependencies) {
if (new Set(declarations.map(({ version }) => version)).size > 1) {
problems.push(
`${name}: ${declarations
.map(({ directory, version }) => `${directory}=${version}`)
.join(', ')}`
)
}
}
return problems
}

if (import.meta.main) {
const manifests = Object.fromEntries(
await Promise.all(
['.', ...appDirectories].map(async (directory) => [
directory,
JSON.parse(
await readFile(
new URL(`../${directory}/package.json`, import.meta.url),
'utf8'
)
),
])
)
)
const problems = findAppVersionMismatches(manifests)
if (problems.length > 0) {
console.error('App versions have drifted:\n' + problems.join('\n'))
process.exitCode = 1
} else {
console.log('App versions and shared dependency declarations are aligned.')
}
}
63 changes: 63 additions & 0 deletions scripts/check-app-versions.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { expect, test } from 'bun:test'
import { findAppVersionMismatches } from './check-app-versions.mjs'

function aligned() {
return {
'.': { version: '1.0.0', devDependencies: { react: '19.2.3' } },
'apps/example': {
version: '1.0.0',
dependencies: { 'react': '19.2.3', 'react-native': '0.85.3' },
devDependencies: {
'react-native-harness': '1.4.1',
'@babel/core': '^7.29.7',
},
},
'apps/benchmark': {
version: '1.0.0',
dependencies: { 'react': '19.2.3', 'react-native': '0.85.3' },
devDependencies: { '@babel/core': '^7.29.7' },
},
}
}

test('allows app-specific dependencies such as Harness', () => {
expect(findAppVersionMismatches(aligned())).toEqual([])
})

test('reports React Native drift with both declared versions', () => {
const manifests = aligned()
manifests['apps/benchmark'].dependencies['react-native'] = '0.86.0'
expect(findAppVersionMismatches(manifests)).toEqual([
'react-native: apps/example=0.85.3, apps/benchmark=0.86.0',
])
})

test('checks the root React version too', () => {
const manifests = aligned()
manifests['.'].devDependencies.react = '19.1.0'
expect(findAppVersionMismatches(manifests)[0]).toContain('react: .=19.1.0')
})

test('checks shared build tools without equating different version ranges', () => {
const manifests = aligned()
manifests['apps/benchmark'].devDependencies['@babel/core'] = '7.29.7'
expect(findAppVersionMismatches(manifests)[0]).toContain('@babel/core:')
})

test('checks app versions used by release automation', () => {
const manifests = aligned()
manifests['apps/benchmark'].version = '0.9.0'
expect(findAppVersionMismatches(manifests)[0]).toContain(
'must match root 1.0.0'
)
})

test('does not silently accept a missing app or missing runtime dependency', () => {
const manifests = aligned()
delete manifests['apps/benchmark'].dependencies.react
expect(findAppVersionMismatches(manifests)[0]).toContain('direct dependency')
delete manifests['apps/benchmark']
expect(findAppVersionMismatches(manifests)).toEqual([
'apps/benchmark/package.json is missing.',
])
})
Loading