@capawesome/cli 4.21.0 → 4.23.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +23 -0
- package/dist/commands/apps/automations/create.js +2 -2
- package/dist/commands/apps/automations/update.js +3 -3
- package/dist/commands/apps/builds/create.js +71 -90
- package/dist/commands/apps/builds/create.test.js +122 -0
- package/dist/commands/apps/builds/download.js +46 -133
- package/dist/commands/apps/builds/download.test.js +85 -0
- package/dist/commands/apps/builds/run.js +2 -1
- package/dist/commands/apps/deployments/create.js +12 -0
- package/dist/commands/apps/deployments/create.test.js +70 -0
- package/dist/commands/apps/destinations/create.js +217 -58
- package/dist/commands/apps/destinations/create.test.js +291 -0
- package/dist/commands/apps/destinations/update.js +47 -1
- package/dist/commands/apps/destinations/update.test.js +138 -0
- package/dist/commands/apps/import.js +2 -4
- package/dist/commands/apps/liveupdates/create.js +2 -2
- package/dist/utils/app-build-artifacts.js +87 -0
- package/dist/utils/app-build-artifacts.test.js +51 -0
- package/dist/utils/appflow-export.js +27 -5
- package/dist/utils/appflow-export.test.js +33 -0
- package/dist/utils/cli-options.js +7 -0
- package/dist/utils/cli-options.test.js +16 -0
- package/dist/utils/release-notes.js +55 -0
- package/dist/utils/release-notes.test.js +74 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,29 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file. See [commit-and-tag-version](https://github.com/absolute-version/commit-and-tag-version) for commit guidelines.
|
|
4
4
|
|
|
5
|
+
## [4.23.0](https://github.com/capawesome-team/cli/compare/v4.22.0...v4.23.0) (2026-09-30)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
### Features
|
|
9
|
+
|
|
10
|
+
* **apps:** add release notes and App Store submission flags ([#232](https://github.com/capawesome-team/cli/issues/232)) ([e32b057](https://github.com/capawesome-team/cli/commit/e32b057e0b06cd4e78c1d6c0dba25bceb2323e7e))
|
|
11
|
+
* **apps:** support Huawei AppGallery and Firebase App Distribution destinations ([#229](https://github.com/capawesome-team/cli/issues/229)) ([78d3859](https://github.com/capawesome-team/cli/commit/78d38595feebfc5d1f81ca411ec6fe1aa5f7943f))
|
|
12
|
+
* deploy builds to channels server-side and support multiple channels ([#210](https://github.com/capawesome-team/cli/issues/210)) ([c1722b9](https://github.com/capawesome-team/cli/commit/c1722b9d40b3d491228cedae95ee8e7a172d2cd9))
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
### Bug Fixes
|
|
16
|
+
|
|
17
|
+
* **apps:** rename Appflow tool version variables on import ([#230](https://github.com/capawesome-team/cli/issues/230)) ([b155a3a](https://github.com/capawesome-team/cli/commit/b155a3aa0eb62a67f922bb9783bf0740f345ffe0))
|
|
18
|
+
|
|
19
|
+
## [4.22.0](https://github.com/capawesome-team/cli/compare/v4.21.0...v4.22.0) (2026-09-24)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
### Features
|
|
23
|
+
|
|
24
|
+
* **apps:** add `--app` flag to download the simulator app bundle ([#225](https://github.com/capawesome-team/cli/issues/225)) ([ee40c92](https://github.com/capawesome-team/cli/commit/ee40c92f2d73e878b7fa1f9089cdf9420daa2ae4))
|
|
25
|
+
* **apps:** add the `macos-golden-gate` build stack ([#226](https://github.com/capawesome-team/cli/issues/226)) ([23d4d39](https://github.com/capawesome-team/cli/commit/23d4d3961a752a6bf087f86aa9e93805a9026647))
|
|
26
|
+
* **apps:** download build artifacts by form factor ([#216](https://github.com/capawesome-team/cli/issues/216)) ([cd06192](https://github.com/capawesome-team/cli/commit/cd061920c69ac796e271a00141ec2284cc0ae526))
|
|
27
|
+
|
|
5
28
|
## [4.21.0](https://github.com/capawesome-team/cli/compare/v4.20.0...v4.21.0) (2026-09-22)
|
|
6
29
|
|
|
7
30
|
|
|
@@ -28,8 +28,8 @@ export default defineCommand({
|
|
|
28
28
|
.optional()
|
|
29
29
|
.describe('The platform for the build. Supported values are `android`, `ios`, and `web`.'),
|
|
30
30
|
stack: z
|
|
31
|
-
.enum(['macos-sequoia', 'macos-tahoe'], {
|
|
32
|
-
message: 'Build stack must be
|
|
31
|
+
.enum(['macos-sequoia', 'macos-tahoe', 'macos-golden-gate'], {
|
|
32
|
+
message: 'Build stack must be one of `macos-sequoia`, `macos-tahoe`, or `macos-golden-gate`.',
|
|
33
33
|
})
|
|
34
34
|
.optional()
|
|
35
35
|
.describe('The build stack to use for the build process.'),
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import appAutomationsService from '../../../services/app-automations.js';
|
|
2
2
|
import { withAuth } from '../../../utils/auth.js';
|
|
3
|
+
import { clearableValue } from '../../../utils/cli-options.js';
|
|
3
4
|
import { isInteractive } from '../../../utils/environment.js';
|
|
4
5
|
import { prompt, promptAppSelection, promptOrganizationSelection } from '../../../utils/prompt.js';
|
|
5
6
|
import consola from 'consola';
|
|
6
7
|
import { z } from 'zod';
|
|
7
8
|
import { defineCommand, defineOptions } from 'zodline';
|
|
8
|
-
const clearableValue = (value) => (value === '' ? null : value);
|
|
9
9
|
const clearableValues = (values) => values?.filter((value) => value !== '');
|
|
10
10
|
export default defineCommand({
|
|
11
11
|
description: 'Update an existing app automation.',
|
|
@@ -45,8 +45,8 @@ export default defineCommand({
|
|
|
45
45
|
.optional()
|
|
46
46
|
.describe('The platform for the build. Supported values are `android`, `ios`, and `web`.'),
|
|
47
47
|
stack: z
|
|
48
|
-
.enum(['macos-sequoia', 'macos-tahoe'], {
|
|
49
|
-
message: 'Build stack must be
|
|
48
|
+
.enum(['macos-sequoia', 'macos-tahoe', 'macos-golden-gate'], {
|
|
49
|
+
message: 'Build stack must be one of `macos-sequoia`, `macos-tahoe`, or `macos-golden-gate`.',
|
|
50
50
|
})
|
|
51
51
|
.optional()
|
|
52
52
|
.describe('The build stack to use for the build process.'),
|
|
@@ -5,15 +5,18 @@ import appCertificatesService from '../../../services/app-certificates.js';
|
|
|
5
5
|
import appConfigurationsService from '../../../services/app-configurations.js';
|
|
6
6
|
import appEnvironmentsService from '../../../services/app-environments.js';
|
|
7
7
|
import appsService from '../../../services/apps.js';
|
|
8
|
+
import { APP_BUILD_ARTIFACT_FORM_FACTORS, APP_BUILD_ARTIFACT_TYPES_BY_PLATFORM, handleAppBuildArtifactDownload, } from '../../../utils/app-build-artifacts.js';
|
|
8
9
|
import { getAppBuildShareUrls } from '../../../utils/app-build-shares.js';
|
|
9
10
|
import { parseKeyValuePairs } from '../../../utils/app-environments.js';
|
|
10
11
|
import { withAuth } from '../../../utils/auth.js';
|
|
11
12
|
import { createBufferFromPath } from '../../../utils/buffer.js';
|
|
13
|
+
import { parseListOption } from '../../../utils/cli-options.js';
|
|
12
14
|
import { isInteractive } from '../../../utils/environment.js';
|
|
13
15
|
import { offerJobFailureSummary } from '../../../utils/job-failure-summary.js';
|
|
14
16
|
import { isDirectory, isReadable } from '../../../utils/file.js';
|
|
15
17
|
import { waitForJobCompletion } from '../../../utils/job.js';
|
|
16
18
|
import { prompt, promptAppSelection, promptOrganizationSelection } from '../../../utils/prompt.js';
|
|
19
|
+
import { parseReleaseNotes } from '../../../utils/release-notes.js';
|
|
17
20
|
import zip from '../../../utils/zip.js';
|
|
18
21
|
import consola from 'consola';
|
|
19
22
|
import fs from 'fs/promises';
|
|
@@ -33,6 +36,10 @@ export default defineCommand({
|
|
|
33
36
|
.union([z.boolean(), z.string()])
|
|
34
37
|
.optional()
|
|
35
38
|
.describe('Download the generated APK file (Android only). Optionally provide a file path.'),
|
|
39
|
+
app: z
|
|
40
|
+
.union([z.boolean(), z.string()])
|
|
41
|
+
.optional()
|
|
42
|
+
.describe('Download the generated APP file, a zipped `.app` bundle (iOS simulator builds only). Optionally provide a file path.'),
|
|
36
43
|
appId: z
|
|
37
44
|
.uuid({
|
|
38
45
|
message: 'App ID must be a UUID.',
|
|
@@ -40,7 +47,10 @@ export default defineCommand({
|
|
|
40
47
|
.optional()
|
|
41
48
|
.describe('App ID to create the build for.'),
|
|
42
49
|
certificate: z.string().optional().describe('The name of the certificate to use for the build.'),
|
|
43
|
-
channel: z
|
|
50
|
+
channel: z
|
|
51
|
+
.array(z.string())
|
|
52
|
+
.optional()
|
|
53
|
+
.describe('The name of a channel to deploy to (Web only). Can be specified multiple times or comma-separated.'),
|
|
44
54
|
configuration: z.string().optional().describe('The name of the native configuration (Android/iOS only).'),
|
|
45
55
|
destination: z.string().optional().describe('The name of the destination to deploy to (Android/iOS only).'),
|
|
46
56
|
detached: z
|
|
@@ -52,6 +62,12 @@ export default defineCommand({
|
|
|
52
62
|
.boolean()
|
|
53
63
|
.optional()
|
|
54
64
|
.describe('Request an AI-powered failure summary (Capawesome Cloud Assist) if the build fails.'),
|
|
65
|
+
formFactor: z
|
|
66
|
+
.enum(APP_BUILD_ARTIFACT_FORM_FACTORS, {
|
|
67
|
+
message: 'Invalid form factor. Must be one of `mobile`, `watch`, `tv`, or `automotive`.',
|
|
68
|
+
})
|
|
69
|
+
.optional()
|
|
70
|
+
.describe('The form factor of the Android artifact to download with `--apk` or `--aab`. Supported values are `mobile`, `watch`, `tv`, and `automotive`. Without it, the first artifact in the order `mobile`, `watch`, `tv`, `automotive` is downloaded. The build always includes every application module.'),
|
|
55
71
|
gitRef: z.string().optional().describe('The Git reference (branch, tag, or commit SHA) to build.'),
|
|
56
72
|
ipa: z
|
|
57
73
|
.union([z.boolean(), z.string()])
|
|
@@ -65,6 +81,18 @@ export default defineCommand({
|
|
|
65
81
|
})
|
|
66
82
|
.optional()
|
|
67
83
|
.describe('The platform for the build. Supported values are `ios`, `android`, and `web`.'),
|
|
84
|
+
releaseNotes: z
|
|
85
|
+
.string()
|
|
86
|
+
.optional()
|
|
87
|
+
.describe('Release notes for the deployment (default text). Requires --destination.'),
|
|
88
|
+
releaseNotesFile: z
|
|
89
|
+
.string()
|
|
90
|
+
.optional()
|
|
91
|
+
.describe('Path to a JSON file with release notes by locale, e.g. `{"default": "…", "de-DE": "…"}`. Requires --destination.'),
|
|
92
|
+
releaseNotesLocale: z
|
|
93
|
+
.array(z.string())
|
|
94
|
+
.optional()
|
|
95
|
+
.describe('Translated release notes as `<locale>=<text>`, e.g. `de-DE=Fehlerbehebungen`. Can be specified multiple times. Requires --destination.'),
|
|
68
96
|
share: z.boolean().optional().describe('Create a public share link for the build after it succeeds.'),
|
|
69
97
|
shareDescription: z
|
|
70
98
|
.string()
|
|
@@ -79,8 +107,8 @@ export default defineCommand({
|
|
|
79
107
|
.optional()
|
|
80
108
|
.describe('Number of days until the share link expires. Requires --share.'),
|
|
81
109
|
stack: z
|
|
82
|
-
.enum(['macos-sequoia', 'macos-tahoe'], {
|
|
83
|
-
message: 'Build stack must be
|
|
110
|
+
.enum(['macos-sequoia', 'macos-tahoe', 'macos-golden-gate'], {
|
|
111
|
+
message: 'Build stack must be one of `macos-sequoia`, `macos-tahoe`, or `macos-golden-gate`.',
|
|
84
112
|
})
|
|
85
113
|
.optional()
|
|
86
114
|
.describe('The build stack to use for the build process.'),
|
|
@@ -105,14 +133,15 @@ export default defineCommand({
|
|
|
105
133
|
}), { y: 'yes' }),
|
|
106
134
|
action: withAuth(async (options) => {
|
|
107
135
|
let { appId, platform, type, gitRef, environment, certificate, configuration, json, stack, path: sourcePath, url, } = options;
|
|
136
|
+
const channels = parseListOption(options.channel);
|
|
108
137
|
// Validate that detached flag cannot be used with artifact flags
|
|
109
|
-
if (options.detached && (options.apk || options.aab || options.ipa || options.zip)) {
|
|
110
|
-
consola.error('The --detached flag cannot be used with --apk, --aab, --ipa, or --zip flags.');
|
|
138
|
+
if (options.detached && (options.apk || options.aab || options.ipa || options.app || options.zip)) {
|
|
139
|
+
consola.error('The --detached flag cannot be used with --apk, --aab, --ipa, --app, or --zip flags.');
|
|
111
140
|
process.exit(1);
|
|
112
141
|
}
|
|
113
|
-
// Validate that detached flag cannot be used with
|
|
114
|
-
if (options.detached &&
|
|
115
|
-
consola.error('The --detached flag cannot be used with
|
|
142
|
+
// Validate that detached flag cannot be used with destination
|
|
143
|
+
if (options.detached && options.destination) {
|
|
144
|
+
consola.error('The --detached flag cannot be used with the --destination flag.');
|
|
116
145
|
process.exit(1);
|
|
117
146
|
}
|
|
118
147
|
// Validate that detached flag cannot be used with failure summary
|
|
@@ -131,10 +160,16 @@ export default defineCommand({
|
|
|
131
160
|
process.exit(1);
|
|
132
161
|
}
|
|
133
162
|
// Validate that channel and destination cannot be used together
|
|
134
|
-
if (
|
|
163
|
+
if (channels?.length && options.destination) {
|
|
135
164
|
consola.error('The --channel and --destination flags cannot be used together.');
|
|
136
165
|
process.exit(1);
|
|
137
166
|
}
|
|
167
|
+
// Validate the release notes before the build starts since they are only used for the deployment afterwards
|
|
168
|
+
const releaseNotes = await parseReleaseNotes(options);
|
|
169
|
+
if (releaseNotes && !options.destination) {
|
|
170
|
+
consola.error('The --release-notes, --release-notes-file, and --release-notes-locale flags require --destination.');
|
|
171
|
+
process.exit(1);
|
|
172
|
+
}
|
|
138
173
|
// Validate that path, url, and gitRef cannot be used together
|
|
139
174
|
if (sourcePath && gitRef) {
|
|
140
175
|
consola.error('The --path and --git-ref flags cannot be used together.');
|
|
@@ -244,7 +279,7 @@ export default defineCommand({
|
|
|
244
279
|
process.exit(1);
|
|
245
280
|
}
|
|
246
281
|
// Validate that channel is only used with web platform
|
|
247
|
-
if (
|
|
282
|
+
if (channels?.length && platform !== 'web') {
|
|
248
283
|
consola.error('The --channel flag can only be used with the web platform.');
|
|
249
284
|
process.exit(1);
|
|
250
285
|
}
|
|
@@ -377,6 +412,7 @@ export default defineCommand({
|
|
|
377
412
|
adHocEnvironmentVariables,
|
|
378
413
|
appBuildSourceId,
|
|
379
414
|
appCertificateName: certificate,
|
|
415
|
+
appChannelNames: channels,
|
|
380
416
|
appConfigurationName: configuration,
|
|
381
417
|
appEnvironmentName: environment,
|
|
382
418
|
appId,
|
|
@@ -389,6 +425,9 @@ export default defineCommand({
|
|
|
389
425
|
consola.info(`Build Number: ${response.numberAsString}`);
|
|
390
426
|
consola.info(`Build URL: ${DEFAULT_CONSOLE_BASE_URL}/apps/${appId}/builds/${response.id}`);
|
|
391
427
|
consola.success('Build created successfully.');
|
|
428
|
+
if (channels?.length) {
|
|
429
|
+
consola.info(`The build will be deployed to the following ${channels.length === 1 ? 'channel' : 'channels'} once it succeeds: ${channels.join(', ')}.`);
|
|
430
|
+
}
|
|
392
431
|
// Wait for build job to complete by default, unless --detached flag is set
|
|
393
432
|
const shouldWait = !options.detached;
|
|
394
433
|
if (shouldWait) {
|
|
@@ -411,41 +450,18 @@ export default defineCommand({
|
|
|
411
450
|
consola.success('Build completed successfully.');
|
|
412
451
|
console.log();
|
|
413
452
|
// Download artifacts if flags are set
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
buildId: response.id,
|
|
427
|
-
buildArtifacts: appBuild.appBuildArtifacts,
|
|
428
|
-
artifactType: 'aab',
|
|
429
|
-
filePath: typeof options.aab === 'string' ? options.aab : undefined,
|
|
430
|
-
});
|
|
431
|
-
}
|
|
432
|
-
if (options.ipa && platform === 'ios') {
|
|
433
|
-
await handleArtifactDownload({
|
|
434
|
-
appId,
|
|
435
|
-
buildId: response.id,
|
|
436
|
-
buildArtifacts: appBuild.appBuildArtifacts,
|
|
437
|
-
artifactType: 'ipa',
|
|
438
|
-
filePath: typeof options.ipa === 'string' ? options.ipa : undefined,
|
|
439
|
-
});
|
|
440
|
-
}
|
|
441
|
-
if (options.zip && platform === 'web') {
|
|
442
|
-
await handleArtifactDownload({
|
|
443
|
-
appId,
|
|
444
|
-
buildId: response.id,
|
|
445
|
-
buildArtifacts: appBuild.appBuildArtifacts,
|
|
446
|
-
artifactType: 'zip',
|
|
447
|
-
filePath: typeof options.zip === 'string' ? options.zip : undefined,
|
|
448
|
-
});
|
|
453
|
+
for (const artifactType of APP_BUILD_ARTIFACT_TYPES_BY_PLATFORM[platform]) {
|
|
454
|
+
const option = options[artifactType];
|
|
455
|
+
if (option) {
|
|
456
|
+
await handleAppBuildArtifactDownload({
|
|
457
|
+
appId,
|
|
458
|
+
buildId: response.id,
|
|
459
|
+
buildArtifacts: appBuild.appBuildArtifacts,
|
|
460
|
+
artifactType,
|
|
461
|
+
formFactor: options.formFactor,
|
|
462
|
+
filePath: typeof option === 'string' ? option : undefined,
|
|
463
|
+
});
|
|
464
|
+
}
|
|
449
465
|
}
|
|
450
466
|
// Create a public share link if requested
|
|
451
467
|
let appBuildShare;
|
|
@@ -471,9 +487,16 @@ export default defineCommand({
|
|
|
471
487
|
consola.info(`Share URL: ${shareUrls.webUrl}`);
|
|
472
488
|
}
|
|
473
489
|
}
|
|
474
|
-
// Create deployment if
|
|
475
|
-
if (options.
|
|
476
|
-
await (await import('../../../commands/apps/deployments/create.js').then((mod) => mod.default)).action({
|
|
490
|
+
// Create deployment if destination is set
|
|
491
|
+
if (options.destination) {
|
|
492
|
+
await (await import('../../../commands/apps/deployments/create.js').then((mod) => mod.default)).action({
|
|
493
|
+
appId,
|
|
494
|
+
buildId: response.id,
|
|
495
|
+
destination: options.destination,
|
|
496
|
+
releaseNotes: options.releaseNotes,
|
|
497
|
+
releaseNotesFile: options.releaseNotesFile,
|
|
498
|
+
releaseNotesLocale: options.releaseNotesLocale,
|
|
499
|
+
}, undefined);
|
|
477
500
|
}
|
|
478
501
|
// Output JSON if json flag is set
|
|
479
502
|
if (json) {
|
|
@@ -500,45 +523,3 @@ export default defineCommand({
|
|
|
500
523
|
}
|
|
501
524
|
}),
|
|
502
525
|
});
|
|
503
|
-
/**
|
|
504
|
-
* Download a build artifact (APK, AAB, IPA, or ZIP).
|
|
505
|
-
*/
|
|
506
|
-
const handleArtifactDownload = async (options) => {
|
|
507
|
-
const { appId, buildId, buildArtifacts, artifactType, filePath } = options;
|
|
508
|
-
try {
|
|
509
|
-
const artifactTypeUpper = artifactType.toUpperCase();
|
|
510
|
-
consola.start(`Downloading ${artifactTypeUpper}...`);
|
|
511
|
-
// Find the artifact
|
|
512
|
-
const artifact = buildArtifacts?.find((artifact) => artifact.type === artifactType);
|
|
513
|
-
if (!artifact) {
|
|
514
|
-
consola.warn(`No ${artifactTypeUpper} artifact found for this build.`);
|
|
515
|
-
return;
|
|
516
|
-
}
|
|
517
|
-
if (artifact.status !== 'ready') {
|
|
518
|
-
consola.warn(`${artifactTypeUpper} artifact is not ready (status: ${artifact.status}).`);
|
|
519
|
-
return;
|
|
520
|
-
}
|
|
521
|
-
// Download the artifact
|
|
522
|
-
const artifactData = await appBuildsService.downloadArtifact({
|
|
523
|
-
appId,
|
|
524
|
-
appBuildId: buildId,
|
|
525
|
-
artifactId: artifact.id,
|
|
526
|
-
});
|
|
527
|
-
// Determine the file path
|
|
528
|
-
let outputPath;
|
|
529
|
-
if (filePath) {
|
|
530
|
-
// Use provided path (can be relative or absolute)
|
|
531
|
-
outputPath = path.resolve(filePath);
|
|
532
|
-
}
|
|
533
|
-
else {
|
|
534
|
-
// Default to current working directory with build ID as filename
|
|
535
|
-
outputPath = path.resolve(`${buildId}.${artifactType}`);
|
|
536
|
-
}
|
|
537
|
-
// Save the file
|
|
538
|
-
await fs.writeFile(outputPath, Buffer.from(artifactData));
|
|
539
|
-
consola.success(`${artifactTypeUpper} downloaded successfully: ${outputPath}`);
|
|
540
|
-
}
|
|
541
|
-
catch (error) {
|
|
542
|
-
consola.error(`Failed to download ${artifactType.toUpperCase()}:`, error);
|
|
543
|
-
}
|
|
544
|
-
};
|
|
@@ -2,7 +2,10 @@ import { DEFAULT_API_BASE_URL, DEFAULT_CONSOLE_BASE_URL } from '../../../config/
|
|
|
2
2
|
import authorizationService from '../../../services/authorization-service.js';
|
|
3
3
|
import userConfig from '../../../utils/user-config.js';
|
|
4
4
|
import consola from 'consola';
|
|
5
|
+
import fs from 'fs/promises';
|
|
5
6
|
import nock from 'nock';
|
|
7
|
+
import os from 'os';
|
|
8
|
+
import path from 'path';
|
|
6
9
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
7
10
|
import createCommand from './create.js';
|
|
8
11
|
// Mock dependencies
|
|
@@ -77,6 +80,40 @@ describe('apps-builds-create', () => {
|
|
|
77
80
|
appBuildShare: { id: shareId, qrCodeUrl, webUrl, expiresAt: null },
|
|
78
81
|
}, null, 2));
|
|
79
82
|
});
|
|
83
|
+
it('should download the app artifact of an iOS simulator build with --app', async () => {
|
|
84
|
+
const outputDirectory = await fs.mkdtemp(path.join(os.tmpdir(), 'capawesome-cli-create-'));
|
|
85
|
+
const outputPath = path.join(outputDirectory, 'simulator.app.zip');
|
|
86
|
+
const artifactId = 'artifact-1';
|
|
87
|
+
const artifactContent = 'zipped app bundle';
|
|
88
|
+
const options = { appId, platform: 'ios', type: 'simulator', gitRef: 'main', app: outputPath };
|
|
89
|
+
const buildScope = nock(DEFAULT_API_BASE_URL)
|
|
90
|
+
.post(`/v1/apps/${appId}/builds`, (body) => body.platform === 'ios' && body.type === 'simulator')
|
|
91
|
+
.reply(201, { id: buildId, jobId: 'job-1', numberAsString: '42' });
|
|
92
|
+
const findScope = nock(DEFAULT_API_BASE_URL)
|
|
93
|
+
.get(`/v1/apps/${appId}/builds/${buildId}`)
|
|
94
|
+
.query({ relations: 'appBuildArtifacts' })
|
|
95
|
+
.reply(200, {
|
|
96
|
+
id: buildId,
|
|
97
|
+
appBuildArtifacts: [
|
|
98
|
+
{ id: 'artifact-0', status: 'ready', type: 'xcarchive' },
|
|
99
|
+
{ formFactor: 'mobile', id: artifactId, status: 'ready', type: 'app' },
|
|
100
|
+
],
|
|
101
|
+
});
|
|
102
|
+
const downloadScope = nock(DEFAULT_API_BASE_URL)
|
|
103
|
+
.get(`/v1/apps/${appId}/builds/${buildId}/artifacts/${artifactId}/download`)
|
|
104
|
+
.reply(200, artifactContent);
|
|
105
|
+
try {
|
|
106
|
+
await createCommand.action(options, undefined);
|
|
107
|
+
expect(buildScope.isDone()).toBe(true);
|
|
108
|
+
expect(findScope.isDone()).toBe(true);
|
|
109
|
+
expect(downloadScope.isDone()).toBe(true);
|
|
110
|
+
expect(await fs.readFile(outputPath, 'utf-8')).toBe(artifactContent);
|
|
111
|
+
expect(mockConsola.success).toHaveBeenCalledWith(`APP downloaded successfully: ${outputPath}`);
|
|
112
|
+
}
|
|
113
|
+
finally {
|
|
114
|
+
await fs.rm(outputDirectory, { force: true, recursive: true });
|
|
115
|
+
}
|
|
116
|
+
});
|
|
80
117
|
it('should reject --share combined with --detached', async () => {
|
|
81
118
|
const options = { appId, platform: 'web', gitRef: 'main', share: true, detached: true };
|
|
82
119
|
await expect(createCommand.action(options, undefined)).rejects.toThrow('Process exited with code 1');
|
|
@@ -107,6 +144,91 @@ describe('apps-builds-create', () => {
|
|
|
107
144
|
expect(buildScope.isDone()).toBe(true);
|
|
108
145
|
expect(mockConsola.error).not.toHaveBeenCalled();
|
|
109
146
|
});
|
|
147
|
+
it('should send the channels as appChannelNames', async () => {
|
|
148
|
+
const options = { appId, platform: 'web', gitRef: 'main', channel: ['beta', 'alpha'], detached: true };
|
|
149
|
+
const buildScope = nock(DEFAULT_API_BASE_URL)
|
|
150
|
+
.post(`/v1/apps/${appId}/builds`, (body) => JSON.stringify(body.appChannelNames) === JSON.stringify(['beta', 'alpha']))
|
|
151
|
+
.matchHeader('Authorization', `Bearer ${testToken}`)
|
|
152
|
+
.reply(201, { id: buildId, jobId: 'job-1', numberAsString: '42' });
|
|
153
|
+
await createCommand.action(options, undefined);
|
|
154
|
+
expect(buildScope.isDone()).toBe(true);
|
|
155
|
+
expect(mockConsola.error).not.toHaveBeenCalled();
|
|
156
|
+
});
|
|
157
|
+
it('should split comma-separated channels into separate appChannelNames', async () => {
|
|
158
|
+
const options = { appId, platform: 'web', gitRef: 'main', channel: ['beta, alpha'], detached: true };
|
|
159
|
+
const buildScope = nock(DEFAULT_API_BASE_URL)
|
|
160
|
+
.post(`/v1/apps/${appId}/builds`, (body) => JSON.stringify(body.appChannelNames) === JSON.stringify(['beta', 'alpha']))
|
|
161
|
+
.matchHeader('Authorization', `Bearer ${testToken}`)
|
|
162
|
+
.reply(201, { id: buildId, jobId: 'job-1', numberAsString: '42' });
|
|
163
|
+
await createCommand.action(options, undefined);
|
|
164
|
+
expect(buildScope.isDone()).toBe(true);
|
|
165
|
+
expect(mockConsola.error).not.toHaveBeenCalled();
|
|
166
|
+
});
|
|
167
|
+
it('should accept --channel combined with --detached', async () => {
|
|
168
|
+
const options = { appId, platform: 'web', gitRef: 'main', channel: ['beta'], detached: true };
|
|
169
|
+
const buildScope = nock(DEFAULT_API_BASE_URL)
|
|
170
|
+
.post(`/v1/apps/${appId}/builds`)
|
|
171
|
+
.matchHeader('Authorization', `Bearer ${testToken}`)
|
|
172
|
+
.reply(201, { id: buildId, jobId: 'job-1', numberAsString: '42' });
|
|
173
|
+
await createCommand.action(options, undefined);
|
|
174
|
+
expect(buildScope.isDone()).toBe(true);
|
|
175
|
+
expect(mockConsola.error).not.toHaveBeenCalled();
|
|
176
|
+
});
|
|
177
|
+
it('should reject --detached combined with --destination', async () => {
|
|
178
|
+
const options = { appId, platform: 'ios', gitRef: 'main', destination: 'testflight', detached: true };
|
|
179
|
+
await expect(createCommand.action(options, undefined)).rejects.toThrow('Process exited with code 1');
|
|
180
|
+
expect(mockConsola.error).toHaveBeenCalledWith('The --detached flag cannot be used with the --destination flag.');
|
|
181
|
+
});
|
|
182
|
+
it('should reject release notes without --destination', async () => {
|
|
183
|
+
const options = { appId, platform: 'android', gitRef: 'main', releaseNotes: 'Bug fixes' };
|
|
184
|
+
await expect(createCommand.action(options, undefined)).rejects.toThrow('Process exited with code 1');
|
|
185
|
+
expect(mockConsola.error).toHaveBeenCalledWith('The --release-notes, --release-notes-file, and --release-notes-locale flags require --destination.');
|
|
186
|
+
});
|
|
187
|
+
it('should send the release notes with the deployment to the destination', async () => {
|
|
188
|
+
const options = {
|
|
189
|
+
appId,
|
|
190
|
+
platform: 'android',
|
|
191
|
+
gitRef: 'main',
|
|
192
|
+
destination: 'Google Play',
|
|
193
|
+
releaseNotes: 'Bug fixes',
|
|
194
|
+
releaseNotesLocale: ['de-DE=Fehlerbehebungen'],
|
|
195
|
+
};
|
|
196
|
+
const buildScope = nock(DEFAULT_API_BASE_URL)
|
|
197
|
+
.post(`/v1/apps/${appId}/builds`)
|
|
198
|
+
.reply(201, { id: buildId, jobId: 'job-1', numberAsString: '42' });
|
|
199
|
+
const findScope = nock(DEFAULT_API_BASE_URL)
|
|
200
|
+
.get(`/v1/apps/${appId}/builds/${buildId}`)
|
|
201
|
+
.query({ relations: 'appBuildArtifacts' })
|
|
202
|
+
.reply(200, { id: buildId, appBuildArtifacts: [] });
|
|
203
|
+
const deploymentBuildScope = nock(DEFAULT_API_BASE_URL)
|
|
204
|
+
.get(`/v1/apps/${appId}/builds/${buildId}`)
|
|
205
|
+
.reply(200, { id: buildId, platform: 'android' });
|
|
206
|
+
const deploymentScope = nock(DEFAULT_API_BASE_URL)
|
|
207
|
+
.post(`/v1/apps/${appId}/deployments`, {
|
|
208
|
+
appId,
|
|
209
|
+
appBuildId: buildId,
|
|
210
|
+
appDestinationName: 'Google Play',
|
|
211
|
+
releaseNotes: { default: 'Bug fixes', 'de-DE': 'Fehlerbehebungen' },
|
|
212
|
+
})
|
|
213
|
+
.matchHeader('Authorization', `Bearer ${testToken}`)
|
|
214
|
+
.reply(201, { id: 'deployment-1', jobId: 'job-2' });
|
|
215
|
+
await createCommand.action(options, undefined);
|
|
216
|
+
expect(buildScope.isDone()).toBe(true);
|
|
217
|
+
expect(findScope.isDone()).toBe(true);
|
|
218
|
+
expect(deploymentBuildScope.isDone()).toBe(true);
|
|
219
|
+
expect(deploymentScope.isDone()).toBe(true);
|
|
220
|
+
});
|
|
221
|
+
it('should parse a single channel into an array', () => {
|
|
222
|
+
const schema = createCommand.options?.schema;
|
|
223
|
+
const result = schema?.safeParse({
|
|
224
|
+
appId: validAppId,
|
|
225
|
+
platform: 'web',
|
|
226
|
+
gitRef: 'main',
|
|
227
|
+
channel: ['beta'],
|
|
228
|
+
});
|
|
229
|
+
expect(result?.success).toBe(true);
|
|
230
|
+
expect(result?.data?.channel).toEqual(['beta']);
|
|
231
|
+
});
|
|
110
232
|
it('should reject a non-positive shareExpiresInDays value', () => {
|
|
111
233
|
const schema = createCommand.options?.schema;
|
|
112
234
|
for (const shareExpiresInDays of [0, -1]) {
|