UNPKG

eas-cli

Version:

EAS command line tool

418 lines (417 loc) 20.4 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.getOrAskUpdateMessageAsync = exports.getUpdateGroupOrAskForUpdateGroupAsync = exports.askUpdateGroupForEachPublishPlatformFilteringByRuntimeVersionAsync = exports.getUpdateGroupAsync = exports.republishAsync = void 0; const tslib_1 = require("tslib"); const assert_1 = tslib_1.__importDefault(require("assert")); const nullthrows_1 = tslib_1.__importDefault(require("nullthrows")); const getBranchFromChannelNameAndCreateAndLinkIfNotExistsAsync_1 = require("./getBranchFromChannelNameAndCreateAndLinkIfNotExistsAsync"); const queries_1 = require("./queries"); const utils_1 = require("./utils"); const queries_2 = require("../branch/queries"); const url_1 = require("../build/utils/url"); const queries_3 = require("../channel/queries"); const pagination_1 = require("../commandUtils/pagination"); const fetch_1 = tslib_1.__importDefault(require("../fetch")); const PublishMutation_1 = require("../graphql/mutations/PublishMutation"); const UpdateQuery_1 = require("../graphql/queries/UpdateQuery"); const log_1 = tslib_1.__importStar(require("../log")); const ora_1 = require("../ora"); const projectUtils_1 = require("../project/projectUtils"); const publish_1 = require("../project/publish"); const prompts_1 = require("../prompts"); const code_signing_1 = require("../utils/code-signing"); const formatFields_1 = tslib_1.__importDefault(require("../utils/formatFields")); const json_1 = require("../utils/json"); /** * @param updatesToPublish The update group to republish * @param targetBranch The branch to repubish the update group on */ async function republishAsync({ graphqlClient, app, updatesToPublish, targetBranch, updateMessage, codeSigningInfo, json, rolloutPercentage, }) { const { branchName: targetBranchName, branchId: targetBranchId } = targetBranch; // The update group properties are the same for all updates (0, assert_1.default)(updatesToPublish.length > 0, 'Updates to republish must be provided'); const arbitraryUpdate = updatesToPublish[0]; const isSameGroup = (update) => update.groupId === arbitraryUpdate.groupId && update.branchId === arbitraryUpdate.branchId && update.branchName === arbitraryUpdate.branchName && update.runtimeVersion === arbitraryUpdate.runtimeVersion && update.manifestHostOverride === arbitraryUpdate.manifestHostOverride && update.assetHostOverride === arbitraryUpdate.assetHostOverride; (0, assert_1.default)(updatesToPublish.every(isSameGroup), 'All updates being republished must belong to the same update group'); (0, assert_1.default)(updatesToPublish.every(u => u.isRollBackToEmbedded) || updatesToPublish.every(u => !u.isRollBackToEmbedded), 'All updates must either be roll back to embedded updates or not'); (0, assert_1.default)(!updatesToPublish.some(u => !!u.rolloutControlUpdate), 'Cannot republish an update that is being rolled-out. Either complete the update rollout and then republish or publish a new rollout update.'); const { runtimeVersion } = arbitraryUpdate; // If codesigning was created for the original update, we need to add it to the republish. // If one wishes to not sign the republish or sign with a different key, a normal publish should // be performed. const shouldRepublishWithCodesigning = updatesToPublish.some(update => update.codeSigningInfo); if (shouldRepublishWithCodesigning) { if (!codeSigningInfo) { throw new Error('Must specify --private-key-path argument to sign republished update group for code signing'); } for (const update of updatesToPublish) { if ((0, nullthrows_1.default)(update.codeSigningInfo).alg !== codeSigningInfo.codeSigningMetadata.alg || (0, nullthrows_1.default)(update.codeSigningInfo).keyid !== codeSigningInfo.codeSigningMetadata.keyid) { throw new Error('Republished updates must use the same code signing key and algorithm as original update'); } } log_1.default.withTick(`The republished update group will be signed with the same code signing key and algorithm as the original update`); } const publishIndicator = (0, ora_1.ora)('Republishing...').start(); let updatesRepublished; try { const arbitraryUpdate = updatesToPublish[0]; const objectToMergeIn = arbitraryUpdate.isRollBackToEmbedded ? { rollBackToEmbeddedInfoGroup: Object.fromEntries(updatesToPublish.map(update => [update.platform, true])), } : { updateInfoGroup: Object.fromEntries(updatesToPublish.map(update => [update.platform, JSON.parse(update.manifestFragment)])), fingerprintInfoGroup: Object.fromEntries(updatesToPublish.map(update => { const fingerprint = update.fingerprint; if (!fingerprint) { return [update.platform, undefined]; } return [ update.platform, { fingerprintHash: fingerprint.hash, fingerprintSource: fingerprint.source ? { type: fingerprint.source.type, bucketKey: fingerprint.source.bucketKey, isDebugFingerprint: fingerprint.source.isDebugFingerprint, } : undefined, }, ]; })), rolloutInfoGroup: rolloutPercentage ? await (0, publish_1.getUpdateRolloutInfoGroupAsync)(graphqlClient, { appId: app.projectId, branchName: targetBranchName, runtimeVersion, rolloutPercentage, platforms: updatesToPublish.map(update => update.platform), }) : null, }; updatesRepublished = await PublishMutation_1.PublishMutation.publishUpdateGroupAsync(graphqlClient, [ { branchId: targetBranchId, runtimeVersion, message: updateMessage, ...objectToMergeIn, gitCommitHash: updatesToPublish[0].gitCommitHash, isGitWorkingTreeDirty: updatesToPublish[0].isGitWorkingTreeDirty, environment: updatesToPublish[0].environment, awaitingCodeSigningInfo: !!codeSigningInfo, manifestHostOverride: updatesToPublish[0].manifestHostOverride, assetHostOverride: updatesToPublish[0].assetHostOverride, }, ]); if (codeSigningInfo) { log_1.default.log('🔒 Signing republished update group'); await Promise.all(updatesRepublished.map(async (newUpdate) => { const response = await (0, fetch_1.default)(newUpdate.manifestPermalink, { method: 'GET', headers: { accept: 'multipart/mixed' }, }); let signature; if (newUpdate.isRollBackToEmbedded) { const directiveBody = (0, nullthrows_1.default)(await (0, code_signing_1.getDirectiveBodyAsync)(response)); (0, code_signing_1.checkDirectiveBodyAgainstUpdateInfoGroup)(directiveBody); signature = (0, code_signing_1.signBody)(directiveBody, codeSigningInfo); } else { const manifestBody = (0, nullthrows_1.default)(await (0, code_signing_1.getManifestBodyAsync)(response)); (0, code_signing_1.checkManifestBodyAgainstUpdateInfoGroup)(manifestBody, (0, nullthrows_1.default)((0, nullthrows_1.default)(objectToMergeIn.updateInfoGroup)[newUpdate.platform])); signature = (0, code_signing_1.signBody)(manifestBody, codeSigningInfo); } await PublishMutation_1.PublishMutation.setCodeSigningInfoAsync(graphqlClient, newUpdate.id, { alg: codeSigningInfo.codeSigningMetadata.alg, keyid: codeSigningInfo.codeSigningMetadata.keyid, sig: signature, }); })); } publishIndicator.succeed('Republished update group'); } catch (error) { publishIndicator.fail('Failed to republish update group'); throw error; } if (json) { (0, json_1.printJsonOnlyOutput)(updatesRepublished); return; } const updatesRepublishedByPlatform = Object.fromEntries(updatesRepublished.map(update => [update.platform, update])); const arbitraryRepublishedUpdate = updatesRepublished[0]; const updateGroupUrl = (0, url_1.getUpdateGroupUrl)((await (0, projectUtils_1.getOwnerAccountForProjectIdAsync)(graphqlClient, app.projectId)).name, app.exp.slug, arbitraryRepublishedUpdate.group); log_1.default.addNewLineIfNone(); log_1.default.log((0, formatFields_1.default)([ { label: 'Branch', value: targetBranchName }, { label: 'Runtime version', value: arbitraryRepublishedUpdate.runtimeVersion }, { label: 'Platform', value: updatesRepublished.map(update => update.platform).join(', ') }, { label: 'Update group ID', value: arbitraryRepublishedUpdate.group }, ...(updatesRepublishedByPlatform.android ? [{ label: 'Android update ID', value: updatesRepublishedByPlatform.android.id }] : []), ...(updatesRepublishedByPlatform.ios ? [{ label: 'iOS update ID', value: updatesRepublishedByPlatform.ios.id }] : []), ...(updatesRepublishedByPlatform.android?.rolloutControlUpdate ? [ { label: 'Android Rollout', value: `${updatesRepublishedByPlatform.android?.rolloutPercentage}% (Base update ID: ${updatesRepublishedByPlatform.android?.rolloutControlUpdate.id})`, }, ] : []), ...(updatesRepublishedByPlatform.ios?.rolloutControlUpdate ? [ { label: 'iOS Rollout', value: `${updatesRepublishedByPlatform.ios?.rolloutPercentage}% (Base update ID: ${updatesRepublishedByPlatform.ios?.rolloutControlUpdate.id})`, }, ] : []), { label: 'Message', value: updateMessage }, { label: 'EAS Dashboard', value: (0, log_1.link)(updateGroupUrl, { dim: false }) }, ])); } exports.republishAsync = republishAsync; async function getUpdateGroupAsync(graphqlClient, groupId) { const updateGroup = await UpdateQuery_1.UpdateQuery.viewUpdateGroupAsync(graphqlClient, { groupId, }); return updateGroup.map(update => ({ ...update, groupId: update.group, branchId: update.branch.id, branchName: update.branch.name, })); } exports.getUpdateGroupAsync = getUpdateGroupAsync; async function askUpdateGroupForEachPublishPlatformFilteringByRuntimeVersionAsync(graphqlClient, projectId, options) { if (options.nonInteractive) { throw new Error('Must supply --group when in non-interactive mode'); } if (options.branchName) { return await askUpdateGroupForEachPublishPlatformFromBranchNameFilteringByRuntimeVersionAsync(graphqlClient, { ...options, branchName: options.branchName, projectId, }); } if (options.channelName) { return await askUpdateGroupForEachPublishPlatformFromChannelNameFilteringByRuntimeVersionAsync(graphqlClient, { ...options, channelName: options.channelName, projectId, }); } const { choice } = await (0, prompts_1.promptAsync)({ type: 'select', message: 'Find update by branch or channel?', name: 'choice', choices: [ { title: 'Branch', value: 'branch' }, { title: 'Channel', value: 'channel' }, ], }); if (choice === 'channel') { const { name } = await (0, queries_3.selectChannelOnAppAsync)(graphqlClient, { projectId, selectionPromptTitle: 'Select a channel to view', paginatedQueryOptions: { json: options.json, nonInteractive: options.nonInteractive, offset: 0, }, }); return await askUpdateGroupForEachPublishPlatformFromChannelNameFilteringByRuntimeVersionAsync(graphqlClient, { ...options, channelName: name, projectId, }); } else if (choice === 'branch') { const { name } = await (0, queries_2.selectBranchOnAppAsync)(graphqlClient, { projectId, promptTitle: 'Select branch from which to choose update', displayTextForListItem: updateBranch => ({ title: updateBranch.name, }), // discard limit and offset because this query is not their intended target paginatedQueryOptions: { json: options.json, nonInteractive: options.nonInteractive, offset: 0, }, }); return await askUpdateGroupForEachPublishPlatformFromBranchNameFilteringByRuntimeVersionAsync(graphqlClient, { ...options, branchName: name, projectId, }); } else { throw new Error('Must choose update via channel or branch'); } } exports.askUpdateGroupForEachPublishPlatformFilteringByRuntimeVersionAsync = askUpdateGroupForEachPublishPlatformFilteringByRuntimeVersionAsync; async function getUpdateGroupOrAskForUpdateGroupAsync(graphqlClient, projectId, options) { if (options.groupId) { return await getUpdateGroupAsync(graphqlClient, options.groupId); } if (options.nonInteractive) { throw new Error('Must supply --group when in non-interactive mode'); } if (options.branchName) { return await askUpdatesFromBranchNameAsync(graphqlClient, { ...options, branchName: options.branchName, projectId, }); } if (options.channelName) { return await askUpdatesFromChannelNameAsync(graphqlClient, { ...options, channelName: options.channelName, projectId, }); } const { choice } = await (0, prompts_1.promptAsync)({ type: 'select', message: 'Find update by branch or channel?', name: 'choice', choices: [ { title: 'Branch', value: 'branch' }, { title: 'Channel', value: 'channel' }, ], }); if (choice === 'channel') { const { name } = await (0, queries_3.selectChannelOnAppAsync)(graphqlClient, { projectId, selectionPromptTitle: 'Select a channel to view', paginatedQueryOptions: { json: options.json, nonInteractive: options.nonInteractive, offset: 0, }, }); return await askUpdatesFromChannelNameAsync(graphqlClient, { ...options, channelName: name, projectId, }); } else if (choice === 'branch') { const { name } = await (0, queries_2.selectBranchOnAppAsync)(graphqlClient, { projectId, promptTitle: 'Select branch from which to choose update', displayTextForListItem: updateBranch => ({ title: updateBranch.name, }), // discard limit and offset because this query is not their intended target paginatedQueryOptions: { json: options.json, nonInteractive: options.nonInteractive, offset: 0, }, }); return await askUpdatesFromBranchNameAsync(graphqlClient, { ...options, branchName: name, projectId, }); } else { throw new Error('Must choose update via channel or branch'); } } exports.getUpdateGroupOrAskForUpdateGroupAsync = getUpdateGroupOrAskForUpdateGroupAsync; async function askUpdateGroupForEachPublishPlatformFromBranchNameFilteringByRuntimeVersionAsync(graphqlClient, { projectId, branchName, json, nonInteractive, }) { const publishPlatformToLatestUpdateGroup = await (0, queries_1.selectRuntimeAndGetLatestUpdateGroupForEachPublishPlatformOnBranchAsync)(graphqlClient, { projectId, branchName, paginatedQueryOptions: (0, pagination_1.getPaginatedQueryOptions)({ json, 'non-interactive': nonInteractive }), }); return { ios: publishPlatformToLatestUpdateGroup.ios?.map(update => ({ ...update, groupId: update.group, branchId: update.branch.id, branchName: update.branch.name, })), android: publishPlatformToLatestUpdateGroup.android?.map(update => ({ ...update, groupId: update.group, branchId: update.branch.id, branchName: update.branch.name, })), }; } async function askUpdatesFromBranchNameAsync(graphqlClient, { projectId, branchName, json, nonInteractive, }) { const updateGroup = await (0, queries_1.selectUpdateGroupOnBranchAsync)(graphqlClient, { projectId, branchName, paginatedQueryOptions: (0, pagination_1.getPaginatedQueryOptions)({ json, 'non-interactive': nonInteractive }), }); return updateGroup.map(update => ({ ...update, groupId: update.group, branchId: update.branch.id, branchName: update.branch.name, })); } async function askUpdateGroupForEachPublishPlatformFromChannelNameFilteringByRuntimeVersionAsync(graphqlClient, { projectId, channelName, json, nonInteractive, }) { const { branchName } = await (0, getBranchFromChannelNameAndCreateAndLinkIfNotExistsAsync_1.getBranchFromChannelNameAndCreateAndLinkIfNotExistsAsync)(graphqlClient, projectId, channelName); return await askUpdateGroupForEachPublishPlatformFromBranchNameFilteringByRuntimeVersionAsync(graphqlClient, { projectId, branchName, json, nonInteractive, }); } async function askUpdatesFromChannelNameAsync(graphqlClient, { projectId, channelName, json, nonInteractive, }) { const { branchName } = await (0, getBranchFromChannelNameAndCreateAndLinkIfNotExistsAsync_1.getBranchFromChannelNameAndCreateAndLinkIfNotExistsAsync)(graphqlClient, projectId, channelName); return await askUpdatesFromBranchNameAsync(graphqlClient, { projectId, branchName, json, nonInteractive, }); } /** * Get or ask the user for the update (group) message for the republish */ async function getOrAskUpdateMessageAsync(updateGroup, options) { if (options.updateMessage) { return sanitizeUpdateMessage(options.updateMessage); } if (options.nonInteractive || options.json) { throw new Error('Must supply --message when in non-interactive mode'); } // This command only uses a single update group to republish, meaning these values are always identical const oldGroupId = updateGroup[0].groupId; const oldUpdateMessage = updateGroup[0].message; const { updateMessage } = await (0, prompts_1.promptAsync)({ type: 'text', name: 'updateMessage', message: 'Provide an update message.', initial: `Republish "${oldUpdateMessage}" - group: ${oldGroupId}`, validate: (value) => (value ? true : 'Update message may not be empty.'), }); return sanitizeUpdateMessage(updateMessage); } exports.getOrAskUpdateMessageAsync = getOrAskUpdateMessageAsync; function sanitizeUpdateMessage(updateMessage) { if (updateMessage !== (0, utils_1.truncateString)(updateMessage, 1024)) { log_1.default.warn('Update message exceeds the allowed 1024 character limit, truncated update message.'); return (0, utils_1.truncateString)(updateMessage, 1024); } return updateMessage; }