mirror of
https://github.com/jjbrunton/Sauci.git
synced 2026-10-31 18:19:43 +00:00
feat(release): enforce manifest and Info.plist release invariants
Extend release-preflight to reject an Android manifest missing the READ_MEDIA_IMAGES/READ_MEDIA_VIDEO tools:node="remove" attributes and an iOS Info.plist that regained dev-launcher-only keys, both regressions introduced silently by expo prebuild during the SDK 54 to 56 upgrade. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
7a76936829
commit
ecc31a13a2
3 files changed
+46
No files matched your search
@@ -13,3 +13,5 @@ native iOS/Android targets.
|
|||||||
- Add stable accessibility labels or `testID` values for critical interactions.
|
- Add stable accessibility labels or `testID` values for critical interactions.
|
||||||
- Verify focused Jest tests, typecheck, lint, then native/Expo behavior as risk
|
- Verify focused Jest tests, typecheck, lint, then native/Expo behavior as risk
|
||||||
requires. UI work needs observable simulator evidence.
|
requires. UI work needs observable simulator evidence.
|
||||||
|
- Before and after any `expo prebuild`, follow the "After native regeneration"
|
||||||
|
checklist in `docs/releasing.md`.
|
||||||
@@ -39,6 +39,7 @@ export function validateReleaseSources({ root = repoRoot, isTracked } = {}) {
|
|||||||
const androidGradle = file('apps/mobile/android/app/build.gradle');
|
const androidGradle = file('apps/mobile/android/app/build.gradle');
|
||||||
const androidStrings = file('apps/mobile/android/app/src/main/res/values/strings.xml');
|
const androidStrings = file('apps/mobile/android/app/src/main/res/values/strings.xml');
|
||||||
const xcode = file('apps/mobile/ios/Sauci.xcodeproj/project.pbxproj');
|
const xcode = file('apps/mobile/ios/Sauci.xcodeproj/project.pbxproj');
|
||||||
|
const androidManifest = file('apps/mobile/android/app/src/main/AndroidManifest.xml');
|
||||||
const iosInfo = file('apps/mobile/ios/Sauci/Info.plist');
|
const iosInfo = file('apps/mobile/ios/Sauci/Info.plist');
|
||||||
const widgetInfo = file('apps/mobile/targets/widget/Info.plist');
|
const widgetInfo = file('apps/mobile/targets/widget/Info.plist');
|
||||||
const iosExpo = file('apps/mobile/ios/Sauci/Supporting/Expo.plist');
|
const iosExpo = file('apps/mobile/ios/Sauci/Supporting/Expo.plist');
|
||||||
@@ -70,6 +71,17 @@ export function validateReleaseSources({ root = repoRoot, isTracked } = {}) {
|
|||||||
fail(`iOS app, widget, and effective widget Xcode build numbers disagree: app ${appBuild}; widget plist ${widgetBuild}; widget settings ${widgetBuildSettings.join(', ')}`);
|
fail(`iOS app, widget, and effective widget Xcode build numbers disagree: app ${appBuild}; widget plist ${widgetBuild}; widget settings ${widgetBuildSettings.join(', ')}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for (const permission of ['READ_MEDIA_IMAGES', 'READ_MEDIA_VIDEO']) {
|
||||||
|
const removed = new RegExp(`<uses-permission[^>]*android:name="android\\.permission\\.${permission}"[^>]*tools:node="remove"`).test(androidManifest);
|
||||||
|
if (!removed) fail(`AndroidManifest.xml must keep tools:node="remove" on ${permission} (expo prebuild regresses this)`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const forbiddenPlistKeys = ['NSBonjourServices', 'NSLocalNetworkUsageDescription', 'RCTMetroPort'];
|
||||||
|
const foundDevLauncherKeys = forbiddenPlistKeys.filter((key) => iosInfo.includes(key));
|
||||||
|
if (foundDevLauncherKeys.length) {
|
||||||
|
fail(`ios/Sauci/Info.plist must not contain dev-launcher-only keys reintroduced by expo prebuild: ${foundDevLauncherKeys.join(', ')}`);
|
||||||
|
}
|
||||||
|
|
||||||
const tracked = isTracked ?? ((relative) => gitStatus(['ls-files', '--error-unmatch', relative], root) === 0);
|
const tracked = isTracked ?? ((relative) => gitStatus(['ls-files', '--error-unmatch', relative], root) === 0);
|
||||||
if (!tracked(certificatePath)) fail(`${certificatePath} must be tracked`);
|
if (!tracked(certificatePath)) fail(`${certificatePath} must be tracked`);
|
||||||
const archiveIgnore = existsSync(join(root, '.easignore')) ? '.easignore' : '.gitignore';
|
const archiveIgnore = existsSync(join(root, '.easignore')) ? '.easignore' : '.gitignore';
|
||||||
|
|||||||
@@ -67,6 +67,38 @@ test('rejects an OTA certificate without its root archive allowlist', () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('rejects an Android manifest that lost the media permission removal (expo prebuild regression)', () => {
|
||||||
|
const root = fixture();
|
||||||
|
try {
|
||||||
|
const manifest = 'apps/mobile/android/app/src/main/AndroidManifest.xml';
|
||||||
|
replace(
|
||||||
|
root,
|
||||||
|
manifest,
|
||||||
|
'<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" tools:node="remove"/>',
|
||||||
|
'<uses-permission android:name="android.permission.READ_MEDIA_IMAGES"/>',
|
||||||
|
);
|
||||||
|
assert.throws(() => validate(root), /READ_MEDIA_IMAGES \(expo prebuild regresses this\)/);
|
||||||
|
} finally {
|
||||||
|
rmSync(root, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('rejects an iOS Info.plist that regained dev-launcher-only keys (expo prebuild regression)', () => {
|
||||||
|
const root = fixture();
|
||||||
|
try {
|
||||||
|
const infoPlist = 'apps/mobile/ios/Sauci/Info.plist';
|
||||||
|
replace(
|
||||||
|
root,
|
||||||
|
infoPlist,
|
||||||
|
'</dict>\n</plist>',
|
||||||
|
'<key>NSBonjourServices</key>\n<array><string>_expo._tcp</string></array>\n</dict>\n</plist>',
|
||||||
|
);
|
||||||
|
assert.throws(() => validate(root), /dev-launcher-only keys reintroduced by expo prebuild: NSBonjourServices/);
|
||||||
|
} finally {
|
||||||
|
rmSync(root, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
test('requires the local EAS plugin executable rather than its package directory', () => {
|
test('requires the local EAS plugin executable rather than its package directory', () => {
|
||||||
const root = mkdtempSync(join(tmpdir(), 'sauci-eas-plugin-'));
|
const root = mkdtempSync(join(tmpdir(), 'sauci-eas-plugin-'));
|
||||||
const executable = join(root, 'bin-run');
|
const executable = join(root, 'bin-run');
|
||||||
|
|||||||
Reference in new issue
Block a user