Compare commits

...
Author SHA1 Message Date
Brent Vatne 64d985fcda v3.1.0 2022-12-16 15:54:02 -08:00
Jon Samp bd2bb4dcf6 [eas-cli] Add --channel flag to eas update command (#1567)
* Add --channel flag to eas update command

* use existing method

* adds undefined to return type

* move to getBranchNameFromChannelNameAsync.ts

* renames the function to singular "update"
2022-12-15 10:56:30 -06:00
Wojciech Kozyra 17db6c4cf1 [eas-cli] Add build:resign command (#1575) 2022-12-15 14:23:10 +01:00
Jon SampandDominik Sokal 5203096aca [eas-cli] Adds channels to build profiles on eas update:configure (#1570)
* Adds eas.json configuration during eas update configure

* updates next steps message

* renames eas updates to eas update

* self feedback

* update CHANGELOG.md

* Update packages/eas-cli/src/commands/update/configure.ts

Co-authored-by: Dominik Sokal <[email protected]>

* Update packages/eas-cli/src/commands/update/configure.ts

Co-authored-by: Dominik Sokal <[email protected]>

* code style feedback incorp

* use EasJsonAccessor

* Update packages/eas-cli/src/update/configure.ts

Co-authored-by: Dominik Sokal <[email protected]>

* lint

Co-authored-by: Dominik Sokal <[email protected]>
2022-12-12 10:59:41 -06:00
30 changed files with 1424 additions and 1323 deletions

No files matched your search

+4
View File
@@ -8,6 +8,10 @@ This is the log of notable changes to EAS CLI and related packages.
### 🎉 New features
- Adds `--channel` flag to `eas update`. ([#1567](https://github.com/expo/eas-cli/pull/1567) by [@jonsamp](https://github.com/jonsamp))
- Add channel configurations to **eas.json** during `eas update:configure`. ([#1570](https://github.com/expo/eas-cli/pull/1570) by [@jonsamp](https://github.com/jonsamp))
- Add `build:resign` command. ([#1575](https://github.com/expo/eas-cli/pull/1575) by [@wkozyra95](https://github.com/wkozyra95))
### 🐛 Bug fixes
### 🧹 Chores
+1 -1
View File
@@ -1,5 +1,5 @@
{
"npmClient": "yarn",
"useWorkspaces": true,
"version": "3.0.0"
"version": "3.1.0"
}
File diff suppressed because it is too large. Load diff
+552 -56
View File
@@ -2258,22 +2258,6 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "redirectUri",
"description": "",
"args": [],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "revokeEndpoint",
"description": "",
@@ -2438,22 +2422,6 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "redirectUri",
"description": "",
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
},
"defaultValue": null,
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "revokeEndpoint",
"description": "",
@@ -2742,22 +2710,6 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "redirectUri",
"description": "",
"args": [],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "revokeEndpoint",
"description": "",
@@ -5129,6 +5081,18 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "mode",
"description": "",
"type": {
"kind": "ENUM",
"name": "BuildMode",
"ofType": null
},
"defaultValue": null,
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "projectArchive",
"description": "",
@@ -5435,6 +5399,18 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "mode",
"description": "",
"type": {
"kind": "ENUM",
"name": "BuildMode",
"ofType": null
},
"defaultValue": null,
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "releaseChannel",
"description": "",
@@ -6623,6 +6599,51 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "deploymentNew",
"description": "",
"args": [
{
"name": "channel",
"description": "",
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
},
"defaultValue": null,
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "runtimeVersion",
"description": "",
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
},
"defaultValue": null,
"isDeprecated": false,
"deprecationReason": null
}
],
"type": {
"kind": "OBJECT",
"name": "DeploymentNew",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "deployments",
"description": "Deployments associated with this app",
@@ -12012,7 +12033,20 @@
{
"name": "canRetry",
"description": "",
"args": [],
"args": [
{
"name": "newMode",
"description": "",
"type": {
"kind": "ENUM",
"name": "BuildMode",
"ofType": null
},
"defaultValue": null,
"isDeprecated": false,
"deprecationReason": null
}
],
"type": {
"kind": "NON_NULL",
"name": null,
@@ -13860,6 +13894,29 @@
"enumValues": null,
"possibleTypes": null
},
{
"kind": "ENUM",
"name": "BuildMode",
"description": "",
"fields": null,
"inputFields": null,
"interfaces": null,
"enumValues": [
{
"name": "BUILD",
"description": "",
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "RESIGN",
"description": "",
"isDeprecated": false,
"deprecationReason": null
}
],
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "BuildMutation",
@@ -14713,6 +14770,29 @@
"enumValues": null,
"possibleTypes": null
},
{
"kind": "INPUT_OBJECT",
"name": "BuildResignInput",
"description": "",
"fields": null,
"inputFields": [
{
"name": "applicationArchiveSource",
"description": "",
"type": {
"kind": "INPUT_OBJECT",
"name": "ProjectArchiveSourceInput",
"ofType": null
},
"defaultValue": null,
"isDeprecated": false,
"deprecationReason": null
}
],
"interfaces": null,
"enumValues": null,
"possibleTypes": null
},
{
"kind": "ENUM",
"name": "BuildResourceClass",
@@ -14849,6 +14929,12 @@
"description": "",
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "UNKNOWN",
"description": "",
"isDeprecated": false,
"deprecationReason": null
}
],
"possibleTypes": null
@@ -16499,6 +16585,224 @@
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "DeploymentBuildEdge",
"description": "",
"fields": [
{
"name": "cursor",
"description": "",
"args": [],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "node",
"description": "",
"args": [],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "Build",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [],
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "DeploymentBuildsConnection",
"description": "",
"fields": [
{
"name": "edges",
"description": "",
"args": [],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "DeploymentBuildEdge",
"ofType": null
}
}
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "pageInfo",
"description": "",
"args": [],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "PageInfo",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [],
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "DeploymentNew",
"description": "Represents a Deployment - a set of Builds with the same Runtime Version and Channel",
"fields": [
{
"name": "builds",
"description": "",
"args": [
{
"name": "after",
"description": "",
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"defaultValue": null,
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "before",
"description": "",
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"defaultValue": null,
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "first",
"description": "",
"type": {
"kind": "SCALAR",
"name": "Int",
"ofType": null
},
"defaultValue": null,
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "last",
"description": "",
"type": {
"kind": "SCALAR",
"name": "Int",
"ofType": null
},
"defaultValue": null,
"isDeprecated": false,
"deprecationReason": null
}
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "DeploymentBuildsConnection",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "channel",
"description": "",
"args": [],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "UpdateChannel",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "id",
"description": "",
"args": [],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "ID",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "runtime",
"description": "",
"args": [],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "Runtime",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [],
"enumValues": null,
"possibleTypes": null
},
{
"kind": "INPUT_OBJECT",
"name": "DeploymentOptions",
@@ -17580,13 +17884,9 @@
"description": "",
"args": [],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
@@ -20750,6 +21050,18 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "mode",
"description": "",
"type": {
"kind": "ENUM",
"name": "BuildMode",
"ofType": null
},
"defaultValue": null,
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "projectArchive",
"description": "",
@@ -21021,6 +21333,18 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "mode",
"description": "",
"type": {
"kind": "ENUM",
"name": "BuildMode",
"ofType": null
},
"defaultValue": null,
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "releaseChannel",
"description": "",
@@ -21033,6 +21357,18 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "resign",
"description": "",
"type": {
"kind": "INPUT_OBJECT",
"name": "BuildResignInput",
"ofType": null
},
"defaultValue": null,
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "scheme",
"description": "",
@@ -21069,6 +21405,18 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "type",
"description": "",
"type": {
"kind": "ENUM",
"name": "BuildWorkflow",
"ofType": null
},
"defaultValue": null,
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "updates",
"description": "",
@@ -22479,6 +22827,73 @@
],
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "PageInfo",
"description": "",
"fields": [
{
"name": "endCursor",
"description": "",
"args": [],
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "hasNextPage",
"description": "",
"args": [],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "Boolean",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "hasPreviousPage",
"description": "",
"args": [],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "Boolean",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "startCursor",
"description": "",
"args": [],
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [],
"enumValues": null,
"possibleTypes": null
},
{
"kind": "INPUT_OBJECT",
"name": "PartialManifest",
@@ -22938,6 +23353,12 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "NONE",
"description": "",
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "S3",
"description": "",
@@ -25130,6 +25551,81 @@
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "Runtime",
"description": "",
"fields": [
{
"name": "app",
"description": "",
"args": [],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "App",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "firstBuildCreatedAt",
"description": "",
"args": [],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "DateTime",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "id",
"description": "",
"args": [],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "ID",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "version",
"description": "",
"args": [],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [],
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "SecondFactorBooleanResult",
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "eas-cli",
"description": "EAS command line tool",
"version": "3.0.0",
"version": "3.1.0",
"author": "Expo <[email protected]>",
"bin": {
"eas": "./bin/run"
+2 -1
View File
@@ -15,6 +15,7 @@ import Log from '../log';
import { formatBranch, getBranchDescription } from '../update/utils';
import { printJsonOnlyOutput } from '../utils/json';
import {
SelectPromptEntry,
paginatedQueryWithConfirmPromptAsync,
paginatedQueryWithSelectPromptAsync,
} from '../utils/queries';
@@ -31,7 +32,7 @@ export async function selectBranchOnAppAsync(
paginatedQueryOptions,
}: {
projectId: string;
displayTextForListItem: (queryItem: UpdateBranchFragment) => string;
displayTextForListItem: (queryItem: UpdateBranchFragment) => SelectPromptEntry;
promptTitle: string;
paginatedQueryOptions: PaginatedQueryOptions;
}
@@ -1,5 +1,7 @@
import { Platform } from '@expo/eas-build-job';
import { BuildProfile } from '@expo/eas-json';
import { CredentialsContext } from '../../credentials/context';
import IosCredentialsProvider from '../../credentials/ios/IosCredentialsProvider';
import { getAppFromContextAsync } from '../../credentials/ios/actions/BuildCredentialsUtils';
import { IosCredentials, Target } from '../../credentials/ios/types';
@@ -31,6 +33,27 @@ export async function ensureIosCredentialsAsync(
};
}
export async function ensureIosCredentialsForBuildResignAsync(
credentialsCtx: CredentialsContext,
targets: Target[],
buildProfile: BuildProfile<Platform.IOS>
): Promise<CredentialsResult<IosCredentials>> {
const provider = new IosCredentialsProvider(credentialsCtx, {
app: await getAppFromContextAsync(credentialsCtx),
targets,
distribution: 'internal',
enterpriseProvisioning: buildProfile.enterpriseProvisioning,
});
const { credentialsSource } = buildProfile;
logCredentialsSource(credentialsSource, Platform.IOS);
return {
credentials: await provider.getCredentialsAsync(credentialsSource),
source: credentialsSource,
};
}
function shouldProvideCredentials(buildCtx: BuildContext<Platform.IOS>): boolean {
return !buildCtx.buildProfile.simulator;
}
+1 -1
View File
@@ -26,7 +26,7 @@ export function transformJob(job: Ios.Job): IosJobInput {
};
}
function transformIosSecrets(secrets: {
export function transformIosSecrets(secrets: {
buildCredentials?: Ios.BuildCredentials;
}): IosJobSecretsInput {
const buildCredentials: IosJobSecretsInput['buildCredentials'] = [];
@@ -1,8 +1,10 @@
import { ArchiveSource, Ios, Job, Platform, sanitizeJob } from '@expo/eas-build-job';
import nullthrows from 'nullthrows';
import path from 'path';
import slash from 'slash';
import { IosCredentials, TargetCredentials } from '../../credentials/ios/types';
import { IosJobSecretsInput } from '../../graphql/generated';
import { getUsername } from '../../project/projectUtils';
import { getVcsClient } from '../../vcs';
import { BuildContext } from '../context';
@@ -79,6 +81,22 @@ export async function prepareJobAsync(
return sanitizeJob(job);
}
export function prepareCredentialsToResign(credentials: IosCredentials): IosJobSecretsInput {
const buildCredentials: IosJobSecretsInput['buildCredentials'] = [];
for (const targetName of Object.keys(credentials ?? {})) {
buildCredentials.push({
targetName,
provisioningProfileBase64: nullthrows(credentials?.[targetName].provisioningProfile),
distributionCertificate: {
dataBase64: nullthrows(credentials?.[targetName].distributionCertificate.certificateP12),
password: nullthrows(credentials?.[targetName].distributionCertificate.certificatePassword),
},
});
}
return { buildCredentials };
}
function prepareTargetCredentials(targetCredentials: TargetCredentials): Ios.TargetCredentials {
return {
provisioningProfileBase64: targetCredentials.provisioningProfile,
+40 -1
View File
@@ -8,7 +8,10 @@ import Log from '../log';
import { promptAsync } from '../prompts';
import { fromNow } from '../utils/date';
import { printJsonOnlyOutput } from '../utils/json';
import { paginatedQueryWithConfirmPromptAsync } from '../utils/queries';
import {
paginatedQueryWithConfirmPromptAsync,
paginatedQueryWithSelectPromptAsync,
} from '../utils/queries';
import { formatGraphQLBuild } from './utils/formatBuild';
export const BUILDS_LIMIT = 50;
@@ -55,6 +58,42 @@ export async function listAndRenderBuildsOnAppAsync(
}
}
export async function listAndSelectBuildOnAppAsync(
graphqlClient: ExpoGraphqlClient,
{
projectId,
title,
filter,
paginatedQueryOptions,
}: {
projectId: string;
title: string;
filter?: BuildFilter;
paginatedQueryOptions: PaginatedQueryOptions;
}
): Promise<BuildFragment | void> {
if (paginatedQueryOptions.nonInteractive) {
throw new Error('Unable to select a build in non-interactive mode.');
} else {
return await paginatedQueryWithSelectPromptAsync({
limit: paginatedQueryOptions.limit ?? BUILDS_LIMIT,
offset: paginatedQueryOptions.offset,
queryToPerform: (limit, offset) =>
BuildQuery.viewBuildsOnAppAsync(graphqlClient, {
appId: projectId,
limit,
offset,
filter,
}),
promptOptions: {
title,
getIdentifierForQueryItem: build => build.id,
createDisplayTextForSelectionPromptListItem: formatBuildChoiceTitleAndDescription,
},
});
}
}
function formatBuildChoiceValue(value: string | undefined | null): string {
return value ? chalk.bold(value) : chalk.dim('Unknown');
}
+5
View File
@@ -0,0 +1,5 @@
export class ChannelNotFoundError extends Error {
constructor(message?: string) {
super(message ?? 'Channel not found.');
}
}
+1 -1
View File
@@ -46,7 +46,7 @@ export async function selectChannelOnAppAsync(
queryChannelsOnAppAsync(graphqlClient, { appId: projectId, limit, offset }),
promptOptions: {
title: selectionPromptTitle,
createDisplayTextForSelectionPromptListItem: updateChannel => updateChannel.name,
createDisplayTextForSelectionPromptListItem: updateChannel => ({ title: updateChannel.name }),
getIdentifierForQueryItem: updateChannel => updateChannel.id,
},
});
@@ -120,7 +120,7 @@ export default class BranchDelete extends EasCommand {
}
({ name: branchName } = await selectBranchOnAppAsync(graphqlClient, {
projectId,
displayTextForListItem: updateBranch => updateBranch.name,
displayTextForListItem: updateBranch => ({ title: updateBranch.name }),
promptTitle: 'Which branch would you like to delete?',
paginatedQueryOptions,
}));
+3 -1
View File
@@ -57,7 +57,9 @@ export default class BranchView extends EasCommand {
({ name: branchName } = await selectBranchOnAppAsync(graphqlClient, {
projectId,
promptTitle: 'Which branch would you like to view?',
displayTextForListItem: updateBranch => updateBranch.name,
displayTextForListItem: updateBranch => ({
title: updateBranch.name,
}),
// discard limit and offset because this query is not their intended target
paginatedQueryOptions: {
json: paginatedQueryOptions.json,
+1 -1
View File
@@ -245,7 +245,7 @@ export default class Build extends EasCommand {
}
}
async function handleDeprecatedEasJsonAsync(
export async function handleDeprecatedEasJsonAsync(
projectDir: string,
nonInteractive: boolean
): Promise<void> {
@@ -0,0 +1,296 @@
import { Platform } from '@expo/eas-build-job';
import { CredentialsSource, EasJson, EasJsonAccessor, EasJsonUtils } from '@expo/eas-json';
import { Flags } from '@oclif/core';
import assert from 'assert';
import chalk from 'chalk';
import { handleDeprecatedEasJsonAsync } from '.';
import { waitForBuildEndAsync } from '../../build/build';
import { ensureIosCredentialsForBuildResignAsync } from '../../build/ios/credentials';
import { prepareCredentialsToResign } from '../../build/ios/prepareJob';
import { listAndSelectBuildOnAppAsync } from '../../build/queries';
import { printBuildResults, printLogsUrls } from '../../build/utils/printBuildInfo';
import EasCommand from '../../commandUtils/EasCommand';
import { ExpoGraphqlClient } from '../../commandUtils/context/contextUtils/createGraphqlClient';
import { EasNonInteractiveAndJsonFlags } from '../../commandUtils/flags';
import { EasPaginatedQueryFlags } from '../../commandUtils/pagination';
import { CredentialsContext } from '../../credentials/context';
import {
BuildFragment,
BuildMode,
BuildStatus,
BuildWorkflow,
DistributionType,
IosJobOverridesInput,
ProjectArchiveSourceType,
StatuspageServiceName,
} from '../../graphql/generated';
import { BuildMutation } from '../../graphql/mutations/BuildMutation';
import { BuildQuery } from '../../graphql/queries/BuildQuery';
import { toAppPlatform } from '../../graphql/types/AppPlatform';
import Log from '../../log';
import { requestedPlatformDisplayNames, selectPlatformAsync } from '../../platform';
import { resolveXcodeBuildContextAsync } from '../../project/ios/scheme';
import { resolveTargetsAsync } from '../../project/ios/target';
import { getOwnerAccountForProjectIdAsync } from '../../project/projectUtils';
import { enableJsonOutput, printJsonOnlyOutput } from '../../utils/json';
import { maybeWarnAboutEasOutagesAsync } from '../../utils/statuspageService';
interface BuildResignFlags {
json: boolean;
nonInteractive: boolean;
offset?: number;
limit?: number;
platform?: Platform;
profile?: string;
maybeBuildId?: string;
wait: boolean;
}
interface RawBuildResignFlags {
json: boolean;
'non-interactive': boolean;
offset: number | undefined;
limit: number | undefined;
platform: 'android' | 'ios' | undefined;
profile: string | undefined;
wait: boolean;
id: string | undefined;
}
export default class BuildResign extends EasCommand {
static override description = 're-sign a build archive';
static override flags = {
platform: Flags.enum({
char: 'p',
options: ['android', 'ios'],
}),
profile: Flags.string({
char: 'e',
description:
'Name of the build profile from eas.json. Defaults to "production" if defined in eas.json.',
helpValue: 'PROFILE_NAME',
}),
wait: Flags.boolean({
default: true,
allowNo: true,
description: 'Wait for build(s) to complete.',
}),
id: Flags.string({
description: 'ID of the build to re-sign.',
}),
...EasPaginatedQueryFlags,
...EasNonInteractiveAndJsonFlags,
};
static override contextDefinition = {
...this.ContextOptions.LoggedIn,
...this.ContextOptions.DynamicProjectConfig,
...this.ContextOptions.ProjectDir,
...this.ContextOptions.Analytics,
};
async runAsync(): Promise<void> {
const { flags: rawFlags } = await this.parse(BuildResign);
if (rawFlags.json) {
enableJsonOutput();
}
const flags = this.sanitizeFlags(rawFlags as RawBuildResignFlags);
const { limit, offset, nonInteractive } = flags;
const {
loggedIn: { actor, graphqlClient },
getDynamicProjectConfigAsync,
projectDir,
analytics,
} = await this.getContextAsync(BuildResign, {
nonInteractive: flags.nonInteractive,
});
const maybeBuild = flags.maybeBuildId
? await this.maybeGetBuildAsync(graphqlClient, flags.maybeBuildId)
: undefined;
const platform =
(maybeBuild?.platform.toLowerCase() as Platform | undefined) ??
(await selectPlatformAsync(flags.platform));
if (platform === Platform.ANDROID) {
throw new Error('Re-signing archives is not supported on Android yet.');
}
await handleDeprecatedEasJsonAsync(projectDir, flags.nonInteractive);
await maybeWarnAboutEasOutagesAsync(graphqlClient, [StatuspageServiceName.EasBuild]);
const easJsonAccessor = new EasJsonAccessor(projectDir);
const easJsonCliConfig: EasJson['cli'] =
(await EasJsonUtils.getCliConfigAsync(easJsonAccessor)) ?? {};
const buildProfile = await EasJsonUtils.getBuildProfileAsync(
easJsonAccessor,
platform,
flags.profile ?? 'production'
);
const { exp, projectId } = await getDynamicProjectConfigAsync({ env: buildProfile.env });
const account = await getOwnerAccountForProjectIdAsync(graphqlClient, projectId);
const build = await this.ensureBuildSelectedAsync(
{ graphqlClient, projectId, platform, nonInteractive, limit, offset },
maybeBuild
);
const credentialsCtx = new CredentialsContext({
projectInfo: { exp, projectId },
nonInteractive,
projectDir,
user: actor,
graphqlClient,
analytics,
env: buildProfile.env,
easJsonCliConfig,
});
if (buildProfile.credentialsSource !== CredentialsSource.LOCAL && !nonInteractive) {
await credentialsCtx.appStore.ensureAuthenticatedAsync();
}
const xcodeBuildContext = await resolveXcodeBuildContextAsync(
{
projectDir,
nonInteractive,
exp,
},
buildProfile
);
const targets = await resolveTargetsAsync({
projectDir,
exp,
xcodeBuildContext,
env: buildProfile.env,
});
const credentialsResult = await ensureIosCredentialsForBuildResignAsync(
credentialsCtx,
targets,
buildProfile
);
assert(build.artifacts?.applicationArchiveUrl, 'Missing application archive.');
const jobOverrides: IosJobOverridesInput = {
mode: BuildMode.Resign,
type: BuildWorkflow.Unknown,
resign: {
applicationArchiveSource: {
type: ProjectArchiveSourceType.Url,
url: build.artifacts?.applicationArchiveUrl,
},
},
secrets: prepareCredentialsToResign(credentialsResult.credentials),
builderEnvironment: { image: 'default' },
};
const newBuild = await BuildMutation.retryIosBuildAsync(graphqlClient, {
buildId: build.id,
jobOverrides,
});
Log.addNewLineIfNone();
printLogsUrls([newBuild]);
Log.newLine();
if (!flags.wait) {
if (flags.json) {
printJsonOnlyOutput(newBuild);
}
return;
}
const buildResult = await waitForBuildEndAsync(graphqlClient, {
buildIds: [newBuild.id],
accountName: account.name,
});
if (!flags.json) {
printBuildResults(buildResult);
} else {
assert(buildResult[0], 'missing build results');
printJsonOnlyOutput(buildResult[0]);
}
}
sanitizeFlags(flags: RawBuildResignFlags): BuildResignFlags {
const nonInteractive = flags['non-interactive'];
if (nonInteractive && !flags.id) {
throw new Error(
`${chalk.bold('--id')} is required when running with ${chalk.bold(
'--non-interactive'
)} flag.`
);
}
return {
json: flags.json,
nonInteractive,
offset: flags.offset,
limit: flags.limit,
platform: flags.platform as Platform | undefined,
profile: flags.profile,
maybeBuildId: flags.id,
wait: flags.wait,
};
}
async ensureBuildSelectedAsync(
{
graphqlClient,
projectId,
platform,
nonInteractive,
limit,
offset,
}: {
graphqlClient: ExpoGraphqlClient;
projectId: string;
platform: Platform;
nonInteractive: boolean;
limit?: number;
offset?: number;
},
maybeBuild?: BuildFragment
): Promise<BuildFragment> {
if (maybeBuild) {
return maybeBuild;
}
const build = await listAndSelectBuildOnAppAsync(graphqlClient, {
projectId,
title: 'Which build would you like to re-sign with new credentials?',
paginatedQueryOptions: {
limit,
offset: offset ?? 0,
nonInteractive,
json: false,
},
filter: {
distribution: DistributionType.Internal,
platform: toAppPlatform(platform),
status: BuildStatus.Finished,
},
});
if (!build) {
throw new Error('There are no builds that can be re-signed on this project.');
}
return build;
}
async maybeGetBuildAsync(
graphqlClient: ExpoGraphqlClient,
maybeBuildId?: string,
maybePlatform?: Platform
): Promise<BuildFragment | undefined> {
if (maybeBuildId) {
const build = await BuildQuery.byIdAsync(graphqlClient, maybeBuildId);
if (build.distribution !== DistributionType.Internal) {
throw new Error('This is not an internal distribution build.');
}
if (build.status !== BuildStatus.Finished) {
throw new Error('Only builds that finished successfully can be re-signed.');
}
if (maybePlatform && build.platform !== toAppPlatform(maybePlatform)) {
throw new Error(
`Build with ID ${maybeBuildId} was not created for platform ${requestedPlatformDisplayNames[maybePlatform]}.`
);
}
return build;
}
return undefined;
}
}
@@ -107,7 +107,7 @@ export default class ChannelEdit extends EasCommand {
: await selectBranchOnAppAsync(graphqlClient, {
projectId,
promptTitle: `Which branch would you like ${existingChannel.name} to point at?`,
displayTextForListItem: updateBranch => updateBranch.name,
displayTextForListItem: updateBranch => ({ title: updateBranch.name }),
paginatedQueryOptions: {
json,
nonInteractive,
@@ -1,10 +1,11 @@
import { Flags } from '@oclif/core';
import chalk from 'chalk';
import EasCommand from '../../commandUtils/EasCommand';
import { EASNonInteractiveFlag } from '../../commandUtils/flags';
import Log from '../../log';
import { RequestedPlatform } from '../../platform';
import { ensureEASUpdatesIsConfiguredAsync } from '../../update/configure';
import { ensureEASUpdateIsConfiguredAsync } from '../../update/configure';
export default class UpdateConfigure extends EasCommand {
static override description = 'configure the project to support EAS Update';
@@ -38,7 +39,7 @@ export default class UpdateConfigure extends EasCommand {
'💡 The following process will configure your project to run EAS Update. These changes only apply to your local project files and you can safely revert them at any time.'
);
await ensureEASUpdatesIsConfiguredAsync(graphqlClient, {
await ensureEASUpdateIsConfiguredAsync(graphqlClient, {
exp,
projectId,
projectDir,
@@ -46,6 +47,23 @@ export default class UpdateConfigure extends EasCommand {
});
Log.addNewLineIfNone();
Log.log(`🎉 Your app is configured to run EAS Update!`);
Log.log(`🎉 Your app is configured with EAS Update!`);
Log.newLine();
Log.log(`${chalk.bold('Next steps')}:`);
Log.newLine();
Log.log('Update a production build:');
Log.log(`1. Create a new build. Example: ${chalk.bold('eas build --profile production')}.`);
Log.log('2. Make changes in your project.');
Log.log(`3. Publish an update. Example: ${chalk.bold('eas update --channel production')}.`);
Log.log('4. Force close and reopen the app at least twice to view the update.');
Log.newLine();
Log.log('Preview an update:');
Log.log(
`1. Publish an update to a branch. Example: ${chalk.bold('eas update --branch new-feature')}.`
);
Log.log(
'2. In Expo Go or a development build, navigate to Projects > [project name] > Branch > Open.'
);
}
}
+29 -9
View File
@@ -37,7 +37,8 @@ import {
} from '../../project/publish';
import { resolveWorkflowAsync } from '../../project/workflow';
import { promptAsync } from '../../prompts';
import { ensureEASUpdatesIsConfiguredAsync } from '../../update/configure';
import { ensureEASUpdateIsConfiguredAsync } from '../../update/configure';
import { getBranchNameFromChannelNameAsync } from '../../update/getBranchNameFromChannelNameAsync';
import { formatUpdateMessage, truncateString as truncateUpdateMessage } from '../../update/utils';
import {
checkManifestBodyAgainstUpdateInfoGroup,
@@ -56,6 +57,7 @@ export const defaultPublishPlatforms: Partial<PublishPlatform>[] = ['android', '
type RawUpdateFlags = {
auto: boolean;
branch?: string;
channel?: string;
message?: string;
platform: string;
'input-dir': string;
@@ -73,6 +75,7 @@ type UpdateFlags = {
auto: boolean;
platform: ExpoCLIExportPlatformFlag;
branchName?: string;
channelName?: string;
updateMessage?: string;
inputDir: string;
skipBundler: boolean;
@@ -104,6 +107,10 @@ export default class UpdatePublish extends EasCommand {
description: 'Branch to publish the update group on',
required: false,
}),
channel: Flags.string({
description: 'Channel that the published update should affect',
required: false,
}),
message: Flags.string({
description: 'A short message describing the update',
required: false,
@@ -158,7 +165,7 @@ export default class UpdatePublish extends EasCommand {
let {
auto: autoFlag,
platform: platformFlag,
branchName,
channelName,
updateMessage,
inputDir,
skipBundler,
@@ -166,6 +173,7 @@ export default class UpdatePublish extends EasCommand {
json: jsonFlag,
nonInteractive,
} = this.sanitizeFlags(rawFlags);
let branchName = this.sanitizeFlags(rawFlags).branchName;
const {
getDynamicProjectConfigAsync,
@@ -192,7 +200,7 @@ export default class UpdatePublish extends EasCommand {
await maybeWarnAboutEasOutagesAsync(graphqlClient, [StatuspageServiceName.EasUpdate]);
await ensureEASUpdatesIsConfiguredAsync(graphqlClient, {
await ensureEASUpdateIsConfiguredAsync(graphqlClient, {
exp: expPossiblyWithoutEasUpdateConfigured,
platform: getRequestedPlatform(platformFlag),
projectDir,
@@ -204,20 +212,31 @@ export default class UpdatePublish extends EasCommand {
let realizedPlatforms: PublishPlatform[] = [];
if (channelName && branchName) {
throw new Error(
'Cannot specify both --channel and --branch. Specify either --channel, --branch, or --auto'
);
}
if (channelName) {
branchName = await getBranchNameFromChannelNameAsync(graphqlClient, projectId, channelName);
}
if (!branchName) {
if (autoFlag) {
branchName = await getDefaultBranchNameAsync();
} else if (nonInteractive) {
throw new Error('Must supply --branch or use --auto when in non-interactive mode');
throw new Error('Must supply --channel, --branch or --auto when in non-interactive mode');
} else {
try {
const branch = await selectBranchOnAppAsync(graphqlClient, {
projectId,
promptTitle: `Which branch would you like to publish on?`,
displayTextForListItem: updateBranch =>
`${updateBranch.name} ${chalk.grey(
displayTextForListItem: updateBranch => ({
title: `${updateBranch.name} ${chalk.grey(
`- current update: ${formatUpdateMessage(updateBranch.updates[0])}`
)}`,
}),
paginatedQueryOptions,
});
branchName = branch.name;
@@ -489,10 +508,10 @@ export default class UpdatePublish extends EasCommand {
private sanitizeFlags(flags: RawUpdateFlags): UpdateFlags {
const nonInteractive = flags['non-interactive'] ?? false;
const { auto, branch: branchName, message: updateMessage } = flags;
if (nonInteractive && !auto && !(branchName && updateMessage)) {
const { auto, branch: branchName, channel: channelName, message: updateMessage } = flags;
if (nonInteractive && !auto && !(branchName && channelName && updateMessage)) {
Errors.error(
'--auto or both --branch and --message are required when updating in non-interactive mode',
'--auto or both --channel or --branch and --message are required when updating in non-interactive mode',
{ exit: 1 }
);
}
@@ -517,6 +536,7 @@ export default class UpdatePublish extends EasCommand {
return {
auto,
branchName,
channelName,
updateMessage,
inputDir: flags['input-dir'],
skipBundler: flags['skip-bundler'],
+3 -1
View File
@@ -70,7 +70,9 @@ export default class UpdateList extends EasCommand {
const selectedBranch = await selectBranchOnAppAsync(graphqlClient, {
projectId,
promptTitle: 'Which branch would you like to view?',
displayTextForListItem: updateBranch => updateBranch.name,
displayTextForListItem: updateBranch => ({
title: updateBranch.name,
}),
paginatedQueryOptions:
// discard limit and offset because this query is not those flag's intended target
{
+6 -3
View File
@@ -44,10 +44,11 @@ export async function selectAppleTeamOnAccountAsync(
}),
promptOptions: {
title: selectionPromptTitle,
createDisplayTextForSelectionPromptListItem: appleTeam =>
appleTeam.appleTeamName
createDisplayTextForSelectionPromptListItem: appleTeam => ({
title: appleTeam.appleTeamName
? `${appleTeam.appleTeamName} (ID: ${appleTeam.appleTeamIdentifier})`
: appleTeam.appleTeamIdentifier,
}),
getIdentifierForQueryItem: appleTeam => appleTeam.id,
},
});
@@ -87,7 +88,9 @@ export async function selectAppleDeviceOnAppleTeamAsync(
}),
promptOptions: {
title: selectionPromptTitle,
createDisplayTextForSelectionPromptListItem: appleDevice => formatDeviceLabel(appleDevice),
createDisplayTextForSelectionPromptListItem: appleDevice => ({
title: formatDeviceLabel(appleDevice),
}),
getIdentifierForQueryItem: appleTeam => appleTeam.id,
},
});
+87 -5
View File
@@ -400,7 +400,6 @@ export type AccountSsoConfiguration = {
id: Scalars['ID'];
issuer?: Maybe<Scalars['String']>;
jwksEndpoint?: Maybe<Scalars['String']>;
redirectUri: Scalars['String'];
revokeEndpoint?: Maybe<Scalars['String']>;
tokenEndpoint?: Maybe<Scalars['String']>;
updatedAt: Scalars['DateTime'];
@@ -415,7 +414,6 @@ export type AccountSsoConfigurationData = {
clientSecret: Scalars['String'];
issuer?: InputMaybe<Scalars['String']>;
jwksEndpoint?: InputMaybe<Scalars['String']>;
redirectUri: Scalars['String'];
revokeEndpoint?: InputMaybe<Scalars['String']>;
tokenEndpoint?: InputMaybe<Scalars['String']>;
userInfoEndpoint?: InputMaybe<Scalars['String']>;
@@ -458,7 +456,6 @@ export type AccountSsoConfigurationPublicData = {
id: Scalars['ID'];
issuer?: Maybe<Scalars['String']>;
jwksEndpoint?: Maybe<Scalars['String']>;
redirectUri: Scalars['String'];
revokeEndpoint?: Maybe<Scalars['String']>;
tokenEndpoint?: Maybe<Scalars['String']>;
userInfoEndpoint?: Maybe<Scalars['String']>;
@@ -787,6 +784,7 @@ export type AndroidJobInput = {
developmentClient?: InputMaybe<Scalars['Boolean']>;
experimental?: InputMaybe<Scalars['JSONObject']>;
gradleCommand?: InputMaybe<Scalars['String']>;
mode?: InputMaybe<BuildMode>;
projectArchive: ProjectArchiveSourceInput;
projectRootDirectory: Scalars['String'];
releaseChannel?: InputMaybe<Scalars['String']>;
@@ -815,6 +813,7 @@ export type AndroidJobOverridesInput = {
developmentClient?: InputMaybe<Scalars['Boolean']>;
experimental?: InputMaybe<Scalars['JSONObject']>;
gradleCommand?: InputMaybe<Scalars['String']>;
mode?: InputMaybe<BuildMode>;
releaseChannel?: InputMaybe<Scalars['String']>;
secrets?: InputMaybe<AndroidJobSecretsInput>;
updates?: InputMaybe<BuildUpdatesInput>;
@@ -922,6 +921,7 @@ export type App = Project & {
/** Classic update release channel names that have at least one build */
buildsReleaseChannels: Array<Scalars['String']>;
deployment?: Maybe<Deployment>;
deploymentNew?: Maybe<DeploymentNew>;
/** Deployments associated with this app */
deployments: Array<Deployment>;
description: Scalars['String'];
@@ -1051,6 +1051,13 @@ export type AppDeploymentArgs = {
};
/** Represents an Exponent App (or Experience in legacy terms) */
export type AppDeploymentNewArgs = {
channel: Scalars['String'];
runtimeVersion: Scalars['String'];
};
/** Represents an Exponent App (or Experience in legacy terms) */
export type AppDeploymentsArgs = {
limit: Scalars['Int'];
@@ -1800,6 +1807,12 @@ export type Build = ActivityTimelineProjectActivity & BuildOrBuildJob & {
workerStartedAt?: Maybe<Scalars['DateTime']>;
};
/** Represents an EAS Build */
export type BuildCanRetryArgs = {
newMode?: InputMaybe<BuildMode>;
};
export type BuildArtifact = {
__typename?: 'BuildArtifact';
manifestPlistUrl?: Maybe<Scalars['String']>;
@@ -1960,6 +1973,11 @@ export type BuildMetrics = {
buildWaitTime?: Maybe<Scalars['Int']>;
};
export enum BuildMode {
Build = 'BUILD',
Resign = 'RESIGN'
}
export type BuildMutation = {
__typename?: 'BuildMutation';
/**
@@ -2116,6 +2134,10 @@ export type BuildQueryByIdArgs = {
buildId: Scalars['ID'];
};
export type BuildResignInput = {
applicationArchiveSource?: InputMaybe<ProjectArchiveSourceInput>;
};
export enum BuildResourceClass {
AndroidDefault = 'ANDROID_DEFAULT',
AndroidLarge = 'ANDROID_LARGE',
@@ -2141,7 +2163,8 @@ export type BuildUpdatesInput = {
export enum BuildWorkflow {
Generic = 'GENERIC',
Managed = 'MANAGED'
Managed = 'MANAGED',
Unknown = 'UNKNOWN'
}
export enum CacheControlScope {
@@ -2381,6 +2404,36 @@ export type Deployment = {
runtimeVersion: Scalars['String'];
};
export type DeploymentBuildEdge = {
__typename?: 'DeploymentBuildEdge';
cursor: Scalars['String'];
node: Build;
};
export type DeploymentBuildsConnection = {
__typename?: 'DeploymentBuildsConnection';
edges: Array<DeploymentBuildEdge>;
pageInfo: PageInfo;
};
/** Represents a Deployment - a set of Builds with the same Runtime Version and Channel */
export type DeploymentNew = {
__typename?: 'DeploymentNew';
builds: DeploymentBuildsConnection;
channel: UpdateChannel;
id: Scalars['ID'];
runtime: Runtime;
};
/** Represents a Deployment - a set of Builds with the same Runtime Version and Channel */
export type DeploymentNewBuildsArgs = {
after?: InputMaybe<Scalars['String']>;
before?: InputMaybe<Scalars['String']>;
first?: InputMaybe<Scalars['Int']>;
last?: InputMaybe<Scalars['Int']>;
};
export type DeploymentOptions = {
/** Max number of associated builds to return */
buildListMaxSize?: InputMaybe<Scalars['Int']>;
@@ -2547,7 +2600,7 @@ export type GitHubAppInstallationAccessibleRepositoriesArgs = {
export type GitHubAppInstallationAccessibleRepository = {
__typename?: 'GitHubAppInstallationAccessibleRepository';
defaultBranch: Scalars['String'];
defaultBranch?: Maybe<Scalars['String']>;
description?: Maybe<Scalars['String']>;
id: Scalars['Int'];
name: Scalars['String'];
@@ -2982,6 +3035,7 @@ export type IosJobInput = {
/** @deprecated */
distribution?: InputMaybe<DistributionType>;
experimental?: InputMaybe<Scalars['JSONObject']>;
mode?: InputMaybe<BuildMode>;
projectArchive: ProjectArchiveSourceInput;
projectRootDirectory: Scalars['String'];
releaseChannel?: InputMaybe<Scalars['String']>;
@@ -3008,10 +3062,13 @@ export type IosJobOverridesInput = {
/** @deprecated */
distribution?: InputMaybe<DistributionType>;
experimental?: InputMaybe<Scalars['JSONObject']>;
mode?: InputMaybe<BuildMode>;
releaseChannel?: InputMaybe<Scalars['String']>;
resign?: InputMaybe<BuildResignInput>;
scheme?: InputMaybe<Scalars['String']>;
secrets?: InputMaybe<IosJobSecretsInput>;
simulator?: InputMaybe<Scalars['Boolean']>;
type?: InputMaybe<BuildWorkflow>;
updates?: InputMaybe<BuildUpdatesInput>;
username?: InputMaybe<Scalars['String']>;
version?: InputMaybe<IosJobVersionInput>;
@@ -3249,6 +3306,14 @@ export enum Order {
Desc = 'DESC'
}
export type PageInfo = {
__typename?: 'PageInfo';
endCursor?: Maybe<Scalars['String']>;
hasNextPage: Scalars['Boolean'];
hasPreviousPage: Scalars['Boolean'];
startCursor?: Maybe<Scalars['String']>;
};
export type PartialManifest = {
assets: Array<InputMaybe<PartialManifestAsset>>;
extra?: InputMaybe<Scalars['JSONObject']>;
@@ -3298,6 +3363,7 @@ export type ProjectArchiveSourceInput = {
export enum ProjectArchiveSourceType {
Gcs = 'GCS',
None = 'NONE',
S3 = 'S3',
Url = 'URL'
}
@@ -3632,6 +3698,14 @@ export type RootQueryUserByUsernameArgs = {
username: Scalars['String'];
};
export type Runtime = {
__typename?: 'Runtime';
app: App;
firstBuildCreatedAt: Scalars['DateTime'];
id: Scalars['ID'];
version: Scalars['String'];
};
export type SecondFactorBooleanResult = {
__typename?: 'SecondFactorBooleanResult';
success: Scalars['Boolean'];
@@ -5032,6 +5106,14 @@ export type CreateIosBuildMutationVariables = Exact<{
export type CreateIosBuildMutation = { __typename?: 'RootMutation', build: { __typename?: 'BuildMutation', createIosBuild: { __typename?: 'CreateBuildResult', build: { __typename?: 'Build', id: string, status: BuildStatus, platform: AppPlatform, channel?: string | null, releaseChannel?: string | null, distribution?: DistributionType | null, iosEnterpriseProvisioning?: BuildIosEnterpriseProvisioning | null, buildProfile?: string | null, sdkVersion?: string | null, appVersion?: string | null, appBuildVersion?: string | null, runtimeVersion?: string | null, gitCommitHash?: string | null, gitCommitMessage?: string | null, initialQueuePosition?: number | null, queuePosition?: number | null, estimatedWaitTimeLeftSeconds?: number | null, priority: BuildPriority, createdAt: any, updatedAt: any, error?: { __typename?: 'BuildError', errorCode: string, message: string, docsUrl?: string | null } | null, artifacts?: { __typename?: 'BuildArtifacts', buildUrl?: string | null, xcodeBuildLogsUrl?: string | null, applicationArchiveUrl?: string | null } | null, initiatingActor?: { __typename: 'Robot', id: string, displayName: string } | { __typename: 'User', id: string, displayName: string } | null, project: { __typename: 'App', id: string, name: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string } } | { __typename: 'Snack', id: string, name: string, slug: string } }, deprecationInfo?: { __typename?: 'EASBuildDeprecationInfo', type: EasBuildDeprecationInfoType, message: string } | null } } };
export type RetryIosBuildMutationVariables = Exact<{
buildId: Scalars['ID'];
jobOverrides: IosJobOverridesInput;
}>;
export type RetryIosBuildMutation = { __typename?: 'RootMutation', build: { __typename?: 'BuildMutation', retryIosBuild: { __typename?: 'Build', id: string, status: BuildStatus, platform: AppPlatform, channel?: string | null, releaseChannel?: string | null, distribution?: DistributionType | null, iosEnterpriseProvisioning?: BuildIosEnterpriseProvisioning | null, buildProfile?: string | null, sdkVersion?: string | null, appVersion?: string | null, appBuildVersion?: string | null, runtimeVersion?: string | null, gitCommitHash?: string | null, gitCommitMessage?: string | null, initialQueuePosition?: number | null, queuePosition?: number | null, estimatedWaitTimeLeftSeconds?: number | null, priority: BuildPriority, createdAt: any, updatedAt: any, error?: { __typename?: 'BuildError', errorCode: string, message: string, docsUrl?: string | null } | null, artifacts?: { __typename?: 'BuildArtifacts', buildUrl?: string | null, xcodeBuildLogsUrl?: string | null, applicationArchiveUrl?: string | null } | null, initiatingActor?: { __typename: 'Robot', id: string, displayName: string } | { __typename: 'User', id: string, displayName: string } | null, project: { __typename: 'App', id: string, name: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string } } | { __typename: 'Snack', id: string, name: string, slug: string } } } };
export type CreateEnvironmentSecretForAccountMutationVariables = Exact<{
input: CreateEnvironmentSecretInput;
accountId: Scalars['String'];
@@ -15,6 +15,9 @@ import {
CreateIosBuildMutationVariables,
EasBuildDeprecationInfo,
IosJobInput,
IosJobOverridesInput,
RetryIosBuildMutation,
RetryIosBuildMutationVariables,
} from '../generated';
import { BuildFragmentNode } from '../types/Build';
@@ -116,4 +119,32 @@ export const BuildMutation = {
);
return nullthrows(data.build?.createIosBuild);
},
async retryIosBuildAsync(
graphqlClient: ExpoGraphqlClient,
input: {
buildId: string;
jobOverrides: IosJobOverridesInput;
}
): Promise<BuildFragment> {
const data = await withErrorHandlingAsync(
graphqlClient
.mutation<RetryIosBuildMutation, RetryIosBuildMutationVariables>(
gql`
mutation RetryIosBuildMutation($buildId: ID!, $jobOverrides: IosJobOverridesInput!) {
build {
retryIosBuild(buildId: $buildId, jobOverrides: $jobOverrides) {
id
...BuildFragment
}
}
}
${print(BuildFragmentNode)}
`,
input,
{ noRetry: true }
)
.toPromise()
);
return nullthrows(data.build?.retryIosBuild);
},
};
@@ -1,6 +1,7 @@
import { print } from 'graphql';
import gql from 'graphql-tag';
import { ChannelNotFoundError } from '../../channel/errors';
import { ExpoGraphqlClient } from '../../commandUtils/context/contextUtils/createGraphqlClient';
import { withErrorHandlingAsync } from '../client';
import {
@@ -59,7 +60,7 @@ export const ChannelQuery = {
const { updateChannelByName } = response.app.byId;
if (!updateChannelByName) {
throw new Error(`Could not find channel with the name ${channelName}`);
throw new ChannelNotFoundError(`Could not find channel with the name ${channelName}`);
}
return updateChannelByName;
@@ -0,0 +1,155 @@
import { instance, mock } from 'ts-mockito';
import { ExpoGraphqlClient } from '../../commandUtils/context/contextUtils/createGraphqlClient';
import { UpdateBranch, UpdateChannel } from '../../graphql/generated';
import { ChannelQuery } from '../../graphql/queries/ChannelQuery';
import { getBranchNameFromChannelNameAsync } from '../getBranchNameFromChannelNameAsync';
jest.mock('../../graphql/queries/ChannelQuery');
jest.mock('../../graphql/queries/BranchQuery');
describe(getBranchNameFromChannelNameAsync, () => {
beforeEach(() => {
jest.resetAllMocks();
});
test('returns branch name when channel exists and has one connected branch', async () => {
const graphqlClient = instance(mock<ExpoGraphqlClient>());
jest.mocked(ChannelQuery.viewUpdateChannelAsync).mockImplementationOnce(
async () =>
mockUpdateChannel({
channelName: 'test-channel-name',
branchNames: ['test-branch-name'],
}) as any
);
const result = await getBranchNameFromChannelNameAsync(
graphqlClient,
'test-project-id',
'channel-name'
);
expect(result).toBe('test-branch-name');
});
test('errors when no branch is connected to channel', async () => {
const graphqlClient = instance(mock<ExpoGraphqlClient>());
jest.mocked(ChannelQuery.viewUpdateChannelAsync).mockImplementationOnce(
async () =>
mockUpdateChannel({
channelName: 'test-channel-name',
}) as any
);
expect(
getBranchNameFromChannelNameAsync(graphqlClient, 'test-project-id', 'test-channel-name')
).rejects.toThrow(
"Channel has no branches associated with it. Run 'eas channel:edit' to map a branch"
);
});
test('errors when more than one branch is connected to channel', async () => {
const graphqlClient = instance(mock<ExpoGraphqlClient>());
jest.mocked(ChannelQuery.viewUpdateChannelAsync).mockImplementationOnce(
async () =>
mockUpdateChannel({
channelName: 'test-channel-name',
branchNames: ['test-branch-name', 'test-branch-name-2'],
}) as any
);
expect(
getBranchNameFromChannelNameAsync(graphqlClient, 'test-project-id', 'test-channel-name')
).rejects.toThrow(
"Channel has multiple branches associated with it. Instead, use 'eas update --branch'"
);
});
test('creates channel and branch when channel does not exist, and returns branch name', async () => {
const graphqlClient = instance(mock<ExpoGraphqlClient>());
jest.mocked(ChannelQuery.viewUpdateChannelAsync).mockImplementationOnce(
async () =>
mockUpdateChannel({
channelName: 'test-channel-name',
branchNames: ['test-branch-name'],
}) as any
);
const result = await getBranchNameFromChannelNameAsync(
graphqlClient,
'test-project-id',
'test-channel-name'
);
expect(result).toBe('test-branch-name');
});
});
function mockUpdateChannel({
channelName,
branchNames,
}: {
channelName?: string;
branchNames?: string[];
}): UpdateChannel {
return {
id: 'a2a1fa12-9d6a-433a-a432-49c64ef8439f',
name: channelName ?? 'default-channel-name',
createdAt: '2022-12-07T02:24:29.786Z',
appId: '123',
updatedAt: '2022-12-07T02:24:29.786Z',
branchMapping:
'{"data":[{"branchId":"f9dc4dbb-663f-4a19-8cf2-a783b484d2db","branchMappingLogic":"true"}],"version":0}',
updateBranches: branchNames ? mockUpdateBranches(branchNames) : [],
__typename: 'UpdateChannel',
};
}
function mockUpdateBranches(branchNames: string[]): UpdateBranch[] {
return branchNames.map(branchName => ({
id: 'f9dc4dbb-663f-4a19-8cf2-a783b484d2db',
name: branchName ?? 'default-branch-name',
appId: '123',
createdAt: '2022-12-07T02:24:29.786Z',
updatedAt: '2022-12-07T02:24:29.786Z',
updates: [],
updateGroups: [
[
{
id: '8503286d-4b18-4b38-a0f7-2b9ae09531a0',
group: 'd715bc20-76b5-477b-a060-3f8184aae87a',
message: 'my message',
createdAt: '2022-12-07T02:24:43.487Z',
runtimeVersion: '1.0.0',
platform: 'ios',
manifestFragment: '...',
activityTimestamp: '2022-12-07T02:24:43.487Z',
branchId: '',
awaitingCodeSigningInfo: false,
isGitWorkingTreeDirty: false,
manifestPermalink: '...',
updatedAt: '2022-12-07T02:24:43.487Z',
gitCommitHash: 'abc5baf77eee821b800af97879d19cc3132b46a7',
actor: {
__typename: 'User' as const,
id: 'f42b73a3-ce6c-4059-a825-e5ddafa022f7',
username: 'jonexpo',
} as any,
branch: {
id: 'f9dc4dbb-663f-4a19-8cf2-a783b484d2db',
name: 'production3',
appId: '123',
createdAt: '2022-12-07T02:24:29.786Z',
updatedAt: '2022-12-07T02:24:29.786Z',
updateGroups: [],
updates: [],
__typename: 'UpdateBranch' as const,
},
codeSigningInfo: null,
__typename: 'Update' as const,
},
],
],
__typename: 'UpdateBranch' as const,
}));
}
+68 -3
View File
@@ -1,7 +1,9 @@
import { modifyConfigAsync } from '@expo/config';
import { ExpoConfig } from '@expo/config-types';
import { Platform, Workflow } from '@expo/eas-build-job';
import { EasJsonAccessor } from '@expo/eas-json';
import chalk from 'chalk';
import fs from 'fs-extra';
import { getEASUpdateURL } from '../api';
import { ExpoGraphqlClient } from '../commandUtils/context/contextUtils/createGraphqlClient';
@@ -225,7 +227,7 @@ function warnEASUpdatesManualConfig({
/**
* Make sure that the current `app.json` configuration for EAS Updates is set natively.
*/
async function ensureEASUpdatesIsConfiguredNativelyAsync(
async function ensureEASUpdateIsConfiguredNativelyAsync(
graphqlClient: ExpoGraphqlClient,
{
exp,
@@ -252,6 +254,67 @@ async function ensureEASUpdatesIsConfiguredNativelyAsync(
}
}
/**
* Make sure EAS Build profiles are configured to work with EAS Update by adding channels to build profiles.
*/
async function ensureEASUpdateIsConfiguredInEasJsonAsync(projectDir: string): Promise<void> {
const easJsonPath = EasJsonAccessor.formatEasJsonPath(projectDir);
if (!(await fs.pathExists(easJsonPath))) {
Log.warn(
`EAS Build is not configured. If you'd like to use EAS Build with EAS Update, run ${chalk.bold(
'eas build:configure'
)}, then re-run ${chalk.bold('eas update:configure')} to configure ${chalk.bold(
'eas.json'
)} with EAS Update.`
);
return;
}
try {
const easJsonAccessor = new EasJsonAccessor(projectDir);
await easJsonAccessor.readRawJsonAsync();
easJsonAccessor.patch(easJsonRawObject => {
const easBuildProfilesWithChannels = Object.keys(easJsonRawObject.build).reduce(
(acc, profileNameKey) => {
const buildProfile = easJsonRawObject.build[profileNameKey];
const isNotAlreadyConfigured = !buildProfile.channel && !buildProfile.releaseChannel;
if (isNotAlreadyConfigured) {
return {
...acc,
[profileNameKey]: {
...buildProfile,
channel: profileNameKey,
},
};
}
return {
...acc,
[profileNameKey]: {
...easJsonRawObject.build[profileNameKey],
},
};
},
{}
);
return {
...easJsonRawObject,
build: easBuildProfilesWithChannels,
};
});
await easJsonAccessor.writeAsync();
Log.withTick(`Configured ${chalk.bold('eas.json')}.`);
} catch (error) {
Log.error(`We were not able to configure ${chalk.bold('eas.json')}. Error: ${error}.`);
}
}
/**
* Make sure EAS Update is fully configured in the current project.
* This goes over a checklist and performs the following checks or changes:
@@ -261,7 +324,7 @@ async function ensureEASUpdatesIsConfiguredNativelyAsync(
* - Sets `updates.url` if not set
* - Ensure latest changes are reflected in the native config, if any
*/
export async function ensureEASUpdatesIsConfiguredAsync(
export async function ensureEASUpdateIsConfiguredAsync(
graphqlClient: ExpoGraphqlClient,
{
exp: expWithoutUpdates,
@@ -300,8 +363,10 @@ export async function ensureEASUpdatesIsConfiguredAsync(
workflows,
});
await ensureEASUpdateIsConfiguredInEasJsonAsync(projectDir);
if (projectChanged || !hasExpoUpdates) {
await ensureEASUpdatesIsConfiguredNativelyAsync(graphqlClient, {
await ensureEASUpdateIsConfiguredNativelyAsync(graphqlClient, {
exp: expWithUpdates,
projectDir,
projectId,
@@ -0,0 +1,58 @@
import { ensureBranchExistsAsync } from '../branch/queries';
import { ChannelNotFoundError } from '../channel/errors';
import { createChannelOnAppAsync } from '../channel/queries';
import { ExpoGraphqlClient } from '../commandUtils/context/contextUtils/createGraphqlClient';
import { ChannelQuery } from '../graphql/queries/ChannelQuery';
export async function getBranchNameFromChannelNameAsync(
graphqlClient: ExpoGraphqlClient,
projectId: string,
channelName: string
): Promise<string> {
let branchName;
try {
const channel = await ChannelQuery.viewUpdateChannelAsync(graphqlClient, {
appId: projectId,
channelName,
});
if (channel.updateBranches.length === 1) {
branchName = channel.updateBranches[0].name;
} else if (channel.updateBranches.length === 0) {
throw new Error(
"Channel has no branches associated with it. Run 'eas channel:edit' to map a branch"
);
} else {
throw new Error(
"Channel has multiple branches associated with it. Instead, use 'eas update --branch'"
);
}
} catch (error) {
if (!(error instanceof ChannelNotFoundError)) {
throw error;
}
const { branchId } = await ensureBranchExistsAsync(graphqlClient, {
appId: projectId,
branchName: channelName,
});
const {
updateChannel: { createUpdateChannelForApp: newChannel },
} = await createChannelOnAppAsync(graphqlClient, {
appId: projectId,
channelName,
branchId,
});
if (!newChannel) {
throw new Error(
`Could not create channel with name ${channelName} on project with id ${projectId}`
);
}
branchName = channelName;
}
return branchName;
}
+3 -1
View File
@@ -126,7 +126,9 @@ export async function selectUpdateGroupOnBranchAsync(
}),
promptOptions: {
title: 'Load more update groups?',
createDisplayTextForSelectionPromptListItem: updateGroup => formatUpdateTitle(updateGroup[0]),
createDisplayTextForSelectionPromptListItem: updateGroup => ({
title: formatUpdateTitle(updateGroup[0]),
}),
getIdentifierForQueryItem: updateGroup => updateGroup[0].group,
},
});
@@ -129,7 +129,7 @@ describe(paginatedQueryWithSelectPromptAsync.name, () => {
promptOptions: {
title: '',
getIdentifierForQueryItem: item => item.id,
createDisplayTextForSelectionPromptListItem: item => 'item: ' + item.value,
createDisplayTextForSelectionPromptListItem: item => ({ title: 'item: ' + item.value }),
},
});
@@ -179,7 +179,7 @@ describe(paginatedQueryWithSelectPromptAsync.name, () => {
promptOptions: {
title: '',
getIdentifierForQueryItem: item => item.id,
createDisplayTextForSelectionPromptListItem: item => item.value,
createDisplayTextForSelectionPromptListItem: item => ({ title: item.value }),
},
});
+9 -2
View File
@@ -3,6 +3,11 @@ import uniqBy from './expodash/uniqBy';
const fetchMoreValue = '_fetchMore';
export interface SelectPromptEntry {
title: string;
description?: string;
}
type BasePaginatedQueryArgs<QueryReturnType extends Record<string, any>> = {
limit: number;
offset: number;
@@ -21,7 +26,9 @@ type PaginatedQueryWithSelectPromptArgs<QueryReturnType extends Record<string, a
BasePaginatedQueryArgs<QueryReturnType> & {
promptOptions: {
readonly title: string;
createDisplayTextForSelectionPromptListItem: (queryItem: QueryReturnType) => string;
createDisplayTextForSelectionPromptListItem: (
queryItem: QueryReturnType
) => SelectPromptEntry;
getIdentifierForQueryItem: (queryItem: QueryReturnType) => string;
};
};
@@ -100,7 +107,7 @@ async function paginatedQueryWithSelectPromptInternalAsync<
const selectionPromptListItems = uniqBy(newAccumulator, queryItem =>
promptOptions.getIdentifierForQueryItem(queryItem)
).map(queryItem => ({
title: promptOptions.createDisplayTextForSelectionPromptListItem(queryItem),
...promptOptions.createDisplayTextForSelectionPromptListItem(queryItem),
value: promptOptions.getIdentifierForQueryItem(queryItem),
}));
if (areMorePagesAvailable) {