UNPKG

eas-cli

Version:
413 lines (412 loc) 16.4 kB
"use strict"; 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;