Skip to content
Merged
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
63 changes: 42 additions & 21 deletions .github/workflows/apply-expo-dependencies.yml
Original file line number Diff line number Diff line change
Expand Up @@ -56,17 +56,28 @@ jobs:
[[ "$EVENT_HEAD_BRANCH" =~ ^dependabot/npm_and_yarn/(expo-compatible-patches|expo-sdk)-[A-Za-z0-9._/-]+$ ]]
git check-ref-format "refs/heads/$EVENT_HEAD_BRANCH"

expected_entries=(
'example|d'
'example/app.json|f'
'example/package.json|f'
'metadata.json|f'
'package.json|f'
# Sort both lists with the same collation: '/' sorts before '|' in
# the C locale, so a hand-ordered list would never match.
mapfile -t expected_entries < <(
printf '%s\n' \
'appium-test-environment|d' \
'appium-test-environment/app.json|f' \
'appium-test-environment/package.json|f' \
'example|d' \
'example/app.json|f' \
'example/package.json|f' \
'metadata.json|f' \
'package.json|f' |
LC_ALL=C sort
)
mapfile -t actual_entries < <(
find "$INCOMING" -mindepth 1 -printf '%P|%y\n' | sort
find "$INCOMING" -mindepth 1 -printf '%P|%y\n' | LC_ALL=C sort
)
[[ "${actual_entries[*]}" == "${expected_entries[*]}" ]]
if [[ "${actual_entries[*]}" != "${expected_entries[*]}" ]]; then
echo "Unexpected alignment artifact contents:" >&2
printf ' %s\n' "${actual_entries[@]}" >&2
exit 1
fi

metadata="$INCOMING/metadata.json"
jq -e \
Expand Down Expand Up @@ -109,7 +120,10 @@ jobs:
((${#pr_files[@]} > 0))
for file in "${pr_files[@]}"; do
case "$file" in
package.json|example/package.json|example/app.json|yarn.lock) ;;
package.json|yarn.lock) ;;
example/package.json|example/app.json) ;;
appium-test-environment/package.json) ;;
appium-test-environment/app.json) ;;
*)
echo "Unexpected file in Expo Dependabot PR: $file" >&2
exit 1
Expand Down Expand Up @@ -158,17 +172,25 @@ jobs:
cp "$INCOMING/package.json" package.json
cp "$INCOMING/example/package.json" example/package.json
cp "$INCOMING/example/app.json" example/app.json
cp "$INCOMING/appium-test-environment/package.json" \
appium-test-environment/package.json
cp "$INCOMING/appium-test-environment/app.json" \
appium-test-environment/app.json

if git diff --quiet HEAD -- \
package.json example/package.json example/app.json; then
manifests=(
package.json
example/package.json
example/app.json
appium-test-environment/package.json
appium-test-environment/app.json
)

if git diff --quiet HEAD -- "${manifests[@]}"; then
echo 'has_changes=false' >> "$GITHUB_OUTPUT"
exit 0
fi

git add -- \
package.json \
example/package.json \
example/app.json
git add -- "${manifests[@]}"
node "$validator" "$GITHUB_WORKSPACE"

# The checked-in Yarn binary regenerates the lockfile without running
Expand All @@ -184,19 +206,18 @@ jobs:
)
for file in "${changed_files[@]}"; do
case "$file" in
package.json|example/package.json|example/app.json|yarn.lock) ;;
package.json|yarn.lock) ;;
example/package.json|example/app.json) ;;
appium-test-environment/package.json) ;;
appium-test-environment/app.json) ;;
*)
echo "Lockfile generation changed an unexpected file: $file" >&2
exit 1
;;
esac
done

git add -- \
package.json \
example/package.json \
example/app.json \
yarn.lock
git add -- "${manifests[@]}" yarn.lock
node "$validator" "$GITHUB_WORKSPACE"
git diff --cached --check HEAD --
echo 'has_changes=true' >> "$GITHUB_OUTPUT"
Expand Down
7 changes: 6 additions & 1 deletion .github/workflows/expo-dependencies.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ on:
- 'package.json'
- 'example/package.json'
- 'example/app.json'
- 'appium-test-environment/package.json'
- 'appium-test-environment/app.json'
- 'yarn.lock'
permissions:
contents: read
Expand Down Expand Up @@ -62,9 +64,12 @@ jobs:
run: |
set -euo pipefail
output="$RUNNER_TEMP/expo-alignment"
mkdir -p "$output/example"
mkdir -p "$output/example" "$output/appium-test-environment"
cp package.json "$output/"
cp example/package.json example/app.json "$output/example/"
cp appium-test-environment/package.json \
appium-test-environment/app.json \
"$output/appium-test-environment/"

jq -n \
--argjson pr_number "$PR_NUMBER" \
Expand Down
92 changes: 53 additions & 39 deletions scripts/expo-dependencies.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,34 +4,43 @@ const { join } = require('node:path');

const ROOT_DIRECTORY = join(__dirname, '..');
const ROOT_PACKAGE_PATH = join(ROOT_DIRECTORY, 'package.json');
const EXAMPLE_PACKAGE_PATH = join(ROOT_DIRECTORY, 'example', 'package.json');
// Every Expo app workspace must stay on the Expo SDK cohort. The example app
// is the source of truth for the root's shared development dependencies.
const APP_WORKSPACES = [
{ name: 'example', manifest: 'example/package.json' },
{
name: 'TransactAppiumTestEnvironment',
manifest: 'appium-test-environment/package.json',
},
];
const [EXAMPLE_WORKSPACE] = APP_WORKSPACES;

const SHARED_DEPENDENCIES = [
{
name: 'react',
rootSection: 'devDependencies',
exampleSection: 'dependencies',
appSection: 'dependencies',
},
{
name: 'react-native',
rootSection: 'devDependencies',
exampleSection: 'dependencies',
appSection: 'dependencies',
},
{
name: '@react-native/babel-preset',
exampleName: 'react-native',
appName: 'react-native',
rootSection: 'devDependencies',
exampleSection: 'dependencies',
appSection: 'dependencies',
},
{
name: '@types/react',
rootSection: 'devDependencies',
exampleSection: 'devDependencies',
appSection: 'devDependencies',
},
{
name: 'typescript',
rootSection: 'devDependencies',
exampleSection: 'devDependencies',
appSection: 'devDependencies',
},
];

Expand All @@ -51,68 +60,73 @@ function dependencyVersion(specifier, dependency, manifest) {
return match[1];
}

function sharedDependencyState(rootPackage, examplePackage) {
function sharedDependencyState(rootPackage, workspace, appPackage) {
return SHARED_DEPENDENCIES.map((dependency) => {
const exampleName = dependency.exampleName ?? dependency.name;
const appName = dependency.appName ?? dependency.name;
const rootSpecifier =
rootPackage[dependency.rootSection]?.[dependency.name];
const exampleSpecifier =
examplePackage[dependency.exampleSection]?.[exampleName];
const appSpecifier = appPackage[dependency.appSection]?.[appName];

if (!rootSpecifier || !exampleSpecifier) {
if (!rootSpecifier || !appSpecifier) {
throw new Error(
`${dependency.name} must exist in the root and ${exampleName} must exist in the example manifest`
`${dependency.name} must exist in the root and ${appName} must exist in ${workspace.manifest}`
);
}

return {
...dependency,
rootSpecifier,
exampleSpecifier,
appSpecifier,
rootVersion: dependencyVersion(
rootSpecifier,
dependency.name,
'package.json'
),
exampleVersion: dependencyVersion(
exampleSpecifier,
exampleName,
'example/package.json'
),
appVersion: dependencyVersion(appSpecifier, appName, workspace.manifest),
};
});
}

function checkSharedDependencies(rootPackage, examplePackage) {
const mismatches = sharedDependencyState(rootPackage, examplePackage).filter(
({ rootVersion, exampleVersion }) => rootVersion !== exampleVersion
function checkSharedDependencies(rootPackage) {
const mismatches = APP_WORKSPACES.flatMap((workspace) =>
sharedDependencyState(
rootPackage,
workspace,
readPackage(join(ROOT_DIRECTORY, workspace.manifest))
)
.filter(({ rootVersion, appVersion }) => rootVersion !== appVersion)
.map((dependency) => ({ ...dependency, workspace }))
);

if (mismatches.length > 0) {
const details = mismatches
.map(
({ name, rootSpecifier, exampleSpecifier }) =>
` - ${name}: root ${rootSpecifier}, example ${exampleSpecifier}`
({ name, rootSpecifier, appSpecifier, workspace }) =>
` - ${name}: root ${rootSpecifier}, ${workspace.manifest} ${appSpecifier}`
)
.join('\n');

throw new Error(`Shared Expo dependencies are not aligned:\n${details}`);
}

console.log('Shared root and example dependencies are aligned');
console.log('Shared root and app workspace dependencies are aligned');
}

function syncSharedDependencies(rootPackage, examplePackage) {
const updates = [];

for (const dependency of sharedDependencyState(rootPackage, examplePackage)) {
if (dependency.rootVersion === dependency.exampleVersion) {
for (const dependency of sharedDependencyState(
rootPackage,
EXAMPLE_WORKSPACE,
examplePackage
)) {
if (dependency.rootVersion === dependency.appVersion) {
continue;
}

const rangePrefix = dependency.rootSpecifier.match(/^[~^]/)?.[0] ?? '';
rootPackage[dependency.rootSection][dependency.name] =
`${rangePrefix}${dependency.exampleVersion}`;
`${rangePrefix}${dependency.appVersion}`;
updates.push(dependency.name);
}

Expand Down Expand Up @@ -146,28 +160,28 @@ function main() {
const mode = process.argv[2];

if (mode === 'check') {
runYarn(['workspace', 'example', 'expo', 'install', '--check']);
checkSharedDependencies(
readPackage(ROOT_PACKAGE_PATH),
readPackage(EXAMPLE_PACKAGE_PATH)
);
for (const workspace of APP_WORKSPACES) {
runYarn(['workspace', workspace.name, 'expo', 'install', '--check']);
}
checkSharedDependencies(readPackage(ROOT_PACKAGE_PATH));
return;
}

if (mode === 'fix') {
runYarn(['workspace', 'example', 'expo', 'install', '--fix']);
for (const workspace of APP_WORKSPACES) {
runYarn(['workspace', workspace.name, 'expo', 'install', '--fix']);
}

const rootPackage = readPackage(ROOT_PACKAGE_PATH);
const examplePackage = readPackage(EXAMPLE_PACKAGE_PATH);
const examplePackage = readPackage(
join(ROOT_DIRECTORY, EXAMPLE_WORKSPACE.manifest)
);
syncSharedDependencies(rootPackage, examplePackage);

// This command intentionally updates the root manifest, so CI's default
// immutable install mode must be disabled while regenerating the lockfile.
runYarn(['install', '--no-immutable']);
checkSharedDependencies(
readPackage(ROOT_PACKAGE_PATH),
readPackage(EXAMPLE_PACKAGE_PATH)
);
checkSharedDependencies(readPackage(ROOT_PACKAGE_PATH));
return;
}

Expand Down
Loading
Loading