@capawesome/cli 4.18.3 → 4.19.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 +15 -0
- package/README.md +1 -1
- package/dist/commands/apps/automations/create.js +5 -5
- package/dist/commands/apps/automations/create.test.js +2 -2
- package/dist/commands/apps/automations/update.js +6 -5
- package/dist/commands/apps/automations/update.test.js +4 -4
- package/dist/commands/apps/import.js +479 -0
- package/dist/commands/apps/import.test.js +375 -0
- package/dist/commands/apps/liveupdates/upload.js +2 -2
- package/dist/index.js +2 -0
- package/dist/services/apps.js +9 -0
- package/dist/utils/app-import.js +10 -0
- package/dist/utils/appflow-export.js +425 -0
- package/dist/utils/appflow-export.test.js +340 -0
- package/dist/utils/file.js +27 -0
- package/dist/utils/file.test.js +46 -0
- package/dist/utils/manifest.js +2 -3
- package/package.json +5 -5
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,21 @@
|
|
|
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.19.0](https://github.com/capawesome-team/cli/compare/v4.18.4...v4.19.0) (2026-09-22)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
### Features
|
|
9
|
+
|
|
10
|
+
* add `apps:import` command ([#217](https://github.com/capawesome-team/cli/issues/217)) ([db90d04](https://github.com/capawesome-team/cli/commit/db90d04d3001e517fa105f37b79be82c146260ba))
|
|
11
|
+
* **apps:** support multiple and negated trigger patterns ([#214](https://github.com/capawesome-team/cli/issues/214)) ([0f56afa](https://github.com/capawesome-team/cli/commit/0f56afadfb24c3411f63d380d66188055a23388c))
|
|
12
|
+
|
|
13
|
+
## [4.18.4](https://github.com/capawesome-team/cli/compare/v4.18.3...v4.18.4) (2026-08-20)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
### Bug Fixes
|
|
17
|
+
|
|
18
|
+
* report unreadable files with an actionable error message ([#208](https://github.com/capawesome-team/cli/issues/208)) ([cc18dee](https://github.com/capawesome-team/cli/commit/cc18dee898658c99affd660c99e64ef8e45cdf9a))
|
|
19
|
+
|
|
5
20
|
## [4.18.3](https://github.com/capawesome-team/cli/compare/v4.18.2...v4.18.3) (2026-08-18)
|
|
6
21
|
|
|
7
22
|
|
package/README.md
CHANGED
|
@@ -22,7 +22,7 @@ The Capawesome Cloud CLI can be invoked with the `@capawesome/cli` command.
|
|
|
22
22
|
npx @capawesome/cli <command> [options]
|
|
23
23
|
```
|
|
24
24
|
|
|
25
|
-
You can find a list of available commands in the [Command Reference](https://capawesome.io/cloud/cli/).
|
|
25
|
+
You can find a list of available commands in the [Command Reference](https://capawesome.io/docs/cloud/cli/commands/).
|
|
26
26
|
|
|
27
27
|
## Help
|
|
28
28
|
|
|
@@ -34,9 +34,9 @@ export default defineCommand({
|
|
|
34
34
|
.optional()
|
|
35
35
|
.describe('The build stack to use for the build process.'),
|
|
36
36
|
triggerPattern: z
|
|
37
|
-
.string()
|
|
37
|
+
.array(z.string())
|
|
38
38
|
.optional()
|
|
39
|
-
.describe('Only trigger for branches or tags matching this pattern. Defaults to all.'),
|
|
39
|
+
.describe('Only trigger for branches or tags matching this pattern. Prefix with `!` to exclude. Can be specified multiple times. Defaults to all.'),
|
|
40
40
|
triggerType: z
|
|
41
41
|
.enum(['branch', 'tag'], {
|
|
42
42
|
message: 'Trigger type must be either `branch` or `tag`.',
|
|
@@ -44,8 +44,8 @@ export default defineCommand({
|
|
|
44
44
|
.optional()
|
|
45
45
|
.describe('What triggers the automation. Supported values are `branch` and `tag`.'),
|
|
46
46
|
type: z
|
|
47
|
-
.enum(['app-store', 'ad-hoc', 'debug', 'development', 'release', 'simulator'], {
|
|
48
|
-
message: 'Build type must be one of `app-store`, `ad-hoc`, `debug`, `development`, `release`, or `simulator`.',
|
|
47
|
+
.enum(['app-store', 'ad-hoc', 'debug', 'development', 'enterprise', 'release', 'simulator'], {
|
|
48
|
+
message: 'Build type must be one of `app-store`, `ad-hoc`, `debug`, `development`, `enterprise`, `release`, or `simulator`.',
|
|
49
49
|
})
|
|
50
50
|
.optional()
|
|
51
51
|
.describe('The type of build to create.'),
|
|
@@ -127,7 +127,7 @@ export default defineCommand({
|
|
|
127
127
|
commitMessagePattern: options.commitMessagePattern,
|
|
128
128
|
name,
|
|
129
129
|
platform,
|
|
130
|
-
|
|
130
|
+
triggerPatterns: options.triggerPattern,
|
|
131
131
|
triggerType,
|
|
132
132
|
});
|
|
133
133
|
if (json) {
|
|
@@ -45,7 +45,7 @@ describe('apps-automations-create', () => {
|
|
|
45
45
|
name: 'nightly',
|
|
46
46
|
platform: 'android',
|
|
47
47
|
stack: 'macos-tahoe',
|
|
48
|
-
triggerPattern: '
|
|
48
|
+
triggerPattern: ['release/*', '!release/*-alpha'],
|
|
49
49
|
triggerType: 'branch',
|
|
50
50
|
type: 'release',
|
|
51
51
|
};
|
|
@@ -58,7 +58,7 @@ describe('apps-automations-create', () => {
|
|
|
58
58
|
buildType: 'release',
|
|
59
59
|
name: 'nightly',
|
|
60
60
|
platform: 'android',
|
|
61
|
-
|
|
61
|
+
triggerPatterns: ['release/*', '!release/*-alpha'],
|
|
62
62
|
triggerType: 'branch',
|
|
63
63
|
})
|
|
64
64
|
.matchHeader('Authorization', 'Bearer test-token')
|
|
@@ -6,6 +6,7 @@ import consola from 'consola';
|
|
|
6
6
|
import { z } from 'zod';
|
|
7
7
|
import { defineCommand, defineOptions } from 'zodline';
|
|
8
8
|
const clearableValue = (value) => (value === '' ? null : value);
|
|
9
|
+
const clearableValues = (values) => values?.filter((value) => value !== '');
|
|
9
10
|
export default defineCommand({
|
|
10
11
|
description: 'Update an existing app automation.',
|
|
11
12
|
options: defineOptions(z.object({
|
|
@@ -50,9 +51,9 @@ export default defineCommand({
|
|
|
50
51
|
.optional()
|
|
51
52
|
.describe('The build stack to use for the build process.'),
|
|
52
53
|
triggerPattern: z
|
|
53
|
-
.string()
|
|
54
|
+
.array(z.string())
|
|
54
55
|
.optional()
|
|
55
|
-
.describe('Only trigger for branches or tags matching this pattern. Pass an empty string to clear
|
|
56
|
+
.describe('Only trigger for branches or tags matching this pattern. Prefix with `!` to exclude. Can be specified multiple times. Pass an empty string to clear them.'),
|
|
56
57
|
triggerType: z
|
|
57
58
|
.enum(['branch', 'tag'], {
|
|
58
59
|
message: 'Trigger type must be either `branch` or `tag`.',
|
|
@@ -60,8 +61,8 @@ export default defineCommand({
|
|
|
60
61
|
.optional()
|
|
61
62
|
.describe('What triggers the automation. Supported values are `branch` and `tag`.'),
|
|
62
63
|
type: z
|
|
63
|
-
.enum(['app-store', 'ad-hoc', 'debug', 'development', 'release', 'simulator'], {
|
|
64
|
-
message: 'Build type must be one of `app-store`, `ad-hoc`, `debug`, `development`, `release`, or `simulator`.',
|
|
64
|
+
.enum(['app-store', 'ad-hoc', 'debug', 'development', 'enterprise', 'release', 'simulator'], {
|
|
65
|
+
message: 'Build type must be one of `app-store`, `ad-hoc`, `debug`, `development`, `enterprise`, `release`, or `simulator`.',
|
|
65
66
|
})
|
|
66
67
|
.optional()
|
|
67
68
|
.describe('The type of build to create.'),
|
|
@@ -105,7 +106,7 @@ export default defineCommand({
|
|
|
105
106
|
commitMessagePattern: clearableValue(options.commitMessagePattern),
|
|
106
107
|
name: options.name,
|
|
107
108
|
platform: options.platform,
|
|
108
|
-
|
|
109
|
+
triggerPatterns: clearableValues(options.triggerPattern),
|
|
109
110
|
triggerType: options.triggerType,
|
|
110
111
|
});
|
|
111
112
|
if (json) {
|
|
@@ -42,13 +42,13 @@ describe('apps-automations-update', () => {
|
|
|
42
42
|
automationId,
|
|
43
43
|
configuration: 'Production',
|
|
44
44
|
name: 'renamed',
|
|
45
|
-
triggerPattern: '
|
|
45
|
+
triggerPattern: ['v*', '!v*-*'],
|
|
46
46
|
};
|
|
47
47
|
const scope = nock(DEFAULT_API_BASE_URL)
|
|
48
48
|
.patch(`/v1/apps/${appId}/automations/${automationId}`, {
|
|
49
49
|
appConfigurationName: 'Production',
|
|
50
50
|
name: 'renamed',
|
|
51
|
-
|
|
51
|
+
triggerPatterns: ['v*', '!v*-*'],
|
|
52
52
|
})
|
|
53
53
|
.matchHeader('Authorization', 'Bearer test-token')
|
|
54
54
|
.reply(204);
|
|
@@ -59,11 +59,11 @@ describe('apps-automations-update', () => {
|
|
|
59
59
|
it('should clear a reference when an empty string is passed', async () => {
|
|
60
60
|
const appId = 'app-123';
|
|
61
61
|
const automationId = 'automation-456';
|
|
62
|
-
const options = { appId, automationId, certificate: '', triggerPattern: '' };
|
|
62
|
+
const options = { appId, automationId, certificate: '', triggerPattern: [''] };
|
|
63
63
|
const scope = nock(DEFAULT_API_BASE_URL)
|
|
64
64
|
.patch(`/v1/apps/${appId}/automations/${automationId}`, {
|
|
65
65
|
appCertificateName: null,
|
|
66
|
-
|
|
66
|
+
triggerPatterns: [],
|
|
67
67
|
})
|
|
68
68
|
.matchHeader('Authorization', 'Bearer test-token')
|
|
69
69
|
.reply(204);
|
|
@@ -0,0 +1,479 @@
|
|
|
1
|
+
import { DEFAULT_CONSOLE_BASE_URL } from '../../config/consts.js';
|
|
2
|
+
import appAutomationsService from '../../services/app-automations.js';
|
|
3
|
+
import appCertificatesService from '../../services/app-certificates.js';
|
|
4
|
+
import appChannelsService from '../../services/app-channels.js';
|
|
5
|
+
import appConfigurationsService from '../../services/app-configurations.js';
|
|
6
|
+
import appDestinationsService from '../../services/app-destinations.js';
|
|
7
|
+
import appEnvironmentsService from '../../services/app-environments.js';
|
|
8
|
+
import appGoogleServiceAccountKeysService from '../../services/app-google-service-account-keys.js';
|
|
9
|
+
import appProvisioningProfilesService from '../../services/app-provisioning-profiles.js';
|
|
10
|
+
import appsService from '../../services/apps.js';
|
|
11
|
+
import { generateUniqueAppName } from '../../utils/app-import.js';
|
|
12
|
+
import { parseAppflowExport } from '../../utils/appflow-export.js';
|
|
13
|
+
import { withAuth } from '../../utils/auth.js';
|
|
14
|
+
import { isInteractive } from '../../utils/environment.js';
|
|
15
|
+
import { getMessageFromUnknownError, UserError } from '../../utils/error.js';
|
|
16
|
+
import { isReadable } from '../../utils/file.js';
|
|
17
|
+
import { prompt, promptOrganizationSelection } from '../../utils/prompt.js';
|
|
18
|
+
import zip from '../../utils/zip.js';
|
|
19
|
+
import { defineCommand, defineOptions } from 'zodline';
|
|
20
|
+
import consola from 'consola';
|
|
21
|
+
import fs from 'fs';
|
|
22
|
+
import os from 'os';
|
|
23
|
+
import path from 'path';
|
|
24
|
+
import { z } from 'zod';
|
|
25
|
+
export default defineCommand({
|
|
26
|
+
description: 'Import apps from an Ionic Appflow export.',
|
|
27
|
+
options: defineOptions(z.object({
|
|
28
|
+
dryRun: z.boolean().optional().describe('Preview the import without creating any resources.'),
|
|
29
|
+
file: z.string().optional().describe('Path to the export file (.zip).'),
|
|
30
|
+
include: z
|
|
31
|
+
.array(z.string())
|
|
32
|
+
.optional()
|
|
33
|
+
.describe('Filter which apps to import by name or ID. Can be specified multiple times or comma-separated. Defaults to all apps.'),
|
|
34
|
+
ionicAppType: z
|
|
35
|
+
.enum(['capacitor', 'cordova'], {
|
|
36
|
+
message: 'Ionic app type must be either `capacitor` or `cordova`.',
|
|
37
|
+
})
|
|
38
|
+
.optional()
|
|
39
|
+
.describe('The actual app type of apps exported with the ambiguous app type `ionic`. Prompted for each app if not provided.'),
|
|
40
|
+
json: z.boolean().optional().describe('Output in JSON format.'),
|
|
41
|
+
organizationId: z.string().optional().describe('ID of the organization to import the apps into.'),
|
|
42
|
+
})),
|
|
43
|
+
action: withAuth(async (options, args) => {
|
|
44
|
+
let { dryRun, file, include, ionicAppType, json, organizationId } = options;
|
|
45
|
+
if (!file) {
|
|
46
|
+
if (!isInteractive()) {
|
|
47
|
+
consola.error('You must provide the export file path when running in non-interactive environment.');
|
|
48
|
+
process.exit(1);
|
|
49
|
+
}
|
|
50
|
+
file = await prompt('Enter the path to the export file (.zip):', { type: 'text' });
|
|
51
|
+
if (!file) {
|
|
52
|
+
consola.error('You must provide an export file path.');
|
|
53
|
+
process.exit(1);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
if (!zip.isZipped(file)) {
|
|
57
|
+
consola.error('The export file must be a `.zip` file.');
|
|
58
|
+
process.exit(1);
|
|
59
|
+
}
|
|
60
|
+
const fileReadable = await isReadable(file);
|
|
61
|
+
if (!fileReadable) {
|
|
62
|
+
consola.error(`The export file does not exist or is not accessible: ${file}`);
|
|
63
|
+
process.exit(1);
|
|
64
|
+
}
|
|
65
|
+
if (!organizationId) {
|
|
66
|
+
if (!isInteractive()) {
|
|
67
|
+
consola.error('You must provide an organization ID when running in non-interactive environment.');
|
|
68
|
+
process.exit(1);
|
|
69
|
+
}
|
|
70
|
+
organizationId = await promptOrganizationSelection({
|
|
71
|
+
message: 'Which organization do you want to import the apps into?',
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
const tempDirectory = fs.mkdtempSync(path.join(os.tmpdir(), 'capawesome-appflow-import-'));
|
|
75
|
+
try {
|
|
76
|
+
await zip.unzipToFolder(fs.readFileSync(file), tempDirectory);
|
|
77
|
+
const { apps, skippedApps, warnings } = await parseAppflowExport(tempDirectory);
|
|
78
|
+
for (const warning of warnings) {
|
|
79
|
+
consola.warn(warning);
|
|
80
|
+
}
|
|
81
|
+
const { selectedApps, selectedSkippedApps } = await selectApps(apps, skippedApps, include);
|
|
82
|
+
if (selectedApps.length === 0 && selectedSkippedApps.length === 0) {
|
|
83
|
+
throw new UserError('No apps found in the export that match the provided filters.');
|
|
84
|
+
}
|
|
85
|
+
const { resolvedApps, skippedApps: unresolvedApps } = await resolveIonicAppTypes(selectedApps, ionicAppType);
|
|
86
|
+
selectedSkippedApps.push(...unresolvedApps);
|
|
87
|
+
await assignUniqueAppNames(resolvedApps, organizationId);
|
|
88
|
+
const outcomes = [];
|
|
89
|
+
for (const app of resolvedApps) {
|
|
90
|
+
const outcome = {
|
|
91
|
+
app,
|
|
92
|
+
errors: [],
|
|
93
|
+
created: {
|
|
94
|
+
automations: 0,
|
|
95
|
+
certificates: 0,
|
|
96
|
+
channels: 0,
|
|
97
|
+
configurations: 0,
|
|
98
|
+
destinations: 0,
|
|
99
|
+
environments: 0,
|
|
100
|
+
},
|
|
101
|
+
};
|
|
102
|
+
outcomes.push(outcome);
|
|
103
|
+
if (dryRun) {
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
await importApp(organizationId, outcome);
|
|
107
|
+
}
|
|
108
|
+
const errorCount = outcomes.reduce((count, outcome) => count + outcome.errors.length, 0);
|
|
109
|
+
if (json) {
|
|
110
|
+
printJsonSummary(outcomes, selectedSkippedApps, warnings, dryRun === true);
|
|
111
|
+
}
|
|
112
|
+
else {
|
|
113
|
+
printSummary(outcomes, selectedSkippedApps, dryRun === true, file);
|
|
114
|
+
}
|
|
115
|
+
if (errorCount > 0) {
|
|
116
|
+
// Set the exit code instead of calling `process.exit` so the `finally` block
|
|
117
|
+
// still deletes the extracted export, which contains secrets.
|
|
118
|
+
process.exitCode = 1;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
finally {
|
|
122
|
+
fs.rmSync(tempDirectory, { recursive: true, force: true });
|
|
123
|
+
}
|
|
124
|
+
}),
|
|
125
|
+
});
|
|
126
|
+
const selectApps = async (apps, skippedApps, include) => {
|
|
127
|
+
const filters = include
|
|
128
|
+
?.flatMap((value) => value.split(','))
|
|
129
|
+
.map((value) => value.trim())
|
|
130
|
+
.filter((value) => value.length > 0);
|
|
131
|
+
if (filters && filters.length > 0) {
|
|
132
|
+
const matchesFilter = (sourceId, sourceName) => filters.some((filter) => filter === sourceId || filter === sourceName);
|
|
133
|
+
for (const filter of filters) {
|
|
134
|
+
const matched = apps.some((app) => app.sourceId === filter || app.sourceName === filter) ||
|
|
135
|
+
skippedApps.some((app) => app.sourceId === filter || app.sourceName === filter);
|
|
136
|
+
if (!matched) {
|
|
137
|
+
consola.warn(`No app in the export matches the filter \`${filter}\`.`);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return {
|
|
141
|
+
selectedApps: apps.filter((app) => matchesFilter(app.sourceId, app.sourceName)),
|
|
142
|
+
selectedSkippedApps: skippedApps.filter((app) => matchesFilter(app.sourceId, app.sourceName)),
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
if (isInteractive() && apps.length > 1) {
|
|
146
|
+
// @ts-ignore wait till https://github.com/unjs/consola/pull/280 is merged
|
|
147
|
+
const selectedIds = await prompt('Which apps do you want to import?', {
|
|
148
|
+
type: 'multiselect',
|
|
149
|
+
options: apps.map((app) => ({ label: `${app.sourceName} (${app.sourceId})`, value: app.sourceId })),
|
|
150
|
+
initial: apps.map((app) => app.sourceId),
|
|
151
|
+
});
|
|
152
|
+
return {
|
|
153
|
+
selectedApps: apps.filter((app) => selectedIds.includes(app.sourceId)),
|
|
154
|
+
selectedSkippedApps: skippedApps,
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
return { selectedApps: apps, selectedSkippedApps: skippedApps };
|
|
158
|
+
};
|
|
159
|
+
const resolveIonicAppTypes = async (apps, ionicAppType) => {
|
|
160
|
+
const resolvedApps = [];
|
|
161
|
+
const skippedApps = [];
|
|
162
|
+
for (const app of apps) {
|
|
163
|
+
if (app.sourceAppType !== 'ionic') {
|
|
164
|
+
resolvedApps.push(app);
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
let type = ionicAppType;
|
|
168
|
+
if (!type && isInteractive()) {
|
|
169
|
+
// @ts-ignore wait till https://github.com/unjs/consola/pull/280 is merged
|
|
170
|
+
type = await prompt(`The app \`${app.sourceName}\` was exported with the app type \`ionic\` which can mean Capacitor or Cordova. Which framework does the app use?`, {
|
|
171
|
+
type: 'select',
|
|
172
|
+
options: [
|
|
173
|
+
{ label: 'Capacitor', value: 'capacitor' },
|
|
174
|
+
{ label: 'Cordova', value: 'cordova' },
|
|
175
|
+
],
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
if (!type) {
|
|
179
|
+
skippedApps.push({
|
|
180
|
+
sourceId: app.sourceId,
|
|
181
|
+
sourceName: app.sourceName,
|
|
182
|
+
reason: 'The app was exported with the ambiguous app type `ionic` which can mean Capacitor or Cordova. Provide the `--ionic-app-type` option or run the import interactively.',
|
|
183
|
+
retryLater: false,
|
|
184
|
+
});
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
187
|
+
app.type = type;
|
|
188
|
+
app.notes.push(`The app type \`ionic\` was resolved to \`${type}\`.`);
|
|
189
|
+
resolvedApps.push(app);
|
|
190
|
+
}
|
|
191
|
+
return { resolvedApps, skippedApps };
|
|
192
|
+
};
|
|
193
|
+
const assignUniqueAppNames = async (apps, organizationId) => {
|
|
194
|
+
const takenNames = new Set();
|
|
195
|
+
const limit = 50;
|
|
196
|
+
let offset = 0;
|
|
197
|
+
while (true) {
|
|
198
|
+
const page = await appsService.findAll({ organizationId, limit, offset });
|
|
199
|
+
for (const app of page) {
|
|
200
|
+
takenNames.add(app.name);
|
|
201
|
+
}
|
|
202
|
+
if (page.length < limit) {
|
|
203
|
+
break;
|
|
204
|
+
}
|
|
205
|
+
offset += limit;
|
|
206
|
+
}
|
|
207
|
+
for (const app of apps) {
|
|
208
|
+
app.name = generateUniqueAppName(app.sourceName, takenNames);
|
|
209
|
+
takenNames.add(app.name);
|
|
210
|
+
if (app.name !== app.sourceName) {
|
|
211
|
+
app.notes.push(`An app with the name \`${app.sourceName}\` already exists in the organization. The app is imported as \`${app.name}\`.`);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
};
|
|
215
|
+
const importApp = async (organizationId, outcome) => {
|
|
216
|
+
const { app } = outcome;
|
|
217
|
+
consola.start(`Importing app \`${app.sourceName}\`...`);
|
|
218
|
+
let appId;
|
|
219
|
+
try {
|
|
220
|
+
const createdApp = await appsService.create({ name: app.name, organizationId, type: app.type });
|
|
221
|
+
appId = createdApp.id;
|
|
222
|
+
outcome.id = appId;
|
|
223
|
+
consola.success(`Created app \`${app.name}\`.`);
|
|
224
|
+
}
|
|
225
|
+
catch (error) {
|
|
226
|
+
outcome.errors.push(`Failed to create app: ${getMessageFromUnknownError(error)}`);
|
|
227
|
+
consola.error(`Failed to create app \`${app.name}\`.`);
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
if (app.latestBuildNumber && app.latestBuildNumber > 0) {
|
|
231
|
+
const nextAppBuildNumber = app.latestBuildNumber + 1;
|
|
232
|
+
try {
|
|
233
|
+
await appsService.update({ appId, nextAppBuildNumber });
|
|
234
|
+
app.notes.push(`The next build number was set to \`${nextAppBuildNumber}\` to continue after the latest build number \`${app.latestBuildNumber}\`.`);
|
|
235
|
+
consola.success(`Set the next build number to ${nextAppBuildNumber}.`);
|
|
236
|
+
}
|
|
237
|
+
catch (error) {
|
|
238
|
+
outcome.errors.push(`Failed to set the next build number: ${getMessageFromUnknownError(error)}`);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
for (const certificate of app.certificates) {
|
|
242
|
+
try {
|
|
243
|
+
const provisioningProfileIds = [];
|
|
244
|
+
for (const profilePath of certificate.provisioningProfilePaths) {
|
|
245
|
+
const profile = await appProvisioningProfilesService.create({
|
|
246
|
+
appId,
|
|
247
|
+
buffer: fs.readFileSync(profilePath),
|
|
248
|
+
fileName: path.basename(profilePath),
|
|
249
|
+
});
|
|
250
|
+
provisioningProfileIds.push(profile.id);
|
|
251
|
+
}
|
|
252
|
+
const createdCertificate = await appCertificatesService.create({
|
|
253
|
+
appId,
|
|
254
|
+
buffer: fs.readFileSync(certificate.filePath),
|
|
255
|
+
fileName: path.basename(certificate.filePath),
|
|
256
|
+
name: certificate.name,
|
|
257
|
+
platform: certificate.platform,
|
|
258
|
+
password: certificate.password,
|
|
259
|
+
keyAlias: certificate.keyAlias,
|
|
260
|
+
keyPassword: certificate.keyPassword,
|
|
261
|
+
});
|
|
262
|
+
if (provisioningProfileIds.length > 0) {
|
|
263
|
+
await appProvisioningProfilesService.updateMany({
|
|
264
|
+
appId,
|
|
265
|
+
ids: provisioningProfileIds,
|
|
266
|
+
appCertificateId: createdCertificate.id,
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
outcome.created.certificates++;
|
|
270
|
+
consola.success(`Created certificate \`${certificate.name}\`.`);
|
|
271
|
+
}
|
|
272
|
+
catch (error) {
|
|
273
|
+
outcome.errors.push(`Failed to create certificate \`${certificate.name}\`: ${getMessageFromUnknownError(error)}`);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
for (const environment of app.environments) {
|
|
277
|
+
try {
|
|
278
|
+
const createdEnvironment = await appEnvironmentsService.create({ appId, name: environment.name });
|
|
279
|
+
if (environment.variables.length > 0) {
|
|
280
|
+
await appEnvironmentsService.setVariables({
|
|
281
|
+
appId,
|
|
282
|
+
environmentId: createdEnvironment.id,
|
|
283
|
+
variables: environment.variables,
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
if (environment.secrets.length > 0) {
|
|
287
|
+
await appEnvironmentsService.setSecrets({
|
|
288
|
+
appId,
|
|
289
|
+
environmentId: createdEnvironment.id,
|
|
290
|
+
secrets: environment.secrets,
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
outcome.created.environments++;
|
|
294
|
+
consola.success(`Created environment \`${environment.name}\`.`);
|
|
295
|
+
}
|
|
296
|
+
catch (error) {
|
|
297
|
+
outcome.errors.push(`Failed to create environment \`${environment.name}\`: ${getMessageFromUnknownError(error)}`);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
let existingChannelNames = new Set();
|
|
301
|
+
try {
|
|
302
|
+
const existingChannels = await appChannelsService.findAll({ appId });
|
|
303
|
+
existingChannelNames = new Set(existingChannels.map((channel) => channel.name));
|
|
304
|
+
}
|
|
305
|
+
catch {
|
|
306
|
+
// If the lookup fails, channel creation errors are reported individually below.
|
|
307
|
+
}
|
|
308
|
+
for (const channel of app.channels) {
|
|
309
|
+
if (existingChannelNames.has(channel)) {
|
|
310
|
+
outcome.created.channels++;
|
|
311
|
+
continue;
|
|
312
|
+
}
|
|
313
|
+
try {
|
|
314
|
+
await appChannelsService.create({ appId, name: channel });
|
|
315
|
+
outcome.created.channels++;
|
|
316
|
+
consola.success(`Created channel \`${channel}\`.`);
|
|
317
|
+
}
|
|
318
|
+
catch (error) {
|
|
319
|
+
outcome.errors.push(`Failed to create channel \`${channel}\`: ${getMessageFromUnknownError(error)}`);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
for (const configuration of app.configurations) {
|
|
323
|
+
try {
|
|
324
|
+
await appConfigurationsService.create({
|
|
325
|
+
appId,
|
|
326
|
+
name: configuration.name,
|
|
327
|
+
displayName: configuration.displayName,
|
|
328
|
+
packageName: configuration.packageName,
|
|
329
|
+
});
|
|
330
|
+
outcome.created.configurations++;
|
|
331
|
+
consola.success(`Created configuration \`${configuration.name}\`.`);
|
|
332
|
+
}
|
|
333
|
+
catch (error) {
|
|
334
|
+
outcome.errors.push(`Failed to create configuration \`${configuration.name}\`: ${getMessageFromUnknownError(error)}`);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
for (const destination of app.destinations) {
|
|
338
|
+
try {
|
|
339
|
+
let appGoogleServiceAccountKeyId;
|
|
340
|
+
if (destination.googleServiceAccountKeyPath) {
|
|
341
|
+
const key = await appGoogleServiceAccountKeysService.create({
|
|
342
|
+
appId,
|
|
343
|
+
buffer: fs.readFileSync(destination.googleServiceAccountKeyPath),
|
|
344
|
+
fileName: path.basename(destination.googleServiceAccountKeyPath),
|
|
345
|
+
});
|
|
346
|
+
appGoogleServiceAccountKeyId = key.id;
|
|
347
|
+
}
|
|
348
|
+
await appDestinationsService.create({
|
|
349
|
+
appId,
|
|
350
|
+
name: destination.name,
|
|
351
|
+
platform: destination.platform,
|
|
352
|
+
androidPackageName: destination.androidPackageName,
|
|
353
|
+
androidBuildArtifactType: destination.androidBuildArtifactType,
|
|
354
|
+
androidReleaseStatus: destination.androidReleaseStatus,
|
|
355
|
+
googlePlayTrack: destination.googlePlayTrack,
|
|
356
|
+
appGoogleServiceAccountKeyId,
|
|
357
|
+
appleId: destination.appleId,
|
|
358
|
+
appleAppId: destination.appleAppId,
|
|
359
|
+
appleTeamId: destination.appleTeamId,
|
|
360
|
+
appleAppPassword: destination.appleAppPassword,
|
|
361
|
+
});
|
|
362
|
+
outcome.created.destinations++;
|
|
363
|
+
consola.success(`Created destination \`${destination.name}\`.`);
|
|
364
|
+
}
|
|
365
|
+
catch (error) {
|
|
366
|
+
outcome.errors.push(`Failed to create destination \`${destination.name}\`: ${getMessageFromUnknownError(error)}`);
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
for (const automation of app.automations) {
|
|
370
|
+
try {
|
|
371
|
+
await appAutomationsService.create({
|
|
372
|
+
appId,
|
|
373
|
+
name: automation.name,
|
|
374
|
+
platform: automation.platform,
|
|
375
|
+
triggerType: 'branch',
|
|
376
|
+
triggerPatterns: [automation.triggerPattern],
|
|
377
|
+
buildType: automation.buildType,
|
|
378
|
+
enabled: automation.enabled,
|
|
379
|
+
appCertificateName: automation.appCertificateName,
|
|
380
|
+
appChannelName: automation.appChannelName,
|
|
381
|
+
appConfigurationName: automation.appConfigurationName,
|
|
382
|
+
appDestinationName: automation.appDestinationName,
|
|
383
|
+
appEnvironmentName: automation.appEnvironmentName,
|
|
384
|
+
});
|
|
385
|
+
outcome.created.automations++;
|
|
386
|
+
consola.success(`Created automation \`${automation.name}\`.`);
|
|
387
|
+
}
|
|
388
|
+
catch (error) {
|
|
389
|
+
outcome.errors.push(`Failed to create automation \`${automation.name}\`: ${getMessageFromUnknownError(error)}`);
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
if (app.repository) {
|
|
393
|
+
try {
|
|
394
|
+
await appsService.linkRepository({
|
|
395
|
+
appId,
|
|
396
|
+
ownerSlug: app.repository.ownerSlug,
|
|
397
|
+
provider: app.repository.provider,
|
|
398
|
+
repositorySlug: app.repository.repositorySlug,
|
|
399
|
+
projectSlug: app.repository.projectSlug,
|
|
400
|
+
});
|
|
401
|
+
consola.success(`Linked repository \`${app.repository.ownerSlug}/${app.repository.repositorySlug}\`.`);
|
|
402
|
+
}
|
|
403
|
+
catch (error) {
|
|
404
|
+
app.notes.push(`The repository \`${app.repository.ownerSlug}/${app.repository.repositorySlug}\` could not be linked: ${getMessageFromUnknownError(error)} Make sure the git provider is connected in the Capawesome Cloud Console and link the repository manually.`);
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
};
|
|
408
|
+
const printSummary = (outcomes, skippedApps, dryRun, file) => {
|
|
409
|
+
consola.log('');
|
|
410
|
+
if (dryRun) {
|
|
411
|
+
consola.info('Dry run. No resources were created.');
|
|
412
|
+
}
|
|
413
|
+
console.table(outcomes.map((outcome) => ({
|
|
414
|
+
App: outcome.app.sourceName,
|
|
415
|
+
'Imported as': dryRun || outcome.id ? outcome.app.name : '—',
|
|
416
|
+
Certificates: formatCount(outcome.created.certificates, outcome.app.certificates.length, dryRun),
|
|
417
|
+
Environments: formatCount(outcome.created.environments, outcome.app.environments.length, dryRun),
|
|
418
|
+
Channels: formatCount(outcome.created.channels, outcome.app.channels.length, dryRun),
|
|
419
|
+
Configurations: formatCount(outcome.created.configurations, outcome.app.configurations.length, dryRun),
|
|
420
|
+
Destinations: formatCount(outcome.created.destinations, outcome.app.destinations.length, dryRun),
|
|
421
|
+
Automations: formatCount(outcome.created.automations, outcome.app.automations.length, dryRun),
|
|
422
|
+
Errors: outcome.errors.length,
|
|
423
|
+
})));
|
|
424
|
+
for (const outcome of outcomes) {
|
|
425
|
+
if (outcome.app.notes.length === 0 && outcome.errors.length === 0) {
|
|
426
|
+
continue;
|
|
427
|
+
}
|
|
428
|
+
consola.log(`\n${outcome.app.sourceName}:`);
|
|
429
|
+
for (const note of outcome.app.notes) {
|
|
430
|
+
consola.warn(note);
|
|
431
|
+
}
|
|
432
|
+
for (const error of outcome.errors) {
|
|
433
|
+
consola.error(error);
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
for (const outcome of outcomes) {
|
|
437
|
+
if (outcome.id) {
|
|
438
|
+
consola.info(`App URL: ${DEFAULT_CONSOLE_BASE_URL}/apps/${outcome.id}`);
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
const retryLaterApps = skippedApps.filter((app) => app.retryLater);
|
|
442
|
+
const otherSkippedApps = skippedApps.filter((app) => !app.retryLater);
|
|
443
|
+
for (const app of otherSkippedApps) {
|
|
444
|
+
consola.warn(`The app \`${app.sourceName}\` was skipped: ${app.reason}`);
|
|
445
|
+
}
|
|
446
|
+
if (retryLaterApps.length > 0) {
|
|
447
|
+
const includeValues = retryLaterApps
|
|
448
|
+
.map((app) => (app.sourceName.includes(',') ? app.sourceId : app.sourceName))
|
|
449
|
+
.join(',');
|
|
450
|
+
consola.info(`The apps ${retryLaterApps.map((app) => `\`${app.sourceName}\``).join(', ')} were skipped because their app type is not yet supported. Support is coming soon. You can import the remaining apps later by running:\n\nnpx @capawesome/cli apps:import --file "${file}" --include "${includeValues}"`);
|
|
451
|
+
}
|
|
452
|
+
if (!dryRun) {
|
|
453
|
+
consola.success('Import completed.');
|
|
454
|
+
}
|
|
455
|
+
};
|
|
456
|
+
const formatCount = (created, total, dryRun) => {
|
|
457
|
+
return dryRun ? `${total}` : `${created}/${total}`;
|
|
458
|
+
};
|
|
459
|
+
const printJsonSummary = (outcomes, skippedApps, warnings, dryRun) => {
|
|
460
|
+
console.log(JSON.stringify({
|
|
461
|
+
dryRun,
|
|
462
|
+
warnings,
|
|
463
|
+
apps: outcomes.map((outcome) => ({
|
|
464
|
+
id: outcome.id ?? null,
|
|
465
|
+
name: outcome.app.name,
|
|
466
|
+
sourceId: outcome.app.sourceId,
|
|
467
|
+
sourceName: outcome.app.sourceName,
|
|
468
|
+
webUrl: outcome.id ? `${DEFAULT_CONSOLE_BASE_URL}/apps/${outcome.id}` : null,
|
|
469
|
+
created: outcome.created,
|
|
470
|
+
notes: outcome.app.notes,
|
|
471
|
+
errors: outcome.errors,
|
|
472
|
+
})),
|
|
473
|
+
skippedApps: skippedApps.map((app) => ({
|
|
474
|
+
sourceId: app.sourceId,
|
|
475
|
+
sourceName: app.sourceName,
|
|
476
|
+
reason: app.reason,
|
|
477
|
+
})),
|
|
478
|
+
}, null, 2));
|
|
479
|
+
};
|