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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ This is the log of notable changes to EAS CLI and related packages.

### 🎉 New features

- [eas-cli] Add optional `ios.refreshAdHocProvisioningProfile` eas.json setting to enable ad-hoc profile refresh in CI without passing `--refresh-ad-hoc-provisioning-profile` on every run. ([#3886](https://github.com/expo/eas-cli/pull/3886) by [@sswrk](https://github.com/sswrk))
- [eas-cli] New command `observe:session` for inspecting events by session ID. ([#3868](https://github.com/expo/eas-cli/pull/3868) by [@douglowder](https://github.com/douglowder))

### 🐛 Bug fixes
Expand Down
14 changes: 13 additions & 1 deletion packages/eas-cli/src/build/createContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ import { BuildContext, CommonContext } from './context';
import { createIosContextAsync } from './ios/build';
import { LocalBuildMode, LocalBuildOptions } from './local';
import { resolveBuildResourceClass } from './utils/resourceClass';
import {
assertRefreshAdHocProvisioningProfileCompatibleWithFreezeCredentials,
resolveRefreshAdHocProvisioningProfile,
} from './utils/refreshAdHocProvisioningProfile';
import { Analytics, AnalyticsEventProperties, BuildEvent } from '../analytics/AnalyticsManager';
import { DynamicConfigContextFn } from '../commandUtils/context/DynamicProjectConfigContextField';
import { ExpoGraphqlClient } from '../commandUtils/context/contextUtils/createGraphqlClient';
Expand Down Expand Up @@ -92,7 +96,15 @@ export async function createBuildContextAsync<T extends Platform>({

const requiredPackageManager = resolvePackageManager(projectDir);

const refreshAdHocProvisioningProfile = refreshAdHocProvisioningProfileFlag ?? false;
const refreshAdHocProvisioningProfile = resolveRefreshAdHocProvisioningProfile({
platform,
buildProfile,
refreshAdHocProvisioningProfileFlag,
});
assertRefreshAdHocProvisioningProfileCompatibleWithFreezeCredentials(
refreshAdHocProvisioningProfile,
freezeCredentials
);

const credentialsCtx = new CredentialsContext({
projectInfo: { exp, projectId },
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
import { Platform } from '@expo/eas-build-job';
import { BuildProfile } from '@expo/eas-json';

import {
assertExplicitRefreshAdHocProvisioningProfileFlagValid,
assertRefreshAdHocProvisioningProfileCompatibleWithFreezeCredentials,
resolveRefreshAdHocProvisioningProfile,
} from '../refreshAdHocProvisioningProfile';

const iosProfileWithRefresh = {
distribution: 'internal',
credentialsSource: 'remote',
refreshAdHocProvisioningProfile: true,
} as BuildProfile<Platform.IOS>;

const iosProfileWithoutRefresh = {
distribution: 'internal',
credentialsSource: 'remote',
} as BuildProfile<Platform.IOS>;

const androidProfileWithRefresh = {
distribution: 'internal',
credentialsSource: 'remote',
refreshAdHocProvisioningProfile: true,
} as BuildProfile<Platform.ANDROID>;

describe(resolveRefreshAdHocProvisioningProfile, () => {
it('enables refresh when eas.json sets refreshAdHocProvisioningProfile and no flag is passed', () => {
expect(
resolveRefreshAdHocProvisioningProfile({
platform: Platform.IOS,
buildProfile: iosProfileWithRefresh,
})
).toBe(true);
});

it('disables refresh when eas.json omits refreshAdHocProvisioningProfile and no flag is passed', () => {
expect(
resolveRefreshAdHocProvisioningProfile({
platform: Platform.IOS,
buildProfile: iosProfileWithoutRefresh,
})
).toBe(false);
});

it('disables refresh when eas.json sets true but explicit --no-refresh-ad-hoc-provisioning-profile is passed', () => {
expect(
resolveRefreshAdHocProvisioningProfile({
platform: Platform.IOS,
buildProfile: iosProfileWithRefresh,
refreshAdHocProvisioningProfileFlag: false,
})
).toBe(false);
});

it('enables refresh when explicit --refresh-ad-hoc-provisioning-profile is passed', () => {
expect(
resolveRefreshAdHocProvisioningProfile({
platform: Platform.IOS,
buildProfile: iosProfileWithoutRefresh,
refreshAdHocProvisioningProfileFlag: true,
})
).toBe(true);
});

it('ignores refreshAdHocProvisioningProfile on Android profiles', () => {
expect(
resolveRefreshAdHocProvisioningProfile({
platform: Platform.ANDROID,
buildProfile: androidProfileWithRefresh,
})
).toBe(false);
});
});

describe(assertRefreshAdHocProvisioningProfileCompatibleWithFreezeCredentials, () => {
it('throws when resolved refresh is enabled with freeze-credentials', () => {
expect(() =>
assertRefreshAdHocProvisioningProfileCompatibleWithFreezeCredentials(true, true)
).toThrow(/Cannot refresh ad-hoc provisioning profile when credentials are frozen/);
});

it('does not throw when refresh is disabled with freeze-credentials', () => {
expect(() =>
assertRefreshAdHocProvisioningProfileCompatibleWithFreezeCredentials(false, true)
).not.toThrow();
});
});

describe(assertExplicitRefreshAdHocProvisioningProfileFlagValid, () => {
it('throws when explicit flag is used in interactive mode', () => {
expect(() =>
assertExplicitRefreshAdHocProvisioningProfileFlagValid({
refreshAdHocProvisioningProfileFlag: true,
nonInteractive: false,
freezeCredentials: false,
})
).toThrow('--refresh-ad-hoc-provisioning-profile can only be used in non-interactive mode.');
});

it('throws when explicit flag is used with freeze-credentials', () => {
expect(() =>
assertExplicitRefreshAdHocProvisioningProfileFlagValid({
refreshAdHocProvisioningProfileFlag: true,
nonInteractive: true,
freezeCredentials: true,
})
).toThrow('Cannot use --refresh-ad-hoc-provisioning-profile with --freeze-credentials.');
});

it('does not throw when flag is omitted', () => {
expect(() =>
assertExplicitRefreshAdHocProvisioningProfileFlagValid({
nonInteractive: false,
freezeCredentials: false,
})
).not.toThrow();
});

it('does not throw when explicit --no-refresh-ad-hoc-provisioning-profile is passed', () => {
expect(() =>
assertExplicitRefreshAdHocProvisioningProfileFlagValid({
refreshAdHocProvisioningProfileFlag: false,
nonInteractive: false,
freezeCredentials: true,
})
).not.toThrow();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { Platform } from '@expo/eas-build-job';
import { BuildProfile } from '@expo/eas-json';

export function resolveRefreshAdHocProvisioningProfile<T extends Platform>({
platform,
buildProfile,
refreshAdHocProvisioningProfileFlag,
}: {
platform: T;
buildProfile: BuildProfile<T>;
refreshAdHocProvisioningProfileFlag?: boolean;
}): boolean {
if (refreshAdHocProvisioningProfileFlag !== undefined) {
return refreshAdHocProvisioningProfileFlag;
}
if (platform === Platform.IOS) {
return (
(buildProfile as BuildProfile<Platform.IOS>).refreshAdHocProvisioningProfile ?? false
);
}
return false;
}

export function assertExplicitRefreshAdHocProvisioningProfileFlagValid({
refreshAdHocProvisioningProfileFlag,
nonInteractive,
freezeCredentials,
}: {
refreshAdHocProvisioningProfileFlag?: boolean;
nonInteractive: boolean;
freezeCredentials: boolean;
}): void {
if (refreshAdHocProvisioningProfileFlag !== true) {
return;
}
if (!nonInteractive) {
throw new Error(
'--refresh-ad-hoc-provisioning-profile can only be used in non-interactive mode.'
);
}
if (freezeCredentials) {
throw new Error(
'Cannot use --refresh-ad-hoc-provisioning-profile with --freeze-credentials.'
);
}
}

export function assertRefreshAdHocProvisioningProfileCompatibleWithFreezeCredentials(
refreshAdHocProvisioningProfile: boolean,
freezeCredentials: boolean
): void {
if (refreshAdHocProvisioningProfile && freezeCredentials) {
throw new Error(
'Cannot refresh ad-hoc provisioning profile when credentials are frozen. Remove --freeze-credentials or disable refreshAdHocProvisioningProfile in eas.json.'
);
}
}
26 changes: 11 additions & 15 deletions packages/eas-cli/src/commands/build/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import path from 'path';

import { LocalBuildMode } from '../../build/local';
import { BuildFlags, runBuildAndSubmitAsync } from '../../build/runBuildAndSubmit';
import { assertExplicitRefreshAdHocProvisioningProfileFlagValid } from '../../build/utils/refreshAdHocProvisioningProfile';
import EasCommand from '../../commandUtils/EasCommand';
import {
EasNonInteractiveAndJsonFlags,
Expand Down Expand Up @@ -40,7 +41,7 @@ interface RawBuildFlags {
message?: string;
'build-logger-level'?: LoggerLevel;
'freeze-credentials': boolean;
'refresh-ad-hoc-provisioning-profile': boolean;
'refresh-ad-hoc-provisioning-profile'?: boolean;
'verbose-logs'?: boolean;
'what-to-test'?: string;
}
Expand Down Expand Up @@ -123,7 +124,7 @@ export default class Build extends EasCommand {
description: 'Prevent the build from updating credentials in non-interactive mode',
}),
'refresh-ad-hoc-provisioning-profile': Flags.boolean({
default: false,
allowNo: true,
description:
'Refresh managed ad-hoc provisioning profiles from App Store Connect before gathering build credentials',
}),
Expand Down Expand Up @@ -194,19 +195,14 @@ export default class Build extends EasCommand {
flags: RawBuildFlags
): Omit<BuildFlags, 'requestedPlatform'> & { requestedPlatform?: RequestedPlatform } {
const { json, nonInteractive } = resolveNonInteractiveAndJsonFlags(flags);
if (flags['refresh-ad-hoc-provisioning-profile']) {
if (!nonInteractive) {
Errors.error(
'--refresh-ad-hoc-provisioning-profile can only be used in non-interactive mode.',
{ exit: 1 }
);
}
if (flags['freeze-credentials']) {
Errors.error(
'Cannot use --refresh-ad-hoc-provisioning-profile with --freeze-credentials.',
{ exit: 1 }
);
}
try {
assertExplicitRefreshAdHocProvisioningProfileFlagValid({
refreshAdHocProvisioningProfileFlag: flags['refresh-ad-hoc-provisioning-profile'],
nonInteractive,
freezeCredentials: flags['freeze-credentials'],
});
} catch (error) {
Errors.error(error instanceof Error ? error.message : String(error), { exit: 1 });
}
if (!flags.local && flags.output) {
Errors.error('--output is allowed only for local builds', { exit: 1 });
Expand Down
2 changes: 1 addition & 1 deletion packages/eas-cli/src/commands/build/internal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ export default class BuildInternal extends EasCommand {
exclusive: ['auto-submit'],
}),
'refresh-ad-hoc-provisioning-profile': Flags.boolean({
default: false,
allowNo: true,
description:
'Refresh managed ad-hoc provisioning profiles from App Store Connect before gathering build credentials',
}),
Expand Down
87 changes: 87 additions & 0 deletions packages/eas-json/src/__tests__/buildProfiles-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -981,3 +981,90 @@ test('invalid build profile with caching with both paths and customPaths - error
await EasJsonUtils.getBuildProfileAsync(accessor, Platform.ANDROID, 'production');
}).rejects.toThrow(expectedError);
});

test('valid eas.json with ios.refreshAdHocProvisioningProfile', async () => {
await fs.writeJson('/project/eas.json', {
build: {
preview: {
distribution: 'internal',
ios: {
refreshAdHocProvisioningProfile: true,
},
},
},
});

const accessor = EasJsonAccessor.fromProjectPath('/project');
const iosProfile = await EasJsonUtils.getBuildProfileAsync(accessor, Platform.IOS, 'preview');

expect(iosProfile).toEqual(
expect.objectContaining({
distribution: 'internal',
refreshAdHocProvisioningProfile: true,
})
);
});

test('invalid eas.json with non-boolean ios.refreshAdHocProvisioningProfile', async () => {
await fs.writeJson('/project/eas.json', {
build: {
preview: {
ios: {
refreshAdHocProvisioningProfile: 'yes',
},
},
},
});

const accessor = EasJsonAccessor.fromProjectPath('/project');

await expect(async () => {
await EasJsonUtils.getBuildProfileAsync(accessor, Platform.IOS, 'preview');
}).rejects.toThrow(InvalidEasJsonError);
});

test('valid profile extending other profile with ios.refreshAdHocProvisioningProfile', async () => {
await fs.writeJson('/project/eas.json', {
build: {
base: {
ios: {
refreshAdHocProvisioningProfile: true,
},
},
extension: {
extends: 'base',
distribution: 'internal',
},
override: {
extends: 'base',
distribution: 'internal',
ios: {
refreshAdHocProvisioningProfile: false,
},
},
},
});

const accessor = EasJsonAccessor.fromProjectPath('/project');
const extendedIosProfile = await EasJsonUtils.getBuildProfileAsync(
accessor,
Platform.IOS,
'extension'
);
const overrideIosProfile = await EasJsonUtils.getBuildProfileAsync(
accessor,
Platform.IOS,
'override'
);

expect(extendedIosProfile).toEqual({
distribution: 'internal',
credentialsSource: 'remote',
refreshAdHocProvisioningProfile: true,
});
expect(overrideIosProfile).toEqual({
distribution: 'internal',
credentialsSource: 'remote',
refreshAdHocProvisioningProfile: false,
});
});
1 change: 1 addition & 0 deletions packages/eas-json/src/build/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,7 @@ const IosBuildProfileSchema = PlatformBuildProfileSchema.concat(

// credentials
enterpriseProvisioning: Joi.string().valid('adhoc', 'universal'),
refreshAdHocProvisioningProfile: Joi.boolean(),

// build configuration
simulator: Joi.boolean(),
Expand Down
1 change: 1 addition & 0 deletions packages/eas-json/src/build/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ export interface IosBuildProfile extends PlatformBuildProfile {

// credentials
enterpriseProvisioning?: IosEnterpriseProvisioning;
refreshAdHocProvisioningProfile?: boolean;

// build configuration
simulator?: boolean;
Expand Down
Loading