diff --git a/packages/build-tools/package.json b/packages/build-tools/package.json
index 7a52582d..e579a19a 100644
--- a/packages/build-tools/package.json
+++ b/packages/build-tools/package.json
@@ -34,9 +34,7 @@
"fs-extra": "^9.0.0",
"node-forge": "^0.9.1",
"nullthrows": "^1.1.1",
- "plist": "^3.0.1",
- "uuid": "^3.3.3",
- "xml2js": "^0.4.23"
+ "plist": "^3.0.1"
},
"devDependencies": {
"@types/fs-extra": "^9.0.1",
diff --git a/packages/build-tools/src/__mocks__/fs.ts b/packages/build-tools/src/__mocks__/fs.ts
index 7b6f1561..8314c390 100644
--- a/packages/build-tools/src/__mocks__/fs.ts
+++ b/packages/build-tools/src/__mocks__/fs.ts
@@ -3,5 +3,8 @@ import { fs } from 'memfs';
// `temp-dir` dependency of `tempy` is using `fs.realpathSync('/tmp')`
// on import to verify existence of tmp directory
fs.mkdirSync('/tmp');
+if (process.env.TMPDIR) {
+ fs.mkdirSync(process.env.TMPDIR, { recursive: true });
+}
module.exports = fs;
diff --git a/packages/build-tools/src/android/__tests__/expoUpdates.test.ts b/packages/build-tools/src/android/__tests__/expoUpdates.test.ts
new file mode 100644
index 00000000..acd5583f
--- /dev/null
+++ b/packages/build-tools/src/android/__tests__/expoUpdates.test.ts
@@ -0,0 +1,156 @@
+import path from 'path';
+
+import fs from 'fs-extra';
+import { AndroidConfig } from '@expo/config-plugins';
+
+import {
+ AndroidMetadataName,
+ androidGetNativelyDefinedClassicReleaseChannelAsync,
+ androidSetChannelNativelyAsync,
+ androidSetClassicReleaseChannelNativelyAsync,
+} from '../expoUpdates';
+
+jest.mock('fs');
+
+const channel = 'main';
+const noMetadataAndroidManifest = `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+`;
+describe(androidSetClassicReleaseChannelNativelyAsync, () => {
+ test('sets the release channel', async () => {
+ const reactNativeProjectDirectory = fs.mkdtempSync('/expo-project-');
+ fs.ensureDirSync(reactNativeProjectDirectory);
+ const releaseChannel = 'default';
+ const ctx = {
+ reactNativeProjectDirectory,
+ job: { releaseChannel },
+ logger: { info: () => {} },
+ };
+
+ fs.ensureDirSync(path.join(reactNativeProjectDirectory, 'android'));
+ const manifestPath = await AndroidConfig.Paths.getAndroidManifestAsync(
+ reactNativeProjectDirectory
+ );
+ const manifestDirectory = path.dirname(manifestPath);
+
+ fs.ensureDirSync(manifestDirectory);
+ fs.writeFileSync(manifestPath, Buffer.from(noMetadataAndroidManifest));
+ const androidManifest = await AndroidConfig.Manifest.readAndroidManifestAsync(manifestPath);
+ expect(
+ AndroidConfig.Manifest.getMainApplicationMetaDataValue(androidManifest, 'releaseChannel')
+ ).toBe(null);
+
+ await androidSetClassicReleaseChannelNativelyAsync(ctx as any);
+
+ const newAndroidManifest = await AndroidConfig.Manifest.readAndroidManifestAsync(manifestPath);
+ expect(
+ AndroidConfig.Manifest.getMainApplicationMetaDataValue(
+ newAndroidManifest,
+ AndroidMetadataName.RELEASE_CHANNEL
+ )
+ ).toBe(releaseChannel);
+ });
+});
+describe(androidSetChannelNativelyAsync, () => {
+ it('sets the channel', async () => {
+ const reactNativeProjectDirectory = fs.mkdtempSync('/expo-project-');
+ fs.ensureDirSync(reactNativeProjectDirectory);
+ const ctx = {
+ reactNativeProjectDirectory,
+ job: { updates: { channel } },
+ logger: { info: () => {} },
+ };
+
+ fs.ensureDirSync(path.join(reactNativeProjectDirectory, 'android'));
+ const manifestPath = await AndroidConfig.Paths.getAndroidManifestAsync(
+ reactNativeProjectDirectory
+ );
+ const manifestDirectory = path.dirname(manifestPath);
+
+ fs.ensureDirSync(manifestDirectory);
+ fs.writeFileSync(manifestPath, noMetadataAndroidManifest);
+
+ const androidManifest = await AndroidConfig.Manifest.readAndroidManifestAsync(manifestPath);
+ expect(
+ AndroidConfig.Manifest.getMainApplicationMetaDataValue(
+ androidManifest,
+ AndroidMetadataName.UPDATES_CONFIGURATION_REQUEST_HEADERS_KEY
+ )
+ ).toBe(null);
+
+ await androidSetChannelNativelyAsync(ctx as any);
+
+ const newAndroidManifest = await AndroidConfig.Manifest.readAndroidManifestAsync(manifestPath);
+ const newValue = AndroidConfig.Manifest.getMainApplicationMetaDataValue(
+ newAndroidManifest,
+ AndroidMetadataName.UPDATES_CONFIGURATION_REQUEST_HEADERS_KEY
+ );
+ expect(newValue).toBeDefined();
+ expect(JSON.parse(newValue!)).toEqual({ 'expo-channel-name': channel });
+ });
+});
+describe(androidGetNativelyDefinedClassicReleaseChannelAsync, () => {
+ it('gets the natively defined release channel', async () => {
+ const reactNativeProjectDirectory = fs.mkdtempSync('/expo-project-');
+ fs.ensureDirSync(reactNativeProjectDirectory);
+ const releaseChannel = 'default';
+ const ctx = {
+ reactNativeProjectDirectory,
+ logger: { info: () => {} },
+ };
+
+ const releaseChannelInAndroidManifest = `
+
+
+
+
+
+
+
+
+
+
+
+
+ `;
+ fs.ensureDirSync(path.join(reactNativeProjectDirectory, 'android'));
+ const manifestPath = await AndroidConfig.Paths.getAndroidManifestAsync(
+ reactNativeProjectDirectory
+ );
+ const manifestDirectory = path.dirname(manifestPath);
+
+ fs.ensureDirSync(manifestDirectory);
+ fs.writeFileSync(manifestPath, releaseChannelInAndroidManifest);
+
+ const nativelyDefinedReleaseChannel = await androidGetNativelyDefinedClassicReleaseChannelAsync(
+ ctx as any
+ );
+ expect(nativelyDefinedReleaseChannel).toBe(releaseChannel);
+ });
+});
diff --git a/packages/build-tools/src/android/expoUpdates.ts b/packages/build-tools/src/android/expoUpdates.ts
new file mode 100644
index 00000000..f28f8b98
--- /dev/null
+++ b/packages/build-tools/src/android/expoUpdates.ts
@@ -0,0 +1,81 @@
+import assert from 'assert';
+
+import fs from 'fs-extra';
+import { AndroidConfig } from '@expo/config-plugins';
+import { Job } from '@expo/eas-build-job';
+
+import { BuildContext } from '../context';
+
+export enum AndroidMetadataName {
+ UPDATES_CONFIGURATION_REQUEST_HEADERS_KEY = 'expo.modules.updates.UPDATES_CONFIGURATION_REQUEST_HEADERS_KEY',
+ RELEASE_CHANNEL = 'expo.modules.updates.EXPO_RELEASE_CHANNEL',
+}
+
+export async function androidSetChannelNativelyAsync(ctx: BuildContext): Promise {
+ assert(ctx.job.updates?.channel, 'updates.channel must be defined');
+
+ const manifestPath = await AndroidConfig.Paths.getAndroidManifestAsync(
+ ctx.reactNativeProjectDirectory
+ );
+
+ if (!(await fs.pathExists(manifestPath))) {
+ throw new Error(`Couldn't find Android manifest at ${manifestPath}`);
+ }
+
+ const androidManifest = await AndroidConfig.Manifest.readAndroidManifestAsync(manifestPath);
+ const mainApp = AndroidConfig.Manifest.getMainApplicationOrThrow(androidManifest);
+ const stringifiedUpdatesRequestHeaders = AndroidConfig.Manifest.getMainApplicationMetaDataValue(
+ androidManifest,
+ AndroidMetadataName.UPDATES_CONFIGURATION_REQUEST_HEADERS_KEY
+ );
+ AndroidConfig.Manifest.addMetaDataItemToMainApplication(
+ mainApp,
+ AndroidMetadataName.UPDATES_CONFIGURATION_REQUEST_HEADERS_KEY,
+ JSON.stringify({
+ ...JSON.parse(stringifiedUpdatesRequestHeaders ?? '{}'),
+ 'expo-channel-name': ctx.job.updates.channel,
+ }),
+ 'value'
+ );
+ await AndroidConfig.Manifest.writeAndroidManifestAsync(manifestPath, androidManifest);
+}
+
+export async function androidSetClassicReleaseChannelNativelyAsync(
+ ctx: BuildContext
+): Promise {
+ assert(ctx.job.releaseChannel, 'releaseChannel must be defined');
+
+ const manifestPath = await AndroidConfig.Paths.getAndroidManifestAsync(
+ ctx.reactNativeProjectDirectory
+ );
+ if (!(await fs.pathExists(manifestPath))) {
+ throw new Error(`Couldn't find Android manifest at ${manifestPath}`);
+ }
+
+ const androidManifest = await AndroidConfig.Manifest.readAndroidManifestAsync(manifestPath);
+ const mainApp = AndroidConfig.Manifest.getMainApplicationOrThrow(androidManifest);
+ AndroidConfig.Manifest.addMetaDataItemToMainApplication(
+ mainApp,
+ AndroidMetadataName.RELEASE_CHANNEL,
+ ctx.job.releaseChannel,
+ 'value'
+ );
+ await AndroidConfig.Manifest.writeAndroidManifestAsync(manifestPath, androidManifest);
+}
+
+export async function androidGetNativelyDefinedClassicReleaseChannelAsync(
+ ctx: BuildContext
+): Promise {
+ const manifestPath = await AndroidConfig.Paths.getAndroidManifestAsync(
+ ctx.reactNativeProjectDirectory
+ );
+ if (!(await fs.pathExists(manifestPath))) {
+ return null;
+ }
+
+ const androidManifest = await AndroidConfig.Manifest.readAndroidManifestAsync(manifestPath);
+ return AndroidConfig.Manifest.getMainApplicationMetaDataValue(
+ androidManifest,
+ AndroidMetadataName.RELEASE_CHANNEL
+ );
+}
diff --git a/packages/build-tools/src/android/releaseChannel.ts b/packages/build-tools/src/android/releaseChannel.ts
deleted file mode 100644
index d171b4c9..00000000
--- a/packages/build-tools/src/android/releaseChannel.ts
+++ /dev/null
@@ -1,91 +0,0 @@
-import path from 'path';
-
-import * as xml from 'xml2js';
-import fs from 'fs-extra';
-
-const RELEASE_CHANNEL = 'expo.modules.updates.EXPO_RELEASE_CHANNEL';
-
-async function updateReleaseChannel(
- reactNativeProjectDirectory: string,
- releaseChannel: string
-): Promise {
- const manifestPath = path.join(
- reactNativeProjectDirectory,
- 'android',
- 'app',
- 'src',
- 'main',
- 'AndroidManifest.xml'
- );
-
- if (!(await fs.pathExists(manifestPath))) {
- throw new Error(`Couldn't find Android manifest at ${manifestPath}`);
- }
-
- const manifestContent = await fs.readFile(manifestPath, 'utf8');
- const manifest = await xml.parseStringPromise(manifestContent);
-
- const mainApplication = manifest?.manifest?.application?.find(
- (e: any) => e?.['$']?.['android:name'] === '.MainApplication'
- );
-
- if (!mainApplication) {
- throw new Error(`Couldn't find '.MainApplication' in the manifest at ${manifestPath}`);
- }
-
- const newItem = {
- $: {
- 'android:name': RELEASE_CHANNEL,
- 'android:value': releaseChannel,
- },
- };
-
- if (mainApplication['meta-data']) {
- const existingMetaDataItem = mainApplication['meta-data'].find(
- (e: any) => e.$['android:name'] === RELEASE_CHANNEL
- );
-
- if (existingMetaDataItem) {
- existingMetaDataItem.$['android:value'] = releaseChannel;
- } else {
- mainApplication['meta-data'].push(newItem);
- }
- } else {
- mainApplication['meta-data'] = [newItem];
- }
-
- const manifestXml = new xml.Builder().buildObject(manifest);
- await fs.writeFile(manifestPath, manifestXml);
-}
-
-async function getReleaseChannel(reactNativeProjectDirectory: string): Promise {
- const manifestPath = path.join(
- reactNativeProjectDirectory,
- 'android',
- 'app',
- 'src',
- 'main',
- 'AndroidManifest.xml'
- );
-
- if (!(await fs.pathExists(manifestPath))) {
- throw new Error(`Couldn't find Android manifest at ${manifestPath}`);
- }
-
- const manifestContent = await fs.readFile(manifestPath, 'utf8');
- const manifest = await xml.parseStringPromise(manifestContent);
-
- const mainApplication = manifest?.manifest?.application?.find(
- (e: any) => e?.['$']?.['android:name'] === '.MainApplication'
- );
-
- if (!mainApplication?.['meta-data']) {
- return;
- }
- const existingMetaDataItem = mainApplication['meta-data'].find(
- (e: any) => e.$['android:name'] === RELEASE_CHANNEL
- );
- return existingMetaDataItem?.$?.['android:value'];
-}
-
-export { getReleaseChannel, updateReleaseChannel };
diff --git a/packages/build-tools/src/builders/androidGeneric.ts b/packages/build-tools/src/builders/androidGeneric.ts
index f196b501..7b549b09 100644
--- a/packages/build-tools/src/builders/androidGeneric.ts
+++ b/packages/build-tools/src/builders/androidGeneric.ts
@@ -4,10 +4,9 @@ import { BuildContext } from '../context';
import { setup } from '../utils/project';
import { findBuildArtifacts } from '../utils/buildArtifacts';
import { Hook, runHookIfPresent } from '../utils/hooks';
-import { getReleaseChannel, updateReleaseChannel } from '../android/releaseChannel';
import { restoreCredentials } from '../android/credentials';
import { runGradleCommand, ensureLFLineEndingsInGradlewScript } from '../android/gradle';
-import { configureExpoUpdatesIfInstalled } from '../generic/expoUpdates';
+import { configureExpoUpdatesIfInstalledAsync } from '../utils/expoUpdates';
export default async function androidGenericBuilder(
ctx: BuildContext
@@ -33,7 +32,7 @@ export default async function androidGenericBuilder(
}
await ctx.runBuildPhase(BuildPhase.CONFIGURE_EXPO_UPDATES, async () => {
- await configureExpoUpdatesIfInstalled(ctx, { getReleaseChannel, updateReleaseChannel });
+ await configureExpoUpdatesIfInstalledAsync(ctx);
});
await ctx.runBuildPhase(BuildPhase.RUN_GRADLEW, async () => {
diff --git a/packages/build-tools/src/builders/androidManaged.ts b/packages/build-tools/src/builders/androidManaged.ts
index 5cd146fd..9620c62e 100644
--- a/packages/build-tools/src/builders/androidManaged.ts
+++ b/packages/build-tools/src/builders/androidManaged.ts
@@ -2,11 +2,10 @@ import { AndroidConfig } from '@expo/config-plugins';
import { Android, BuildPhase } from '@expo/eas-build-job';
import { ManagedBuildContext } from '../managed/context';
-import { configureExpoUpdatesIfInstalled } from '../managed/expoUpdates';
+import { configureExpoUpdatesIfInstalledAsync } from '../utils/expoUpdates';
import { setup } from '../utils/project';
import { findSingleBuildArtifact } from '../utils/buildArtifacts';
import { Hook, runHookIfPresent } from '../utils/hooks';
-import { updateReleaseChannel } from '../android/releaseChannel';
import { restoreCredentials } from '../android/credentials';
import { runGradleCommand } from '../android/gradle';
@@ -33,7 +32,7 @@ export default async function androidManagedBuilder(
});
}
await ctx.runBuildPhase(BuildPhase.CONFIGURE_EXPO_UPDATES, async () => {
- await configureExpoUpdatesIfInstalled(ctx, updateReleaseChannel);
+ await configureExpoUpdatesIfInstalledAsync(ctx);
});
await ctx.runBuildPhase(BuildPhase.RUN_GRADLEW, async () => {
diff --git a/packages/build-tools/src/builders/iosGeneric.ts b/packages/build-tools/src/builders/iosGeneric.ts
index 6097fbca..f818f2be 100644
--- a/packages/build-tools/src/builders/iosGeneric.ts
+++ b/packages/build-tools/src/builders/iosGeneric.ts
@@ -4,12 +4,11 @@ import { BuildContext } from '../context';
import { setup } from '../utils/project';
import { findBuildArtifacts } from '../utils/buildArtifacts';
import { Hook, runHookIfPresent } from '../utils/hooks';
-import { updateReleaseChannel, getReleaseChannel } from '../ios/releaseChannel';
import CredentialsManager from '../ios/credentials/manager';
import { configureXcodeProject } from '../ios/configure';
import { runFastlaneGym } from '../ios/fastlane';
import { installPods } from '../ios/pod';
-import { configureExpoUpdatesIfInstalled } from '../generic/expoUpdates';
+import { configureExpoUpdatesIfInstalledAsync } from '../utils/expoUpdates';
export default async function iosGenericBuilder(
ctx: BuildContext
@@ -40,7 +39,7 @@ export default async function iosGenericBuilder(
}
await ctx.runBuildPhase(BuildPhase.CONFIGURE_EXPO_UPDATES, async () => {
- await configureExpoUpdatesIfInstalled(ctx, { getReleaseChannel, updateReleaseChannel });
+ await configureExpoUpdatesIfInstalledAsync(ctx);
});
await ctx.runBuildPhase(BuildPhase.RUN_FASTLANE, async () => {
diff --git a/packages/build-tools/src/builders/iosManaged.ts b/packages/build-tools/src/builders/iosManaged.ts
index 470cf2a9..ac8c55e1 100644
--- a/packages/build-tools/src/builders/iosManaged.ts
+++ b/packages/build-tools/src/builders/iosManaged.ts
@@ -4,11 +4,10 @@ import { IOSConfig } from '@expo/config-plugins';
import { BuildPhase, Ios } from '@expo/eas-build-job';
import { ManagedBuildContext } from '../managed/context';
-import { configureExpoUpdatesIfInstalled } from '../managed/expoUpdates';
+import { configureExpoUpdatesIfInstalledAsync } from '../utils/expoUpdates';
import { setup } from '../utils/project';
import { findSingleBuildArtifact } from '../utils/buildArtifacts';
import { Hook, runHookIfPresent } from '../utils/hooks';
-import { updateReleaseChannel } from '../ios/releaseChannel';
import { configureXcodeProject } from '../ios/configure';
import CredentialsManager from '../ios/credentials/manager';
import { runFastlaneGym } from '../ios/fastlane';
@@ -47,7 +46,7 @@ export default async function iosManagedBuilder(
}
await ctx.runBuildPhase(BuildPhase.CONFIGURE_EXPO_UPDATES, async () => {
- await configureExpoUpdatesIfInstalled(ctx, updateReleaseChannel);
+ await configureExpoUpdatesIfInstalledAsync(ctx);
});
await ctx.runBuildPhase(BuildPhase.RUN_FASTLANE, async () => {
diff --git a/packages/build-tools/src/generic/expoUpdates.ts b/packages/build-tools/src/generic/expoUpdates.ts
deleted file mode 100644
index 419e6d39..00000000
--- a/packages/build-tools/src/generic/expoUpdates.ts
+++ /dev/null
@@ -1,36 +0,0 @@
-import { Ios, Android, Platform } from '@expo/eas-build-job';
-
-import { BuildContext } from '../context';
-import isExpoUpdatesInstalledAsync from '../utils/isExpoUpdatesInstalled';
-
-type GenericJob = Ios.GenericJob | Android.GenericJob;
-
-export async function configureExpoUpdatesIfInstalled(
- ctx: BuildContext,
- {
- getReleaseChannel,
- updateReleaseChannel,
- }: {
- getReleaseChannel: (dir: string) => Promise;
- updateReleaseChannel: (dir: string, releaseChannel: string) => Promise;
- }
-): Promise {
- if (await isExpoUpdatesInstalledAsync(ctx.reactNativeProjectDirectory)) {
- if (ctx.job.releaseChannel) {
- const configFile =
- ctx.job.platform === Platform.ANDROID ? 'AndroidManifest.xml' : 'Expo.plist';
- ctx.logger.info(
- `Setting the release channel in '${configFile}' to '${ctx.job.releaseChannel}'`
- );
- await updateReleaseChannel(ctx.reactNativeProjectDirectory, ctx.job.releaseChannel);
- } else {
- const channel = await getReleaseChannel(ctx.reactNativeProjectDirectory);
- if (!channel || channel === 'default') {
- ctx.logger.info(`Using default release channel for 'expo-updates' (default)`);
- } else {
- ctx.logger.info(`Using the release channel pre-configured in native project (${channel})`);
- ctx.logger.warn('Please add the "releaseChannel" field to your build profile (eas.json)');
- }
- }
- }
-}
diff --git a/packages/build-tools/src/ios/__tests__/expoUpdates.test.ts b/packages/build-tools/src/ios/__tests__/expoUpdates.test.ts
new file mode 100644
index 00000000..2c9b99d8
--- /dev/null
+++ b/packages/build-tools/src/ios/__tests__/expoUpdates.test.ts
@@ -0,0 +1,136 @@
+import path from 'path';
+
+import fs from 'fs-extra';
+import plist from '@expo/plist';
+import { IOSConfig } from '@expo/config-plugins';
+
+import {
+ iosGetNativelyDefinedClassicReleaseChannelAsync,
+ IosMetadataName,
+ iosSetChannelNativelyAsync,
+ iosSetClassicReleaseChannelNativelyAsync,
+} from '../../ios/expoUpdates';
+
+jest.mock('fs');
+
+const noItemsExpoPlist = `
+
+
+
+
+
+`;
+const channel = 'main';
+
+describe(iosSetClassicReleaseChannelNativelyAsync, () => {
+ test('sets the release channel', async () => {
+ const reactNativeProjectDirectory = fs.mkdtempSync('/expo-project-');
+ fs.ensureDirSync(reactNativeProjectDirectory);
+ const releaseChannel = 'default';
+ const ctx = {
+ reactNativeProjectDirectory,
+ job: { releaseChannel },
+ logger: { info: () => {} },
+ };
+
+ fs.ensureDirSync(path.join(reactNativeProjectDirectory, '/ios/test/'));
+ fs.writeFileSync(
+ path.join(reactNativeProjectDirectory, '/ios/test/AppDelegate.m'),
+ Buffer.from('placeholder')
+ );
+
+ const expoPlistPath = IOSConfig.Paths.getExpoPlistPath(ctx.reactNativeProjectDirectory);
+ const expoPlistDirectory = path.dirname(expoPlistPath);
+
+ fs.ensureDirSync(expoPlistDirectory);
+ fs.writeFileSync(expoPlistPath, noItemsExpoPlist);
+
+ await iosSetClassicReleaseChannelNativelyAsync(ctx as any);
+
+ const newExpoPlist = await fs.readFile(expoPlistPath, 'utf8');
+ expect(plist.parse(newExpoPlist)[IosMetadataName.RELEASE_CHANNEL]).toEqual(releaseChannel);
+ });
+});
+
+describe(iosSetChannelNativelyAsync, () => {
+ it('sets the channel', async () => {
+ const reactNativeProjectDirectory = fs.mkdtempSync('/expo-project-');
+ fs.ensureDirSync(reactNativeProjectDirectory);
+ const ctx = {
+ reactNativeProjectDirectory,
+ job: { updates: { channel } },
+ logger: { info: () => {} },
+ };
+
+ fs.ensureDirSync(path.join(reactNativeProjectDirectory, '/ios/Pods.xcodeproj/'));
+ fs.writeFileSync(
+ path.join(reactNativeProjectDirectory, '/ios/Pods.xcodeproj/project.pbxproj'),
+ Buffer.from('placeholder')
+ );
+
+ fs.ensureDirSync(path.join(reactNativeProjectDirectory, '/ios/test/'));
+ fs.writeFileSync(
+ path.join(reactNativeProjectDirectory, '/ios/test/AppDelegate.m'),
+ Buffer.from('placeholder')
+ );
+
+ const expoPlistPath = IOSConfig.Paths.getExpoPlistPath(ctx.reactNativeProjectDirectory);
+ const expoPlistDirectory = path.dirname(expoPlistPath);
+
+ fs.ensureDirSync(expoPlistDirectory);
+ fs.writeFileSync(expoPlistPath, noItemsExpoPlist);
+
+ await iosSetChannelNativelyAsync(ctx as any);
+
+ const newExpoPlist = await fs.readFile(expoPlistPath, 'utf8');
+ expect(
+ plist.parse(newExpoPlist)[IosMetadataName.UPDATES_CONFIGURATION_REQUEST_HEADERS_KEY]
+ ).toEqual({ 'expo-channel-name': channel });
+ });
+});
+
+describe(iosGetNativelyDefinedClassicReleaseChannelAsync, () => {
+ it('gets the natively defined release channel', async () => {
+ const reactNativeProjectDirectory = fs.mkdtempSync('/expo-project-');
+ fs.ensureDirSync(reactNativeProjectDirectory);
+ const releaseChannel = 'default';
+ const ctx = {
+ reactNativeProjectDirectory,
+ logger: { info: () => {} },
+ };
+
+ const releaseChannelInPlist = `
+
+
+
+
+ ${IosMetadataName.RELEASE_CHANNEL}
+ ${releaseChannel}
+
+ `;
+
+ fs.ensureDirSync(path.join(reactNativeProjectDirectory, '/ios/Pods.xcodeproj/'));
+ fs.writeFileSync(
+ path.join(reactNativeProjectDirectory, '/ios/Pods.xcodeproj/project.pbxproj'),
+ Buffer.from('placeholder')
+ );
+
+ fs.ensureDirSync(path.join(reactNativeProjectDirectory, '/ios/test/'));
+ fs.writeFileSync(
+ path.join(reactNativeProjectDirectory, '/ios/test/AppDelegate.m'),
+ Buffer.from('placeholder')
+ );
+
+ const expoPlistPath = IOSConfig.Paths.getExpoPlistPath(ctx.reactNativeProjectDirectory);
+ const expoPlistDirectory = path.dirname(expoPlistPath);
+
+ fs.ensureDirSync(expoPlistDirectory);
+ fs.writeFileSync(expoPlistPath, releaseChannelInPlist);
+
+ const nativelyDefinedReleaseChannel = await iosGetNativelyDefinedClassicReleaseChannelAsync(
+ ctx as any
+ );
+
+ expect(nativelyDefinedReleaseChannel).toBe(releaseChannel);
+ });
+});
diff --git a/packages/build-tools/src/ios/expoUpdates.ts b/packages/build-tools/src/ios/expoUpdates.ts
new file mode 100644
index 00000000..ace05019
--- /dev/null
+++ b/packages/build-tools/src/ios/expoUpdates.ts
@@ -0,0 +1,70 @@
+import assert from 'assert';
+
+import { IOSConfig } from '@expo/config-plugins';
+import fs from 'fs-extra';
+import plist from '@expo/plist';
+import { Job } from '@expo/eas-build-job';
+
+import { BuildContext } from '../context';
+
+export enum IosMetadataName {
+ UPDATES_CONFIGURATION_REQUEST_HEADERS_KEY = 'EXUpdatesRequestHeaders',
+ RELEASE_CHANNEL = 'EXUpdatesReleaseChannel',
+}
+
+export async function iosSetChannelNativelyAsync(ctx: BuildContext): Promise {
+ assert(ctx.job.updates?.channel, 'updates.channel must be defined');
+
+ const expoPlistPath = IOSConfig.Paths.getExpoPlistPath(ctx.reactNativeProjectDirectory);
+
+ if (!(await fs.pathExists(expoPlistPath))) {
+ throw new Error(`${expoPlistPath} does no exist`);
+ }
+
+ const expoPlistContent = await fs.readFile(expoPlistPath, 'utf8');
+ const items: Record> = plist.parse(expoPlistContent);
+ items[IosMetadataName.UPDATES_CONFIGURATION_REQUEST_HEADERS_KEY] = {
+ ...((items[IosMetadataName.UPDATES_CONFIGURATION_REQUEST_HEADERS_KEY] as Record<
+ string,
+ string
+ >) ?? {}),
+ 'expo-channel-name': ctx.job.updates.channel,
+ };
+ const expoPlist = plist.build(items);
+
+ await fs.writeFile(expoPlistPath, expoPlist);
+}
+
+export async function iosSetClassicReleaseChannelNativelyAsync(
+ ctx: BuildContext
+): Promise {
+ assert(ctx.job.releaseChannel, 'releaseChannel must be defined');
+
+ const expoPlistPath = IOSConfig.Paths.getExpoPlistPath(ctx.reactNativeProjectDirectory);
+
+ if (!(await fs.pathExists(expoPlistPath))) {
+ throw new Error(`${expoPlistPath} does not exist`);
+ }
+
+ const expoPlistContent = await fs.readFile(expoPlistPath, 'utf8');
+ const items: Record> = plist.parse(expoPlistContent);
+ items[IosMetadataName.RELEASE_CHANNEL] = ctx.job.releaseChannel;
+ const expoPlist = plist.build(items);
+
+ await fs.writeFile(expoPlistPath, expoPlist);
+}
+
+export async function iosGetNativelyDefinedClassicReleaseChannelAsync(
+ ctx: BuildContext
+): Promise {
+ const expoPlistPath = IOSConfig.Paths.getExpoPlistPath(ctx.reactNativeProjectDirectory);
+ if (!(await fs.pathExists(expoPlistPath))) {
+ return null;
+ }
+ const expoPlistContent = await fs.readFile(expoPlistPath, 'utf8');
+ const parsedPlist = plist.parse(expoPlistContent);
+ if (!parsedPlist) {
+ return null;
+ }
+ return parsedPlist[IosMetadataName.RELEASE_CHANNEL] ?? null;
+}
diff --git a/packages/build-tools/src/ios/releaseChannel.ts b/packages/build-tools/src/ios/releaseChannel.ts
deleted file mode 100644
index 2f5cda2d..00000000
--- a/packages/build-tools/src/ios/releaseChannel.ts
+++ /dev/null
@@ -1,71 +0,0 @@
-import path from 'path';
-
-import plist from '@expo/plist';
-import fg from 'fast-glob';
-import fs from 'fs-extra';
-
-async function updateReleaseChannel(
- reactNativeProjectDirectory: string,
- releaseChannel: string
-): Promise {
- const pbxprojPaths = await fg('ios/*/project.pbxproj', { cwd: reactNativeProjectDirectory });
-
- const pbxprojPath = pbxprojPaths.length > 0 ? pbxprojPaths[0] : undefined;
-
- if (!pbxprojPath) {
- throw new Error(`Couldn't find an iOS project at '${reactNativeProjectDirectory}'`);
- }
-
- const xcodeprojPath = path.resolve(pbxprojPath, '..');
- const expoPlistPath = path.resolve(
- reactNativeProjectDirectory,
- 'ios',
- path.basename(xcodeprojPath).replace(/\.xcodeproj$/, ''),
- 'Supporting',
- 'Expo.plist'
- );
-
- let items: Record = {};
-
- if (await fs.pathExists(expoPlistPath)) {
- const expoPlistContent = await fs.readFile(expoPlistPath, 'utf8');
- items = plist.parse(expoPlistContent);
- }
-
- items.EXUpdatesReleaseChannel = releaseChannel;
-
- const expoPlist = plist.build(items);
-
- if (!(await fs.pathExists(path.dirname(expoPlistPath)))) {
- await fs.mkdirp(path.dirname(expoPlistPath));
- }
-
- await fs.writeFile(expoPlistPath, expoPlist);
-}
-
-async function getReleaseChannel(reactNativeProjectDirectory: string): Promise {
- const pbxprojPaths = await fg('ios/*/project.pbxproj', { cwd: reactNativeProjectDirectory });
-
- const pbxprojPath = pbxprojPaths.length > 0 ? pbxprojPaths[0] : undefined;
-
- if (!pbxprojPath) {
- throw new Error(`Couldn't find an iOS project at '${reactNativeProjectDirectory}'`);
- }
-
- const xcodeprojPath = path.resolve(pbxprojPath, '..');
- const expoPlistPath = path.resolve(
- reactNativeProjectDirectory,
- 'ios',
- path.basename(xcodeprojPath).replace(/\.xcodeproj$/, ''),
- 'Supporting',
- 'Expo.plist'
- );
-
- if (!(await fs.pathExists(expoPlistPath))) {
- return;
- }
- const expoPlistContent = await fs.readFile(expoPlistPath, 'utf8');
- return plist.parse(expoPlistContent)?.EXUpdatesReleaseChannel;
-}
-
-export { updateReleaseChannel, getReleaseChannel };
diff --git a/packages/build-tools/src/managed/expoUpdates.ts b/packages/build-tools/src/managed/expoUpdates.ts
deleted file mode 100644
index c6f36349..00000000
--- a/packages/build-tools/src/managed/expoUpdates.ts
+++ /dev/null
@@ -1,23 +0,0 @@
-import { Platform } from '@expo/eas-build-job';
-
-import isExpoUpdatesInstalledAsync from '../utils/isExpoUpdatesInstalled';
-
-import { ManagedBuildContext, ManagedJob } from './context';
-
-export async function configureExpoUpdatesIfInstalled(
- ctx: ManagedBuildContext,
- updateReleaseChannel: (dir: string, releaseChannel: string) => Promise
-): Promise {
- if (await isExpoUpdatesInstalledAsync(ctx.reactNativeProjectDirectory)) {
- if (ctx.job.releaseChannel) {
- const configFile =
- ctx.job.platform === Platform.ANDROID ? 'AndroidManifest.xml' : 'Expo.plist';
- ctx.logger.info(
- `Setting the release channel in '${configFile}' to '${ctx.job.releaseChannel}'`
- );
- await updateReleaseChannel(ctx.reactNativeProjectDirectory, ctx.job.releaseChannel);
- } else {
- ctx.logger.info(`Using default release channel for 'expo-updates' (default)`);
- }
- }
-}
diff --git a/packages/build-tools/src/utils/__tests__/expoUpdates.test.ts b/packages/build-tools/src/utils/__tests__/expoUpdates.test.ts
new file mode 100644
index 00000000..c8f6cb50
--- /dev/null
+++ b/packages/build-tools/src/utils/__tests__/expoUpdates.test.ts
@@ -0,0 +1,104 @@
+import { Platform } from '@expo/eas-build-job';
+
+import { ManagedBuildContext, ManagedJob } from '../../managed/context';
+import * as expoUpdates from '../expoUpdates';
+import isExpoUpdatesInstalledAsync from '../isExpoUpdatesInstalled';
+
+jest.mock('../isExpoUpdatesInstalled', () => jest.fn());
+jest.mock('fs');
+
+describe(expoUpdates.configureExpoUpdatesIfInstalledAsync, () => {
+ beforeAll(() => {
+ jest.restoreAllMocks();
+ });
+ it('aborts if expo-updates is not installed', async () => {
+ (isExpoUpdatesInstalledAsync as jest.Mock).mockReturnValue(false);
+ jest.spyOn(expoUpdates, 'configureEASExpoUpdatesAsync');
+ jest.spyOn(expoUpdates, 'configureClassicExpoUpdatesAsync');
+
+ await expoUpdates.configureExpoUpdatesIfInstalledAsync({
+ job: { Platform: Platform.IOS },
+ } as any);
+
+ expect(expoUpdates.configureEASExpoUpdatesAsync).not.toBeCalled();
+ expect(expoUpdates.configureClassicExpoUpdatesAsync).not.toBeCalled();
+ expect(isExpoUpdatesInstalledAsync).toHaveBeenCalledTimes(1);
+ });
+
+ it('configures for EAS if the updates.channel field is set', async () => {
+ (isExpoUpdatesInstalledAsync as jest.Mock).mockReturnValue(true);
+ jest.spyOn(expoUpdates, 'configureEASExpoUpdatesAsync').mockImplementation();
+ jest.spyOn(expoUpdates, 'configureClassicExpoUpdatesAsync');
+
+ const managedCtx: ManagedBuildContext = {
+ job: { updates: { channel: 'main' }, Platform: Platform.IOS },
+ } as any;
+ await expoUpdates.configureExpoUpdatesIfInstalledAsync(managedCtx);
+
+ expect(expoUpdates.configureEASExpoUpdatesAsync).toBeCalledTimes(1);
+ expect(expoUpdates.configureClassicExpoUpdatesAsync).not.toBeCalled();
+ expect(isExpoUpdatesInstalledAsync).toHaveBeenCalledTimes(1);
+ });
+
+ it('configures for classic updates if the updates.channel field is not set', async () => {
+ (isExpoUpdatesInstalledAsync as jest.Mock).mockReturnValue(true);
+ jest.spyOn(expoUpdates, 'configureEASExpoUpdatesAsync');
+ jest.spyOn(expoUpdates, 'configureClassicExpoUpdatesAsync').mockImplementation();
+
+ const managedCtx: ManagedBuildContext = {
+ job: { platform: Platform.IOS },
+ logger: { info: () => {} },
+ } as any;
+ await expoUpdates.configureExpoUpdatesIfInstalledAsync(managedCtx);
+
+ expect(expoUpdates.configureEASExpoUpdatesAsync).not.toBeCalled();
+ expect(expoUpdates.configureClassicExpoUpdatesAsync).toBeCalledTimes(1);
+ expect(isExpoUpdatesInstalledAsync).toHaveBeenCalledTimes(1);
+ });
+});
+
+describe(expoUpdates.configureClassicExpoUpdatesAsync, () => {
+ beforeAll(() => {
+ jest.restoreAllMocks();
+ });
+ it('sets the release channel if it is supplied in ctx.job.releaseChannel', async () => {
+ (isExpoUpdatesInstalledAsync as jest.Mock).mockReturnValue(true);
+ jest.spyOn(expoUpdates, 'setClassicReleaseChannelNativelyAsync').mockImplementation();
+
+ const managedCtx: ManagedBuildContext = {
+ job: { releaseChannel: 'default', platform: Platform.IOS },
+ } as any;
+ await expoUpdates.configureExpoUpdatesIfInstalledAsync(managedCtx);
+
+ expect(expoUpdates.setClassicReleaseChannelNativelyAsync).toBeCalledTimes(1);
+ });
+ it('searches for the natively defined releaseChannel if it is not supplied by ctx.job.releaseChannel', async () => {
+ (isExpoUpdatesInstalledAsync as jest.Mock).mockReturnValue(true);
+ jest.spyOn(expoUpdates, 'getNativelyDefinedClassicReleaseChannelAsync').mockImplementation();
+
+ const managedCtx: ManagedBuildContext = {
+ job: { platform: Platform.IOS },
+ logger: { info: () => {}, warn: () => {} },
+ } as any;
+ await expoUpdates.configureExpoUpdatesIfInstalledAsync(managedCtx);
+
+ expect(expoUpdates.getNativelyDefinedClassicReleaseChannelAsync).toBeCalledTimes(1);
+ });
+ it('uses the default release channel if the releaseChannel is not defined in ctx.job.releaseChannel nor natively.', async () => {
+ (isExpoUpdatesInstalledAsync as jest.Mock).mockReturnValue(true);
+ jest
+ .spyOn(expoUpdates, 'getNativelyDefinedClassicReleaseChannelAsync')
+ .mockImplementation(async () => {
+ return null;
+ });
+
+ const infoLogger = jest.fn();
+ const managedCtx: ManagedBuildContext = {
+ job: { platform: Platform.IOS },
+ logger: { info: infoLogger },
+ } as any;
+ await expoUpdates.configureExpoUpdatesIfInstalledAsync(managedCtx);
+
+ expect(infoLogger).toBeCalledWith(`Using default release channel for 'expo-updates' (default)`);
+ });
+});
diff --git a/packages/build-tools/src/utils/expoUpdates.ts b/packages/build-tools/src/utils/expoUpdates.ts
new file mode 100644
index 00000000..60a50441
--- /dev/null
+++ b/packages/build-tools/src/utils/expoUpdates.ts
@@ -0,0 +1,136 @@
+import assert from 'assert';
+
+import { Ios, Android, Platform, Job } from '@expo/eas-build-job';
+
+import {
+ androidSetChannelNativelyAsync,
+ androidSetClassicReleaseChannelNativelyAsync,
+ androidGetNativelyDefinedClassicReleaseChannelAsync,
+} from '../android/expoUpdates';
+import {
+ iosSetChannelNativelyAsync,
+ iosSetClassicReleaseChannelNativelyAsync,
+ iosGetNativelyDefinedClassicReleaseChannelAsync,
+} from '../ios/expoUpdates';
+import { BuildContext } from '../context';
+
+import isExpoUpdatesInstalledAsync from './isExpoUpdatesInstalled';
+export type GenericJob = Ios.GenericJob | Android.GenericJob;
+
+/**
+ * Used for when Expo Updates is pointed at an EAS server.
+ * @param ctx
+ * @param platform
+ */
+export const setChannelNativelyAsync = async (ctx: BuildContext): Promise => {
+ assert(ctx.job.updates?.channel, 'updates.channel must be defined');
+ const newUpdateRequestHeaders: Record = {
+ 'expo-channel-name': ctx.job.updates.channel,
+ };
+
+ const configFile = ctx.job.platform === Platform.ANDROID ? 'AndroidManifest.xml' : 'Expo.plist';
+ ctx.logger.info(
+ `Setting the update response headers in '${configFile}' to '${JSON.stringify(
+ newUpdateRequestHeaders
+ )}'`
+ );
+
+ switch (ctx.job.platform) {
+ case Platform.ANDROID: {
+ await androidSetChannelNativelyAsync(ctx);
+ return;
+ }
+ case Platform.IOS: {
+ await iosSetChannelNativelyAsync(ctx);
+ return;
+ }
+ default:
+ throw new Error(`Platform is not supported.`);
+ }
+};
+
+/**
+ * Used for classic Expo Updates
+ * @param ctx
+ * @param platform
+ */
+export const setClassicReleaseChannelNativelyAsync = async (
+ ctx: BuildContext
+): Promise => {
+ assert(ctx.job.releaseChannel, 'releaseChannel must be defined');
+
+ const configFile = ctx.job.platform === Platform.ANDROID ? 'AndroidManifest.xml' : 'Expo.plist';
+ ctx.logger.info(`Setting the release channel in '${configFile}' to '${ctx.job.releaseChannel}'`);
+
+ switch (ctx.job.platform) {
+ case Platform.ANDROID: {
+ await androidSetClassicReleaseChannelNativelyAsync(ctx);
+ return;
+ }
+ case Platform.IOS: {
+ await iosSetClassicReleaseChannelNativelyAsync(ctx);
+ return;
+ }
+ default:
+ throw new Error(`Platform is not supported.`);
+ }
+};
+
+/**
+ * Used for classic Expo Updates
+ * @param ctx
+ * @param platform
+ */
+export const getNativelyDefinedClassicReleaseChannelAsync = async (
+ ctx: BuildContext
+): Promise => {
+ switch (ctx.job.platform) {
+ case Platform.ANDROID: {
+ return androidGetNativelyDefinedClassicReleaseChannelAsync(ctx);
+ }
+ case Platform.IOS: {
+ return iosGetNativelyDefinedClassicReleaseChannelAsync(ctx);
+ }
+ default:
+ throw new Error(`Platform is not supported.`);
+ }
+};
+
+export const configureClassicExpoUpdatesAsync = async (ctx: BuildContext): Promise => {
+ if (ctx.job.releaseChannel) {
+ await setClassicReleaseChannelNativelyAsync(ctx);
+ } else {
+ /**
+ * If releaseChannel is not defined:
+ * 1. Try to infer it from the native value.
+ * 2. If it is not set, fallback to 'default'.
+ */
+ const releaseChannel = await getNativelyDefinedClassicReleaseChannelAsync(ctx);
+ if (releaseChannel) {
+ ctx.logger.info(
+ `Using the release channel pre-configured in native project (${releaseChannel})`
+ );
+ ctx.logger.warn('Please add the "releaseChannel" field to your build profile (eas.json)');
+ } else {
+ ctx.logger.info(`Using default release channel for 'expo-updates' (default)`);
+ }
+ }
+};
+
+export const configureEASExpoUpdatesAsync = async (ctx: BuildContext): Promise => {
+ await setChannelNativelyAsync(ctx);
+};
+
+export const configureExpoUpdatesIfInstalledAsync = async (
+ ctx: BuildContext
+): Promise => {
+ if (!(await isExpoUpdatesInstalledAsync(ctx.reactNativeProjectDirectory))) {
+ return;
+ }
+
+ if (ctx.job.updates?.channel) {
+ await configureEASExpoUpdatesAsync(ctx);
+ } else {
+ await configureClassicExpoUpdatesAsync(ctx);
+ }
+};
diff --git a/packages/eas-build-job/src/__tests__/android.test.ts b/packages/eas-build-job/src/__tests__/android.test.ts
index a3c2af71..b54ffc0e 100644
--- a/packages/eas-build-job/src/__tests__/android.test.ts
+++ b/packages/eas-build-job/src/__tests__/android.test.ts
@@ -123,4 +123,44 @@ describe('Android.ManagedJobSchema', () => {
);
expect(value).not.toMatchObject(managedJob);
});
+ test('validates channel', () => {
+ const managedJob = {
+ secrets,
+ type: Workflow.MANAGED,
+ platform: Platform.ANDROID,
+ updates: {
+ channel: 'main',
+ },
+ projectArchive: {
+ type: ArchiveSourceType.URL,
+ url: 'http://localhost:3000',
+ },
+ projectRootDirectory: '.',
+ };
+
+ const { value, error } = Android.ManagedJobSchema.validate(managedJob, joiOptions);
+ expect(value).toMatchObject(managedJob);
+ expect(error).toBeFalsy();
+ });
+ test('fails when both releaseChannel and updates.channel are defined', () => {
+ const managedJob = {
+ secrets,
+ type: Workflow.MANAGED,
+ platform: Platform.ANDROID,
+ releaseChannel: 'default',
+ updates: {
+ channel: 'main',
+ },
+ projectArchive: {
+ type: ArchiveSourceType.URL,
+ url: 'http://localhost:3000',
+ },
+ projectRootDirectory: '.',
+ };
+
+ const { error } = Android.ManagedJobSchema.validate(managedJob, joiOptions);
+ expect(error?.message).toBe(
+ '"value" contains a conflict between optional exclusive peers [releaseChannel, updates.channel]'
+ );
+ });
});
diff --git a/packages/eas-build-job/src/__tests__/ios.test.ts b/packages/eas-build-job/src/__tests__/ios.test.ts
index 6e9262c4..1cf14cd1 100644
--- a/packages/eas-build-job/src/__tests__/ios.test.ts
+++ b/packages/eas-build-job/src/__tests__/ios.test.ts
@@ -132,4 +132,50 @@ describe('Ios.ManagedJobSchema', () => {
);
expect(value).not.toMatchObject(managedJob);
});
+ test('validates channel', () => {
+ const managedJob = {
+ secrets: {
+ buildCredentials,
+ },
+ type: Workflow.MANAGED,
+ platform: Platform.IOS,
+ updates: {
+ channel: 'main',
+ },
+ projectArchive: {
+ type: ArchiveSourceType.URL,
+ url: 'http://localhost:3000',
+ },
+ projectRootDirectory: '.',
+ distribution: 'store',
+ };
+
+ const { value, error } = Ios.ManagedJobSchema.validate(managedJob, joiOptions);
+ expect(value).toMatchObject(managedJob);
+ expect(error).toBeFalsy();
+ });
+ test('fails when both releaseChannel and updates.channel are defined', () => {
+ const managedJob = {
+ secrets: {
+ buildCredentials,
+ },
+ type: Workflow.MANAGED,
+ platform: Platform.IOS,
+ releaseChannel: 'default',
+ updates: {
+ channel: 'main',
+ },
+ projectArchive: {
+ type: ArchiveSourceType.URL,
+ url: 'http://localhost:3000',
+ },
+ projectRootDirectory: '.',
+ distribution: 'store',
+ };
+
+ const { error } = Ios.ManagedJobSchema.validate(managedJob, joiOptions);
+ expect(error?.message).toBe(
+ '"value" contains a conflict between optional exclusive peers [releaseChannel, updates.channel]'
+ );
+ });
});
diff --git a/packages/eas-build-job/src/android.ts b/packages/eas-build-job/src/android.ts
index b6bf7e6f..fe4eec40 100644
--- a/packages/eas-build-job/src/android.ts
+++ b/packages/eas-build-job/src/android.ts
@@ -57,6 +57,9 @@ interface BaseJob {
platform: Platform.ANDROID;
projectRootDirectory: string;
releaseChannel?: string;
+ updates?: {
+ channel?: string;
+ };
secrets: {
buildCredentials?: {
keystore: Keystore;
@@ -72,13 +75,16 @@ const BaseJobSchema = Joi.object({
platform: Joi.string().valid(Platform.ANDROID).required(),
projectRootDirectory: Joi.string().required(),
releaseChannel: Joi.string(),
+ updates: Joi.object({
+ channel: Joi.string(),
+ }),
secrets: Joi.object({
buildCredentials: Joi.object({ keystore: KeystoreSchema.required() }),
environmentSecrets: EnvSchema,
}).required(),
builderEnvironment: BuilderEnvironmentSchema,
cache: CacheSchema.default(),
-});
+}).oxor('releaseChannel', 'updates.channel');
export interface GenericJob extends BaseJob {
type: Workflow.GENERIC;
diff --git a/packages/eas-build-job/src/ios.ts b/packages/eas-build-job/src/ios.ts
index 516414fd..0031bb90 100644
--- a/packages/eas-build-job/src/ios.ts
+++ b/packages/eas-build-job/src/ios.ts
@@ -76,6 +76,9 @@ interface BaseJob {
platform: Platform.IOS;
projectRootDirectory: string;
releaseChannel?: string;
+ updates?: {
+ channel?: string;
+ };
distribution?: DistributionType;
secrets: {
buildCredentials?: BuildCredentials;
@@ -85,19 +88,24 @@ interface BaseJob {
cache: Cache;
}
-const BaseJobSchema = Joi.object().keys({
- projectArchive: ArchiveSourceSchema.required(),
- platform: Joi.string().valid(Platform.IOS).required(),
- projectRootDirectory: Joi.string().required(),
- releaseChannel: Joi.string(),
- distribution: Joi.string().valid('store', 'internal', 'simulator'),
- secrets: Joi.object({
- buildCredentials: BuildCredentialsSchema,
- environmentSecrets: EnvSchema,
- }).required(),
- builderEnvironment: BuilderEnvironmentSchema,
- cache: CacheSchema.default(),
-});
+const BaseJobSchema = Joi.object()
+ .keys({
+ projectArchive: ArchiveSourceSchema.required(),
+ platform: Joi.string().valid(Platform.IOS).required(),
+ projectRootDirectory: Joi.string().required(),
+ releaseChannel: Joi.string(),
+ updates: Joi.object({
+ channel: Joi.string(),
+ }),
+ distribution: Joi.string().valid('store', 'internal', 'simulator'),
+ secrets: Joi.object({
+ buildCredentials: BuildCredentialsSchema,
+ environmentSecrets: EnvSchema,
+ }).required(),
+ builderEnvironment: BuilderEnvironmentSchema,
+ cache: CacheSchema.default(),
+ })
+ .oxor('releaseChannel', 'updates.channel');
export interface GenericJob extends BaseJob {
type: Workflow.GENERIC;
diff --git a/packages/eas-build-job/src/metadata.ts b/packages/eas-build-job/src/metadata.ts
index 27e5664f..0f56b0b4 100644
--- a/packages/eas-build-job/src/metadata.ts
+++ b/packages/eas-build-job/src/metadata.ts
@@ -50,11 +50,17 @@ export type Metadata = {
sdkVersion?: string;
/**
- * Release channel (for expo-updates)
- * It's undefined if the expo-updates package is not installed for the project.
+ * Release channel (for classic expo-updates)
+ * It's undefined if the classic expo-updates package is not installed for the project.
*/
releaseChannel?: string;
+ /**
+ * Channel (for Expo Updates when it is configured for for use with EAS)
+ * It's undefined if the expo-updates package is not configured for use with EAS.
+ */
+ channel?: string;
+
/**
* Distribution type
* Indicates whether this is a build for store, internal distribution, or simulator (iOS).
@@ -99,6 +105,7 @@ export const MetadataSchema = Joi.object({
credentialsSource: Joi.string().valid('local', 'remote'),
sdkVersion: Joi.string(),
releaseChannel: Joi.string(),
+ channel: Joi.string(),
appName: Joi.string(),
appIdentifier: Joi.string(),
buildProfile: Joi.string(),
diff --git a/yarn.lock b/yarn.lock
index 679caf74..18a16598 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -15225,7 +15225,7 @@ uuid@^2.0.1:
resolved "https://registry.yarnpkg.com/uuid/-/uuid-2.0.3.tgz#67e2e863797215530dff318e5bf9dcebfd47b21a"
integrity sha1-Z+LoY3lyFVMN/zGOW/nc6/1Hsho=
-uuid@^3.0.1, uuid@^3.1.0, uuid@^3.2.1, uuid@^3.3.2, uuid@^3.3.3, uuid@^3.4.0:
+uuid@^3.0.1, uuid@^3.1.0, uuid@^3.2.1, uuid@^3.3.2, uuid@^3.4.0:
version "3.4.0"
resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee"
integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==