eas-cli
Version:
EAS command line tool
413 lines (412 loc) • 16.4 kB
JavaScript
Object.defineProperty(exports, "__esModule", { value: true });
exports.isUuidV4 = exports.getArchiveAsync = exports.ArchiveSourceType = exports.BUILD_LIST_ITEM_COUNT = void 0;
const tslib_1 = require("tslib");
const eas_build_job_1 = require("@expo/eas-build-job");
const chalk_1 = tslib_1.__importDefault(require("chalk"));
const url_1 = require("url");
const uuid = tslib_1.__importStar(require("uuid"));
const builds_1 = require("./utils/builds");
const files_1 = require("./utils/files");
const generated_1 = require("../graphql/generated");
const BuildQuery_1 = require("../graphql/queries/BuildQuery");
const AppPlatform_1 = require("../graphql/types/AppPlatform");
const log_1 = tslib_1.__importStar(require("../log"));
const platform_1 = require("../platform");
const prompts_1 = require("../prompts");
const date_1 = require("../utils/date");
exports.BUILD_LIST_ITEM_COUNT = 4;
var ArchiveSourceType;
(function (ArchiveSourceType) {
ArchiveSourceType[ArchiveSourceType["url"] = 0] = "url";
ArchiveSourceType[ArchiveSourceType["latest"] = 1] = "latest";
ArchiveSourceType[ArchiveSourceType["path"] = 2] = "path";
ArchiveSourceType[ArchiveSourceType["buildId"] = 3] = "buildId";
ArchiveSourceType[ArchiveSourceType["build"] = 4] = "build";
ArchiveSourceType[ArchiveSourceType["buildList"] = 5] = "buildList";
ArchiveSourceType[ArchiveSourceType["prompt"] = 6] = "prompt";
ArchiveSourceType[ArchiveSourceType["gcs"] = 7] = "gcs";
})(ArchiveSourceType || (exports.ArchiveSourceType = ArchiveSourceType = {}));
const buildStatusMapping = {
[generated_1.BuildStatus.New]: 'new',
[generated_1.BuildStatus.InQueue]: 'in queue',
[generated_1.BuildStatus.InProgress]: 'in progress',
[generated_1.BuildStatus.Finished]: 'finished',
[generated_1.BuildStatus.Errored]: 'errored',
[generated_1.BuildStatus.PendingCancel]: 'canceled',
[generated_1.BuildStatus.Canceled]: 'canceled',
};
async function getArchiveAsync(ctx, source) {
switch (source.sourceType) {
case ArchiveSourceType.prompt: {
return await handlePromptSourceAsync(ctx);
}
case ArchiveSourceType.url: {
return await handleUrlSourceAsync(ctx, source);
}
case ArchiveSourceType.latest: {
return await handleLatestSourceAsync(ctx);
}
case ArchiveSourceType.path: {
return await handlePathSourceAsync(ctx, source);
}
case ArchiveSourceType.buildId: {
return await handleBuildIdSourceAsync(ctx, source);
}
case ArchiveSourceType.buildList: {
return await handleBuildListSourceAsync(ctx);
}
case ArchiveSourceType.gcs: {
return source;
}
case ArchiveSourceType.build: {
return source;
}
}
}
exports.getArchiveAsync = getArchiveAsync;
async function handleUrlSourceAsync(ctx, source) {
const { url } = source;
if (!validateUrl(url)) {
log_1.default.error(chalk_1.default.bold(`The URL you provided is invalid: ${url}`));
return await getArchiveAsync(ctx, {
sourceType: ArchiveSourceType.prompt,
});
}
const maybeBuildId = isBuildDetailsPage(url);
if (maybeBuildId) {
if (await askIfUseBuildIdFromUrlAsync(ctx, source, maybeBuildId)) {
return await getArchiveAsync(ctx, {
sourceType: ArchiveSourceType.buildId,
id: maybeBuildId,
});
}
}
return {
sourceType: ArchiveSourceType.url,
url,
};
}
async function handleLatestSourceAsync(ctx) {
try {
const [latestBuild] = await (0, builds_1.getRecentBuildsForSubmissionAsync)(ctx.graphqlClient, (0, AppPlatform_1.toAppPlatform)(ctx.platform), ctx.projectId);
if (!latestBuild) {
log_1.default.error(chalk_1.default.bold("Couldn't find any builds for this project on EAS servers. It looks like you haven't run 'eas build' yet."));
return await getArchiveAsync(ctx, {
sourceType: ArchiveSourceType.prompt,
});
}
if (new Date() >= new Date(latestBuild.expirationDate)) {
log_1.default.error(chalk_1.default.bold(`The latest build is expired. Run ${chalk_1.default.bold('eas build --auto-submit')} or choose another build.`));
return await getArchiveAsync(ctx, {
sourceType: ArchiveSourceType.prompt,
});
}
return {
sourceType: ArchiveSourceType.build,
build: latestBuild,
};
}
catch (err) {
log_1.default.error(err);
throw err;
}
}
async function handlePathSourceAsync(ctx, source) {
if (!(await (0, files_1.isExistingFileAsync)(source.path))) {
log_1.default.error(chalk_1.default.bold(`${source.path} doesn't exist`));
return await getArchiveAsync(ctx, {
sourceType: ArchiveSourceType.prompt,
});
}
log_1.default.log('Uploading your app archive to EAS Submit');
const bucketKey = await (0, files_1.uploadAppArchiveAsync)(ctx.graphqlClient, source.path);
return {
sourceType: ArchiveSourceType.gcs,
bucketKey,
localSource: {
sourceType: ArchiveSourceType.path,
path: source.path,
},
};
}
async function handleBuildIdSourceAsync(ctx, source) {
try {
const build = await BuildQuery_1.BuildQuery.byIdAsync(ctx.graphqlClient, source.id);
if (build.platform !== (0, AppPlatform_1.toAppPlatform)(ctx.platform)) {
const expectedPlatformName = platform_1.appPlatformDisplayNames[(0, AppPlatform_1.toAppPlatform)(ctx.platform)];
const receivedPlatformName = platform_1.appPlatformDisplayNames[build.platform];
log_1.default.error(chalk_1.default.bold(`Build platform doesn't match! Expected ${expectedPlatformName} build but got ${receivedPlatformName}.`));
return await getArchiveAsync(ctx, {
sourceType: ArchiveSourceType.prompt,
});
}
if (new Date() >= new Date(build.expirationDate)) {
log_1.default.error(chalk_1.default.bold(`The build with ID ${build.id} is expired. Choose another build.`));
return await getArchiveAsync(ctx, {
sourceType: ArchiveSourceType.prompt,
});
}
return {
sourceType: ArchiveSourceType.build,
build,
};
}
catch (err) {
log_1.default.error(chalk_1.default.bold(`Could not find build with ID ${source.id}`));
log_1.default.warn('Are you sure that the given ID corresponds to a build from EAS Build?');
log_1.default.warn(`Build IDs from the classic build service (expo build:[android|ios]) are not supported. ${(0, log_1.learnMore)('https://docs.expo.dev/submit/classic-builds/')}`);
log_1.default.debug('Original error:', err);
return await getArchiveAsync(ctx, {
sourceType: ArchiveSourceType.prompt,
});
}
}
async function handleBuildListSourceAsync(ctx) {
try {
const appPlatform = (0, AppPlatform_1.toAppPlatform)(ctx.platform);
const recentBuilds = await (0, builds_1.getRecentBuildsForSubmissionAsync)(ctx.graphqlClient, appPlatform, ctx.projectId, {
limit: exports.BUILD_LIST_ITEM_COUNT,
});
if (recentBuilds.length < 1) {
log_1.default.error(chalk_1.default.bold(`Couldn't find any ${platform_1.appPlatformDisplayNames[appPlatform]} builds for this project on EAS servers. ` +
"It looks like you haven't run 'eas build' yet."));
return await getArchiveAsync(ctx, {
sourceType: ArchiveSourceType.prompt,
});
}
if (recentBuilds.every(it => new Date(it.expirationDate) <= new Date())) {
log_1.default.error(chalk_1.default.bold('It looks like all of your build artifacts have expired. ' +
'EAS keeps your build artifacts only for 30 days.'));
return await getArchiveAsync(ctx, {
sourceType: ArchiveSourceType.prompt,
});
}
const choices = recentBuilds.map(build => formatBuildChoice(build));
choices.push({
title: 'None of the above (select another option)',
value: null,
});
const { selectedBuild } = await (0, prompts_1.promptAsync)({
name: 'selectedBuild',
type: 'select',
message: 'Which build would you like to submit?',
choices: choices.map(choice => ({ ...choice, title: `- ${choice.title}` })),
warn: 'This artifact has expired',
});
if (selectedBuild == null) {
return await getArchiveAsync(ctx, {
sourceType: ArchiveSourceType.prompt,
});
}
return {
sourceType: ArchiveSourceType.build,
build: selectedBuild,
};
}
catch (err) {
log_1.default.error(err);
throw err;
}
}
function formatBuildChoice(build) {
const { id, updatedAt, runtimeVersion, buildProfile, gitCommitHash, gitCommitMessage, channel, message, status, } = build;
const buildDate = new Date(updatedAt);
const splitCommitMessage = gitCommitMessage?.split('\n');
const formattedCommitData = gitCommitHash && splitCommitMessage && splitCommitMessage.length > 0
? `${gitCommitHash.slice(0, 7)} "${chalk_1.default.bold(splitCommitMessage[0] + (splitCommitMessage.length > 1 ? '…' : ''))}"`
: null;
const title = `${chalk_1.default.bold(`ID:`)} ${id} (${chalk_1.default.bold(`${(0, date_1.fromNow)(buildDate)} ago`)})`;
const descriptionItems = [
{ name: 'Profile', value: buildProfile ? chalk_1.default.bold(buildProfile) : null },
{ name: 'Channel', value: channel ? chalk_1.default.bold(channel) : null },
{ name: 'Runtime version', value: runtimeVersion ? chalk_1.default.bold(runtimeVersion) : null },
{ name: 'Commit', value: formattedCommitData },
{
name: 'Message',
value: message
? chalk_1.default.bold(message.length > 200 ? `${message.slice(0, 200)}...` : message)
: null,
},
{ name: 'Status', value: buildStatusMapping[status] },
];
const filteredDescriptionArray = descriptionItems
.filter(item => item.value)
.map(item => `${chalk_1.default.bold(item.name)}: ${item.value}`);
return {
title,
description: filteredDescriptionArray.length > 0 ? filteredDescriptionArray.join('\n') : '',
value: build,
disabled: new Date(build.expirationDate) < new Date(),
};
}
async function handlePromptSourceAsync(ctx) {
if (ctx.nonInteractive) {
throw new Error('Please run this command with appropriate input.');
}
const { sourceType: sourceTypeRaw } = await (0, prompts_1.promptAsync)({
name: 'sourceType',
type: 'select',
message: 'What would you like to submit?',
choices: [
{
title: 'Select a build from EAS',
value: ArchiveSourceType.buildList,
},
{ title: 'Provide a URL to the app archive', value: ArchiveSourceType.url },
{
title: 'Provide a path to a local app binary file',
value: ArchiveSourceType.path,
},
{
title: 'Provide a build ID to identify a build on EAS',
value: ArchiveSourceType.buildId,
},
],
});
const sourceType = sourceTypeRaw;
switch (sourceType) {
case ArchiveSourceType.url: {
const url = await askForArchiveUrlAsync(ctx.platform);
return await getArchiveAsync(ctx, {
sourceType: ArchiveSourceType.url,
url,
});
}
case ArchiveSourceType.path: {
const path = await askForArchivePathAsync(ctx.platform);
return await getArchiveAsync(ctx, {
sourceType: ArchiveSourceType.path,
path,
});
}
case ArchiveSourceType.buildList: {
return await getArchiveAsync(ctx, {
sourceType: ArchiveSourceType.buildList,
});
}
case ArchiveSourceType.buildId: {
const id = await askForBuildIdAsync();
return await getArchiveAsync(ctx, {
sourceType: ArchiveSourceType.buildId,
id,
});
}
default:
throw new Error('This should never happen');
}
}
async function askForArchiveUrlAsync(platform) {
const isIos = platform === eas_build_job_1.Platform.IOS;
const defaultArchiveUrl = `https://url.to/your/archive.${isIos ? 'ipa' : 'aab'}`;
const { url } = await (0, prompts_1.promptAsync)({
name: 'url',
message: 'URL:',
initial: defaultArchiveUrl,
type: 'text',
validate: (url) => {
if (url === defaultArchiveUrl) {
return 'That was just an example URL, meant to show you the format that we expect for the response.';
}
else if (!validateUrl(url)) {
return `${url} does not conform to HTTP format`;
}
else {
return true;
}
},
});
return url;
}
async function askForArchivePathAsync(platform) {
const isIos = platform === eas_build_job_1.Platform.IOS;
const defaultArchivePath = `/path/to/your/archive.${isIos ? 'ipa' : 'aab'}`;
const { path } = await (0, prompts_1.promptAsync)({
name: 'path',
message: `Path to the app archive file (${isIos ? 'ipa' : 'aab or apk'}):`,
initial: defaultArchivePath,
type: 'text',
// eslint-disable-next-line async-protect/async-suffix
validate: async (path) => {
if (path === defaultArchivePath) {
return 'That was just an example path, meant to show you the format that we expect for the response.';
}
else if (!(await (0, files_1.isExistingFileAsync)(path))) {
return `File ${path} doesn't exist.`;
}
else {
return true;
}
},
});
return path;
}
async function askForBuildIdAsync() {
const { id } = await (0, prompts_1.promptAsync)({
name: 'id',
message: 'Build ID:',
type: 'text',
validate: (val) => {
if (!isUuidV4(val)) {
return `${val} is not a valid ID`;
}
else {
return true;
}
},
});
return id;
}
async function askIfUseBuildIdFromUrlAsync(ctx, source, buildId) {
const { url } = source;
log_1.default.warn(`It seems that you provided a build details page URL: ${url}`);
log_1.default.warn('We expected to see the build artifact URL.');
if (!ctx.nonInteractive) {
const useAsBuildId = await (0, prompts_1.confirmAsync)({
message: `Do you want to submit build ${buildId} instead?`,
});
if (useAsBuildId) {
return true;
}
else {
log_1.default.warn('The submission will most probably fail.');
}
}
else {
log_1.default.warn("Proceeding because you've run this command in non-interactive mode.");
}
return false;
}
function isBuildDetailsPage(url) {
const maybeExpoUrl = url.match(/expo\.(dev|io).*\/builds\/(.{36}).*/);
if (maybeExpoUrl) {
const maybeBuildId = maybeExpoUrl[2];
if (isUuidV4(maybeBuildId)) {
return maybeBuildId;
}
else {
return false;
}
}
else {
return false;
}
}
function validateUrl(url) {
const protocols = ['http', 'https'];
try {
const parsed = new url_1.URL(url);
return protocols
? parsed.protocol
? protocols.map(x => `${x.toLowerCase()}:`).includes(parsed.protocol)
: false
: true;
}
catch {
return false;
}
}
function isUuidV4(s) {
return uuid.validate(s) && uuid.version(s) === 4;
}
exports.isUuidV4 = isUuidV4;
;