From 85ec3a85c73e1d03c5a077b200a282aa6fa8e0ad Mon Sep 17 00:00:00 2001 From: Will Schurman Date: Mon, 19 Aug 2024 14:52:29 -0700 Subject: [PATCH] [eas-cli] Add rollbackPercentage flag to update command --- packages/eas-cli/graphql.schema.json | 793 ++++++++++++++++-- packages/eas-cli/src/commands/update/index.ts | 83 +- packages/eas-cli/src/commands/update/list.ts | 10 +- .../commands/update/roll-back-to-embedded.ts | 11 +- packages/eas-cli/src/graphql/generated.ts | 167 +++- .../src/graphql/queries/BranchQuery.ts | 59 ++ .../queries/EnvironmentVariablesQuery.ts | 4 +- packages/eas-cli/src/graphql/types/Update.ts | 4 + .../src/project/__tests__/publish-test.ts | 45 +- packages/eas-cli/src/project/publish.ts | 176 ++-- packages/eas-cli/src/update/utils.ts | 8 + 11 files changed, 1194 insertions(+), 166 deletions(-) diff --git a/packages/eas-cli/graphql.schema.json b/packages/eas-cli/graphql.schema.json index fa9e4f5123..1170c56e34 100644 --- a/packages/eas-cli/graphql.schema.json +++ b/packages/eas-cli/graphql.schema.json @@ -10260,7 +10260,7 @@ "name": null, "ofType": { "kind": "SCALAR", - "name": "String", + "name": "WorkerDeploymentIdentifier", "ofType": null } }, @@ -10286,7 +10286,7 @@ "description": null, "type": { "kind": "SCALAR", - "name": "String", + "name": "WorkerDeploymentIdentifier", "ofType": null }, "defaultValue": null, @@ -10505,6 +10505,51 @@ }, "isDeprecated": false, "deprecationReason": null + }, + { + "name": "workerDeploymentsRequests", + "description": null, + "args": [ + { + "name": "limit", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + } + }, + "defaultValue": "100", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "timespan", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "RequestsTimespan", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "type": { + "kind": "OBJECT", + "name": "WorkerDeploymentRequests", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null } ], "inputFields": null, @@ -16266,22 +16311,6 @@ "isDeprecated": false, "deprecationReason": null }, - { - "name": "targetEntityTableName", - "description": null, - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "String", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, { "name": "targetEntityTypeName", "description": null, @@ -16495,6 +16524,46 @@ "defaultValue": null, "isDeprecated": false, "deprecationReason": null + }, + { + "name": "targetEntityMutationType", + "description": null, + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "ENUM", + "name": "TargetEntityMutationType", + "ofType": null + } + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "targetEntityTypeName", + "description": null, + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "ENUM", + "name": "EntityTypeName", + "ofType": null + } + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null } ], "type": { @@ -21390,6 +21459,65 @@ "enumValues": null, "possibleTypes": null }, + { + "kind": "ENUM", + "name": "ContinentCode", + "description": null, + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": [ + { + "name": "AF", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "AN", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "AS", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "EU", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "NA", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "OC", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "SA", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "T1", + "description": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "possibleTypes": null + }, { "kind": "INPUT_OBJECT", "name": "CrashesTimespan", @@ -22689,7 +22817,7 @@ "description": null, "type": { "kind": "SCALAR", - "name": "String", + "name": "WorkerDeploymentIdentifier", "ofType": null }, "defaultValue": null, @@ -22932,7 +23060,7 @@ "args": [], "type": { "kind": "SCALAR", - "name": "String", + "name": "WorkerDeploymentIdentifier", "ofType": null }, "isDeprecated": false, @@ -24219,7 +24347,7 @@ "description": null, "type": { "kind": "SCALAR", - "name": "String", + "name": "WorkerDeploymentIdentifier", "ofType": null }, "defaultValue": null, @@ -24325,7 +24453,7 @@ "description": null, "type": { "kind": "SCALAR", - "name": "String", + "name": "WorkerDeploymentIdentifier", "ofType": null }, "defaultValue": null, @@ -24967,6 +25095,131 @@ "enumValues": null, "possibleTypes": null }, + { + "kind": "ENUM", + "name": "EntityTypeName", + "description": null, + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": [ + { + "name": "Account", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "AccountSSOConfiguration", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "AndroidAppCredentials", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "AndroidKeystore", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "App", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "AppStoreConnectApiKey", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "AppleDevice", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "AppleDistributionCertificate", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "AppleProvisioningProfile", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "AppleTeam", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "Branch", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "Channel", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "Customer", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "GoogleServiceAccountKey", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "IosAppCredentials", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "TurtleBuild", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "Update", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "UserInvitation", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "UserPermission", + "description": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "possibleTypes": null + }, { "kind": "OBJECT", "name": "EnvironmentSecret", @@ -33247,13 +33500,9 @@ "name": "otp", "description": null, "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "String", - "ofType": null - } + "kind": "SCALAR", + "name": "String", + "ofType": null }, "defaultValue": null, "isDeprecated": false, @@ -33412,13 +33661,9 @@ "name": "otp", "description": null, "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "String", - "ofType": null - } + "kind": "SCALAR", + "name": "String", + "ofType": null }, "defaultValue": null, "isDeprecated": false, @@ -33494,13 +33739,9 @@ "name": "otp", "description": null, "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "String", - "ofType": null - } + "kind": "SCALAR", + "name": "String", + "ofType": null }, "defaultValue": null, "isDeprecated": false, @@ -33629,13 +33870,9 @@ "name": "otp", "description": null, "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "String", - "ofType": null - } + "kind": "SCALAR", + "name": "String", + "ofType": null }, "defaultValue": null, "isDeprecated": false, @@ -35864,6 +36101,18 @@ "isDeprecated": false, "deprecationReason": null }, + { + "name": "rolloutInfoGroup", + "description": null, + "type": { + "kind": "INPUT_OBJECT", + "name": "UpdateRolloutInfoGroup", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, { "name": "runtimeVersion", "description": null, @@ -35909,6 +36158,45 @@ "enumValues": null, "possibleTypes": null }, + { + "kind": "INPUT_OBJECT", + "name": "RequestsTimespan", + "description": null, + "fields": null, + "inputFields": [ + { + "name": "end", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "DateTime", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "start", + "description": null, + "type": { + "kind": "SCALAR", + "name": "DateTime", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, { "kind": "OBJECT", "name": "RescindUserInvitationResult", @@ -40742,6 +41030,18 @@ "isDeprecated": false, "deprecationReason": null }, + { + "name": "priority", + "description": null, + "args": [], + "type": { + "kind": "ENUM", + "name": "SubmissionPriority", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, { "name": "status", "description": null, @@ -41184,6 +41484,29 @@ "enumValues": null, "possibleTypes": null }, + { + "kind": "ENUM", + "name": "SubmissionPriority", + "description": null, + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": [ + { + "name": "HIGH", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "NORMAL", + "description": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "possibleTypes": null + }, { "kind": "OBJECT", "name": "SubmissionQuery", @@ -42190,6 +42513,30 @@ "isDeprecated": false, "deprecationReason": null }, + { + "name": "rolloutControlUpdate", + "description": null, + "args": [], + "type": { + "kind": "OBJECT", + "name": "Update", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "rolloutPercentage", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, { "name": "runtime", "description": null, @@ -43511,6 +43858,55 @@ }, "isDeprecated": false, "deprecationReason": null + }, + { + "name": "setRolloutPercentage", + "description": "Set rollout percentage for an update", + "args": [ + { + "name": "percentage", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "updateId", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "Update", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null } ], "inputFields": null, @@ -43609,6 +44005,96 @@ "enumValues": null, "possibleTypes": null }, + { + "kind": "INPUT_OBJECT", + "name": "UpdateRolloutInfo", + "description": null, + "fields": null, + "inputFields": [ + { + "name": "rolloutControlUpdateId", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "rolloutPercentage", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "INPUT_OBJECT", + "name": "UpdateRolloutInfoGroup", + "description": null, + "fields": null, + "inputFields": [ + { + "name": "android", + "description": null, + "type": { + "kind": "INPUT_OBJECT", + "name": "UpdateRolloutInfo", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "ios", + "description": null, + "type": { + "kind": "INPUT_OBJECT", + "name": "UpdateRolloutInfo", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "web", + "description": null, + "type": { + "kind": "INPUT_OBJECT", + "name": "UpdateRolloutInfo", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, { "kind": "INPUT_OBJECT", "name": "UpdatesFilter", @@ -47980,7 +48466,7 @@ "name": null, "ofType": { "kind": "SCALAR", - "name": "String", + "name": "WorkerDeploymentIdentifier", "ofType": null } }, @@ -48138,7 +48624,7 @@ "args": [], "type": { "kind": "SCALAR", - "name": "String", + "name": "WorkerDeploymentIdentifier", "ofType": null }, "isDeprecated": false, @@ -48572,6 +49058,16 @@ "enumValues": null, "possibleTypes": null }, + { + "kind": "SCALAR", + "name": "WorkerDeploymentIdentifier", + "description": "A alias name for a serverless deployment", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, { "kind": "ENUM", "name": "WorkerDeploymentLogLevel", @@ -48954,6 +49450,199 @@ "enumValues": null, "possibleTypes": null }, + { + "kind": "OBJECT", + "name": "WorkerDeploymentRequestLocation", + "description": null, + "fields": [ + { + "name": "continent", + "description": null, + "args": [], + "type": { + "kind": "ENUM", + "name": "ContinentCode", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "countryCode", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "regionCode", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "WorkerDeploymentRequestNode", + "description": null, + "fields": [ + { + "name": "location", + "description": null, + "args": [], + "type": { + "kind": "OBJECT", + "name": "WorkerDeploymentRequestLocation", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "method", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "pathname", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "search", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "status", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "timestamp", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "DateTime", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "WorkerDeploymentRequests", + "description": null, + "fields": [ + { + "name": "minRowsWithoutLimit", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "nodes", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "WorkerDeploymentRequestNode", + "ofType": null + } + } + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, { "kind": "OBJECT", "name": "WorkerDeploymentsConnection", diff --git a/packages/eas-cli/src/commands/update/index.ts b/packages/eas-cli/src/commands/update/index.ts index dfb6222187..8fc1b1e82a 100644 --- a/packages/eas-cli/src/commands/update/index.ts +++ b/packages/eas-cli/src/commands/update/index.ts @@ -1,4 +1,3 @@ -import { Platform as PublishPlatform } from '@expo/config'; import { Workflow } from '@expo/eas-build-job'; import { Errors, Flags } from '@oclif/core'; import chalk from 'chalk'; @@ -16,6 +15,7 @@ import { StatuspageServiceName, UpdateInfoGroup, UpdatePublishMutation, + UpdateRolloutInfoGroup, } from '../../graphql/generated'; import { PublishMutation } from '../../graphql/mutations/PublishMutation'; import Log, { learnMore, link } from '../../log'; @@ -23,17 +23,17 @@ import { ora } from '../../ora'; import { RequestedPlatform } from '../../platform'; import { getOwnerAccountForProjectIdAsync } from '../../project/projectUtils'; import { - ExpoCLIExportPlatformFlag, RawAsset, + UpdatePublishPlatform, buildBundlesAsync, buildUnsortedUpdateInfoGroupAsync, collectAssetsAsync, defaultPublishPlatforms, - filterExportedPlatformsByFlag, + filterCollectedAssetsByRequestedPlatforms, generateEasMetadataAsync, getBranchNameForCommandAsync, - getRequestedPlatform, getRuntimeToPlatformMappingFromRuntimeVersions, + getRuntimeToUpdateRolloutInfoGroupMappingAsync, getRuntimeVersionObjectAsync, getUpdateMessageForCommandAsync, isUploadedAssetCountAboveWarningThreshold, @@ -66,14 +66,15 @@ type RawUpdateFlags = { 'skip-bundler': boolean; 'clear-cache': boolean; 'private-key-path'?: string; - 'non-interactive': boolean; 'emit-metadata': boolean; + 'rollout-percentage'?: number; + 'non-interactive': boolean; json: boolean; }; type UpdateFlags = { auto: boolean; - platform: ExpoCLIExportPlatformFlag; + platform: RequestedPlatform; branchName?: string; channelName?: string; updateMessage?: string; @@ -81,9 +82,10 @@ type UpdateFlags = { skipBundler: boolean; clearCache: boolean; privateKeyPath?: string; + emitMetadata: boolean; + rolloutPercentage?: number; json: boolean; nonInteractive: boolean; - emitMetadata: boolean; }; export default class UpdatePublish extends EasCommand { @@ -120,6 +122,12 @@ export default class UpdatePublish extends EasCommand { description: `Emit "eas-update-metadata.json" in the bundle folder with detailed information about the generated updates`, default: false, }), + 'rollout-percentage': Flags.integer({ + description: `Percentage of users this update should be immediately available to. Users not in the rollout will be served the previous latest update on the branch, even if that update is itself being rolled out. The specified number must be an integer between 1 and 100. When not specified, this defaults to 100.`, + required: false, + min: 0, + max: 100, + }), platform: Flags.enum({ char: 'p', options: [ @@ -153,7 +161,7 @@ export default class UpdatePublish extends EasCommand { const paginatedQueryOptions = getPaginatedQueryOptions(rawFlags); const { auto: autoFlag, - platform: platformFlag, + platform: requestedPlatform, channelName: channelNameArg, updateMessage: updateMessageArg, inputDir, @@ -164,6 +172,7 @@ export default class UpdatePublish extends EasCommand { nonInteractive, branchName: branchNameArg, emitMetadata, + rolloutPercentage, } = this.sanitizeFlags(rawFlags); const { @@ -192,7 +201,7 @@ export default class UpdatePublish extends EasCommand { await ensureEASUpdateIsConfiguredAsync({ exp: expPossiblyWithoutEasUpdateConfigured, - platform: getRequestedPlatform(platformFlag), + platform: requestedPlatform, projectDir, projectId, vcsClient, @@ -225,7 +234,13 @@ export default class UpdatePublish extends EasCommand { if (!skipBundler) { const bundleSpinner = ora().start('Exporting...'); try { - await buildBundlesAsync({ projectDir, inputDir, exp, platformFlag, clearCache }); + await buildBundlesAsync({ + projectDir, + inputDir, + exp, + platformFlag: requestedPlatform, + clearCache, + }); bundleSpinner.succeed('Exported bundle(s)'); } catch (e) { bundleSpinner.fail('Export failed'); @@ -240,12 +255,12 @@ export default class UpdatePublish extends EasCommand { let unsortedUpdateInfoGroups: UpdateInfoGroup = {}; let uploadedAssetCount = 0; let assetLimitPerUpdateGroup = 0; - let realizedPlatforms: PublishPlatform[] = []; + let realizedPlatforms: UpdatePublishPlatform[] = []; try { const collectedAssets = await collectAssetsAsync(distRoot); - const assets = filterExportedPlatformsByFlag(collectedAssets, platformFlag); - realizedPlatforms = Object.keys(assets) as PublishPlatform[]; + const assets = filterCollectedAssetsByRequestedPlatforms(collectedAssets, requestedPlatform); + realizedPlatforms = Object.keys(assets) as UpdatePublishPlatform[]; // Timeout mechanism: // - Start with NO_ACTIVITY_TIMEOUT. 180 seconds is chosen because the cloud function that processes @@ -338,7 +353,7 @@ export default class UpdatePublish extends EasCommand { Log.debug(chalk.dim(`- ${uploadedAssetPath}`)); } - const platformString = (Object.keys(assets) as PublishPlatform[]) + const platformString = realizedPlatforms .map(platform => { const collectedAssetForPlatform = nullthrows(assets[platform]); const totalAssetsForPlatform = collectedAssetForPlatform.assets.length + 1; // launch asset @@ -376,6 +391,16 @@ export default class UpdatePublish extends EasCommand { branchName, }); + const runtimeVersionToRolloutInfoGroup = + rolloutPercentage !== undefined + ? await getRuntimeToUpdateRolloutInfoGroupMappingAsync(graphqlClient, { + appId: projectId, + branchName, + rolloutPercentage, + runtimeToPlatformMapping, + }) + : undefined; + const gitCommitHash = await vcsClient.getCommitHashAsync(); const isGitWorkingTreeDirty = await vcsClient.hasUncommittedChangesAsync(); @@ -389,9 +414,22 @@ export default class UpdatePublish extends EasCommand { ]) ); + const rolloutInfoGroupForRuntimeVersion = runtimeVersionToRolloutInfoGroup + ? runtimeVersionToRolloutInfoGroup.get(runtimeVersion) + : null; + const localRolloutInfoGroup = rolloutInfoGroupForRuntimeVersion + ? Object.fromEntries( + platforms.map(platform => [ + platform, + rolloutInfoGroupForRuntimeVersion[platform as keyof UpdateRolloutInfoGroup], + ]) + ) + : null; + return { branchId, updateInfoGroup: localUpdateInfoGroup, + rolloutInfoGroup: localRolloutInfoGroup, runtimeVersion, message: updateMessage, gitCommitHash, @@ -505,6 +543,22 @@ export default class UpdatePublish extends EasCommand { ? [{ label: 'Android update ID', value: newAndroidUpdate.id }] : []), ...(newIosUpdate ? [{ label: 'iOS update ID', value: newIosUpdate.id }] : []), + ...(newAndroidUpdate && newAndroidUpdate.rolloutControlUpdate + ? [ + { + label: 'Android Rollout', + value: `${newAndroidUpdate.rolloutPercentage}% (Base update ID: ${newAndroidUpdate.rolloutControlUpdate.id})`, + }, + ] + : []), + ...(newIosUpdate && newIosUpdate.rolloutControlUpdate + ? [ + { + label: 'iOS Rollout', + value: `${newIosUpdate.rolloutPercentage}% (Base update ID: ${newIosUpdate.rolloutControlUpdate.id})`, + }, + ] + : []), { label: 'Message', value: updateMessage ?? '' }, ...(gitCommitHash ? [ @@ -565,6 +619,7 @@ export default class UpdatePublish extends EasCommand { clearCache: flags['clear-cache'], platform: flags.platform as RequestedPlatform, privateKeyPath: flags['private-key-path'], + rolloutPercentage: flags['rollout-percentage'], nonInteractive, emitMetadata, json: flags.json ?? false, diff --git a/packages/eas-cli/src/commands/update/list.ts b/packages/eas-cli/src/commands/update/list.ts index f5a6f7333c..66f930513a 100644 --- a/packages/eas-cli/src/commands/update/list.ts +++ b/packages/eas-cli/src/commands/update/list.ts @@ -53,10 +53,13 @@ export default class UpdateList extends EasCommand { } if (all) { - listAndRenderUpdateGroupsOnAppAsync(graphqlClient, { projectId, paginatedQueryOptions }); + await listAndRenderUpdateGroupsOnAppAsync(graphqlClient, { + projectId, + paginatedQueryOptions, + }); } else { if (branchFlag) { - listAndRenderUpdateGroupsOnBranchAsync(graphqlClient, { + await listAndRenderUpdateGroupsOnBranchAsync(graphqlClient, { projectId, branchName: branchFlag, paginatedQueryOptions, @@ -81,7 +84,8 @@ export default class UpdateList extends EasCommand { offset: 0, }, }); - listAndRenderUpdateGroupsOnBranchAsync(graphqlClient, { + + await listAndRenderUpdateGroupsOnBranchAsync(graphqlClient, { projectId, branchName: selectedBranch.name, paginatedQueryOptions, diff --git a/packages/eas-cli/src/commands/update/roll-back-to-embedded.ts b/packages/eas-cli/src/commands/update/roll-back-to-embedded.ts index 7128ed4043..5cdee9faf1 100644 --- a/packages/eas-cli/src/commands/update/roll-back-to-embedded.ts +++ b/packages/eas-cli/src/commands/update/roll-back-to-embedded.ts @@ -24,10 +24,9 @@ import { getOwnerAccountForProjectIdAsync, } from '../../project/projectUtils'; import { - ExpoCLIExportPlatformFlag, + UpdatePublishPlatform, defaultPublishPlatforms, getBranchNameForCommandAsync, - getRequestedPlatform, getRuntimeToPlatformMappingFromRuntimeVersions, getRuntimeVersionObjectAsync, getUpdateMessageForCommandAsync, @@ -60,7 +59,7 @@ type RawUpdateFlags = { type UpdateFlags = { auto: boolean; - platform: ExpoCLIExportPlatformFlag; + platform: RequestedPlatform; branchName?: string; channelName?: string; updateMessage?: string; @@ -150,7 +149,7 @@ export default class UpdateRollBackToEmbedded extends EasCommand { await ensureEASUpdateIsConfiguredAsync({ exp: expPossiblyWithoutEasUpdateConfigured, - platform: getRequestedPlatform(platformFlag), + platform: platformFlag, projectDir, projectId, vcsClient, @@ -182,7 +181,7 @@ export default class UpdateRollBackToEmbedded extends EasCommand { jsonFlag, }); - const realizedPlatforms: PublishPlatform[] = + const realizedPlatforms: UpdatePublishPlatform[] = platformFlag === 'all' ? defaultPublishPlatforms : [platformFlag]; const { branchId } = await ensureBranchExistsAsync(graphqlClient, { @@ -300,7 +299,7 @@ export default class UpdateRollBackToEmbedded extends EasCommand { updateMessage: string | undefined; branchId: string; codeSigningInfo: CodeSigningInfo | undefined; - runtimeVersions: { platform: string; runtimeVersion: string }[]; + runtimeVersions: { platform: UpdatePublishPlatform; runtimeVersion: string }[]; realizedPlatforms: PublishPlatform[]; }): Promise { const runtimeToPlatformMapping = diff --git a/packages/eas-cli/src/graphql/generated.ts b/packages/eas-cli/src/graphql/generated.ts index c811b962aa..d19df38ddf 100644 --- a/packages/eas-cli/src/graphql/generated.ts +++ b/packages/eas-cli/src/graphql/generated.ts @@ -23,6 +23,7 @@ export type Scalars = { DevDomainName: { input: any; output: any; } JSON: { input: any; output: any; } JSONObject: { input: any; output: any; } + WorkerDeploymentIdentifier: { input: any; output: any; } }; export type AcceptUserInvitationResult = { @@ -1352,6 +1353,7 @@ export type App = Project & { workerDeployments: WorkerDeploymentsConnection; workerDeploymentsCrashes?: Maybe; workerDeploymentsMetrics?: Maybe; + workerDeploymentsRequests?: Maybe; }; @@ -1570,13 +1572,13 @@ export type AppWebhooksArgs = { /** Represents an Exponent App (or Experience in legacy terms) */ export type AppWorkerDeploymentArgs = { - deploymentIdentifier: Scalars['String']['input']; + deploymentIdentifier: Scalars['WorkerDeploymentIdentifier']['input']; }; /** Represents an Exponent App (or Experience in legacy terms) */ export type AppWorkerDeploymentAliasArgs = { - aliasName?: InputMaybe; + aliasName?: InputMaybe; }; @@ -1610,6 +1612,13 @@ export type AppWorkerDeploymentsMetricsArgs = { timespan: MetricsTimespan; }; + +/** Represents an Exponent App (or Experience in legacy terms) */ +export type AppWorkerDeploymentsRequestsArgs = { + limit?: Scalars['Int']['input']; + timespan: RequestsTimespan; +}; + export type AppBranchEdge = { __typename?: 'AppBranchEdge'; cursor: Scalars['String']['output']; @@ -2443,7 +2452,6 @@ export type AuditLog = { metadata?: Maybe; targetEntityId: Scalars['ID']['output']; targetEntityMutationType: TargetEntityMutationType; - targetEntityTableName: Scalars['String']['output']; targetEntityTypeName: Scalars['String']['output']; websiteMessage: Scalars['String']['output']; }; @@ -2477,6 +2485,8 @@ export type AuditLogQueryByAccountIdArgs = { accountId: Scalars['ID']['input']; limit: Scalars['Int']['input']; offset: Scalars['Int']['input']; + targetEntityMutationType?: InputMaybe>; + targetEntityTypeName?: InputMaybe>; }; export enum AuditLogsExportFormat { @@ -3146,6 +3156,17 @@ export type Concurrencies = { total: Scalars['Int']['output']; }; +export enum ContinentCode { + Af = 'AF', + An = 'AN', + As = 'AS', + Eu = 'EU', + Na = 'NA', + Oc = 'OC', + Sa = 'SA', + T1 = 'T1' +} + export type CrashesTimespan = { end: Scalars['DateTime']['input']; start?: InputMaybe; @@ -3308,7 +3329,7 @@ export type CustomDomainMutationRefreshCustomDomainArgs = { export type CustomDomainMutationRegisterCustomDomainArgs = { - aliasName?: InputMaybe; + aliasName?: InputMaybe; appId: Scalars['ID']['input']; hostname: Scalars['String']['input']; }; @@ -3345,7 +3366,7 @@ export type DeleteAccountSsoConfigurationResult = { export type DeleteAliasResult = { __typename?: 'DeleteAliasResult'; - aliasName?: Maybe; + aliasName?: Maybe; id: Scalars['ID']['output']; }; @@ -3562,7 +3583,7 @@ export type DeploymentsMutation = { export type DeploymentsMutationAssignAliasArgs = { - aliasName?: InputMaybe; + aliasName?: InputMaybe; appId: Scalars['ID']['input']; deploymentIdentifier: Scalars['ID']['input']; }; @@ -3575,7 +3596,7 @@ export type DeploymentsMutationCreateSignedDeploymentUrlArgs = { export type DeploymentsMutationDeleteAliasArgs = { - aliasName?: InputMaybe; + aliasName?: InputMaybe; appId: Scalars['ID']['input']; }; @@ -3680,6 +3701,28 @@ export type EmailSubscriptionMutationAddUserArgs = { addUserInput: AddUserInput; }; +export enum EntityTypeName { + Account = 'Account', + AccountSsoConfiguration = 'AccountSSOConfiguration', + AndroidAppCredentials = 'AndroidAppCredentials', + AndroidKeystore = 'AndroidKeystore', + App = 'App', + AppStoreConnectApiKey = 'AppStoreConnectApiKey', + AppleDevice = 'AppleDevice', + AppleDistributionCertificate = 'AppleDistributionCertificate', + AppleProvisioningProfile = 'AppleProvisioningProfile', + AppleTeam = 'AppleTeam', + Branch = 'Branch', + Channel = 'Channel', + Customer = 'Customer', + GoogleServiceAccountKey = 'GoogleServiceAccountKey', + IosAppCredentials = 'IosAppCredentials', + TurtleBuild = 'TurtleBuild', + Update = 'Update', + UserInvitation = 'UserInvitation', + UserPermission = 'UserPermission' +} + export type EnvironmentSecret = { __typename?: 'EnvironmentSecret'; createdAt: Scalars['DateTime']['output']; @@ -4851,7 +4894,7 @@ export type MeMutation = { export type MeMutationAddSecondFactorDeviceArgs = { deviceConfiguration: SecondFactorDeviceConfiguration; - otp: Scalars['String']['input']; + otp?: InputMaybe; }; @@ -4876,7 +4919,7 @@ export type MeMutationDeleteSsoUserArgs = { export type MeMutationDeleteSecondFactorDeviceArgs = { - otp: Scalars['String']['input']; + otp?: InputMaybe; userSecondFactorDeviceId: Scalars['ID']['input']; }; @@ -4887,7 +4930,7 @@ export type MeMutationDeleteSnackArgs = { export type MeMutationDisableSecondFactorAuthenticationArgs = { - otp: Scalars['String']['input']; + otp?: InputMaybe; }; @@ -4903,7 +4946,7 @@ export type MeMutationLeaveAccountArgs = { export type MeMutationRegenerateSecondFactorBackupCodesArgs = { - otp: Scalars['String']['input']; + otp?: InputMaybe; }; @@ -5190,11 +5233,17 @@ export type PublishUpdateGroupInput = { isGitWorkingTreeDirty?: InputMaybe; message?: InputMaybe; rollBackToEmbeddedInfoGroup?: InputMaybe; + rolloutInfoGroup?: InputMaybe; runtimeVersion: Scalars['String']['input']; turtleJobRunId?: InputMaybe; updateInfoGroup?: InputMaybe; }; +export type RequestsTimespan = { + end: Scalars['DateTime']['input']; + start?: InputMaybe; +}; + export type RescindUserInvitationResult = { __typename?: 'RescindUserInvitationResult'; id: Scalars['ID']['output']; @@ -5911,6 +5960,7 @@ export type Submission = ActivityTimelineProjectActivity & { maxRetryTimeMinutes: Scalars['Int']['output']; parentSubmission?: Maybe; platform: AppPlatform; + priority?: Maybe; status: SubmissionStatus; submittedBuild?: Maybe; updatedAt: Scalars['DateTime']['output']; @@ -5992,6 +6042,11 @@ export type SubmissionMutationRetrySubmissionArgs = { parentSubmissionId: Scalars['ID']['input']; }; +export enum SubmissionPriority { + High = 'HIGH', + Normal = 'NORMAL' +} + export type SubmissionQuery = { __typename?: 'SubmissionQuery'; /** Look up EAS Submission by submission ID */ @@ -6109,6 +6164,8 @@ export type Update = ActivityTimelineProjectActivity & { manifestPermalink: Scalars['String']['output']; message?: Maybe; platform: Scalars['String']['output']; + rolloutControlUpdate?: Maybe; + rolloutPercentage?: Maybe; runtime: Runtime; /** @deprecated Use 'runtime' field . */ runtimeVersion: Scalars['String']['output']; @@ -6288,6 +6345,8 @@ export type UpdateMutation = { deleteUpdateGroup: DeleteUpdateGroupResult; /** Set code signing info for an update */ setCodeSigningInfo: Update; + /** Set rollout percentage for an update */ + setRolloutPercentage: Update; }; @@ -6301,6 +6360,12 @@ export type UpdateMutationSetCodeSigningInfoArgs = { updateId: Scalars['ID']['input']; }; + +export type UpdateMutationSetRolloutPercentageArgs = { + percentage: Scalars['Int']['input']; + updateId: Scalars['ID']['input']; +}; + export type UpdateQuery = { __typename?: 'UpdateQuery'; /** Query an Update by ID */ @@ -6318,6 +6383,17 @@ export type UpdateRollBackToEmbeddedGroup = { web?: InputMaybe; }; +export type UpdateRolloutInfo = { + rolloutControlUpdateId: Scalars['ID']['input']; + rolloutPercentage: Scalars['Int']['input']; +}; + +export type UpdateRolloutInfoGroup = { + android?: InputMaybe; + ios?: InputMaybe; + web?: InputMaybe; +}; + export type UpdatesFilter = { platform?: InputMaybe; runtimeVersions?: InputMaybe>; @@ -6966,7 +7042,7 @@ export type WorkerDeployment = { aliases?: Maybe>; createdAt: Scalars['DateTime']['output']; deploymentDomain: Scalars['String']['output']; - deploymentIdentifier: Scalars['String']['output']; + deploymentIdentifier: Scalars['WorkerDeploymentIdentifier']['output']; devDomainName: Scalars['DevDomainName']['output']; id: Scalars['ID']['output']; logs?: Maybe; @@ -6988,7 +7064,7 @@ export type WorkerDeploymentMetricsArgs = { export type WorkerDeploymentAlias = { __typename?: 'WorkerDeploymentAlias'; - aliasName?: Maybe; + aliasName?: Maybe; createdAt: Scalars['DateTime']['output']; deploymentDomain: Scalars['String']['output']; devDomainName: Scalars['DevDomainName']['output']; @@ -7091,6 +7167,29 @@ export type WorkerDeploymentQueryByIdArgs = { id: Scalars['ID']['input']; }; +export type WorkerDeploymentRequestLocation = { + __typename?: 'WorkerDeploymentRequestLocation'; + continent?: Maybe; + countryCode?: Maybe; + regionCode?: Maybe; +}; + +export type WorkerDeploymentRequestNode = { + __typename?: 'WorkerDeploymentRequestNode'; + location?: Maybe; + method: Scalars['String']['output']; + pathname: Scalars['String']['output']; + search?: Maybe; + status: Scalars['Int']['output']; + timestamp: Scalars['DateTime']['output']; +}; + +export type WorkerDeploymentRequests = { + __typename?: 'WorkerDeploymentRequests'; + minRowsWithoutLimit?: Maybe; + nodes: Array; +}; + export type WorkerDeploymentsConnection = { __typename?: 'WorkerDeploymentsConnection'; edges: Array; @@ -7750,7 +7849,7 @@ export type UpdatePublishMutationVariables = Exact<{ }>; -export type UpdatePublishMutation = { __typename?: 'RootMutation', updateBranch: { __typename?: 'UpdateBranchMutation', publishUpdateGroups: Array<{ __typename?: 'Update', id: string, group: string, message?: string | null, createdAt: any, runtimeVersion: string, platform: string, manifestFragment: string, isRollBackToEmbedded: boolean, manifestPermalink: string, gitCommitHash?: string | null, actor?: { __typename: 'Robot', firstName?: string | null, id: string } | { __typename: 'SSOUser', username: string, id: string } | { __typename: 'User', username: string, id: string } | null, branch: { __typename?: 'UpdateBranch', id: string, name: string }, codeSigningInfo?: { __typename?: 'CodeSigningInfo', keyid: string, sig: string, alg: string } | null }> } }; +export type UpdatePublishMutation = { __typename?: 'RootMutation', updateBranch: { __typename?: 'UpdateBranchMutation', publishUpdateGroups: Array<{ __typename?: 'Update', id: string, group: string, message?: string | null, createdAt: any, runtimeVersion: string, platform: string, manifestFragment: string, isRollBackToEmbedded: boolean, manifestPermalink: string, gitCommitHash?: string | null, rolloutPercentage?: number | null, actor?: { __typename: 'Robot', firstName?: string | null, id: string } | { __typename: 'SSOUser', username: string, id: string } | { __typename: 'User', username: string, id: string } | null, branch: { __typename?: 'UpdateBranch', id: string, name: string }, codeSigningInfo?: { __typename?: 'CodeSigningInfo', keyid: string, sig: string, alg: string } | null, rolloutControlUpdate?: { __typename?: 'Update', id: string } | null }> } }; export type SetCodeSigningInfoMutationVariables = Exact<{ updateId: Scalars['ID']['input']; @@ -7848,6 +7947,16 @@ export type ViewBranchQueryVariables = Exact<{ export type ViewBranchQuery = { __typename?: 'RootQuery', app: { __typename?: 'AppQuery', byId: { __typename?: 'App', id: string, updateBranchByName?: { __typename?: 'UpdateBranch', id: string, name: string } | null } } }; +export type ViewLatestUpdateOnBranchQueryVariables = Exact<{ + appId: Scalars['String']['input']; + branchName: Scalars['String']['input']; + platform: AppPlatform; + runtimeVersion: Scalars['String']['input']; +}>; + + +export type ViewLatestUpdateOnBranchQuery = { __typename?: 'RootQuery', app: { __typename?: 'AppQuery', byId: { __typename?: 'App', id: string, updateBranchByName?: { __typename?: 'UpdateBranch', id: string, updates: Array<{ __typename?: 'Update', id: string }> } | null } } }; + export type BranchesByAppQueryVariables = Exact<{ appId: Scalars['String']['input']; limit: Scalars['Int']['input']; @@ -7855,7 +7964,7 @@ export type BranchesByAppQueryVariables = Exact<{ }>; -export type BranchesByAppQuery = { __typename?: 'RootQuery', app: { __typename?: 'AppQuery', byId: { __typename?: 'App', id: string, updateBranches: Array<{ __typename?: 'UpdateBranch', id: string, name: string, updates: Array<{ __typename?: 'Update', id: string, group: string, message?: string | null, createdAt: any, runtimeVersion: string, platform: string, manifestFragment: string, isRollBackToEmbedded: boolean, manifestPermalink: string, gitCommitHash?: string | null, actor?: { __typename: 'Robot', firstName?: string | null, id: string } | { __typename: 'SSOUser', username: string, id: string } | { __typename: 'User', username: string, id: string } | null, branch: { __typename?: 'UpdateBranch', id: string, name: string }, codeSigningInfo?: { __typename?: 'CodeSigningInfo', keyid: string, sig: string, alg: string } | null }> }> } } }; +export type BranchesByAppQuery = { __typename?: 'RootQuery', app: { __typename?: 'AppQuery', byId: { __typename?: 'App', id: string, updateBranches: Array<{ __typename?: 'UpdateBranch', id: string, name: string, updates: Array<{ __typename?: 'Update', id: string, group: string, message?: string | null, createdAt: any, runtimeVersion: string, platform: string, manifestFragment: string, isRollBackToEmbedded: boolean, manifestPermalink: string, gitCommitHash?: string | null, rolloutPercentage?: number | null, actor?: { __typename: 'Robot', firstName?: string | null, id: string } | { __typename: 'SSOUser', username: string, id: string } | { __typename: 'User', username: string, id: string } | null, branch: { __typename?: 'UpdateBranch', id: string, name: string }, codeSigningInfo?: { __typename?: 'CodeSigningInfo', keyid: string, sig: string, alg: string } | null, rolloutControlUpdate?: { __typename?: 'Update', id: string } | null }> }> } } }; export type BranchesBasicPaginatedOnAppQueryVariables = Exact<{ appId: Scalars['String']['input']; @@ -7876,7 +7985,7 @@ export type ViewBranchesOnUpdateChannelQueryVariables = Exact<{ }>; -export type ViewBranchesOnUpdateChannelQuery = { __typename?: 'RootQuery', app: { __typename?: 'AppQuery', byId: { __typename?: 'App', id: string, updateChannelByName?: { __typename?: 'UpdateChannel', id: string, updateBranches: Array<{ __typename?: 'UpdateBranch', id: string, name: string, updateGroups: Array> }> } | null } } }; +export type ViewBranchesOnUpdateChannelQuery = { __typename?: 'RootQuery', app: { __typename?: 'AppQuery', byId: { __typename?: 'App', id: string, updateChannelByName?: { __typename?: 'UpdateChannel', id: string, updateBranches: Array<{ __typename?: 'UpdateBranch', id: string, name: string, updateGroups: Array> }> } | null } } }; export type BuildsByIdQueryVariables = Exact<{ buildId: Scalars['ID']['input']; @@ -7909,7 +8018,7 @@ export type ViewUpdateChannelOnAppQueryVariables = Exact<{ }>; -export type ViewUpdateChannelOnAppQuery = { __typename?: 'RootQuery', app: { __typename?: 'AppQuery', byId: { __typename?: 'App', id: string, updateChannelByName?: { __typename?: 'UpdateChannel', id: string, name: string, updatedAt: any, createdAt: any, branchMapping: string, updateBranches: Array<{ __typename?: 'UpdateBranch', id: string, name: string, updateGroups: Array> }> } | null } } }; +export type ViewUpdateChannelOnAppQuery = { __typename?: 'RootQuery', app: { __typename?: 'AppQuery', byId: { __typename?: 'App', id: string, updateChannelByName?: { __typename?: 'UpdateChannel', id: string, name: string, updatedAt: any, createdAt: any, branchMapping: string, updateBranches: Array<{ __typename?: 'UpdateBranch', id: string, name: string, updateGroups: Array> }> } | null } } }; export type ViewUpdateChannelsOnAppQueryVariables = Exact<{ appId: Scalars['String']['input']; @@ -7918,7 +8027,7 @@ export type ViewUpdateChannelsOnAppQueryVariables = Exact<{ }>; -export type ViewUpdateChannelsOnAppQuery = { __typename?: 'RootQuery', app: { __typename?: 'AppQuery', byId: { __typename?: 'App', id: string, updateChannels: Array<{ __typename?: 'UpdateChannel', id: string, name: string, updatedAt: any, createdAt: any, branchMapping: string, updateBranches: Array<{ __typename?: 'UpdateBranch', id: string, name: string, updateGroups: Array> }> }> } } }; +export type ViewUpdateChannelsOnAppQuery = { __typename?: 'RootQuery', app: { __typename?: 'AppQuery', byId: { __typename?: 'App', id: string, updateChannels: Array<{ __typename?: 'UpdateChannel', id: string, name: string, updatedAt: any, createdAt: any, branchMapping: string, updateBranches: Array<{ __typename?: 'UpdateBranch', id: string, name: string, updateGroups: Array> }> }> } } }; export type ViewUpdateChannelsPaginatedOnAppQueryVariables = Exact<{ appId: Scalars['String']['input']; @@ -7933,12 +8042,20 @@ export type ViewUpdateChannelsPaginatedOnAppQuery = { __typename?: 'RootQuery', export type EnvironmentSecretsByAppIdQueryVariables = Exact<{ appId: Scalars['String']['input']; - filterNames?: InputMaybe | Scalars['String']['input']>; }>; export type EnvironmentSecretsByAppIdQuery = { __typename?: 'RootQuery', app: { __typename?: 'AppQuery', byId: { __typename?: 'App', id: string, ownerAccount: { __typename?: 'Account', id: string, environmentSecrets: Array<{ __typename?: 'EnvironmentSecret', id: string, name: string, type: EnvironmentSecretType, createdAt: any }> }, environmentSecrets: Array<{ __typename?: 'EnvironmentSecret', id: string, name: string, type: EnvironmentSecretType, createdAt: any }> } } }; +export type EnvironmentVariablesIncludingSensitiveByAppIdQueryVariables = Exact<{ + appId: Scalars['String']['input']; + filterNames?: InputMaybe | Scalars['String']['input']>; + environment: EnvironmentVariableEnvironment; +}>; + + +export type EnvironmentVariablesIncludingSensitiveByAppIdQuery = { __typename?: 'RootQuery', app: { __typename?: 'AppQuery', byId: { __typename?: 'App', id: string, environmentVariablesIncludingSensitive: Array<{ __typename?: 'EnvironmentVariableWithSecret', id: string, name: string, value?: string | null }> } } }; + export type EnvironmentVariablesByAppIdQueryVariables = Exact<{ appId: Scalars['String']['input']; filterNames?: InputMaybe | Scalars['String']['input']>; @@ -7946,7 +8063,7 @@ export type EnvironmentVariablesByAppIdQueryVariables = Exact<{ }>; -export type EnvironmentVariablesByAppIdQuery = { __typename?: 'RootQuery', app: { __typename?: 'AppQuery', byId: { __typename?: 'App', id: string, ownerAccount: { __typename?: 'Account', id: string, environmentVariables: Array<{ __typename?: 'EnvironmentVariable', id: string, name: string, value?: string | null, environment?: EnvironmentVariableEnvironment | null, createdAt: any, scope: EnvironmentVariableScope, visibility?: EnvironmentVariableVisibility | null }> }, environmentVariables: Array<{ __typename?: 'EnvironmentVariable', id: string, name: string, value?: string | null, environment?: EnvironmentVariableEnvironment | null, createdAt: any, scope: EnvironmentVariableScope, visibility?: EnvironmentVariableVisibility | null }> } } }; +export type EnvironmentVariablesByAppIdQuery = { __typename?: 'RootQuery', app: { __typename?: 'AppQuery', byId: { __typename?: 'App', id: string, environmentVariables: Array<{ __typename?: 'EnvironmentVariable', id: string, name: string, value?: string | null, environment?: EnvironmentVariableEnvironment | null, createdAt: any, scope: EnvironmentVariableScope, visibility?: EnvironmentVariableVisibility | null }> } } }; export type EnvironmentVariablesSharedQueryVariables = Exact<{ appId: Scalars['String']['input']; @@ -8013,7 +8130,7 @@ export type ViewUpdatesByGroupQueryVariables = Exact<{ }>; -export type ViewUpdatesByGroupQuery = { __typename?: 'RootQuery', updatesByGroup: Array<{ __typename?: 'Update', id: string, group: string, message?: string | null, createdAt: any, runtimeVersion: string, platform: string, manifestFragment: string, isRollBackToEmbedded: boolean, manifestPermalink: string, gitCommitHash?: string | null, actor?: { __typename: 'Robot', firstName?: string | null, id: string } | { __typename: 'SSOUser', username: string, id: string } | { __typename: 'User', username: string, id: string } | null, branch: { __typename?: 'UpdateBranch', id: string, name: string }, codeSigningInfo?: { __typename?: 'CodeSigningInfo', keyid: string, sig: string, alg: string } | null }> }; +export type ViewUpdatesByGroupQuery = { __typename?: 'RootQuery', updatesByGroup: Array<{ __typename?: 'Update', id: string, group: string, message?: string | null, createdAt: any, runtimeVersion: string, platform: string, manifestFragment: string, isRollBackToEmbedded: boolean, manifestPermalink: string, gitCommitHash?: string | null, rolloutPercentage?: number | null, actor?: { __typename: 'Robot', firstName?: string | null, id: string } | { __typename: 'SSOUser', username: string, id: string } | { __typename: 'User', username: string, id: string } | null, branch: { __typename?: 'UpdateBranch', id: string, name: string }, codeSigningInfo?: { __typename?: 'CodeSigningInfo', keyid: string, sig: string, alg: string } | null, rolloutControlUpdate?: { __typename?: 'Update', id: string } | null }> }; export type ViewUpdateGroupsOnBranchQueryVariables = Exact<{ appId: Scalars['String']['input']; @@ -8024,7 +8141,7 @@ export type ViewUpdateGroupsOnBranchQueryVariables = Exact<{ }>; -export type ViewUpdateGroupsOnBranchQuery = { __typename?: 'RootQuery', app: { __typename?: 'AppQuery', byId: { __typename?: 'App', id: string, updateBranchByName?: { __typename?: 'UpdateBranch', id: string, updateGroups: Array> } | null } } }; +export type ViewUpdateGroupsOnBranchQuery = { __typename?: 'RootQuery', app: { __typename?: 'AppQuery', byId: { __typename?: 'App', id: string, updateBranchByName?: { __typename?: 'UpdateBranch', id: string, updateGroups: Array> } | null } } }; export type ViewUpdateGroupsOnAppQueryVariables = Exact<{ appId: Scalars['String']['input']; @@ -8034,7 +8151,7 @@ export type ViewUpdateGroupsOnAppQueryVariables = Exact<{ }>; -export type ViewUpdateGroupsOnAppQuery = { __typename?: 'RootQuery', app: { __typename?: 'AppQuery', byId: { __typename?: 'App', id: string, updateGroups: Array> } } }; +export type ViewUpdateGroupsOnAppQuery = { __typename?: 'RootQuery', app: { __typename?: 'AppQuery', byId: { __typename?: 'App', id: string, updateGroups: Array> } } }; export type CurrentUserQueryVariables = Exact<{ [key: string]: never; }>; @@ -8074,9 +8191,9 @@ export type StatuspageServiceFragment = { __typename?: 'StatuspageService', id: export type SubmissionFragment = { __typename?: 'Submission', id: string, status: SubmissionStatus, platform: AppPlatform, logsUrl?: string | null, app: { __typename?: 'App', id: string, name: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string } }, androidConfig?: { __typename?: 'AndroidSubmissionConfig', applicationIdentifier?: string | null, track: SubmissionAndroidTrack, releaseStatus?: SubmissionAndroidReleaseStatus | null, rollout?: number | null } | null, iosConfig?: { __typename?: 'IosSubmissionConfig', ascAppIdentifier: string, appleIdUsername?: string | null } | null, error?: { __typename?: 'SubmissionError', errorCode?: string | null, message?: string | null } | null }; -export type UpdateFragment = { __typename?: 'Update', id: string, group: string, message?: string | null, createdAt: any, runtimeVersion: string, platform: string, manifestFragment: string, isRollBackToEmbedded: boolean, manifestPermalink: string, gitCommitHash?: string | null, actor?: { __typename: 'Robot', firstName?: string | null, id: string } | { __typename: 'SSOUser', username: string, id: string } | { __typename: 'User', username: string, id: string } | null, branch: { __typename?: 'UpdateBranch', id: string, name: string }, codeSigningInfo?: { __typename?: 'CodeSigningInfo', keyid: string, sig: string, alg: string } | null }; +export type UpdateFragment = { __typename?: 'Update', id: string, group: string, message?: string | null, createdAt: any, runtimeVersion: string, platform: string, manifestFragment: string, isRollBackToEmbedded: boolean, manifestPermalink: string, gitCommitHash?: string | null, rolloutPercentage?: number | null, actor?: { __typename: 'Robot', firstName?: string | null, id: string } | { __typename: 'SSOUser', username: string, id: string } | { __typename: 'User', username: string, id: string } | null, branch: { __typename?: 'UpdateBranch', id: string, name: string }, codeSigningInfo?: { __typename?: 'CodeSigningInfo', keyid: string, sig: string, alg: string } | null, rolloutControlUpdate?: { __typename?: 'Update', id: string } | null }; -export type UpdateBranchFragment = { __typename?: 'UpdateBranch', id: string, name: string, updates: Array<{ __typename?: 'Update', id: string, group: string, message?: string | null, createdAt: any, runtimeVersion: string, platform: string, manifestFragment: string, isRollBackToEmbedded: boolean, manifestPermalink: string, gitCommitHash?: string | null, actor?: { __typename: 'Robot', firstName?: string | null, id: string } | { __typename: 'SSOUser', username: string, id: string } | { __typename: 'User', username: string, id: string } | null, branch: { __typename?: 'UpdateBranch', id: string, name: string }, codeSigningInfo?: { __typename?: 'CodeSigningInfo', keyid: string, sig: string, alg: string } | null }> }; +export type UpdateBranchFragment = { __typename?: 'UpdateBranch', id: string, name: string, updates: Array<{ __typename?: 'Update', id: string, group: string, message?: string | null, createdAt: any, runtimeVersion: string, platform: string, manifestFragment: string, isRollBackToEmbedded: boolean, manifestPermalink: string, gitCommitHash?: string | null, rolloutPercentage?: number | null, actor?: { __typename: 'Robot', firstName?: string | null, id: string } | { __typename: 'SSOUser', username: string, id: string } | { __typename: 'User', username: string, id: string } | null, branch: { __typename?: 'UpdateBranch', id: string, name: string }, codeSigningInfo?: { __typename?: 'CodeSigningInfo', keyid: string, sig: string, alg: string } | null, rolloutControlUpdate?: { __typename?: 'Update', id: string } | null }> }; export type UpdateBranchBasicInfoFragment = { __typename?: 'UpdateBranch', id: string, name: string }; diff --git a/packages/eas-cli/src/graphql/queries/BranchQuery.ts b/packages/eas-cli/src/graphql/queries/BranchQuery.ts index 068a46c3de..c64f12c464 100644 --- a/packages/eas-cli/src/graphql/queries/BranchQuery.ts +++ b/packages/eas-cli/src/graphql/queries/BranchQuery.ts @@ -5,6 +5,7 @@ import { BranchNotFoundError } from '../../branch/utils'; import { ExpoGraphqlClient } from '../../commandUtils/context/contextUtils/createGraphqlClient'; import { withErrorHandlingAsync } from '../client'; import { + AppPlatform, BranchesBasicPaginatedOnAppQuery, BranchesBasicPaginatedOnAppQueryVariables, BranchesByAppQuery, @@ -15,6 +16,8 @@ import { ViewBranchQueryVariables, ViewBranchesOnUpdateChannelQuery, ViewBranchesOnUpdateChannelQueryVariables, + ViewLatestUpdateOnBranchQuery, + ViewLatestUpdateOnBranchQueryVariables, } from '../generated'; import { UpdateFragmentNode } from '../types/Update'; import { UpdateBranchFragmentNode } from '../types/UpdateBranch'; @@ -60,6 +63,62 @@ export const BranchQuery = { } return updateBranchByName; }, + async getLatestUpdateIdOnBranchAsync( + graphqlClient: ExpoGraphqlClient, + { + appId, + branchName, + platform, + runtimeVersion, + }: { appId: string; branchName: string; platform: AppPlatform; runtimeVersion: string } + ): Promise { + const response = await withErrorHandlingAsync( + graphqlClient + .query( + gql` + query ViewLatestUpdateOnBranch( + $appId: String! + $branchName: String! + $platform: AppPlatform! + $runtimeVersion: String! + ) { + app { + byId(appId: $appId) { + id + updateBranchByName(name: $branchName) { + id + updates( + offset: 0 + limit: 1 + filter: { platform: $platform, runtimeVersions: [$runtimeVersion] } + ) { + id + } + } + } + } + } + `, + { + appId, + branchName, + platform, + runtimeVersion, + }, + { additionalTypenames: ['UpdateBranch'] } + ) + .toPromise() + ); + const { updateBranchByName } = response.app.byId; + if (!updateBranchByName) { + throw new BranchNotFoundError(`Could not find a branch named "${branchName}".`); + } + const latestUpdate = updateBranchByName.updates[0]; + if (!latestUpdate) { + return null; + } + return latestUpdate.id; + }, async listBranchesOnAppAsync( graphqlClient: ExpoGraphqlClient, { appId, limit, offset }: BranchesByAppQueryVariables diff --git a/packages/eas-cli/src/graphql/queries/EnvironmentVariablesQuery.ts b/packages/eas-cli/src/graphql/queries/EnvironmentVariablesQuery.ts index 14c722e492..9029195a45 100644 --- a/packages/eas-cli/src/graphql/queries/EnvironmentVariablesQuery.ts +++ b/packages/eas-cli/src/graphql/queries/EnvironmentVariablesQuery.ts @@ -7,6 +7,8 @@ import { EnvironmentVariableEnvironment, EnvironmentVariableFragment, EnvironmentVariablesByAppIdQuery, + EnvironmentVariablesSharedQuery, + EnvironmentVariablesSharedQueryVariables, } from '../generated'; import { EnvironmentVariableFragmentNode } from '../types/EnvironmentVariable'; @@ -101,7 +103,7 @@ export const EnvironmentVariablesQuery = { ): Promise { const data = await withErrorHandlingAsync( graphqlClient - .query( + .query( gql` query EnvironmentVariablesShared($appId: String!, $filterNames: [String!]) { app { diff --git a/packages/eas-cli/src/graphql/types/Update.ts b/packages/eas-cli/src/graphql/types/Update.ts index 98c33a2ad8..3b9de01322 100644 --- a/packages/eas-cli/src/graphql/types/Update.ts +++ b/packages/eas-cli/src/graphql/types/Update.ts @@ -31,5 +31,9 @@ export const UpdateFragmentNode = gql` sig alg } + rolloutPercentage + rolloutControlUpdate { + id + } } `; diff --git a/packages/eas-cli/src/project/__tests__/publish-test.ts b/packages/eas-cli/src/project/__tests__/publish-test.ts index 9da4d1eb27..8b5e04b66c 100644 --- a/packages/eas-cli/src/project/__tests__/publish-test.ts +++ b/packages/eas-cli/src/project/__tests__/publish-test.ts @@ -8,13 +8,15 @@ import { ExpoGraphqlClient } from '../../commandUtils/context/contextUtils/creat import { AssetMetadataStatus } from '../../graphql/generated'; import { PublishMutation } from '../../graphql/mutations/PublishMutation'; import { PublishQuery } from '../../graphql/queries/PublishQuery'; +import { RequestedPlatform } from '../../platform'; import { MetadataJoi, + RawAsset, buildUnsortedUpdateInfoGroupAsync, collectAssetsAsync, convertAssetToUpdateInfoGroupFormatAsync, defaultPublishPlatforms, - filterExportedPlatformsByFlag, + filterCollectedAssetsByRequestedPlatforms, filterOutAssetsThatAlreadyExistAsync, getAssetHashFromPath, getBase64URLEncoding, @@ -94,22 +96,47 @@ describe('MetadataJoi', () => { }); }); -describe(filterExportedPlatformsByFlag, () => { +describe(filterCollectedAssetsByRequestedPlatforms, () => { + const rawAsset: RawAsset = { + contentType: 'test', + path: 'wat', + }; + it(`returns all`, () => { - expect(filterExportedPlatformsByFlag({ web: true, ios: true, android: true }, 'all')).toEqual({ - web: true, - ios: true, - android: true, + expect( + filterCollectedAssetsByRequestedPlatforms( + { + web: { launchAsset: rawAsset, assets: [] }, + ios: { launchAsset: rawAsset, assets: [] }, + android: { launchAsset: rawAsset, assets: [] }, + }, + RequestedPlatform.All + ) + ).toEqual({ + ios: { launchAsset: rawAsset, assets: [] }, + android: { launchAsset: rawAsset, assets: [] }, }); }); it(`selects a platform`, () => { - expect(filterExportedPlatformsByFlag({ web: true, ios: true, android: true }, 'ios')).toEqual({ - ios: true, + expect( + filterCollectedAssetsByRequestedPlatforms( + { + web: { launchAsset: rawAsset, assets: [] }, + ios: { launchAsset: rawAsset, assets: [] }, + android: { launchAsset: rawAsset, assets: [] }, + }, + RequestedPlatform.Ios + ) + ).toEqual({ + ios: { launchAsset: rawAsset, assets: [] }, }); }); it(`asserts selected platform missing`, () => { expect(() => - filterExportedPlatformsByFlag({ web: true }, 'ios') + filterCollectedAssetsByRequestedPlatforms( + { web: { launchAsset: rawAsset, assets: [] } }, + RequestedPlatform.Ios + ) ).toThrowErrorMatchingInlineSnapshot( `"--platform="ios" not found in metadata.json. Available platform(s): web"` ); diff --git a/packages/eas-cli/src/project/publish.ts b/packages/eas-cli/src/project/publish.ts index e367215341..04b11d6a97 100644 --- a/packages/eas-cli/src/project/publish.ts +++ b/packages/eas-cli/src/project/publish.ts @@ -1,4 +1,4 @@ -import { ExpoConfig, Platform } from '@expo/config'; +import { ExpoConfig, Platform as ExpoConfigPlatform } from '@expo/config'; import { Updates } from '@expo/config-plugins'; import { Env, Workflow } from '@expo/eas-build-job'; import JsonFile from '@expo/json-file'; @@ -17,8 +17,15 @@ import { selectBranchOnAppAsync } from '../branch/queries'; import { getDefaultBranchNameAsync } from '../branch/utils'; import { ExpoGraphqlClient } from '../commandUtils/context/contextUtils/createGraphqlClient'; import { PaginatedQueryOptions } from '../commandUtils/pagination'; -import { AssetMetadataStatus, PartialManifestAsset } from '../graphql/generated'; +import { + AppPlatform, + AssetMetadataStatus, + PartialManifestAsset, + UpdateRolloutInfo, + UpdateRolloutInfoGroup, +} from '../graphql/generated'; import { PublishMutation } from '../graphql/mutations/PublishMutation'; +import { BranchQuery } from '../graphql/queries/BranchQuery'; import { PublishQuery } from '../graphql/queries/PublishQuery'; import Log, { learnMore } from '../log'; import { RequestedPlatform, requestedPlatformDisplayNames } from '../platform'; @@ -44,13 +51,14 @@ import { truthy } from '../utils/expodash/filter'; import uniqBy from '../utils/expodash/uniqBy'; import { Client } from '../vcs/vcs'; -export type ExpoCLIExportPlatformFlag = Platform | 'all'; +// update publish does not currently support web +export type UpdatePublishPlatform = 'ios' | 'android'; type Metadata = { version: number; bundler: 'metro'; fileMetadata: { - [key in Platform]: { assets: { path: string; ext: string }[]; bundle: string }; + [key in ExpoConfigPlatform]: { assets: { path: string; ext: string }[]; bundle: string }; }; }; export type RawAsset = { @@ -62,7 +70,7 @@ export type RawAsset = { }; type CollectedAssets = { - [platform in Platform]?: { + [platform in ExpoConfigPlatform]?: { launchAsset: RawAsset; assets: RawAsset[]; }; @@ -78,7 +86,7 @@ type ManifestFragment = { extra?: ManifestExtra; }; type UpdateInfoGroup = { - [key in Platform]: ManifestFragment; + [key in UpdatePublishPlatform]: ManifestFragment; }; // Partial copy of `@expo/dev-server` `BundleAssetWithFileHashes` @@ -168,10 +176,10 @@ export async function convertAssetToUpdateInfoGroupFormatAsync( * This will be sorted later based on the platform's runtime versions. */ export async function buildUnsortedUpdateInfoGroupAsync( - assets: CollectedAssets, + assets: FilteredCollectedAssets, exp: ExpoConfig -): Promise { - let platform: Platform; +): Promise> { + let platform: 'ios' | 'android'; const updateInfoGroup: Partial = {}; for (platform in assets) { updateInfoGroup[platform] = { @@ -184,9 +192,11 @@ export async function buildUnsortedUpdateInfoGroupAsync( }, }; } - return updateInfoGroup as UpdateInfoGroup; + return updateInfoGroup; } +export type ExpoCLIExportPlatformFlag = ExpoConfigPlatform | 'all'; + export async function buildBundlesAsync({ projectDir, inputDir, @@ -305,25 +315,35 @@ export async function generateEasMetadataAsync( await JsonFile.writeAsync(easMetadataPath, { updates: metadata }); } -export function filterExportedPlatformsByFlag>>( - record: T, - platformFlag: ExpoCLIExportPlatformFlag -): T { - if (platformFlag === 'all') { - return record; +export type FilteredCollectedAssets = { + [RequestedPlatform.Ios]?: NonNullable; + [RequestedPlatform.Android]?: NonNullable; +}; + +export function filterCollectedAssetsByRequestedPlatforms( + collectedAssets: CollectedAssets, + requestedPlatform: RequestedPlatform +): FilteredCollectedAssets { + if (requestedPlatform === RequestedPlatform.All) { + return { + ...('ios' in collectedAssets ? { [RequestedPlatform.Ios]: collectedAssets['ios'] } : {}), + ...('android' in collectedAssets + ? { [RequestedPlatform.Android]: collectedAssets['android'] } + : {}), + }; } - const platform = platformFlag as Platform; + const collectedAssetsKey = requestedPlatform === RequestedPlatform.Android ? 'android' : 'ios'; - if (!record[platform]) { + if (!collectedAssets[collectedAssetsKey]) { throw new Error( - `--platform="${platform}" not found in metadata.json. Available platform(s): ${Object.keys( - record + `--platform="${collectedAssetsKey}" not found in metadata.json. Available platform(s): ${Object.keys( + collectedAssets ).join(', ')}` ); } - return { [platform]: record[platform] } as T; + return { [requestedPlatform]: collectedAssets[collectedAssetsKey] }; } /** Try to load the asset map for logging the names of assets published */ @@ -371,7 +391,7 @@ export async function collectAssetsAsync(dir: string): Promise const collectedAssets: CollectedAssets = {}; - for (const platform of Object.keys(metadata.fileMetadata) as Platform[]) { + for (const platform of Object.keys(metadata.fileMetadata) as ExpoConfigPlatform[]) { collectedAssets[platform] = { launchAsset: { fileExtension: '.bundle', @@ -430,7 +450,7 @@ type AssetUploadResult = { export async function uploadAssetsAsync( graphqlClient: ExpoGraphqlClient, - assetsForUpdateInfoGroup: CollectedAssets, + assetsForUpdateInfoGroup: FilteredCollectedAssets, projectId: string, cancelationToken: { isCanceledOrFinished: boolean }, onAssetUploadResultsChanged: ( @@ -439,7 +459,7 @@ export async function uploadAssetsAsync( onAssetUploadBegin: () => void ): Promise { let assets: RawAsset[] = []; - let platform: keyof CollectedAssets; + let platform: keyof FilteredCollectedAssets; const launchAssets: RawAsset[] = []; for (platform in assetsForUpdateInfoGroup) { launchAssets.push(assetsForUpdateInfoGroup[platform]!.launchAsset); @@ -671,26 +691,8 @@ export async function getUpdateMessageForCommandAsync( return truncatedMessage; } -export const defaultPublishPlatforms: Platform[] = ['android', 'ios']; - -export function getRequestedPlatform( - platform: ExpoCLIExportPlatformFlag -): RequestedPlatform | null { - switch (platform) { - case 'android': - return RequestedPlatform.Android; - case 'ios': - return RequestedPlatform.Ios; - case 'web': - return null; - case 'all': - return RequestedPlatform.All; - default: - throw new Error(`Unsupported platform: ${platform}`); - } -} +export const defaultPublishPlatforms: UpdatePublishPlatform[] = ['android', 'ios']; -/** Get runtime versions grouped by platform. Runtime version is always `null` on web where the platform is always backwards compatible. */ export async function getRuntimeVersionObjectAsync({ exp, platforms, @@ -699,11 +701,11 @@ export async function getRuntimeVersionObjectAsync({ env, }: { exp: ExpoConfig; - platforms: Platform[]; - workflows: Record; + platforms: UpdatePublishPlatform[]; + workflows: Record; projectDir: string; env: Env | undefined; -}): Promise<{ platform: string; runtimeVersion: string }[]> { +}): Promise<{ platform: UpdatePublishPlatform; runtimeVersion: string }[]> { return await Promise.all( platforms.map(async platform => { return { @@ -728,15 +730,11 @@ async function getRuntimeVersionForPlatformAsync({ env, }: { exp: ExpoConfig; - platform: Platform; + platform: UpdatePublishPlatform; workflow: Workflow; projectDir: string; env: Env | undefined; }): Promise { - if (platform === 'web') { - return 'UNVERSIONED'; - } - if (await isModernExpoUpdatesCLIWithRuntimeVersionCommandSupportedAsync(projectDir)) { try { Log.debug('Using expo-updates runtimeversion:resolve CLI for runtime version resolution'); @@ -792,9 +790,10 @@ async function getRuntimeVersionForPlatformAsync({ } export function getRuntimeToPlatformMappingFromRuntimeVersions( - runtimeVersions: { platform: string; runtimeVersion: string }[] -): { runtimeVersion: string; platforms: string[] }[] { - const runtimeToPlatformMapping: { runtimeVersion: string; platforms: string[] }[] = []; + runtimeVersions: { platform: UpdatePublishPlatform; runtimeVersion: string }[] +): { runtimeVersion: string; platforms: UpdatePublishPlatform[] }[] { + const runtimeToPlatformMapping: { runtimeVersion: string; platforms: UpdatePublishPlatform[] }[] = + []; for (const runtime of runtimeVersions) { const platforms = runtimeVersions .filter(({ runtimeVersion }) => runtimeVersion === runtime.runtimeVersion) @@ -806,8 +805,73 @@ export function getRuntimeToPlatformMappingFromRuntimeVersions( return runtimeToPlatformMapping; } -export const platformDisplayNames: Record = { +export const platformDisplayNames: Record = { android: 'Android', ios: 'iOS', - web: 'Web', }; + +export const updatePublishPlatformToAppPlatform: Record = { + android: AppPlatform.Android, + ios: AppPlatform.Ios, +}; + +const mapMapAsync = async function ( + map: ReadonlyMap, + mapper: (value: V, key: K) => Promise +): Promise> { + const resultingMap: Map = new Map(); + await Promise.all( + Array.from(map.keys()).map(async k => { + const initialValue = map.get(k) as V; + const result = await mapper(initialValue, k); + resultingMap.set(k, result); + }) + ); + return resultingMap; +}; + +export async function getRuntimeToUpdateRolloutInfoGroupMappingAsync( + graphqlClient: ExpoGraphqlClient, + { + appId, + branchName, + rolloutPercentage, + runtimeToPlatformMapping, + }: { + appId: string; + branchName: string; + rolloutPercentage: number; + runtimeToPlatformMapping: { runtimeVersion: string; platforms: UpdatePublishPlatform[] }[]; + } +): Promise> { + const runtimeToPlatformsMap = new Map( + runtimeToPlatformMapping.map<[string, UpdatePublishPlatform[]]>(r => [ + r.runtimeVersion, + r.platforms, + ]) + ); + return mapMapAsync(runtimeToPlatformsMap, async (platforms, runtimeVersion) => { + return Object.fromEntries( + await Promise.all( + platforms.map>(async platform => { + const updateIdForPlatform = await BranchQuery.getLatestUpdateIdOnBranchAsync( + graphqlClient, + { + appId, + branchName, + runtimeVersion, + platform: updatePublishPlatformToAppPlatform[platform], + } + ); + if (!updateIdForPlatform) { + throw new Error( + `No updates on branch ${branchName} for platform ${platform} and runtimeVersion ${runtimeVersion} to roll out from.` + ); + } + + return [platform, { rolloutPercentage, rolloutControlUpdateId: updateIdForPlatform }]; + }) + ) + ); + }); +} diff --git a/packages/eas-cli/src/update/utils.ts b/packages/eas-cli/src/update/utils.ts index 8e10a8c73b..b5ba07d298 100644 --- a/packages/eas-cli/src/update/utils.ts +++ b/packages/eas-cli/src/update/utils.ts @@ -52,6 +52,7 @@ export type FormattedUpdateGroupDescription = { runtimeVersion: string; codeSigningKey: string | undefined; isRollBackToEmbedded: boolean; + rolloutPercentage: number | undefined; }; export type FormattedBranchDescription = { @@ -80,6 +81,10 @@ export function formatUpdateGroup(update: FormattedUpdateGroupDescription): stri { label: 'Message', value: update.message }, { label: 'Code Signing Key', value: update.codeSigningKey ?? 'N/A' }, { label: 'Is Roll Back to Embedded', value: update.isRollBackToEmbedded ? 'Yes' : 'No' }, + { + label: 'Rollout Percentage', + value: update.rolloutPercentage !== undefined ? `${update.rolloutPercentage}%` : 'N/A', + }, { label: 'Group ID', value: update.group }, ]); } @@ -221,6 +226,7 @@ export function getUpdateGroupDescriptions( message: formatUpdateMessage(updateGroup[0]), runtimeVersion: updateGroup[0].runtimeVersion, isRollBackToEmbedded: updateGroup[0].isRollBackToEmbedded, + rolloutPercentage: updateGroup[0].rolloutPercentage ?? undefined, codeSigningKey: updateGroup[0].codeSigningInfo?.keyid, group: updateGroup[0].group, platforms: formatPlatformForUpdateGroup(updateGroup), @@ -235,6 +241,7 @@ export function getUpdateGroupDescriptionsWithBranch( message: formatUpdateMessage(updateGroup[0]), runtimeVersion: updateGroup[0].runtimeVersion, isRollBackToEmbedded: updateGroup[0].isRollBackToEmbedded, + rolloutPercentage: updateGroup[0].rolloutPercentage ?? undefined, codeSigningKey: updateGroup[0].codeSigningInfo?.keyid, group: updateGroup[0].group, platforms: formatPlatformForUpdateGroup(updateGroup), @@ -253,6 +260,7 @@ export function getBranchDescription(branch: UpdateBranchFragment): FormattedBra message: formatUpdateMessage(latestUpdate), runtimeVersion: latestUpdate.runtimeVersion, isRollBackToEmbedded: latestUpdate.isRollBackToEmbedded, + rolloutPercentage: latestUpdate.rolloutPercentage ?? undefined, codeSigningKey: latestUpdate.codeSigningInfo?.keyid, group: latestUpdate.group, platforms: getPlatformsForGroup({