Compare commits

...
Author SHA1 Message Date
Dominik Sokal dd7a3912c6 address PR feedback 2022-07-29 14:22:56 +02:00
Dominik Sokal 77ac0550a9 use commit message as default build message 2022-07-29 11:07:02 +02:00
Dominik Sokal f9978baba2 [ENG-5836] add --message param to eas build (#1237)
* generate graphql code

* [eas-cli] allow specifying build message

* upgrade @expo/eas-build-job

* update CHANGELOG.md
2022-07-29 10:41:55 +02:00
Dominik Sokal 2023f6161d [ENG-5622] allow importing devices from Apple Developer Portal (#1236)
* allow importing devices from Apple Developer Portal

* update CHANGELOG.md
2022-07-29 10:30:18 +02:00
Expo CI a4f35466c3 update CHANGELOG.md 2022-07-28 09:53:15 +00:00
20 changed files with 1569 additions and 19 deletions

No files matched your search

+11
View File
@@ -8,9 +8,20 @@ This is the log of notable changes to EAS CLI and related packages.
### 🎉 New features
- Add support for attaching messages to builds. ([#1237](https://github.com/expo/eas-cli/pull/1237) by [@dsokal](https://github.com/dsokal))
### 🐛 Bug fixes
### 🧹 Chores
## [0.56.0](https://github.com/expo/eas-cli/releases/tag/v0.56.0) - 2022-07-28
### 🎉 New features
- Display build queue type when waiting. ([#1217](https://github.com/expo/eas-cli/pull/1217) by [@dsokal](https://github.com/dsokal))
- Add better message on how to upgrade EAS CLI. ([#1222](https://github.com/expo/eas-cli/pull/1222) by [@jeremybarbet](https://github.com/jeremybarbet), [#1231](https://github.com/expo/eas-cli/pull/1231) by [@wkozyra95](https://github.com/wkozyra95))
- Invoke export with sourcemaps on update. ([#1228](https://github.com/expo/eas-cli/pull/1228) by [@kbrandwijk](https://github.com/kbrandwijk))
- Add new option to `eas device:create` - allow importing devices from Apple Developer Portal. ([#1236](https://github.com/expo/eas-cli/pull/1236) by [@dsokal](https://github.com/dsokal))
### 🐛 Bug fixes
File diff suppressed because it is too large. Load diff
+2 -1
View File
@@ -13,7 +13,7 @@
"@expo/config": "6.0.23",
"@expo/config-plugins": "4.1.4",
"@expo/config-types": "45.0.0",
"@expo/eas-build-job": "0.2.77",
"@expo/eas-build-job": "0.2.82",
"@expo/eas-json": "0.56.0",
"@expo/json-file": "8.2.36",
"@expo/multipart-body-parser": "1.1.0",
@@ -103,6 +103,7 @@
"nock": "13.2.4",
"tempy": "0.7.0",
"ts-deepmerge": "2.0.1",
"ts-mockito": "2.6.1",
"typescript": "4.7.4"
},
"engines": {
@@ -0,0 +1,40 @@
import { instance, mock, when } from 'ts-mockito';
import { Client } from '../../vcs/vcs';
import { getFormattedCommitMessageAsync } from '../metadata';
describe(getFormattedCommitMessageAsync, () => {
it(`returns undefined if it's not a git project`, async () => {
const clientMock = mock<Client>();
when(clientMock.getLastCommitMessageAsync()).thenResolve(null);
const client = instance(clientMock);
await expect(getFormattedCommitMessageAsync(client)).resolves.toBe(undefined);
});
it('returns commit message', async () => {
const clientMock = mock<Client>();
when(clientMock.getLastCommitMessageAsync()).thenResolve('lorem ipsum');
const client = instance(clientMock);
await expect(getFormattedCommitMessageAsync(client)).resolves.toBe('lorem ipsum');
});
it('trims long commit messages', async () => {
const clientMock = mock<Client>();
when(clientMock.getLastCommitMessageAsync()).thenResolve('a'.repeat(1500));
const client = instance(clientMock);
await expect(getFormattedCommitMessageAsync(client)).resolves.toBe(`${'a'.repeat(1021)}...`);
});
it('replaces new lines with spaces', async () => {
const clientMock = mock<Client>();
when(clientMock.getLastCommitMessageAsync()).thenResolve(`Lorem\nipsum\ndolor\nsit\namet`);
const client = instance(clientMock);
await expect(getFormattedCommitMessageAsync(client)).resolves.toBe(
`Lorem ipsum dolor sit amet`
);
});
});
+1
View File
@@ -40,6 +40,7 @@ export interface BuildContext<T extends Platform> {
projectDir: string;
projectId: string;
projectName: string;
message?: string;
trackingCtx: TrackingContext;
user: Actor;
workflow: Workflow;
@@ -28,6 +28,7 @@ export async function createBuildContextAsync<T extends Platform>({
platform,
projectDir,
resourceClass,
message,
}: {
buildProfileName: string;
buildProfile: BuildProfile<T>;
@@ -38,6 +39,7 @@ export async function createBuildContextAsync<T extends Platform>({
platform: T;
projectDir: string;
resourceClass: BuildResourceClass;
message?: string;
}): Promise<BuildContext<T>> {
const exp = getExpoConfig(projectDir, { env: buildProfile.env });
@@ -89,6 +91,7 @@ export async function createBuildContextAsync<T extends Platform>({
trackingCtx,
user,
workflow,
message,
};
if (platform === Platform.ANDROID) {
const common = commonContext as CommonContext<Platform.ANDROID>;
+20 -3
View File
@@ -17,6 +17,7 @@ import {
import { ensureLoggedInAsync } from '../user/actions';
import { easCliVersion } from '../utils/easCli';
import { getVcsClient } from '../vcs';
import { Client } from '../vcs/vcs';
import { maybeResolveVersionsAsync as maybeResolveAndroidVersionsAsync } from './android/version';
import { BuildContext } from './context';
import { maybeResolveVersionsAsync as maybeResolveIosVersionsAsync } from './ios/version';
@@ -29,7 +30,9 @@ export async function collectMetadataAsync<T extends Platform>(
('simulator' in ctx.buildProfile && ctx.buildProfile.simulator
? 'simulator'
: ctx.buildProfile.distribution) ?? 'store';
const metadata = {
const vcsClient = getVcsClient();
const metadata: Metadata = {
trackingContext: ctx.trackingCtx,
...(await maybeResolveVersionsAsync(ctx)),
cliVersion: easCliVersion,
@@ -43,9 +46,10 @@ export async function collectMetadataAsync<T extends Platform>(
appName: ctx.exp.name,
appIdentifier: resolveAppIdentifier(ctx),
buildProfile: ctx.buildProfileName,
gitCommitHash: await getVcsClient().getCommitHashAsync(),
isGitWorkingTreeDirty: await getVcsClient().hasUncommittedChangesAsync(),
gitCommitHash: await vcsClient.getCommitHashAsync(),
isGitWorkingTreeDirty: await vcsClient.hasUncommittedChangesAsync(),
username: getUsername(ctx.exp, await ensureLoggedInAsync()),
message: ctx.message ?? (await getFormattedCommitMessageAsync(vcsClient)),
...(ctx.platform === Platform.IOS && {
iosEnterpriseProvisioning: resolveIosEnterpriseProvisioning(
ctx as BuildContext<Platform.IOS>
@@ -145,3 +149,16 @@ function resolveIosEnterpriseProvisioning(
): IosEnterpriseProvisioning | undefined {
return ctx.buildProfile.enterpriseProvisioning;
}
export async function getFormattedCommitMessageAsync(
vcsClient: Client
): Promise<string | undefined> {
const maybeCommitMessage = (await vcsClient.getLastCommitMessageAsync())?.trim();
if (!maybeCommitMessage) {
return undefined;
}
const lines = maybeCommitMessage.split('\n');
const withoutEmptyLines = lines.filter(i => i);
const message = withoutEmptyLines.join(' ');
return message.length > 1024 ? message.substring(0, 1021) + '...' : message;
}
@@ -55,6 +55,7 @@ export interface BuildFlags {
submitProfile?: string;
localBuildOptions: LocalBuildOptions;
userInputResourceClass?: UserInputResourceClass;
message?: string;
}
const platformToGraphQLResourceClassMapping: Record<
@@ -209,6 +210,7 @@ async function prepareAndStartBuildAsync({
projectDir,
localBuildOptions: flags.localBuildOptions,
easJsonCliConfig,
message: flags.message,
});
if (moreBuilds) {
@@ -23,6 +23,7 @@ interface RawBuildFlags {
'auto-submit': boolean;
'auto-submit-with-profile'?: string;
'resource-class'?: UserInputResourceClass;
message?: string;
}
export default class Build extends EasCommand {
@@ -86,6 +87,10 @@ export default class Build extends EasCommand {
hidden: true,
description: 'The instance type that will be used to run this build [experimental]',
}),
message: Flags.string({
char: 'm',
description: 'A short message describing the build',
}),
};
async runAsync(): Promise<void> {
@@ -143,6 +148,11 @@ export default class Build extends EasCommand {
Log.newLine();
}
const message = flags['message'];
if (message && message.length > 1024) {
Errors.error('Message cannot be longer than 1024 characters.', { exit: 1 });
}
const profile = flags['profile'];
return {
requestedPlatform,
@@ -163,6 +173,7 @@ export default class Build extends EasCommand {
autoSubmit: flags['auto-submit'] || flags['auto-submit-with-profile'] !== undefined,
submitProfile: flags['auto-submit-with-profile'] ?? profile,
userInputResourceClass: flags['resource-class'] ?? UserInputResourceClass.DEFAULT,
message,
};
}
}
@@ -254,11 +254,11 @@ export class SetUpAdhocProvisioningProfile {
ctx: CredentialsContext,
appleTeam: AppleTeamFragment
): Promise<AppleDeviceFragment[]> {
const action = new DeviceCreateAction(this.app.account, appleTeam);
const action = new DeviceCreateAction(ctx.appStore, this.app.account, appleTeam);
const method = await action.runAsync();
while (true) {
if (method !== RegistrationMethod.INPUT) {
if (method === RegistrationMethod.WEBSITE) {
Log.newLine();
Log.log(chalk.bold("Press any key if you've already finished device registration."));
await pressAnyKeyToContinueAsync();
@@ -0,0 +1 @@
export const runDeveloperPortalMethodAsync = jest.fn();
@@ -1,13 +1,17 @@
import prompts from 'prompts';
import { instance, mock } from 'ts-mockito';
import AppStoreApi from '../../../../credentials/ios/appstore/AppStoreApi';
import { Account } from '../../../../user/Account';
import DeviceCreateAction, { RegistrationMethod } from '../action';
import { runDeveloperPortalMethodAsync } from '../developerPortalMethod';
import { runInputMethodAsync } from '../inputMethod';
import { runRegistrationUrlMethodAsync } from '../registrationUrlMethod';
jest.mock('prompts');
jest.mock('../registrationUrlMethod');
jest.mock('../developerPortalMethod');
jest.mock('../inputMethod');
jest.mock('../registrationUrlMethod');
beforeEach(() => {
const promptsMock = jest.mocked(prompts);
@@ -25,6 +29,8 @@ describe(DeviceCreateAction, () => {
jest.mocked(prompts).mockImplementationOnce(async () => ({
method: RegistrationMethod.WEBSITE,
}));
const appStoreApiMock = mock<AppStoreApi>();
const appStoreApi = instance(appStoreApiMock);
const account: Account = {
id: 'account_id',
@@ -35,7 +41,7 @@ describe(DeviceCreateAction, () => {
appleTeamIdentifier: 'ABC123Y',
appleTeamName: 'John Doe (Individual)',
};
const action = new DeviceCreateAction(account, appleTeam);
const action = new DeviceCreateAction(appStoreApi, account, appleTeam);
await action.runAsync();
expect(runRegistrationUrlMethodAsync).toBeCalled();
@@ -45,6 +51,8 @@ describe(DeviceCreateAction, () => {
jest.mocked(prompts).mockImplementationOnce(async () => ({
method: RegistrationMethod.INPUT,
}));
const appStoreApiMock = mock<AppStoreApi>();
const appStoreApi = instance(appStoreApiMock);
const account: Account = {
id: 'account_id',
@@ -55,10 +63,32 @@ describe(DeviceCreateAction, () => {
appleTeamIdentifier: 'ABC123Y',
appleTeamName: 'John Doe (Individual)',
};
const action = new DeviceCreateAction(account, appleTeam);
const action = new DeviceCreateAction(appStoreApi, account, appleTeam);
await action.runAsync();
expect(runInputMethodAsync).toBeCalled();
});
it('calls runDeveloperPortalMethodAsync if user chooses the developer portal method', async () => {
jest.mocked(prompts).mockImplementationOnce(async () => ({
method: RegistrationMethod.DEVELOPER_PORTAL,
}));
const appStoreApiMock = mock<AppStoreApi>();
const appStoreApi = instance(appStoreApiMock);
const account: Account = {
id: 'account_id',
name: 'foobar',
};
const appleTeam = {
id: 'apple-team-id',
appleTeamIdentifier: 'ABC123Y',
appleTeamName: 'John Doe (Individual)',
};
const action = new DeviceCreateAction(appStoreApi, account, appleTeam);
await action.runAsync();
expect(runDeveloperPortalMethodAsync).toBeCalled();
});
});
});
@@ -1,20 +1,24 @@
import chalk from 'chalk';
import AppStoreApi from '../../../credentials/ios/appstore/AppStoreApi';
import { AppleTeam } from '../../../graphql/generated';
import Log from '../../../log';
import { promptAsync } from '../../../prompts';
import { Account } from '../../../user/Account';
import { runDeveloperPortalMethodAsync } from './developerPortalMethod';
import { runInputMethodAsync } from './inputMethod';
import { runRegistrationUrlMethodAsync } from './registrationUrlMethod';
export enum RegistrationMethod {
WEBSITE,
INPUT,
DEVELOPER_PORTAL,
EXIT,
}
export default class DeviceCreateAction {
constructor(
private appStoreApi: AppStoreApi,
private account: Account,
private appleTeam: Pick<AppleTeam, 'appleTeamIdentifier' | 'appleTeamName' | 'id'>
) {}
@@ -23,6 +27,8 @@ export default class DeviceCreateAction {
const method = await this.askForRegistrationMethodAsync();
if (method === RegistrationMethod.WEBSITE) {
await runRegistrationUrlMethodAsync(this.account.id, this.appleTeam);
} else if (method === RegistrationMethod.DEVELOPER_PORTAL) {
await runDeveloperPortalMethodAsync(this.appStoreApi, this.account.id, this.appleTeam);
} else if (method === RegistrationMethod.INPUT) {
await runInputMethodAsync(this.account.id, this.appleTeam);
} else if (method === RegistrationMethod.EXIT) {
@@ -44,6 +50,12 @@ export default class DeviceCreateAction {
)} - generates a registration URL to be opened on your devices`,
value: RegistrationMethod.WEBSITE,
},
{
title: `${chalk.bold(
'Developer Portal'
)} - import devices already registered on Apple Developer Portal`,
value: RegistrationMethod.DEVELOPER_PORTAL,
},
{
title: `${chalk.bold('Input')} - allows you to type in UDIDs (advanced option)`,
value: RegistrationMethod.INPUT,
@@ -0,0 +1,131 @@
import { Device, DeviceClass } from '@expo/apple-utils';
import { AppleDeviceMutation } from '../../../credentials/ios/api/graphql/mutations/AppleDeviceMutation';
import {
AppleDeviceFragmentWithAppleTeam,
AppleDeviceQuery,
} from '../../../credentials/ios/api/graphql/queries/AppleDeviceQuery';
import AppStoreApi from '../../../credentials/ios/appstore/AppStoreApi';
import { getRequestContext } from '../../../credentials/ios/appstore/authenticate';
import { AuthCtx } from '../../../credentials/ios/appstore/authenticateTypes';
import { AppleDeviceClass, AppleTeam } from '../../../graphql/generated';
import Log from '../../../log';
import { ora } from '../../../ora';
import { promptAsync } from '../../../prompts';
import chunk from '../../../utils/expodash/chunk';
const DEVICE_IMPORT_CHUNK_SIZE = 10;
const DEVICE_CLASS_TO_GRAPHQL_TYPE: Partial<Record<DeviceClass, AppleDeviceClass>> = {
[DeviceClass.IPAD]: AppleDeviceClass.Ipad,
[DeviceClass.IPHONE]: AppleDeviceClass.Iphone,
};
export async function runDeveloperPortalMethodAsync(
appStoreApi: AppStoreApi,
accountId: string,
appleTeam: Pick<AppleTeam, 'appleTeamIdentifier' | 'appleTeamName' | 'id'>
): Promise<void> {
const appleAuthCtx = await appStoreApi.ensureAuthenticatedAsync();
const unregisteredPortalDevices = await findUnregisteredPortalDevicesAsync(
appleAuthCtx,
accountId,
appleTeam
);
if (unregisteredPortalDevices.length === 0) {
Log.log('All your devices registered on Apple Developer Portal are already imported to EAS.');
return;
}
const devicesToImport = await chooseDevicesToImportAsync(unregisteredPortalDevices);
await importDevicesAsync(accountId, appleTeam, devicesToImport);
}
async function importDevicesAsync(
accountId: string,
appleTeam: Pick<AppleTeam, 'appleTeamIdentifier' | 'appleTeamName' | 'id'>,
devices: Device[]
): Promise<void> {
const spinner = ora(
`Importing ${devices.length} Apple device${devices.length === 1 ? '' : 's'} to EAS`
).start();
const deviceChunks = chunk(devices, DEVICE_IMPORT_CHUNK_SIZE);
try {
for (const deviceChunk of deviceChunks) {
await importDeviceChunkAsync(accountId, appleTeam, deviceChunk);
}
} catch (err) {
spinner.fail();
throw err;
}
spinner.succeed();
}
async function importDeviceChunkAsync(
accountId: string,
appleTeam: Pick<AppleTeam, 'appleTeamIdentifier' | 'appleTeamName' | 'id'>,
devices: Device[]
): Promise<void> {
const promises = devices.map(device => {
return AppleDeviceMutation.createAppleDeviceAsync(
{
appleTeamId: appleTeam.id,
identifier: device.attributes.udid,
name: device.attributes.name,
deviceClass: DEVICE_CLASS_TO_GRAPHQL_TYPE[device.attributes.deviceClass] ?? undefined,
},
accountId
);
});
await Promise.all(promises);
}
async function findUnregisteredPortalDevicesAsync(
appleAuthCtx: AuthCtx,
accountId: string,
appleTeam: Pick<AppleTeam, 'appleTeamIdentifier' | 'appleTeamName' | 'id'>
): Promise<Device[]> {
const expoRegisteredDevices = await AppleDeviceQuery.getAllByAppleTeamIdentifierAsync(
accountId,
appleTeam.appleTeamIdentifier,
{ useCache: false }
);
const expoRegisteredDevicesByUdid = expoRegisteredDevices.reduce((acc, device) => {
acc[device.identifier] = device;
return acc;
}, {} as Record<string, AppleDeviceFragmentWithAppleTeam>);
const portalDevices = await Device.getAllIOSProfileDevicesAsync(getRequestContext(appleAuthCtx));
return portalDevices.filter(
portalDevice =>
!(portalDevice.attributes.udid in expoRegisteredDevicesByUdid) &&
[DeviceClass.IPAD, DeviceClass.IPHONE].includes(portalDevice.attributes.deviceClass)
);
}
async function chooseDevicesToImportAsync(devices: Device[]): Promise<Device[]> {
const { chosenDevices } = await promptAsync({
type: 'multiselect',
name: 'chosenDevices',
message: 'Which devices do you want to import to EAS?',
choices: devices.map(device => ({
value: device,
title: formatDeviceLabel(device),
selected: true,
})),
});
return chosenDevices;
}
export function formatDeviceLabel(device: Device): string {
const deviceDetails = formatDeviceDetails(device);
return `${device.attributes.name} - ${device.attributes.udid})${
deviceDetails !== '' ? ` - ${deviceDetails}` : ''
}`;
}
function formatDeviceDetails(device: Device): string {
let details: string = device.attributes.deviceClass;
if (device.attributes.model) {
details = device.attributes.model;
}
return details === '' ? details : `(${details})`;
}
+4 -4
View File
@@ -29,12 +29,12 @@ export default class DeviceManager {
Log.addNewLineIfNone();
const account = await this.resolveAccountAsync();
const { team } = await this.ctx.appStore.ensureAuthenticatedAsync();
const appleAuthCtx = await this.ctx.appStore.ensureAuthenticatedAsync();
const appleTeam = await ensureAppleTeamExistsAsync(account.id, {
appleTeamIdentifier: team.id,
appleTeamName: team.name,
appleTeamIdentifier: appleAuthCtx.team.id,
appleTeamName: appleAuthCtx.team.name,
});
const action = new DeviceCreateAction(account, appleTeam);
const action = new DeviceCreateAction(this.ctx.appStore, account, appleTeam);
await action.runAsync();
}
+137
View File
@@ -123,6 +123,8 @@ export type Account = {
requiresAccessTokenForPushSecurity: Scalars['Boolean'];
/** Snacks associated with this account */
snacks: Array<Snack>;
/** SSO configuration for this account */
ssoConfiguration?: Maybe<AccountSsoConfiguration>;
/** Subscription info visible to members that have VIEWER role */
subscription?: Maybe<SubscriptionDetails>;
/** @deprecated No longer needed */
@@ -393,6 +395,90 @@ export type AccountQueryByNameArgs = {
accountName: Scalars['String'];
};
/** Auth configuration data for an SSO account. */
export type AccountSsoConfiguration = {
__typename?: 'AccountSSOConfiguration';
authEndpoint?: Maybe<Scalars['String']>;
authProtocol: AuthProtocolType;
authProviderIdentifier: Scalars['String'];
clientIdentifier: Scalars['String'];
clientSecret: Scalars['String'];
createdAt: Scalars['DateTime'];
id: Scalars['ID'];
issuer?: Maybe<Scalars['String']>;
jwksEndpoint?: Maybe<Scalars['String']>;
redirectUri: Scalars['String'];
tokenEndpoint?: Maybe<Scalars['String']>;
updatedAt: Scalars['DateTime'];
userInfoEndpoint?: Maybe<Scalars['String']>;
};
export type AccountSsoConfigurationData = {
authEndpoint?: InputMaybe<Scalars['String']>;
authProtocol: AuthProtocolType;
authProviderIdentifier: Scalars['String'];
clientIdentifier: Scalars['String'];
clientSecret: Scalars['String'];
issuer?: InputMaybe<Scalars['String']>;
jwksEndpoint?: InputMaybe<Scalars['String']>;
redirectUri: Scalars['String'];
tokenEndpoint?: InputMaybe<Scalars['String']>;
userInfoEndpoint?: InputMaybe<Scalars['String']>;
};
export type AccountSsoConfigurationMutation = {
__typename?: 'AccountSSOConfigurationMutation';
/** Create an AccountSSOConfiguration for an Account */
createAccountSSOConfiguration: AccountSsoConfiguration;
/** Delete an AccountSSOConfiguration */
deleteAccountSSOConfiguration: DeleteAccountSsoConfigurationResult;
/** Update an AccountSSOConfiguration */
updateAccountSSOConfiguration: AccountSsoConfiguration;
};
export type AccountSsoConfigurationMutationCreateAccountSsoConfigurationArgs = {
accountId: Scalars['ID'];
accountSSOConfigurationData: AccountSsoConfigurationData;
};
export type AccountSsoConfigurationMutationDeleteAccountSsoConfigurationArgs = {
id: Scalars['ID'];
};
export type AccountSsoConfigurationMutationUpdateAccountSsoConfigurationArgs = {
accountSSOConfigurationData: AccountSsoConfigurationData;
id: Scalars['ID'];
};
/** Public auth configuration data for an SSO account. */
export type AccountSsoConfigurationPublicData = {
__typename?: 'AccountSSOConfigurationPublicData';
authEndpoint?: Maybe<Scalars['String']>;
authProtocol: AuthProtocolType;
authProviderIdentifier: Scalars['String'];
clientIdentifier: Scalars['String'];
id: Scalars['ID'];
issuer?: Maybe<Scalars['String']>;
jwksEndpoint?: Maybe<Scalars['String']>;
redirectUri: Scalars['String'];
tokenEndpoint?: Maybe<Scalars['String']>;
userInfoEndpoint?: Maybe<Scalars['String']>;
};
export type AccountSsoConfigurationPublicDataQuery = {
__typename?: 'AccountSSOConfigurationPublicDataQuery';
/** Get AccountSSOConfiguration public data by account name */
publicDataByAccountName: AccountSsoConfigurationPublicData;
};
export type AccountSsoConfigurationPublicDataQueryPublicDataByAccountNameArgs = {
accountName: Scalars['String'];
};
export type AccountUsageMetric = {
__typename?: 'AccountUsageMetric';
id: Scalars['ID'];
@@ -427,6 +513,7 @@ export type AccountUsageMetricsByBillingPeriodArgs = {
export type AccountUsageMetricsMetricsForServiceMetricArgs = {
filterParams?: InputMaybe<Scalars['JSONObject']>;
granularity: UsageMetricsGranularity;
serviceMetric: EasServiceMetric;
timespan: UsageMetricsTimespan;
@@ -708,6 +795,7 @@ export type AndroidJobInput = {
type: BuildWorkflow;
updates?: InputMaybe<BuildUpdatesInput>;
username?: InputMaybe<Scalars['String']>;
version?: InputMaybe<AndroidJobVersionInput>;
};
export type AndroidJobKeystoreInput = {
@@ -722,6 +810,10 @@ export type AndroidJobSecretsInput = {
environmentSecrets?: InputMaybe<Scalars['JSONObject']>;
};
export type AndroidJobVersionInput = {
versionCode: Scalars['String'];
};
export type AndroidKeystore = {
__typename?: 'AndroidKeystore';
account: Account;
@@ -1593,6 +1685,10 @@ export type AssetQueryMetadataArgs = {
storageKeys: Array<Scalars['String']>;
};
export enum AuthProtocolType {
Oidc = 'OIDC'
}
export type Billing = {
__typename?: 'Billing';
/** History of invoices */
@@ -1621,6 +1717,7 @@ export type Build = ActivityTimelineProjectActivity & BuildOrBuildJob & {
canRetry: Scalars['Boolean'];
cancelingActor?: Maybe<Actor>;
channel?: Maybe<Scalars['String']>;
childBuild?: Maybe<Build>;
completedAt?: Maybe<Scalars['DateTime']>;
createdAt: Scalars['DateTime'];
distribution?: Maybe<DistributionType>;
@@ -1639,6 +1736,9 @@ export type Build = ActivityTimelineProjectActivity & BuildOrBuildJob & {
isGitWorkingTreeDirty?: Maybe<Scalars['Boolean']>;
logFiles: Array<Scalars['String']>;
maxBuildTimeSeconds: Scalars['Int'];
/** Retry time starts after completedAt */
maxRetryTimeMinutes: Scalars['Int'];
message?: Maybe<Scalars['String']>;
metrics?: Maybe<BuildMetrics>;
parentBuild?: Maybe<Build>;
platform: AppPlatform;
@@ -1796,6 +1896,7 @@ export type BuildMetadataInput = {
gitCommitHash?: InputMaybe<Scalars['String']>;
iosEnterpriseProvisioning?: InputMaybe<BuildIosEnterpriseProvisioning>;
isGitWorkingTreeDirty?: InputMaybe<Scalars['Boolean']>;
message?: InputMaybe<Scalars['String']>;
reactNativeVersion?: InputMaybe<Scalars['String']>;
releaseChannel?: InputMaybe<Scalars['String']>;
runtimeVersion?: InputMaybe<Scalars['String']>;
@@ -2084,6 +2185,11 @@ export type DeleteAccountResult = {
id: Scalars['ID'];
};
export type DeleteAccountSsoConfigurationResult = {
__typename?: 'DeleteAccountSSOConfigurationResult';
id: Scalars['ID'];
};
export type DeleteAndroidKeystoreResult = {
__typename?: 'DeleteAndroidKeystoreResult';
id: Scalars['ID'];
@@ -2179,6 +2285,7 @@ export enum EasBuildDeprecationInfoType {
export enum EasServiceMetric {
AssetsRequests = 'ASSETS_REQUESTS',
BandwidthUsage = 'BANDWIDTH_USAGE',
Builds = 'BUILDS',
ManifestRequests = 'MANIFEST_REQUESTS',
UniqueUsers = 'UNIQUE_USERS'
}
@@ -2272,6 +2379,15 @@ export enum Feature {
Teams = 'TEAMS'
}
export type FutureSubscription = {
__typename?: 'FutureSubscription';
createdAt: Scalars['DateTime'];
id: Scalars['ID'];
meteredBillingStatus: MeteredBillingStatus;
planId: Scalars['String'];
startDate: Scalars['DateTime'];
};
export type GetSignedAssetUploadSpecificationsResult = {
__typename?: 'GetSignedAssetUploadSpecificationsResult';
specifications: Array<Scalars['String']>;
@@ -2544,6 +2660,7 @@ export type IosJobInput = {
type: BuildWorkflow;
updates?: InputMaybe<BuildUpdatesInput>;
username?: InputMaybe<Scalars['String']>;
version?: InputMaybe<IosJobVersionInput>;
};
export type IosJobSecretsInput = {
@@ -2557,6 +2674,10 @@ export type IosJobTargetCredentialsInput = {
targetName: Scalars['String'];
};
export type IosJobVersionInput = {
buildNumber: Scalars['String'];
};
/** @deprecated Use developmentClient option instead. */
export enum IosManagedBuildType {
DevelopmentClient = 'DEVELOPMENT_CLIENT',
@@ -2738,6 +2859,11 @@ export type MeMutationUpdateProfileArgs = {
userData: UserDataInput;
};
export type MeteredBillingStatus = {
__typename?: 'MeteredBillingStatus';
EAS_UPDATE: Scalars['Boolean'];
};
export type Offer = {
__typename?: 'Offer';
features?: Maybe<Array<Maybe<Feature>>>;
@@ -2954,6 +3080,8 @@ export type RootMutation = {
accessToken: AccessTokenMutation;
/** Mutations that modify an Account */
account: AccountMutation;
/** Mutations that create, update, and delete an AccountSSOConfiguration */
accountSSOConfiguration: AccountSsoConfigurationMutation;
/** Mutations that modify the build credentials for an Android app */
androidAppBuildCredentials: AndroidAppBuildCredentialsMutation;
/** Mutations that modify the credentials for an Android app */
@@ -3043,6 +3171,8 @@ export type RootQuery = {
_doNotUse?: Maybe<Scalars['String']>;
/** Top-level query object for querying Accounts. */
account: AccountQuery;
/** Top-level query object for querying AccountSSOConfigurationPublicData */
accountSSOConfigurationPublicData: AccountSsoConfigurationPublicDataQuery;
/** Top-level query object for querying Actors. */
actor: ActorQuery;
/**
@@ -3236,12 +3366,16 @@ export type Submission = ActivityTimelineProjectActivity & {
archiveUrl?: Maybe<Scalars['String']>;
canRetry: Scalars['Boolean'];
cancelingActor?: Maybe<Actor>;
childSubmission?: Maybe<Submission>;
completedAt?: Maybe<Scalars['DateTime']>;
createdAt: Scalars['DateTime'];
error?: Maybe<SubmissionError>;
id: Scalars['ID'];
initiatingActor?: Maybe<Actor>;
iosConfig?: Maybe<IosSubmissionConfig>;
logsUrl?: Maybe<Scalars['String']>;
/** Retry time starts after completedAt */
maxRetryTimeMinutes: Scalars['Int'];
parentSubmission?: Maybe<Submission>;
platform: AppPlatform;
status: SubmissionStatus;
@@ -3337,8 +3471,10 @@ export type SubscriptionDetails = {
cancelledAt?: Maybe<Scalars['DateTime']>;
concurrencies?: Maybe<Concurrencies>;
endedAt?: Maybe<Scalars['DateTime']>;
futureSubscription?: Maybe<FutureSubscription>;
id: Scalars['ID'];
isDowngrading?: Maybe<Scalars['Boolean']>;
meteredBillingStatus: MeteredBillingStatus;
name?: Maybe<Scalars['String']>;
nextInvoice?: Maybe<Scalars['DateTime']>;
planId?: Maybe<Scalars['String']>;
@@ -3533,6 +3669,7 @@ export type UsageMetricTotal = {
export enum UsageMetricType {
Bandwidth = 'BANDWIDTH',
Build = 'BUILD',
Request = 'REQUEST',
User = 'USER'
}
@@ -0,0 +1,19 @@
import chunk from '../chunk';
describe(chunk, () => {
it('works with an empty list', () => {
expect(chunk([])).toEqual([]);
});
it('works with list length undividable by chunk size', () => {
expect(chunk([1, 2, 3, 4, 5], 2)).toEqual([[1, 2], [3, 4], [5]]);
});
it('works with list length dividable by chunk size', () => {
expect(chunk([1, 2, 3, 4, 5, 6], 2)).toEqual([
[1, 2],
[3, 4],
[5, 6],
]);
});
});
@@ -0,0 +1,12 @@
export default function chunk<T>(list: T[], size = 1): T[][] {
const result: T[][] = [];
for (let i = 0; i < list.length; i++) {
const isFirstElementInChunk = i % size === 0;
if (isFirstElementInChunk) {
result.push([list[i]]);
} else {
result[result.length - 1].push(list[i]);
}
}
return result;
}
+1 -1
View File
@@ -5,7 +5,7 @@
"author": "Expo <[email protected]>",
"bugs": "https://github.com/expo/eas-cli/issues",
"dependencies": {
"@expo/eas-build-job": "0.2.77",
"@expo/eas-build-job": "0.2.82",
"@expo/json-file": "8.2.36",
"chalk": "4.1.2",
"env-string": "1.0.1",
Generated
+12 -5
View File
@@ -1269,10 +1269,10 @@
slugify "^1.3.4"
sucrase "^3.20.0"
"@expo/[email protected].77":
version "0.2.77"
resolved "https://registry.yarnpkg.com/@expo/eas-build-job/-/eas-build-job-0.2.77.tgz#2ebdb3e6706c4020812e2e8e3d2e7da26e3c7eb1"
integrity sha512-CFS/Uixa3XqV39vdoDnQP93kgd2EGf3gaOqLYl+9gnMCP9h2+Q9fIbfPw5DnFu2LEIyU+Xw5DtpkyTQEBjTLyA==
"@expo/[email protected].82":
version "0.2.82"
resolved "https://registry.yarnpkg.com/@expo/eas-build-job/-/eas-build-job-0.2.82.tgz#b0f5ec0462314bbd94c3b126faf881beee84593a"
integrity sha512-Yamyf0GZ/jjyvsgL4N7szhcht7T22SV6VwLBdddPYUWM53+9w70CRIxsCrmW6BPuW4nQUu0M+WoK5/fkYUd/zg==
dependencies:
joi "^17.4.2"
semver "^7.3.5"
@@ -8930,7 +8930,7 @@ lodash.without@^4.4.0:
resolved "https://registry.yarnpkg.com/lodash.without/-/lodash.without-4.4.0.tgz#3cd4574a00b67bae373a94b748772640507b7aac"
integrity sha1-PNRXSgC2e643OpS3SHcmQFB7eqw=
lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.21, lodash@^4.7.0, lodash@~4.17.0:
lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.21, lodash@^4.17.5, lodash@^4.7.0, lodash@~4.17.0:
version "4.17.21"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
@@ -12132,6 +12132,13 @@ ts-log@^2.2.3:
resolved "https://registry.yarnpkg.com/ts-log/-/ts-log-2.2.3.tgz#4da5640fe25a9fb52642cd32391c886721318efb"
integrity sha512-XvB+OdKSJ708Dmf9ore4Uf/q62AYDTzFcAdxc8KNML1mmAWywRFVt/dn1KYJH8Agt5UJNujfM3znU5PxgAzA2w==
[email protected]:
version "2.6.1"
resolved "https://registry.yarnpkg.com/ts-mockito/-/ts-mockito-2.6.1.tgz#bc9ee2619033934e6fad1c4455aca5b5ace34e73"
integrity sha512-qU9m/oEBQrKq5hwfbJ7MgmVN5Gu6lFnIGWvpxSjrqq6YYEVv+RwVFWySbZMBgazsWqv6ctAyVBpo9TmAxnOEKw==
dependencies:
lodash "^4.17.5"
ts-node@^9:
version "9.0.0"
resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-9.0.0.tgz#e7699d2a110cc8c0d3b831715e417688683460b3"