diff --git a/CHANGELOG.md b/CHANGELOG.md index 1c0d92a9b0..3eaa6ee07e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,8 @@ This is the log of notable changes to EAS CLI and related packages. ### 🎉 New features - Use envs from build profile to resolve app config when auto-submitting. ([#614](https://github.com/expo/eas-cli/pull/614) by [@dsokal](https://github.com/dsokal)) -- Support multiflavor Android projects. ([#595](https://github.com/expo/eas-cli/pull/595) by [@wkozyra95](https://github.com/wkozyra95)) +- Support multi flavor Android projects. ([#595](https://github.com/expo/eas-cli/pull/595) by [@wkozyra95](https://github.com/wkozyra95)) +- Improve experience when using the build details page as a build artifact URL in `eas submit`. ([#620](https://github.com/expo/eas-cli/pull/620) by [@dsokal](https://github.com/dsokal)) ### 🐛 Bug fixes diff --git a/packages/eas-cli/src/submissions/ArchiveSource.ts b/packages/eas-cli/src/submissions/ArchiveSource.ts index 02f3f2fa9f..79ac9994db 100644 --- a/packages/eas-cli/src/submissions/ArchiveSource.ts +++ b/packages/eas-cli/src/submissions/ArchiveSource.ts @@ -6,7 +6,7 @@ import * as uuid from 'uuid'; import { BuildFragment } from '../graphql/generated'; import { toAppPlatform } from '../graphql/types/AppPlatform'; import Log from '../log'; -import { promptAsync } from '../prompts'; +import { confirmAsync, promptAsync } from '../prompts'; import { getBuildByIdForSubmissionAsync, getLatestBuildForSubmissionAsync } from './utils/builds'; import { isExistingFileAsync, uploadAppArchiveAsync } from './utils/files'; @@ -22,6 +22,7 @@ interface ArchiveSourceBase { sourceType: ArchiveSourceType; platform: Platform; projectId: string; + nonInteractive: boolean; } interface ArchiveUrlSource extends ArchiveSourceBase { @@ -81,16 +82,29 @@ export async function getArchiveAsync(source: ArchiveSource): Promise { } async function handleUrlSourceAsync(source: ArchiveUrlSource): Promise { - if (!validateUrl(source.url)) { - Log.error(chalk.bold(`The URL you provided is invalid: ${source.url}`)); + const { url } = source; + + if (!validateUrl(url)) { + Log.error(chalk.bold(`The URL you provided is invalid: ${url}`)); return getArchiveAsync({ ...source, sourceType: ArchiveSourceType.prompt, }); } + const maybeBuildId = isBuildDetailsPage(url); + if (maybeBuildId) { + if (await askIfUseBuildIdFromUrlAsync(source, maybeBuildId)) { + return getArchiveAsync({ + ...source, + sourceType: ArchiveSourceType.buildId, + id: maybeBuildId, + }); + } + } + return { - url: source.url, + url, source, }; } @@ -216,8 +230,6 @@ async function handlePromptSourceAsync(source: ArchivePromptSource): Promise { const defaultArchiveUrl = 'https://url.to/your/archive.aab'; const { url } = await promptAsync({ @@ -266,8 +278,8 @@ async function askForBuildIdAsync(): Promise { message: 'Build ID:', type: 'text', validate: (val: string): string | boolean => { - if (!uuid.validate(val)) { - return `${val} is not a valid id`; + if (!isUuidV4(val)) { + return `${val} is not a valid ID`; } else { return true; } @@ -276,6 +288,42 @@ async function askForBuildIdAsync(): Promise { return id; } +async function askIfUseBuildIdFromUrlAsync( + source: ArchiveUrlSource, + buildId: string +): Promise { + const { url } = source; + Log.warn(`It seems that you provided a build details page URL: ${url}`); + Log.warn('We expected to see the build artifact URL.'); + if (!source.nonInteractive) { + const useAsBuildId = await confirmAsync({ + message: `Do you want to submit build ${buildId} instead?`, + }); + if (useAsBuildId) { + return true; + } else { + Log.warn('The submission will most probably fail.'); + } + } else { + Log.warn("Proceeding because you've run this command in non-interactive mode."); + } + return false; +} + +function isBuildDetailsPage(url: string): string | false { + 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: string): boolean { const protocols = ['http', 'https']; try { @@ -289,3 +337,7 @@ function validateUrl(url: string): boolean { return false; } } + +export function isUuidV4(s: string): boolean { + return uuid.validate(s) && uuid.version(s) === 4; +} diff --git a/packages/eas-cli/src/submissions/__tests__/ArchiveSource-test.ts b/packages/eas-cli/src/submissions/__tests__/ArchiveSource-test.ts index 5522892084..64c8113c14 100644 --- a/packages/eas-cli/src/submissions/__tests__/ArchiveSource-test.ts +++ b/packages/eas-cli/src/submissions/__tests__/ArchiveSource-test.ts @@ -6,7 +6,7 @@ import { v4 as uuidv4 } from 'uuid'; import { asMock } from '../../__tests__/utils'; import { AppPlatform, BuildFragment, UploadSessionType } from '../../graphql/generated'; import { toAppPlatform } from '../../graphql/types/AppPlatform'; -import { promptAsync } from '../../prompts'; +import { confirmAsync, promptAsync } from '../../prompts'; import { uploadAsync } from '../../uploads'; import { Archive, ArchiveSourceType, getArchiveAsync } from '../ArchiveSource'; import { getBuildByIdForSubmissionAsync, getLatestBuildForSubmissionAsync } from '../utils/builds'; @@ -32,6 +32,7 @@ const SOURCE_STUB_INPUT = { projectId: uuidv4(), platform: Platform.ANDROID, projectDir: '.', + nonInteractive: false, }; describe(getArchiveAsync, () => { @@ -42,6 +43,11 @@ describe(getArchiveAsync, () => { asMock(promptAsync).mockImplementation(() => { throw new Error(`unhandled prompts call - this shouldn't happen - fix tests!`); }); + + asMock(confirmAsync).mockReset(); + asMock(confirmAsync).mockImplementation(() => { + throw new Error(`unhandled prompts call - this shouldn't happen - fix tests!`); + }); }); it('handles URL source', async () => { @@ -69,6 +75,20 @@ describe(getArchiveAsync, () => { assertArchiveResult(archive, ArchiveSourceType.url); }); + it('asks the user if use build id instead of build details page url', async () => { + asMock(confirmAsync).mockResolvedValueOnce(true); + + const archive = await getArchiveAsync({ + ...SOURCE_STUB_INPUT, + sourceType: ArchiveSourceType.url, + url: 'https://expo.dev/accounts/turtle/projects/blah/builds/81da6b36-efe4-4262-8970-84f03efeec81', + }); + + expect(confirmAsync).toHaveBeenCalled(); + assertArchiveResult(archive, ArchiveSourceType.buildId); + expect((archive.source as any).id).toBe('81da6b36-efe4-4262-8970-84f03efeec81'); + }); + it('handles prompt source', async () => { asMock(promptAsync) .mockResolvedValueOnce({ sourceType: ArchiveSourceType.url }) diff --git a/packages/eas-cli/src/submissions/commons.ts b/packages/eas-cli/src/submissions/commons.ts index aacdaa6d1f..246b6c0643 100644 --- a/packages/eas-cli/src/submissions/commons.ts +++ b/packages/eas-cli/src/submissions/commons.ts @@ -1,7 +1,6 @@ import { Platform } from '@expo/eas-build-job'; -import * as uuid from 'uuid'; -import { ArchiveSource, ArchiveSourceType } from './ArchiveSource'; +import { ArchiveSource, ArchiveSourceType, isUuidV4 } from './ArchiveSource'; import { SubmissionContext } from './context'; export function resolveArchiveSource( @@ -20,6 +19,7 @@ export function resolveArchiveSource( url, platform, projectId: ctx.projectId, + nonInteractive: ctx.nonInteractive, }; } else if (path) { return { @@ -27,22 +27,25 @@ export function resolveArchiveSource( path, platform, projectId: ctx.projectId, + nonInteractive: ctx.nonInteractive, }; } else if (id) { - if (!uuid.validate(id)) { - throw new Error(`${id} is not an ID`); + if (!isUuidV4(id)) { + throw new Error(`${id} is not a valid ID`); } return { sourceType: ArchiveSourceType.buildId, id, platform, projectId: ctx.projectId, + nonInteractive: ctx.nonInteractive, }; } else if (latest) { return { sourceType: ArchiveSourceType.latest, platform, projectId: ctx.projectId, + nonInteractive: ctx.nonInteractive, }; } else if (ctx.nonInteractive) { throw new Error('You need to specify the archive source when running in non-interactive mode '); @@ -51,6 +54,7 @@ export function resolveArchiveSource( sourceType: ArchiveSourceType.prompt, platform, projectId: ctx.projectId, + nonInteractive: ctx.nonInteractive, }; } }