eas-cli
Version:
EAS command line tool
418 lines (417 loc) • 20.4 kB
JavaScript
"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;
}