Skip to content

Commit cae11db

Browse files
committed
feat(appflow): Support downloading all artifact types from appflow builds
1 parent cee620b commit cae11db

File tree

1 file changed

+123
-19
lines changed
  • packages/@ionic/cli/src/commands/package

1 file changed

+123
-19
lines changed

packages/@ionic/cli/src/commands/package/build.ts

Lines changed: 123 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,9 @@ const ANDROID_BUILD_TYPES = ['debug', 'release'];
2222
const IOS_BUILD_TYPES = ['development', 'ad-hoc', 'app-store', 'enterprise'];
2323
const APP_STORE_COMPATIBLE_TYPES = ['release', 'app-store', 'enterprise'];
2424
const BUILD_TYPES = ANDROID_BUILD_TYPES.concat(IOS_BUILD_TYPES);
25+
const ANDROID_ARTIFACT_TYPES = ['aab', 'apk'];
26+
const IOS_ARTIFACT_TYPES = ['ipa', 'dsym'];
27+
const ARTIFACT_TYPES = ANDROID_ARTIFACT_TYPES.concat(IOS_ARTIFACT_TYPES);
2528
const TARGET_PLATFORM = ['Android', 'iOS - Xcode 11 (Preferred)', 'iOS - Xcode 10'];
2629

2730
export interface PackageBuild {
@@ -74,6 +77,12 @@ Customizing the build:
7477
Deploying the build to an App Store:
7578
- The ${input('--destination')} option can be used to deliver the app created by the build to the configured App Store. \
7679
This can be used only together with build type ${input('release')} for Android and build types ${input('app-store')} or ${input('enterprise')} for iOS.
80+
81+
Downloading build artifacts:
82+
- By default once the build is complete, all artifacts are downloaded for the selected platform. ${input('aab')} and ${input('apk')} for Android \
83+
${input('ipa')} and ${input('dsym')} for iOS.
84+
- The ${input('--artifact-type')} option can be used to limit artifact downloads to only of that type. For instance, with Android, you can specify ${input('aab')} \
85+
if you do not wish to download ${input('apk')}.
7786
`,
7887
footnotes: [
7988
{
@@ -87,8 +96,10 @@ This can be used only together with build type ${input('release')} for Android a
8796
'android debug --environment="My Custom Environment Name"',
8897
'android debug --native-config="My Custom Native Config Name"',
8998
'android debug --commit=2345cd3305a1cf94de34e93b73a932f25baac77c',
99+
'android debug --artifact-type=aab',
100+
'android debug --aab-name="my-app-prod.aab" --apk-name="my-app-prod.apk"',
90101
'ios development --signing-certificate="iOS Signing Certificate Name" --build-stack="iOS - Xcode 9"',
91-
'ios development --signing-certificate="iOS Signing Certificate Name" --build-file-name=my_custom_file_name.ipa',
102+
'ios development --signing-certificate="iOS Signing Certificate Name" --ipa-name=my_custom_file_name.ipa',
92103
'ios app-store --signing-certificate="iOS Signing Certificate Name" --destination="Apple App Store Destination"',
93104
],
94105
inputs: [
@@ -146,9 +157,43 @@ This can be used only together with build type ${input('release')} for Android a
146157
name: 'build-file-name',
147158
summary: 'The name for the downloaded build file',
148159
type: String,
160+
groups: [MetadataGroup.DEPRECATED],
161+
spec: { value: 'name' },
162+
},
163+
{
164+
name: 'ipa-name',
165+
summary: 'The name for the downloaded ipa file',
166+
type: String,
149167
groups: [MetadataGroup.ADVANCED],
150168
spec: { value: 'name' },
151169
},
170+
{
171+
name: 'dsym-name',
172+
summary: 'The name for the downloaded dsym file',
173+
type: String,
174+
groups: [MetadataGroup.ADVANCED],
175+
spec: { value: 'name' },
176+
},
177+
{
178+
name: 'apk-name',
179+
summary: 'The name for the downloaded apk file',
180+
type: String,
181+
groups: [MetadataGroup.ADVANCED],
182+
spec: { value: 'name' },
183+
},
184+
{
185+
name: 'aab-name',
186+
summary: 'The name for the downloaded aab file',
187+
type: String,
188+
groups: [MetadataGroup.ADVANCED],
189+
spec: { value: 'name' },
190+
},
191+
{
192+
name: 'artifact-type',
193+
summary: `The artifact type (${ARTIFACT_TYPES.map(v => input(v)).join(', ')})`,
194+
type: String,
195+
spec: { value: 'name' },
196+
},
152197
],
153198
};
154199
}
@@ -169,14 +214,14 @@ This can be used only together with build type ${input('release')} for Android a
169214
const buildTypes = inputs[0] === 'ios' ? IOS_BUILD_TYPES : ANDROID_BUILD_TYPES;
170215

171216
// validate that the build type is valid for the platform
172-
let reenterBuilType = false;
217+
let reenterBuildType = false;
173218
if (inputs[1] && !buildTypes.includes(inputs[1])) {
174-
reenterBuilType = true;
219+
reenterBuildType = true;
175220
this.env.log.nl();
176221
this.env.log.warn(`Build type ${strong(inputs[1])} incompatible for ${strong(inputs[0])}; please choose a correct one`);
177222
}
178223

179-
if (!inputs[1] || reenterBuilType) {
224+
if (!inputs[1] || reenterBuildType) {
180225
const typeInput = await this.env.prompt({
181226
type: 'list',
182227
name: 'type',
@@ -218,6 +263,26 @@ This can be used only together with build type ${input('release')} for Android a
218263
if (options['destination'] && !APP_STORE_COMPATIBLE_TYPES.includes(inputs[1])) {
219264
throw new FatalException(`Build with type ${strong(String(inputs[1]))} cannot be deployed to App Store`);
220265
}
266+
267+
if (options['artifact-type'] && (Array.isArray(options['artifact-type']) || typeof options['artifact-type'] === 'string')) {
268+
const artifactTypes = Array.isArray(options['artifact-type']) ? options['artifact-type'] : [options['artifact-type']];
269+
let unsupported: string[];
270+
271+
switch (inputs[0]) {
272+
case 'android':
273+
unsupported = artifactTypes.filter(type => !ANDROID_ARTIFACT_TYPES.includes(type));
274+
break;
275+
case 'ios':
276+
unsupported = artifactTypes.filter(type => !IOS_ARTIFACT_TYPES.includes(type));
277+
break;
278+
default:
279+
throw new FatalException(`Unsupported platform ${inputs[0]}`);
280+
}
281+
282+
if (unsupported.length) {
283+
throw new FatalException(`Unsupported artifact types for platform ${inputs[0]}: ${[...unsupported]}`);
284+
}
285+
}
221286
}
222287

223288
async run(inputs: CommandLineInputs, options: CommandLineOptions): Promise<void> {
@@ -228,6 +293,16 @@ This can be used only together with build type ${input('release')} for Android a
228293
const token = await this.env.session.getUserToken();
229294
const appflowId = await this.project.requireAppflowId();
230295
const [ platform, buildType ] = inputs;
296+
let artifactTypes;
297+
298+
if (Array.isArray(options['artifact-type'])) {
299+
artifactTypes = options['artifact-type'].map(val => val.toUpperCase());
300+
} else if (typeof options['artifact-type'] == 'string' && ARTIFACT_TYPES.includes(options['artifact-type'])) {
301+
artifactTypes = [options['artifact-type'].toUpperCase()]
302+
} else {
303+
const useOriginalDefault = !!(options['build-file-name']);
304+
artifactTypes = await this.getDefaultArtifactType(platform, useOriginalDefault);
305+
}
231306

232307
if (!options.commit) {
233308
options.commit = (await this.env.shell.output('git', ['rev-parse', 'HEAD'], { cwd: this.project.directory })).trim();
@@ -237,20 +312,13 @@ This can be used only together with build type ${input('release')} for Android a
237312
let build = await this.createPackageBuild(appflowId, token, platform, buildType, options);
238313
const buildId = build.job_id;
239314

240-
let customBuildFileName = '';
241-
if (options['build-file-name']) {
242-
if (typeof (options['build-file-name']) !== 'string' || !fileUtils.isValidFileName(options['build-file-name'])) {
243-
throw new FatalException(`${strong(String(options['build-file-name']))} is not a valid file name`);
244-
}
245-
customBuildFileName = String(options['build-file-name']);
246-
}
247-
248315
const details = columnar([
249316
['App ID', strong(appflowId)],
250317
['Build ID', strong(buildId.toString())],
251318
['Commit', strong(`${build.commit.sha.substring(0, 6)} ${build.commit.note}`)],
252319
['Target Platform', strong(build.stack.friendly_name)],
253320
['Build Type', strong(build.build_type)],
321+
['Artifact Type(s)', strong(`${artifactTypes}`)],
254322
['Security Profile', build.profile_tag ? strong(build.profile_tag) : weak('not set')],
255323
['Environment', build.environment_name ? strong(build.environment_name) : weak('not set')],
256324
['Native Config', build.native_config_name ? strong(build.native_config_name) : weak('not set')],
@@ -267,13 +335,49 @@ This can be used only together with build type ${input('release')} for Android a
267335
throw new Error(`Build ${build.state}`);
268336
}
269337

270-
const url = await this.getDownloadUrl(appflowId, buildId, token);
271-
if (!url.url) {
272-
throw new Error('Missing URL in response');
338+
for (const artifactType of artifactTypes) {
339+
const url = await this.getDownloadUrl(appflowId, buildId, artifactType, token);
340+
341+
if (!url.url) {
342+
throw new Error('Missing URL in response');
343+
}
344+
345+
let customBuildFileName = '';
346+
347+
if (options['build-file-name']) {
348+
this.env.log.warn(`The ${input('--build-file-name')} option has been deprecated. Please use ${input('--ipa-name')}, ${input('--apk-name')} or ${input('--{ artifact }-name')}.`);
349+
customBuildFileName = await this.sanitizeString(options['build-file-name']);
350+
} else {
351+
customBuildFileName = await this.sanitizeString(options[`${artifactType.toLowerCase()}-name`]);
352+
}
353+
354+
const filename = await this.downloadBuild(url.url, customBuildFileName);
355+
this.env.log.ok(`Artifact downloaded: ${filename}`);
356+
}
357+
}
358+
359+
async sanitizeString(value: string | string[] | boolean | null | undefined): Promise<string> {
360+
361+
if (!value || typeof (value) !== 'string') {
362+
return '';
363+
}
364+
365+
if (!fileUtils.isValidFileName(value)) {
366+
throw new FatalException(`${strong(String(value))} is not a valid file name`);
273367
}
274368

275-
const filename = await this.downloadBuild(url.url, customBuildFileName);
276-
this.env.log.ok(`Build completed: ${filename}`);
369+
return String(value);
370+
}
371+
372+
async getDefaultArtifactType(platform: string, useOriginalDefault?: boolean): Promise<Array<string>> {
373+
switch (platform) {
374+
case 'ios':
375+
return useOriginalDefault ? ['IPA'] : ['IPA', 'DSYM'];
376+
case 'android':
377+
return useOriginalDefault ? ['APK'] : ['APK', 'AAB'] ;
378+
default:
379+
throw new Error(`No default artifact type for platform '${platform}'`);
380+
}
277381
}
278382

279383
async createPackageBuild(appflowId: string, token: string, platform: string, buildType: string, options: CommandLineOptions): Promise<PackageBuild> {
@@ -325,8 +429,8 @@ This can be used only together with build type ${input('release')} for Android a
325429
}
326430
}
327431

328-
async getDownloadUrl(appflowId: string, buildId: number, token: string): Promise<DownloadUrl> {
329-
const { req } = await this.env.client.make('GET', `/apps/${appflowId}/packages/${buildId}/download`);
432+
async getDownloadUrl(appflowId: string, buildId: number, artifactType: string, token: string): Promise<DownloadUrl> {
433+
const { req } = await this.env.client.make('GET', `/apps/${appflowId}/packages/${buildId}/download?artifact_type=${artifactType}`);
330434
req.set('Authorization', `Bearer ${token}`).send();
331435

332436
try {

0 commit comments

Comments
 (0)