diff --git a/CHANGELOG.md b/CHANGELOG.md index 1c67cbb9be..5333b78672 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,8 @@ This is the log of notable changes to EAS CLI and related packages. ### ๐ŸŽ‰ New features +- Add new rollout update type for `eas update` and `eas update:edit`. ([#2502](https://github.com/expo/eas-cli/pull/2502), [#2503](https://github.com/expo/eas-cli/pull/2503) by [@wschurman](https://github.com/wschurman)) + ### ๐Ÿ› Bug fixes ### ๐Ÿงน Chores diff --git a/packages/eas-cli/src/commands/update/edit.ts b/packages/eas-cli/src/commands/update/edit.ts new file mode 100644 index 0000000000..5b5ab67836 --- /dev/null +++ b/packages/eas-cli/src/commands/update/edit.ts @@ -0,0 +1,143 @@ +import { Flags } from '@oclif/core'; +import assert from 'assert'; +import chalk from 'chalk'; + +import EasCommand from '../../commandUtils/EasCommand'; +import { EasNonInteractiveAndJsonFlags } from '../../commandUtils/flags'; +import { PublishMutation } from '../../graphql/mutations/PublishMutation'; +import { UpdateQuery } from '../../graphql/queries/UpdateQuery'; +import Log from '../../log'; +import { promptAsync } from '../../prompts'; +import { + formatUpdateGroup, + getUpdateGroupDescriptions, + getUpdateJsonInfosForUpdates, +} from '../../update/utils'; +import { enableJsonOutput, printJsonOnlyOutput } from '../../utils/json'; + +export default class UpdateEdit extends EasCommand { + static override description = 'edit all the updates in an update group'; + static override hidden = true; + + static override args = [ + { + name: 'groupId', + required: true, + description: 'The ID of an update group to edit.', + }, + ]; + + static override flags = { + 'rollout-percentage': Flags.integer({ + description: `Rollout percentage to set for a rollout update. The specified number must be an integer between 1 and 100.`, + required: false, + min: 0, + max: 100, + }), + ...EasNonInteractiveAndJsonFlags, + }; + + static override contextDefinition = { + ...this.ContextOptions.LoggedIn, + }; + + async runAsync(): Promise { + const { + args: { groupId }, + flags: { + 'rollout-percentage': rolloutPercentage, + json: jsonFlag, + 'non-interactive': nonInteractive, + }, + } = await this.parse(UpdateEdit); + + const { + loggedIn: { graphqlClient }, + } = await this.getContextAsync(UpdateEdit, { nonInteractive }); + + if (jsonFlag) { + enableJsonOutput(); + } + + const proposedUpdatesToEdit = ( + await UpdateQuery.viewUpdateGroupAsync(graphqlClient, { groupId }) + ).map(u => ({ updateId: u.id, rolloutPercentage: u.rolloutPercentage })); + + const updatesToEdit = proposedUpdatesToEdit.filter( + (u): u is { updateId: string; rolloutPercentage: number } => + u.rolloutPercentage !== null && u.rolloutPercentage !== undefined + ); + + if (updatesToEdit.length === 0) { + throw new Error('Cannot edit rollout percentage on update group that is not a rollout.'); + } + + const rolloutPercentagesSet = new Set(updatesToEdit.map(u => u.rolloutPercentage)); + if (rolloutPercentagesSet.size !== 1) { + throw new Error( + 'Cannot edit rollout percentage for a group with non-equal percentages for updates in the group.' + ); + } + + const previousPercentage = updatesToEdit[0].rolloutPercentage; + + if (nonInteractive && rolloutPercentage === undefined) { + throw new Error('Must specify --rollout-percentage in non-interactive mode'); + } + + let rolloutPercentageToSet = rolloutPercentage; + if (rolloutPercentageToSet === undefined) { + const { percentage } = await promptAsync({ + type: 'number', + message: `New rollout percentage (min: ${previousPercentage}, max: 100)`, + validate: value => { + if (value <= previousPercentage) { + return `Rollout percentage must be greater than previous rollout percentage (${previousPercentage})`; + } else if (value > 100) { + return `Rollout percentage must not be greater than 100`; + } else { + return true; + } + }, + name: 'percentage', + }); + + if (!percentage) { + Log.log('Aborted.'); + return; + } + + rolloutPercentageToSet = percentage; + } + + assert(rolloutPercentageToSet !== undefined); + + if (rolloutPercentageToSet < previousPercentage) { + throw new Error( + `Rollout percentage must be greater than previous rollout percentage (${previousPercentage})` + ); + } else if (rolloutPercentageToSet > 100) { + throw new Error('Rollout percentage must not be greater than 100'); + } + + const updatedUpdates = await Promise.all( + updatesToEdit.map(async u => { + return await PublishMutation.setRolloutPercentageAsync( + graphqlClient, + u.updateId, + rolloutPercentageToSet! + ); + }) + ); + + if (jsonFlag) { + printJsonOnlyOutput(getUpdateJsonInfosForUpdates(updatedUpdates)); + } else { + const [updateGroupDescription] = getUpdateGroupDescriptions([updatedUpdates]); + + Log.log(chalk.bold('Update group:')); + + Log.log(formatUpdateGroup(updateGroupDescription)); + } + } +} diff --git a/packages/eas-cli/src/graphql/generated.ts b/packages/eas-cli/src/graphql/generated.ts index d19df38ddf..51836f728f 100644 --- a/packages/eas-cli/src/graphql/generated.ts +++ b/packages/eas-cli/src/graphql/generated.ts @@ -7859,6 +7859,14 @@ export type SetCodeSigningInfoMutationVariables = Exact<{ export type SetCodeSigningInfoMutation = { __typename?: 'RootMutation', update: { __typename?: 'UpdateMutation', setCodeSigningInfo: { __typename?: 'Update', id: string, group: string, awaitingCodeSigningInfo: boolean, codeSigningInfo?: { __typename?: 'CodeSigningInfo', keyid: string, alg: string, sig: string } | null } } }; +export type SetRolloutPercentageMutationVariables = Exact<{ + updateId: Scalars['ID']['input']; + rolloutPercentage: Scalars['Int']['input']; +}>; + + +export type SetRolloutPercentageMutation = { __typename?: 'RootMutation', update: { __typename?: 'UpdateMutation', setRolloutPercentage: { __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 CreateAndroidSubmissionMutationVariables = Exact<{ appId: Scalars['ID']['input']; config: AndroidSubmissionConfigInput; diff --git a/packages/eas-cli/src/graphql/mutations/PublishMutation.ts b/packages/eas-cli/src/graphql/mutations/PublishMutation.ts index 569ebe521e..447a232613 100644 --- a/packages/eas-cli/src/graphql/mutations/PublishMutation.ts +++ b/packages/eas-cli/src/graphql/mutations/PublishMutation.ts @@ -9,6 +9,9 @@ import { GetSignedUploadMutationVariables, PublishUpdateGroupInput, SetCodeSigningInfoMutation, + SetCodeSigningInfoMutationVariables, + SetRolloutPercentageMutation, + SetRolloutPercentageMutationVariables, UpdateFragment, UpdatePublishMutation, } from '../generated'; @@ -78,7 +81,7 @@ export const PublishMutation = { ): Promise { const data = await withErrorHandlingAsync( graphqlClient - .mutation( + .mutation( gql` mutation SetCodeSigningInfoMutation( $updateId: ID! @@ -104,4 +107,31 @@ export const PublishMutation = { ); return data.update.setCodeSigningInfo; }, + + async setRolloutPercentageAsync( + graphqlClient: ExpoGraphqlClient, + updateId: string, + rolloutPercentage: number + ): Promise { + const data = await withErrorHandlingAsync( + graphqlClient + .mutation( + gql` + mutation SetRolloutPercentageMutation($updateId: ID!, $rolloutPercentage: Int!) { + update { + setRolloutPercentage(updateId: $updateId, percentage: $rolloutPercentage) { + id + ...UpdateFragment + } + } + } + ${print(UpdateFragmentNode)} + `, + { updateId, rolloutPercentage }, + { additionalTypenames: ['Update'] } + ) + .toPromise() + ); + return data.update.setRolloutPercentage; + }, };