1eeffdb10STomasz Sapetaimport { Command } from '@expo/commander'; 2eeffdb10STomasz Sapetaimport aws from 'aws-sdk'; 3eeffdb10STomasz Sapetaimport chalk from 'chalk'; 4eeffdb10STomasz Sapetaimport fs from 'fs-extra'; 5eeffdb10STomasz Sapetaimport inquirer from 'inquirer'; 6eeffdb10STomasz Sapetaimport path from 'path'; 7eeffdb10STomasz Sapeta 8eeffdb10STomasz Sapetaimport { EXPO_DIR } from '../Constants'; 9eeffdb10STomasz Sapetaimport { link } from '../Formatter'; 10eeffdb10STomasz Sapetaimport Git from '../Git'; 11eeffdb10STomasz Sapetaimport logger from '../Logger'; 12eeffdb10STomasz Sapetaimport { getNewestSDKVersionAsync } from '../ProjectVersions'; 13*a272999eSBartosz Kaszubowskiimport { modifySdkVersionsAsync, getSdkVersionsAsync } from '../Versions'; 14*a272999eSBartosz Kaszubowskiimport AndroidClientBuilder from '../client-build/AndroidClientBuilder'; 15*a272999eSBartosz Kaszubowskiimport IosClientBuilder from '../client-build/IosClientBuilder'; 16*a272999eSBartosz Kaszubowskiimport { ClientBuilder, ClientBuildFlavor, Platform } from '../client-build/types'; 17*a272999eSBartosz Kaszubowskiimport askForPlatformAsync from '../utils/askForPlatformAsync'; 18*a272999eSBartosz Kaszubowskiimport askForSdkVersionAsync from '../utils/askForSDKVersionAsync'; 19eeffdb10STomasz Sapeta 20eeffdb10STomasz Sapetaconst s3Client = new aws.S3({ region: 'us-east-1' }); 21eeffdb10STomasz Sapetaconst { yellow, blue, magenta } = chalk; 22eeffdb10STomasz Sapeta 23eeffdb10STomasz Sapetatype ActionOptions = { 24eeffdb10STomasz Sapeta platform?: Platform; 25eeffdb10STomasz Sapeta release: boolean; 26cf276e6eSTomasz Sapeta flavor: ClientBuildFlavor; 27eeffdb10STomasz Sapeta}; 28eeffdb10STomasz Sapeta 29cf276e6eSTomasz Sapetaconst flavors = ['versioned', 'unversioned']; 30cf276e6eSTomasz Sapeta 31eeffdb10STomasz Sapetaexport default (program: Command) => { 32eeffdb10STomasz Sapeta program 33eeffdb10STomasz Sapeta .command('client-build') 34eeffdb10STomasz Sapeta .alias('cb') 35eeffdb10STomasz Sapeta .description( 36eeffdb10STomasz Sapeta 'Builds Expo client for iOS simulator or APK for Android, uploads the archive to S3 and saves its url to versions endpoint.' 37eeffdb10STomasz Sapeta ) 38eeffdb10STomasz Sapeta .option('-p, --platform [string]', 'Platform for which the client will be built.') 39eeffdb10STomasz Sapeta .option( 40eeffdb10STomasz Sapeta '-r, --release', 41eeffdb10STomasz Sapeta 'Whether to upload and release the client build to staging versions endpoint.', 42eeffdb10STomasz Sapeta false 43eeffdb10STomasz Sapeta ) 44cf276e6eSTomasz Sapeta .option( 45cf276e6eSTomasz Sapeta '-f, --flavor [string]', 46cf276e6eSTomasz Sapeta `Which build flavor to use. Possible values: ${flavors}`, 47cf276e6eSTomasz Sapeta flavors[0] 48cf276e6eSTomasz Sapeta ) 49eeffdb10STomasz Sapeta .asyncAction(main); 50eeffdb10STomasz Sapeta}; 51eeffdb10STomasz Sapeta 52eeffdb10STomasz Sapetaasync function main(options: ActionOptions) { 53eeffdb10STomasz Sapeta const platform = options.platform || (await askForPlatformAsync()); 54eeffdb10STomasz Sapeta const sdkBranchVersion = await Git.getSDKVersionFromBranchNameAsync(); 55eeffdb10STomasz Sapeta 56eeffdb10STomasz Sapeta if (options.release && !sdkBranchVersion) { 57eeffdb10STomasz Sapeta throw new Error(`Client builds can be released only from the release branch!`); 58eeffdb10STomasz Sapeta } 59cf276e6eSTomasz Sapeta if (!Object.values(ClientBuildFlavor).includes(options.flavor)) { 60cf276e6eSTomasz Sapeta throw new Error(`Flavor "${options.flavor}" is not valid, use one of: ${flavors}`); 61cf276e6eSTomasz Sapeta } 62eeffdb10STomasz Sapeta 63eeffdb10STomasz Sapeta const builder = getBuilderForPlatform(platform); 64eeffdb10STomasz Sapeta const sdkVersion = 65eeffdb10STomasz Sapeta sdkBranchVersion || 66eeffdb10STomasz Sapeta (await askForSdkVersionAsync(platform, await getNewestSDKVersionAsync(platform))); 67eeffdb10STomasz Sapeta const appVersion = await builder.getAppVersionAsync(); 68eeffdb10STomasz Sapeta 69cf276e6eSTomasz Sapeta await buildOrUseCacheAsync(builder, options.flavor); 70eeffdb10STomasz Sapeta 71eeffdb10STomasz Sapeta if (sdkVersion && options.release) { 72eeffdb10STomasz Sapeta await uploadAsync(builder, sdkVersion, appVersion); 73eeffdb10STomasz Sapeta await releaseAsync(builder, sdkVersion, appVersion); 74eeffdb10STomasz Sapeta } 75eeffdb10STomasz Sapeta} 76eeffdb10STomasz Sapeta 77eeffdb10STomasz Sapetafunction getBuilderForPlatform(platform: Platform): ClientBuilder { 78eeffdb10STomasz Sapeta switch (platform) { 79eeffdb10STomasz Sapeta case 'ios': 80eeffdb10STomasz Sapeta return new IosClientBuilder(); 81eeffdb10STomasz Sapeta case 'android': 82eeffdb10STomasz Sapeta return new AndroidClientBuilder(); 83eeffdb10STomasz Sapeta default: { 84eeffdb10STomasz Sapeta throw new Error(`Platform "${platform}" is not supported yet!`); 85eeffdb10STomasz Sapeta } 86eeffdb10STomasz Sapeta } 87eeffdb10STomasz Sapeta} 88eeffdb10STomasz Sapeta 89eeffdb10STomasz Sapetaasync function askToRecreateSimulatorBuildAsync(): Promise<boolean> { 90eeffdb10STomasz Sapeta if (process.env.CI) { 91eeffdb10STomasz Sapeta return false; 92eeffdb10STomasz Sapeta } 93eeffdb10STomasz Sapeta const { createNew } = await inquirer.prompt<{ createNew: boolean }>([ 94eeffdb10STomasz Sapeta { 95eeffdb10STomasz Sapeta type: 'confirm', 96eeffdb10STomasz Sapeta name: 'createNew', 97eeffdb10STomasz Sapeta message: 'Do you want to create a fresh one?', 98eeffdb10STomasz Sapeta default: true, 99eeffdb10STomasz Sapeta }, 100eeffdb10STomasz Sapeta ]); 101eeffdb10STomasz Sapeta return createNew; 102eeffdb10STomasz Sapeta} 103eeffdb10STomasz Sapeta 104eeffdb10STomasz Sapetaasync function askToOverrideBuildAsync(): Promise<boolean> { 105eeffdb10STomasz Sapeta if (process.env.CI) { 106eeffdb10STomasz Sapeta // we should never override anything in CI, too easy to accidentally mess something up in prod 107eeffdb10STomasz Sapeta return false; 108eeffdb10STomasz Sapeta } 109eeffdb10STomasz Sapeta const { override } = await inquirer.prompt<{ override: boolean }>([ 110eeffdb10STomasz Sapeta { 111eeffdb10STomasz Sapeta type: 'confirm', 112eeffdb10STomasz Sapeta name: 'override', 113eeffdb10STomasz Sapeta message: 'Do you want to override it?', 114eeffdb10STomasz Sapeta default: true, 115eeffdb10STomasz Sapeta }, 116eeffdb10STomasz Sapeta ]); 117eeffdb10STomasz Sapeta return override; 118eeffdb10STomasz Sapeta} 119eeffdb10STomasz Sapeta 120cf276e6eSTomasz Sapetaasync function buildOrUseCacheAsync( 121cf276e6eSTomasz Sapeta builder: ClientBuilder, 122cf276e6eSTomasz Sapeta flavor: ClientBuildFlavor 123cf276e6eSTomasz Sapeta): Promise<void> { 124eeffdb10STomasz Sapeta const appPath = builder.getAppPath(); 125eeffdb10STomasz Sapeta 126eeffdb10STomasz Sapeta // Build directory already exists, we could reuse that one — especially useful on the CI. 127eeffdb10STomasz Sapeta if (await fs.pathExists(appPath)) { 128eeffdb10STomasz Sapeta const relativeAppPath = path.relative(EXPO_DIR, appPath); 129eeffdb10STomasz Sapeta logger.info(`Client build already exists at ${magenta.bold(relativeAppPath)}`); 130eeffdb10STomasz Sapeta 131eeffdb10STomasz Sapeta if (!(await askToRecreateSimulatorBuildAsync())) { 132eeffdb10STomasz Sapeta logger.info('Skipped building the app, using cached build instead...'); 133eeffdb10STomasz Sapeta return; 134eeffdb10STomasz Sapeta } 135eeffdb10STomasz Sapeta } 136cf276e6eSTomasz Sapeta await builder.buildAsync(flavor); 137eeffdb10STomasz Sapeta} 138eeffdb10STomasz Sapeta 139eeffdb10STomasz Sapetaasync function uploadAsync( 140eeffdb10STomasz Sapeta builder: ClientBuilder, 141eeffdb10STomasz Sapeta sdkVersion: string, 142eeffdb10STomasz Sapeta appVersion: string 143eeffdb10STomasz Sapeta): Promise<void> { 144eeffdb10STomasz Sapeta const sdkVersions = await getSdkVersionsAsync(sdkVersion); 145eeffdb10STomasz Sapeta 146eeffdb10STomasz Sapeta // Target app url already defined in versions endpoint. 147eeffdb10STomasz Sapeta // We make this check to reduce the risk of unintentional overrides. 148eeffdb10STomasz Sapeta if (sdkVersions?.[`${builder.platform}ClientUrl`] === builder.getClientUrl(appVersion)) { 149eeffdb10STomasz Sapeta logger.info(`Build ${yellow.bold(appVersion)} is already defined in versions endpoint.`); 150eeffdb10STomasz Sapeta logger.info('The new build would be uploaded onto the same URL.'); 151eeffdb10STomasz Sapeta 152eeffdb10STomasz Sapeta if (!(await askToOverrideBuildAsync())) { 153eeffdb10STomasz Sapeta logger.warn('Refused overriding the build, exiting the proces...'); 154eeffdb10STomasz Sapeta process.exit(0); 155eeffdb10STomasz Sapeta return; 156eeffdb10STomasz Sapeta } 157eeffdb10STomasz Sapeta } 158eeffdb10STomasz Sapeta logger.info(`Uploading ${yellow.bold(appVersion)} build`); 159eeffdb10STomasz Sapeta 160eeffdb10STomasz Sapeta await builder.uploadBuildAsync(s3Client, appVersion); 161eeffdb10STomasz Sapeta} 162eeffdb10STomasz Sapeta 163eeffdb10STomasz Sapetaasync function releaseAsync( 164eeffdb10STomasz Sapeta builder: ClientBuilder, 165eeffdb10STomasz Sapeta sdkVersion: string, 166eeffdb10STomasz Sapeta appVersion: string 167eeffdb10STomasz Sapeta): Promise<void> { 168eeffdb10STomasz Sapeta const clientUrl = builder.getClientUrl(appVersion); 169eeffdb10STomasz Sapeta 170eeffdb10STomasz Sapeta logger.info( 171eeffdb10STomasz Sapeta `Updating versions endpoint with client url ${blue.bold(link(clientUrl, clientUrl))}` 172eeffdb10STomasz Sapeta ); 173eeffdb10STomasz Sapeta 174eeffdb10STomasz Sapeta await updateClientUrlAndVersionAsync(builder, sdkVersion, appVersion); 175eeffdb10STomasz Sapeta} 176eeffdb10STomasz Sapeta 177eeffdb10STomasz Sapetaasync function updateClientUrlAndVersionAsync( 178eeffdb10STomasz Sapeta builder: ClientBuilder, 179eeffdb10STomasz Sapeta sdkVersion: string, 180eeffdb10STomasz Sapeta appVersion: string 181eeffdb10STomasz Sapeta) { 182eeffdb10STomasz Sapeta await modifySdkVersionsAsync(sdkVersion, (sdkVersions) => { 183eeffdb10STomasz Sapeta sdkVersions[`${builder.platform}ClientUrl`] = builder.getClientUrl(appVersion); 184eeffdb10STomasz Sapeta sdkVersions[`${builder.platform}ClientVersion`] = appVersion; 185eeffdb10STomasz Sapeta return sdkVersions; 186eeffdb10STomasz Sapeta }); 187eeffdb10STomasz Sapeta} 188