@capawesome/cli 3.11.0 → 4.0.1
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 +34 -0
- package/dist/commands/apps/builds/cancel.js +1 -1
- package/dist/commands/apps/builds/create.js +58 -50
- package/dist/commands/apps/builds/download.js +27 -3
- package/dist/commands/apps/bundles/create.js +5 -449
- package/dist/commands/apps/bundles/delete.js +3 -68
- package/dist/commands/apps/bundles/update.js +3 -66
- package/dist/commands/apps/channels/create.js +5 -8
- package/dist/commands/apps/channels/create.test.js +6 -9
- package/dist/commands/apps/channels/delete.js +3 -2
- package/dist/commands/apps/channels/get.js +2 -12
- package/dist/commands/apps/channels/get.test.js +1 -2
- package/dist/commands/apps/channels/list.js +2 -10
- package/dist/commands/apps/channels/list.test.js +2 -3
- package/dist/commands/apps/channels/pause.js +85 -0
- package/dist/commands/apps/channels/resume.js +85 -0
- package/dist/commands/apps/channels/update.js +4 -7
- package/dist/commands/apps/channels/update.test.js +2 -4
- package/dist/commands/apps/create.js +1 -1
- package/dist/commands/apps/delete.js +3 -2
- package/dist/commands/apps/deployments/cancel.js +1 -1
- package/dist/commands/apps/deployments/create.js +82 -31
- package/dist/commands/apps/devices/delete.js +3 -2
- package/dist/commands/apps/environments/create.js +1 -1
- package/dist/commands/apps/environments/delete.js +3 -2
- package/dist/commands/apps/liveupdates/bundle.js +117 -0
- package/dist/commands/apps/liveupdates/generate-manifest.js +39 -0
- package/dist/commands/{manifests/generate.test.js → apps/liveupdates/generate-manifest.test.js} +6 -6
- package/dist/commands/apps/liveupdates/register.js +291 -0
- package/dist/commands/apps/{bundles/create.test.js → liveupdates/register.test.js} +123 -111
- package/dist/commands/apps/liveupdates/rollback.js +171 -0
- package/dist/commands/apps/liveupdates/rollout.js +147 -0
- package/dist/commands/apps/liveupdates/upload.js +420 -0
- package/dist/commands/apps/liveupdates/upload.test.js +325 -0
- package/dist/commands/manifests/generate.js +2 -27
- package/dist/commands/organizations/create.js +1 -1
- package/dist/index.js +8 -0
- package/dist/services/app-builds.js +9 -2
- package/dist/services/app-channels.js +19 -0
- package/dist/services/app-deployments.js +24 -14
- package/dist/services/config.js +2 -0
- package/dist/utils/app-environments.js +2 -1
- package/dist/utils/time-format.js +26 -0
- package/package.json +3 -3
- package/dist/commands/apps/bundles/delete.test.js +0 -142
- package/dist/commands/apps/bundles/update.test.js +0 -144
- package/dist/utils/capacitor-config.js +0 -96
- package/dist/utils/package-json.js +0 -58
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { DEFAULT_CONSOLE_BASE_URL } from '../../../config/consts.js';
|
|
2
|
+
import appChannelsService from '../../../services/app-channels.js';
|
|
3
|
+
import appDeploymentsService from '../../../services/app-deployments.js';
|
|
4
|
+
import appsService from '../../../services/apps.js';
|
|
5
|
+
import authorizationService from '../../../services/authorization-service.js';
|
|
6
|
+
import organizationsService from '../../../services/organizations.js';
|
|
7
|
+
import { isInteractive } from '../../../utils/environment.js';
|
|
8
|
+
import { prompt } from '../../../utils/prompt.js';
|
|
9
|
+
import { defineCommand, defineOptions } from '@robingenz/zli';
|
|
10
|
+
import consola from 'consola';
|
|
11
|
+
import { z } from 'zod';
|
|
12
|
+
export default defineCommand({
|
|
13
|
+
description: 'Update the rollout percentage of the active build in a channel.',
|
|
14
|
+
options: defineOptions(z.object({
|
|
15
|
+
appId: z
|
|
16
|
+
.uuid({
|
|
17
|
+
message: 'App ID must be a UUID.',
|
|
18
|
+
})
|
|
19
|
+
.optional()
|
|
20
|
+
.describe('App ID of the channel.'),
|
|
21
|
+
channel: z.string().optional().describe('Name of the channel to update rollout for.'),
|
|
22
|
+
percentage: z.coerce
|
|
23
|
+
.number()
|
|
24
|
+
.int({
|
|
25
|
+
message: 'Percentage must be an integer.',
|
|
26
|
+
})
|
|
27
|
+
.min(0, {
|
|
28
|
+
message: 'Percentage must be at least 0.',
|
|
29
|
+
})
|
|
30
|
+
.max(100, {
|
|
31
|
+
message: 'Percentage must be at most 100.',
|
|
32
|
+
})
|
|
33
|
+
.optional()
|
|
34
|
+
.describe('Rollout percentage (0-100).'),
|
|
35
|
+
})),
|
|
36
|
+
action: async (options) => {
|
|
37
|
+
let { appId, channel, percentage } = options;
|
|
38
|
+
// Check if the user is logged in
|
|
39
|
+
if (!authorizationService.hasAuthorizationToken()) {
|
|
40
|
+
consola.error('You must be logged in to run this command. Please run the `login` command first.');
|
|
41
|
+
process.exit(1);
|
|
42
|
+
}
|
|
43
|
+
// Prompt for app ID if not provided
|
|
44
|
+
if (!appId) {
|
|
45
|
+
if (!isInteractive()) {
|
|
46
|
+
consola.error('You must provide an app ID when running in non-interactive environment.');
|
|
47
|
+
process.exit(1);
|
|
48
|
+
}
|
|
49
|
+
const organizations = await organizationsService.findAll();
|
|
50
|
+
if (organizations.length === 0) {
|
|
51
|
+
consola.error('You must create an organization before updating a rollout percentage.');
|
|
52
|
+
process.exit(1);
|
|
53
|
+
}
|
|
54
|
+
// @ts-ignore wait till https://github.com/unjs/consola/pull/280 is merged
|
|
55
|
+
const organizationId = await prompt('Select the organization of the app for which you want to update the rollout percentage.', {
|
|
56
|
+
type: 'select',
|
|
57
|
+
options: organizations.map((organization) => ({ label: organization.name, value: organization.id })),
|
|
58
|
+
});
|
|
59
|
+
if (!organizationId) {
|
|
60
|
+
consola.error('You must select the organization of an app for which you want to update the rollout percentage.');
|
|
61
|
+
process.exit(1);
|
|
62
|
+
}
|
|
63
|
+
const apps = await appsService.findAll({
|
|
64
|
+
organizationId,
|
|
65
|
+
});
|
|
66
|
+
if (apps.length === 0) {
|
|
67
|
+
consola.error('You must create an app before updating a rollout percentage.');
|
|
68
|
+
process.exit(1);
|
|
69
|
+
}
|
|
70
|
+
// @ts-ignore wait till https://github.com/unjs/consola/pull/280 is merged
|
|
71
|
+
appId = await prompt('Which app do you want to update the rollout percentage for:', {
|
|
72
|
+
type: 'select',
|
|
73
|
+
options: apps.map((app) => ({ label: app.name, value: app.id })),
|
|
74
|
+
});
|
|
75
|
+
if (!appId) {
|
|
76
|
+
consola.error('You must select an app to update the rollout percentage for.');
|
|
77
|
+
process.exit(1);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
// Prompt for channel name if not provided
|
|
81
|
+
if (!channel) {
|
|
82
|
+
if (!isInteractive()) {
|
|
83
|
+
consola.error('You must provide a channel when running in non-interactive environment.');
|
|
84
|
+
process.exit(1);
|
|
85
|
+
}
|
|
86
|
+
channel = await prompt('Enter the channel name to update rollout for:', {
|
|
87
|
+
type: 'text',
|
|
88
|
+
});
|
|
89
|
+
if (!channel) {
|
|
90
|
+
consola.error('You must enter a channel name to update rollout for.');
|
|
91
|
+
process.exit(1);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
// Fetch channel by name
|
|
95
|
+
const appChannels = await appChannelsService.findAll({ appId, name: channel });
|
|
96
|
+
if (appChannels.length === 0) {
|
|
97
|
+
consola.error(`Channel not found.`);
|
|
98
|
+
process.exit(1);
|
|
99
|
+
}
|
|
100
|
+
const appChannelId = appChannels[0]?.id;
|
|
101
|
+
if (!appChannelId) {
|
|
102
|
+
consola.error('Channel ID is missing.');
|
|
103
|
+
process.exit(1);
|
|
104
|
+
}
|
|
105
|
+
// Fetch channel with deployment relation
|
|
106
|
+
const appChannel = await appChannelsService.findOneById({
|
|
107
|
+
appId,
|
|
108
|
+
id: appChannelId,
|
|
109
|
+
relations: 'appDeployment',
|
|
110
|
+
});
|
|
111
|
+
// Validate that the channel has an active build assigned
|
|
112
|
+
if (!appChannel.appDeployment) {
|
|
113
|
+
consola.error('Channel has no active build assigned.');
|
|
114
|
+
process.exit(1);
|
|
115
|
+
}
|
|
116
|
+
// Prompt for percentage if not provided
|
|
117
|
+
if (percentage === undefined) {
|
|
118
|
+
if (!isInteractive()) {
|
|
119
|
+
consola.error('You must provide --percentage when running in non-interactive environment.');
|
|
120
|
+
process.exit(1);
|
|
121
|
+
}
|
|
122
|
+
const percentageInput = await prompt('Enter the rollout percentage (0-100):', {
|
|
123
|
+
type: 'text',
|
|
124
|
+
});
|
|
125
|
+
if (!percentageInput) {
|
|
126
|
+
consola.error('You must enter a rollout percentage.');
|
|
127
|
+
process.exit(1);
|
|
128
|
+
}
|
|
129
|
+
percentage = parseInt(percentageInput, 10);
|
|
130
|
+
if (isNaN(percentage) || percentage < 0 || percentage > 100) {
|
|
131
|
+
consola.error('Percentage must be a number between 0 and 100.');
|
|
132
|
+
process.exit(1);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
// Update deployment rollout percentage
|
|
136
|
+
consola.start('Updating rollout percentage...');
|
|
137
|
+
const response = await appDeploymentsService.update({
|
|
138
|
+
appId,
|
|
139
|
+
appDeploymentId: appChannel.appDeployment.id,
|
|
140
|
+
// Convert percentage from 0-100 to 0-1 for API
|
|
141
|
+
rolloutPercentage: percentage / 100,
|
|
142
|
+
});
|
|
143
|
+
consola.info(`Deployment ID: ${response.id}`);
|
|
144
|
+
consola.info(`Deployment URL: ${DEFAULT_CONSOLE_BASE_URL}/apps/${appId}/deployments/${response.id}`);
|
|
145
|
+
consola.success(`Rolled out to ${percentage}%.`);
|
|
146
|
+
},
|
|
147
|
+
});
|
|
@@ -0,0 +1,420 @@
|
|
|
1
|
+
import { DEFAULT_CONSOLE_BASE_URL, MAX_CONCURRENT_UPLOADS } from '../../../config/index.js';
|
|
2
|
+
import appBundleFilesService from '../../../services/app-bundle-files.js';
|
|
3
|
+
import appBundlesService from '../../../services/app-bundles.js';
|
|
4
|
+
import appsService from '../../../services/apps.js';
|
|
5
|
+
import authorizationService from '../../../services/authorization-service.js';
|
|
6
|
+
import organizationsService from '../../../services/organizations.js';
|
|
7
|
+
import { createBufferFromPath, createBufferFromReadStream, createBufferFromString, isPrivateKeyContent, } from '../../../utils/buffer.js';
|
|
8
|
+
import { isInteractive } from '../../../utils/environment.js';
|
|
9
|
+
import { fileExistsAtPath, getFilesInDirectoryAndSubdirectories, isDirectory } from '../../../utils/file.js';
|
|
10
|
+
import { createHash } from '../../../utils/hash.js';
|
|
11
|
+
import { generateManifestJson } from '../../../utils/manifest.js';
|
|
12
|
+
import { formatPrivateKey } from '../../../utils/private-key.js';
|
|
13
|
+
import { prompt } from '../../../utils/prompt.js';
|
|
14
|
+
import { createSignature } from '../../../utils/signature.js';
|
|
15
|
+
import zip from '../../../utils/zip.js';
|
|
16
|
+
import { defineCommand, defineOptions } from '@robingenz/zli';
|
|
17
|
+
import consola from 'consola';
|
|
18
|
+
import { createReadStream } from 'fs';
|
|
19
|
+
import pathModule from 'path';
|
|
20
|
+
import { z } from 'zod';
|
|
21
|
+
export default defineCommand({
|
|
22
|
+
description: 'Upload a bundle to Capawesome Cloud.',
|
|
23
|
+
options: defineOptions(z.object({
|
|
24
|
+
androidMax: z.coerce
|
|
25
|
+
.string()
|
|
26
|
+
.optional()
|
|
27
|
+
.describe('The maximum Android version code (`versionCode`) that the bundle supports.'),
|
|
28
|
+
androidMin: z.coerce
|
|
29
|
+
.string()
|
|
30
|
+
.optional()
|
|
31
|
+
.describe('The minimum Android version code (`versionCode`) that the bundle supports.'),
|
|
32
|
+
androidEq: z.coerce
|
|
33
|
+
.string()
|
|
34
|
+
.optional()
|
|
35
|
+
.describe('The exact Android version code (`versionCode`) that the bundle does not support.'),
|
|
36
|
+
appId: z
|
|
37
|
+
.string({
|
|
38
|
+
message: 'App ID must be a UUID.',
|
|
39
|
+
})
|
|
40
|
+
.uuid({
|
|
41
|
+
message: 'App ID must be a UUID.',
|
|
42
|
+
})
|
|
43
|
+
.optional()
|
|
44
|
+
.describe('App ID to deploy to.'),
|
|
45
|
+
artifactType: z
|
|
46
|
+
.enum(['manifest', 'zip'], {
|
|
47
|
+
message: 'Invalid artifact type. Must be either `manifest` or `zip`.',
|
|
48
|
+
})
|
|
49
|
+
.optional()
|
|
50
|
+
.describe('The type of artifact to deploy. Must be either `manifest` or `zip`. The default is `zip`.')
|
|
51
|
+
.default('zip'),
|
|
52
|
+
channel: z.string().optional().describe('Channel to associate the bundle with.'),
|
|
53
|
+
commitMessage: z
|
|
54
|
+
.string()
|
|
55
|
+
.optional()
|
|
56
|
+
.describe('The commit message related to the bundle. Deprecated, use `--git-ref` instead.'),
|
|
57
|
+
commitRef: z
|
|
58
|
+
.string()
|
|
59
|
+
.optional()
|
|
60
|
+
.describe('The commit ref related to the bundle. Deprecated, use `--git-ref` instead.'),
|
|
61
|
+
commitSha: z
|
|
62
|
+
.string()
|
|
63
|
+
.optional()
|
|
64
|
+
.describe('The commit sha related to the bundle. Deprecated, use `--git-ref` instead.'),
|
|
65
|
+
customProperty: z
|
|
66
|
+
.array(z.string().min(1).max(100))
|
|
67
|
+
.optional()
|
|
68
|
+
.describe('A custom property to assign to the bundle. Must be in the format `key=value`. Can be specified multiple times.'),
|
|
69
|
+
expiresInDays: z.coerce
|
|
70
|
+
.number({
|
|
71
|
+
message: 'Expiration days must be an integer.',
|
|
72
|
+
})
|
|
73
|
+
.int({
|
|
74
|
+
message: 'Expiration days must be an integer.',
|
|
75
|
+
})
|
|
76
|
+
.optional()
|
|
77
|
+
.describe('The number of days until the bundle is automatically deleted.'),
|
|
78
|
+
gitRef: z
|
|
79
|
+
.string()
|
|
80
|
+
.optional()
|
|
81
|
+
.describe('The Git reference (branch, tag, or commit SHA) to associate with the bundle.'),
|
|
82
|
+
iosMax: z
|
|
83
|
+
.string()
|
|
84
|
+
.optional()
|
|
85
|
+
.describe('The maximum iOS bundle version (`CFBundleVersion`) that the bundle supports.'),
|
|
86
|
+
iosMin: z
|
|
87
|
+
.string()
|
|
88
|
+
.optional()
|
|
89
|
+
.describe('The minimum iOS bundle version (`CFBundleVersion`) that the bundle supports.'),
|
|
90
|
+
iosEq: z
|
|
91
|
+
.string()
|
|
92
|
+
.optional()
|
|
93
|
+
.describe('The exact iOS bundle version (`CFBundleVersion`) that the bundle does not support.'),
|
|
94
|
+
path: z
|
|
95
|
+
.string()
|
|
96
|
+
.optional()
|
|
97
|
+
.describe('Path to the bundle to upload. Must be a folder (e.g. `www` or `dist`) or a zip file.'),
|
|
98
|
+
privateKey: z
|
|
99
|
+
.string()
|
|
100
|
+
.optional()
|
|
101
|
+
.describe('The private key to sign the bundle with. Can be a file path to a .pem file or the private key content as plain text.'),
|
|
102
|
+
rolloutPercentage: z.coerce
|
|
103
|
+
.number()
|
|
104
|
+
.int({
|
|
105
|
+
message: 'Percentage must be an integer.',
|
|
106
|
+
})
|
|
107
|
+
.min(0, {
|
|
108
|
+
message: 'Percentage must be at least 0.',
|
|
109
|
+
})
|
|
110
|
+
.max(100, {
|
|
111
|
+
message: 'Percentage must be at most 100.',
|
|
112
|
+
})
|
|
113
|
+
.optional()
|
|
114
|
+
.describe('The percentage of devices to deploy the bundle to. Must be an integer between 0 and 100.'),
|
|
115
|
+
yes: z.boolean().optional().describe('Skip confirmation prompt.'),
|
|
116
|
+
}), { y: 'yes' }),
|
|
117
|
+
action: async (options, args) => {
|
|
118
|
+
let { androidEq, androidMax, androidMin, appId, artifactType, channel, commitMessage, commitRef, commitSha, customProperty, expiresInDays, gitRef, iosEq, iosMax, iosMin, path, privateKey, rolloutPercentage, } = options;
|
|
119
|
+
// Check if the user is logged in
|
|
120
|
+
if (!authorizationService.hasAuthorizationToken()) {
|
|
121
|
+
consola.error('You must be logged in to run this command. Please run the `login` command first.');
|
|
122
|
+
process.exit(1);
|
|
123
|
+
}
|
|
124
|
+
// Calculate the expiration date
|
|
125
|
+
let expiresAt;
|
|
126
|
+
if (expiresInDays) {
|
|
127
|
+
const expiresAtDate = new Date();
|
|
128
|
+
expiresAtDate.setDate(expiresAtDate.getDate() + expiresInDays);
|
|
129
|
+
expiresAt = expiresAtDate.toISOString();
|
|
130
|
+
}
|
|
131
|
+
// Prompt for path if not provided
|
|
132
|
+
if (!path) {
|
|
133
|
+
if (!isInteractive()) {
|
|
134
|
+
consola.error('You must provide a path when running in non-interactive environment.');
|
|
135
|
+
process.exit(1);
|
|
136
|
+
}
|
|
137
|
+
consola.warn('Make sure you have built your web assets before uploading (e.g., `npm run build`).');
|
|
138
|
+
path = await prompt('Enter the path to the web assets directory (e.g., `dist` or `www`):', {
|
|
139
|
+
type: 'text',
|
|
140
|
+
});
|
|
141
|
+
if (!path) {
|
|
142
|
+
consola.error('You must provide a path to the app bundle.');
|
|
143
|
+
process.exit(1);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
// Validate the provided path
|
|
147
|
+
const pathExists = await fileExistsAtPath(path);
|
|
148
|
+
if (!pathExists) {
|
|
149
|
+
consola.error(`The path does not exist.`);
|
|
150
|
+
process.exit(1);
|
|
151
|
+
}
|
|
152
|
+
// Check if the directory contains an index.html file
|
|
153
|
+
const pathIsDirectory = await isDirectory(path);
|
|
154
|
+
if (pathIsDirectory) {
|
|
155
|
+
const files = await getFilesInDirectoryAndSubdirectories(path);
|
|
156
|
+
const indexHtml = files.find((file) => file.href === 'index.html');
|
|
157
|
+
if (!indexHtml) {
|
|
158
|
+
consola.error('The directory must contain an `index.html` file.');
|
|
159
|
+
process.exit(1);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
else if (zip.isZipped(path)) {
|
|
163
|
+
// No-op
|
|
164
|
+
}
|
|
165
|
+
else {
|
|
166
|
+
consola.error('The path must be either a folder or a zip file.');
|
|
167
|
+
process.exit(1);
|
|
168
|
+
}
|
|
169
|
+
// Check that the path is a directory when creating a bundle with an artifact type of manifest
|
|
170
|
+
if (artifactType === 'manifest') {
|
|
171
|
+
const pathIsDirectory = await isDirectory(path);
|
|
172
|
+
if (!pathIsDirectory) {
|
|
173
|
+
consola.error('The path must be a folder when creating a bundle with an artifact type of `manifest`.');
|
|
174
|
+
process.exit(1);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
// Prompt for appId if not provided
|
|
178
|
+
if (!appId) {
|
|
179
|
+
if (!isInteractive()) {
|
|
180
|
+
consola.error('You must provide an app ID when running in non-interactive environment.');
|
|
181
|
+
process.exit(1);
|
|
182
|
+
}
|
|
183
|
+
const organizations = await organizationsService.findAll();
|
|
184
|
+
if (organizations.length === 0) {
|
|
185
|
+
consola.error('You must create an organization before creating a bundle.');
|
|
186
|
+
process.exit(1);
|
|
187
|
+
}
|
|
188
|
+
// @ts-ignore wait till https://github.com/unjs/consola/pull/280 is merged
|
|
189
|
+
const organizationId = await prompt('Select the organization of the app for which you want to create a bundle.', {
|
|
190
|
+
type: 'select',
|
|
191
|
+
options: organizations.map((organization) => ({ label: organization.name, value: organization.id })),
|
|
192
|
+
});
|
|
193
|
+
if (!organizationId) {
|
|
194
|
+
consola.error('You must select the organization of an app for which you want to create a bundle.');
|
|
195
|
+
process.exit(1);
|
|
196
|
+
}
|
|
197
|
+
const apps = await appsService.findAll({
|
|
198
|
+
organizationId,
|
|
199
|
+
});
|
|
200
|
+
if (apps.length === 0) {
|
|
201
|
+
consola.error('You must create an app before creating a bundle.');
|
|
202
|
+
process.exit(1);
|
|
203
|
+
}
|
|
204
|
+
// @ts-ignore wait till https://github.com/unjs/consola/pull/280 is merged
|
|
205
|
+
appId = await prompt('Which app do you want to deploy to:', {
|
|
206
|
+
type: 'select',
|
|
207
|
+
options: apps.map((app) => ({ label: app.name, value: app.id })),
|
|
208
|
+
});
|
|
209
|
+
if (!appId) {
|
|
210
|
+
consola.error('You must select an app to deploy to.');
|
|
211
|
+
process.exit(1);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
// Prompt for channel if interactive
|
|
215
|
+
if (!channel && !options.yes && isInteractive()) {
|
|
216
|
+
const shouldDeployToChannel = await prompt('Do you want to deploy to a specific channel?', {
|
|
217
|
+
type: 'confirm',
|
|
218
|
+
initial: false,
|
|
219
|
+
});
|
|
220
|
+
if (shouldDeployToChannel) {
|
|
221
|
+
channel = await prompt('Enter the channel name:', {
|
|
222
|
+
type: 'text',
|
|
223
|
+
});
|
|
224
|
+
if (!channel) {
|
|
225
|
+
consola.error('The channel name must be at least one character long.');
|
|
226
|
+
process.exit(1);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
// Create the private key buffer
|
|
231
|
+
let privateKeyBuffer;
|
|
232
|
+
if (privateKey) {
|
|
233
|
+
if (isPrivateKeyContent(privateKey)) {
|
|
234
|
+
// Handle plain text private key content
|
|
235
|
+
const formattedPrivateKey = formatPrivateKey(privateKey);
|
|
236
|
+
privateKeyBuffer = createBufferFromString(formattedPrivateKey);
|
|
237
|
+
}
|
|
238
|
+
else if (privateKey.endsWith('.pem')) {
|
|
239
|
+
// Handle file path
|
|
240
|
+
const fileExists = await fileExistsAtPath(privateKey);
|
|
241
|
+
if (fileExists) {
|
|
242
|
+
const keyBuffer = await createBufferFromPath(privateKey);
|
|
243
|
+
const keyContent = keyBuffer.toString('utf8');
|
|
244
|
+
const formattedPrivateKey = formatPrivateKey(keyContent);
|
|
245
|
+
privateKeyBuffer = createBufferFromString(formattedPrivateKey);
|
|
246
|
+
}
|
|
247
|
+
else {
|
|
248
|
+
consola.error('Private key file not found.');
|
|
249
|
+
process.exit(1);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
else {
|
|
253
|
+
consola.error('Private key must be either a path to a .pem file or the private key content as plain text.');
|
|
254
|
+
process.exit(1);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
// Get app details for confirmation
|
|
258
|
+
const app = await appsService.findOne({ appId });
|
|
259
|
+
const appName = app.name;
|
|
260
|
+
// Final confirmation before uploading
|
|
261
|
+
if (!options.yes && isInteractive()) {
|
|
262
|
+
const relativePath = pathModule.relative(process.cwd(), path);
|
|
263
|
+
const confirmed = await prompt(`Are you sure you want to upload a bundle from path "${relativePath}" for app "${appName}" (${appId})?`, {
|
|
264
|
+
type: 'confirm',
|
|
265
|
+
});
|
|
266
|
+
if (!confirmed) {
|
|
267
|
+
consola.info('Bundle upload cancelled.');
|
|
268
|
+
process.exit(0);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
// Create the app bundle
|
|
272
|
+
consola.start('Creating bundle...');
|
|
273
|
+
const response = await appBundlesService.create({
|
|
274
|
+
appId,
|
|
275
|
+
artifactType,
|
|
276
|
+
channelName: channel,
|
|
277
|
+
eqAndroidAppVersionCode: androidEq,
|
|
278
|
+
eqIosAppVersionCode: iosEq,
|
|
279
|
+
gitCommitMessage: commitMessage,
|
|
280
|
+
gitCommitRef: commitRef,
|
|
281
|
+
gitCommitSha: commitSha,
|
|
282
|
+
gitRef,
|
|
283
|
+
customProperties: parseCustomProperties(customProperty),
|
|
284
|
+
expiresAt,
|
|
285
|
+
maxAndroidAppVersionCode: androidMax,
|
|
286
|
+
maxIosAppVersionCode: iosMax,
|
|
287
|
+
minAndroidAppVersionCode: androidMin,
|
|
288
|
+
minIosAppVersionCode: iosMin,
|
|
289
|
+
// Convert percentage from 0-100 to 0-1 for API
|
|
290
|
+
rolloutPercentage: (rolloutPercentage ?? 100) / 100,
|
|
291
|
+
});
|
|
292
|
+
let appBundleFileId;
|
|
293
|
+
// Upload the app bundle files
|
|
294
|
+
if (artifactType === 'manifest') {
|
|
295
|
+
await uploadFiles({ appId, appBundleId: response.id, path, privateKeyBuffer });
|
|
296
|
+
}
|
|
297
|
+
else {
|
|
298
|
+
const result = await uploadZip({ appId, appBundleId: response.id, path, privateKeyBuffer });
|
|
299
|
+
appBundleFileId = result.appBundleFileId;
|
|
300
|
+
}
|
|
301
|
+
// Update the app bundle
|
|
302
|
+
consola.start('Updating bundle...');
|
|
303
|
+
await appBundlesService.update({
|
|
304
|
+
appBundleFileId,
|
|
305
|
+
appId,
|
|
306
|
+
artifactStatus: 'ready',
|
|
307
|
+
appBundleId: response.id,
|
|
308
|
+
});
|
|
309
|
+
consola.info(`Build Artifact ID: ${response.id}`);
|
|
310
|
+
if (response.appDeploymentId) {
|
|
311
|
+
consola.info(`Deployment URL: ${DEFAULT_CONSOLE_BASE_URL}/apps/${appId}/deployments/${response.appDeploymentId}`);
|
|
312
|
+
}
|
|
313
|
+
consola.success('Live Update successfully uploaded.');
|
|
314
|
+
},
|
|
315
|
+
});
|
|
316
|
+
const uploadFile = async (options) => {
|
|
317
|
+
let { appId, appBundleId, buffer, href, mimeType, name, privateKeyBuffer, retryOnFailure } = options;
|
|
318
|
+
try {
|
|
319
|
+
// Generate checksum
|
|
320
|
+
const hash = await createHash(buffer);
|
|
321
|
+
// Sign the bundle
|
|
322
|
+
let signature;
|
|
323
|
+
if (privateKeyBuffer) {
|
|
324
|
+
signature = await createSignature(privateKeyBuffer, buffer);
|
|
325
|
+
}
|
|
326
|
+
// Create the multipart upload
|
|
327
|
+
return await appBundleFilesService.create({
|
|
328
|
+
appId,
|
|
329
|
+
appBundleId,
|
|
330
|
+
buffer,
|
|
331
|
+
checksum: hash,
|
|
332
|
+
href,
|
|
333
|
+
mimeType,
|
|
334
|
+
name,
|
|
335
|
+
signature,
|
|
336
|
+
});
|
|
337
|
+
}
|
|
338
|
+
catch (error) {
|
|
339
|
+
if (retryOnFailure) {
|
|
340
|
+
return uploadFile({
|
|
341
|
+
...options,
|
|
342
|
+
retryOnFailure: false,
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
throw error;
|
|
346
|
+
}
|
|
347
|
+
};
|
|
348
|
+
const uploadFiles = async (options) => {
|
|
349
|
+
let { appId, appBundleId, path, privateKeyBuffer } = options;
|
|
350
|
+
// Generate the manifest file
|
|
351
|
+
await generateManifestJson(path);
|
|
352
|
+
// Get all files in the directory
|
|
353
|
+
const files = await getFilesInDirectoryAndSubdirectories(path);
|
|
354
|
+
// Iterate over each file
|
|
355
|
+
let fileIndex = 0;
|
|
356
|
+
const uploadNextFile = async () => {
|
|
357
|
+
if (fileIndex >= files.length) {
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
const file = files[fileIndex];
|
|
361
|
+
fileIndex++;
|
|
362
|
+
consola.start(`Uploading file (${fileIndex}/${files.length})...`);
|
|
363
|
+
const buffer = await createBufferFromPath(file.path);
|
|
364
|
+
await uploadFile({
|
|
365
|
+
appId,
|
|
366
|
+
appBundleId: appBundleId,
|
|
367
|
+
buffer,
|
|
368
|
+
href: file.href,
|
|
369
|
+
mimeType: file.mimeType,
|
|
370
|
+
name: file.name,
|
|
371
|
+
privateKeyBuffer: privateKeyBuffer,
|
|
372
|
+
retryOnFailure: true,
|
|
373
|
+
});
|
|
374
|
+
await uploadNextFile();
|
|
375
|
+
};
|
|
376
|
+
const uploadPromises = Array.from({ length: MAX_CONCURRENT_UPLOADS });
|
|
377
|
+
for (let i = 0; i < MAX_CONCURRENT_UPLOADS; i++) {
|
|
378
|
+
uploadPromises[i] = uploadNextFile();
|
|
379
|
+
}
|
|
380
|
+
await Promise.all(uploadPromises);
|
|
381
|
+
};
|
|
382
|
+
const uploadZip = async (options) => {
|
|
383
|
+
let { appId, appBundleId, path, privateKeyBuffer } = options;
|
|
384
|
+
// Read the zip file
|
|
385
|
+
let fileBuffer;
|
|
386
|
+
if (zip.isZipped(path)) {
|
|
387
|
+
const readStream = createReadStream(path);
|
|
388
|
+
fileBuffer = await createBufferFromReadStream(readStream);
|
|
389
|
+
}
|
|
390
|
+
else {
|
|
391
|
+
consola.start('Zipping folder...');
|
|
392
|
+
fileBuffer = await zip.zipFolder(path);
|
|
393
|
+
}
|
|
394
|
+
// Upload the zip file
|
|
395
|
+
consola.start('Uploading file...');
|
|
396
|
+
const result = await uploadFile({
|
|
397
|
+
appId,
|
|
398
|
+
appBundleId: appBundleId,
|
|
399
|
+
buffer: fileBuffer,
|
|
400
|
+
mimeType: 'application/zip',
|
|
401
|
+
name: 'bundle.zip',
|
|
402
|
+
privateKeyBuffer: privateKeyBuffer,
|
|
403
|
+
});
|
|
404
|
+
return {
|
|
405
|
+
appBundleFileId: result.id,
|
|
406
|
+
};
|
|
407
|
+
};
|
|
408
|
+
const parseCustomProperties = (customProperty) => {
|
|
409
|
+
let customProperties;
|
|
410
|
+
if (customProperty) {
|
|
411
|
+
customProperties = {};
|
|
412
|
+
for (const property of customProperty) {
|
|
413
|
+
const [key, value] = property.split('=');
|
|
414
|
+
if (key && value) {
|
|
415
|
+
customProperties[key] = value;
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
return customProperties;
|
|
420
|
+
};
|