eoas 3.0.5 → 3.1.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/README.md +5 -5
- package/bin/dev.js +3 -0
- package/bin/run.js +3 -0
- package/dist/commands/generate-certs.js +22 -2
- package/dist/commands/init.js +2 -2
- package/dist/commands/publish.d.ts +1 -0
- package/dist/commands/publish.js +65 -34
- package/dist/commands/republish.d.ts +1 -0
- package/dist/commands/republish.js +93 -38
- package/dist/commands/rollback.d.ts +1 -0
- package/dist/commands/rollback.js +9 -14
- package/dist/commands/server/init.d.ts +17 -0
- package/dist/commands/server/init.js +654 -0
- package/dist/commands/server/validate.d.ts +10 -0
- package/dist/commands/server/validate.js +115 -0
- package/dist/lib/assets.d.ts +30 -8
- package/dist/lib/assets.js +158 -4
- package/dist/lib/auth.js +2 -1
- package/dist/lib/expoConfig.d.ts +1 -1
- package/dist/lib/expoConfig.js +8 -8
- package/dist/lib/log.d.ts +8 -2
- package/dist/lib/log.js +47 -31
- package/dist/lib/ora.d.ts +10 -9
- package/dist/lib/ora.js +49 -88
- package/dist/lib/packageRunner.js +2 -1
- package/dist/lib/prompts.d.ts +32 -0
- package/dist/lib/prompts.js +86 -1
- package/dist/lib/serverConfig/choices.d.ts +62 -0
- package/dist/lib/serverConfig/choices.js +65 -0
- package/dist/lib/serverConfig/envCatalog.d.ts +41 -0
- package/dist/lib/serverConfig/envCatalog.js +582 -0
- package/dist/lib/serverConfig/helmValues.d.ts +22 -0
- package/dist/lib/serverConfig/helmValues.js +281 -0
- package/dist/lib/serverConfig/passwordPolicy.d.ts +3 -0
- package/dist/lib/serverConfig/passwordPolicy.js +36 -0
- package/dist/lib/serverUpdates.d.ts +45 -0
- package/dist/lib/serverUpdates.js +108 -0
- package/dist/lib/utils.d.ts +1 -0
- package/dist/lib/utils.js +18 -14
- package/package.json +8 -6
package/README.md
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
|
-
# EOAS
|
|
1
|
+
# EOAS
|
|
2
2
|
|
|
3
|
-
EOAS
|
|
3
|
+
EOAS is a powerful helper package designed to simplify the setup and update publication process for the [xprem](https://github.com/mercuretechnologies/xprem) project.
|
|
4
4
|
|
|
5
5
|
## Quick Start
|
|
6
6
|
|
|
7
7
|
To get started with EOAS, check out the official documentation:
|
|
8
|
-
[EOAS Official Documentation](https://mercure-technologies.gitbook.io/
|
|
8
|
+
[EOAS Official Documentation](https://mercure-technologies.gitbook.io/xprem/eoas/overview)
|
|
9
9
|
|
|
10
10
|
## Learn More
|
|
11
|
-
For detailed information and to explore the core functionalities of
|
|
12
|
-
[
|
|
11
|
+
For detailed information and to explore the core functionalities of xprem, visit the main repository:
|
|
12
|
+
[xprem on GitHub](https://github.com/mercuretechnologies/xprem)
|
|
13
13
|
|
|
14
14
|
---
|
|
15
15
|
|
package/bin/dev.js
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
// Silence DEP0040 (punycode) emitted by node-fetch@2 -> whatwg-url@5 on Node 21+.
|
|
3
|
+
process.noDeprecation = true;
|
|
4
|
+
|
|
2
5
|
// eslint-disable-next-line node/shebang, unicorn/prefer-top-level-await
|
|
3
6
|
(async () => {
|
|
4
7
|
const oclif = await import('@oclif/core');
|
package/bin/run.js
CHANGED
|
@@ -8,6 +8,9 @@ const path_1 = tslib_1.__importDefault(require("path"));
|
|
|
8
8
|
const log_1 = tslib_1.__importDefault(require("../lib/log"));
|
|
9
9
|
const prompts_1 = require("../lib/prompts");
|
|
10
10
|
const utils_1 = require("../lib/utils");
|
|
11
|
+
// Owner read/write only: the private key signs every update served by the OTA
|
|
12
|
+
// server.
|
|
13
|
+
const PRIVATE_KEY_MODE = 0o600;
|
|
11
14
|
class GenerateCerts extends core_1.Command {
|
|
12
15
|
static args = {};
|
|
13
16
|
static description = 'Generate private & public certificates for code signing';
|
|
@@ -46,6 +49,20 @@ class GenerateCerts extends core_1.Command {
|
|
|
46
49
|
}
|
|
47
50
|
},
|
|
48
51
|
});
|
|
52
|
+
const keyOutput = path_1.default.resolve(process.cwd(), keyOutputDir);
|
|
53
|
+
const privateKeyPath = path_1.default.join(keyOutput, 'private-key.pem');
|
|
54
|
+
if ((0, fs_extra_1.existsSync)(privateKeyPath)) {
|
|
55
|
+
const overwrite = await (0, prompts_1.confirmAsync)({
|
|
56
|
+
message: `${privateKeyPath} already exists. Overwrite it? Updates signed with the current key will no longer be accepted by apps embedding the matching certificate.`,
|
|
57
|
+
name: 'overwritePrivateKey',
|
|
58
|
+
type: 'confirm',
|
|
59
|
+
initial: false,
|
|
60
|
+
});
|
|
61
|
+
if (!overwrite) {
|
|
62
|
+
log_1.default.warn('Aborted: no certificate or key was written.');
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
49
66
|
const { certificateCommonName } = await (0, prompts_1.promptAsync)({
|
|
50
67
|
message: 'Please enter your Organization name',
|
|
51
68
|
name: 'certificateCommonName',
|
|
@@ -66,7 +83,6 @@ class GenerateCerts extends core_1.Command {
|
|
|
66
83
|
});
|
|
67
84
|
const validityDurationYears = Math.floor(Number(certificateValidityDurationYears));
|
|
68
85
|
const certificateOutput = path_1.default.resolve(process.cwd(), certificateOutputDir);
|
|
69
|
-
const keyOutput = path_1.default.resolve(process.cwd(), keyOutputDir);
|
|
70
86
|
const validityNotBefore = new Date();
|
|
71
87
|
const validityNotAfter = new Date();
|
|
72
88
|
validityNotAfter.setFullYear(validityNotAfter.getFullYear() + validityDurationYears);
|
|
@@ -82,9 +98,13 @@ class GenerateCerts extends core_1.Command {
|
|
|
82
98
|
// Before the key touches the disk, so there is no window where it exists
|
|
83
99
|
// uncovered by the ignore rule.
|
|
84
100
|
(0, utils_1.ensurePrivateKeyIgnored)(process.cwd());
|
|
101
|
+
// Removed first: writeFile only applies the mode when it creates the file,
|
|
102
|
+
// so overwriting an existing key would keep its (possibly world readable)
|
|
103
|
+
// permissions.
|
|
104
|
+
await (0, fs_extra_1.remove)(privateKeyPath);
|
|
85
105
|
await Promise.all([
|
|
86
106
|
(0, fs_extra_1.writeFile)(path_1.default.join(keyOutput, 'public-key.pem'), keyPairPEM.publicKeyPEM),
|
|
87
|
-
(0, fs_extra_1.writeFile)(
|
|
107
|
+
(0, fs_extra_1.writeFile)(privateKeyPath, keyPairPEM.privateKeyPEM, { mode: PRIVATE_KEY_MODE }),
|
|
88
108
|
(0, fs_extra_1.writeFile)(path_1.default.join(certificateOutput, 'certificate.pem'), certificatePEM),
|
|
89
109
|
]);
|
|
90
110
|
log_1.default.succeed(`Generated public and private keys output in ${keyOutputDir}. Please follow the documentation to securely store them and do not commit them to your repository.`);
|
package/dist/commands/init.js
CHANGED
|
@@ -12,7 +12,7 @@ const prompts_1 = require("../lib/prompts");
|
|
|
12
12
|
const utils_1 = require("../lib/utils");
|
|
13
13
|
class Init extends core_1.Command {
|
|
14
14
|
static args = {};
|
|
15
|
-
static description = 'Configure your existing expo project with
|
|
15
|
+
static description = 'Configure your existing expo project with xprem';
|
|
16
16
|
static examples = ['<%= config.bin %> <%= command.id %>'];
|
|
17
17
|
static flags = {};
|
|
18
18
|
async run() {
|
|
@@ -31,7 +31,7 @@ class Init extends core_1.Command {
|
|
|
31
31
|
?.projectId;
|
|
32
32
|
const { appId } = await (0, prompts_1.promptAsync)({
|
|
33
33
|
message: 'Enter the project id for this project (sent as the expo-app-id header).\n' +
|
|
34
|
-
' See https://mercure-technologies.gitbook.io/
|
|
34
|
+
' See https://mercure-technologies.gitbook.io/xprem/stateless-mode/getting-started for details.',
|
|
35
35
|
name: 'appId',
|
|
36
36
|
type: 'text',
|
|
37
37
|
initial: detectedAppId,
|
|
@@ -8,6 +8,7 @@ export default class Publish extends Command {
|
|
|
8
8
|
channel: import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
|
|
9
9
|
disableRepositoryCheck: import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
|
|
10
10
|
branch: import("@oclif/core/lib/interfaces").OptionFlag<string, import("@oclif/core/lib/interfaces").CustomOptions>;
|
|
11
|
+
serverUrl: import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
|
|
11
12
|
nonInteractive: import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
|
|
12
13
|
outputDir: import("@oclif/core/lib/interfaces").OptionFlag<string, import("@oclif/core/lib/interfaces").CustomOptions>;
|
|
13
14
|
packageRunner: import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
|
package/dist/commands/publish.js
CHANGED
|
@@ -4,6 +4,7 @@ const tslib_1 = require("tslib");
|
|
|
4
4
|
const eas_build_job_1 = require("@expo/eas-build-job");
|
|
5
5
|
const spawn_async_1 = tslib_1.__importDefault(require("@expo/spawn-async"));
|
|
6
6
|
const core_1 = require("@oclif/core");
|
|
7
|
+
const crypto_1 = require("crypto");
|
|
7
8
|
const form_data_1 = tslib_1.__importDefault(require("form-data"));
|
|
8
9
|
const fs_extra_1 = tslib_1.__importDefault(require("fs-extra"));
|
|
9
10
|
const mime_1 = tslib_1.__importDefault(require("mime"));
|
|
@@ -48,6 +49,10 @@ class Publish extends core_1.Command {
|
|
|
48
49
|
description: 'Name of the branch to point to',
|
|
49
50
|
required: true,
|
|
50
51
|
}),
|
|
52
|
+
serverUrl: core_1.Flags.string({
|
|
53
|
+
description: 'URL of the self-hosted update server to publish to. Defaults to the origin of updates.url from your Expo config',
|
|
54
|
+
required: false,
|
|
55
|
+
}),
|
|
51
56
|
nonInteractive: core_1.Flags.boolean({
|
|
52
57
|
description: 'Run command in non-interactive mode',
|
|
53
58
|
default: false,
|
|
@@ -80,6 +85,7 @@ class Publish extends core_1.Command {
|
|
|
80
85
|
disableRepositoryCheck: flags.disableRepositoryCheck,
|
|
81
86
|
platform: flags.platform,
|
|
82
87
|
branch: flags.branch,
|
|
88
|
+
customServerUrl: flags.serverUrl,
|
|
83
89
|
nonInteractive: flags.nonInteractive,
|
|
84
90
|
outputDir: flags.outputDir,
|
|
85
91
|
packageRunner: (0, packageRunner_1.resolvePackageRunner)(flags.packageRunner, process.cwd()),
|
|
@@ -96,7 +102,7 @@ class Publish extends core_1.Command {
|
|
|
96
102
|
process.exit(1);
|
|
97
103
|
}
|
|
98
104
|
const { flags } = await this.parse(Publish);
|
|
99
|
-
const { platform, nonInteractive, branch, outputDir, packageRunner, providedDeprecatedChannel, disableRepositoryCheck, message, dumpSourcemap, rolloutPercentage, } = this.sanitizeFlags(flags);
|
|
105
|
+
const { platform, nonInteractive, branch, customServerUrl, outputDir, packageRunner, providedDeprecatedChannel, disableRepositoryCheck, message, dumpSourcemap, rolloutPercentage, } = this.sanitizeFlags(flags);
|
|
100
106
|
if (!branch) {
|
|
101
107
|
log_1.default.error('Branch name is required');
|
|
102
108
|
process.exit(1);
|
|
@@ -118,12 +124,14 @@ class Publish extends core_1.Command {
|
|
|
118
124
|
},
|
|
119
125
|
packageRunner,
|
|
120
126
|
});
|
|
121
|
-
const serverUrl = await (0, expoConfig_1.resolveServerUrl)(config).catch(e => {
|
|
127
|
+
const serverUrl = await (0, expoConfig_1.resolveServerUrl)(config, customServerUrl).catch(e => {
|
|
122
128
|
log_1.default.error(e.message);
|
|
123
129
|
process.exit(1);
|
|
124
130
|
});
|
|
125
131
|
const appId = (0, expoConfig_1.requireExpoAppId)(config);
|
|
126
|
-
|
|
132
|
+
// A URL passed on the command line needs no confirmation, and `eoas init`
|
|
133
|
+
// would not fix it anyway.
|
|
134
|
+
if (!nonInteractive && !customServerUrl) {
|
|
127
135
|
const confirmed = await (0, prompts_1.confirmAsync)({
|
|
128
136
|
message: `Is this the correct URL of your self-hosted update server? ${serverUrl}`,
|
|
129
137
|
name: 'export',
|
|
@@ -243,6 +251,9 @@ class Publish extends core_1.Command {
|
|
|
243
251
|
process.exit(1);
|
|
244
252
|
}
|
|
245
253
|
let uploadUrls = [];
|
|
254
|
+
// One group id for the whole run: every platform update of this publish
|
|
255
|
+
// shares it, so control plane servers list them as a single publish.
|
|
256
|
+
const publishGroupId = (0, crypto_1.randomUUID)();
|
|
246
257
|
try {
|
|
247
258
|
uploadUrls = await Promise.all(runtimeVersions.map(async ({ runtimeVersion, platform }) => {
|
|
248
259
|
if (!runtimeVersion) {
|
|
@@ -260,50 +271,56 @@ class Publish extends core_1.Command {
|
|
|
260
271
|
commitHash,
|
|
261
272
|
message: resolvedMessage,
|
|
262
273
|
rolloutPercentage,
|
|
274
|
+
publishGroup: publishGroupId,
|
|
263
275
|
branch,
|
|
264
276
|
})),
|
|
265
277
|
runtimeVersion,
|
|
266
278
|
platform,
|
|
267
279
|
};
|
|
268
280
|
}));
|
|
269
|
-
|
|
270
|
-
|
|
281
|
+
// Every path and URL the server handed back is checked here, before a
|
|
282
|
+
// single file is opened. A server that forges a filePath would otherwise
|
|
283
|
+
// make the CLI read arbitrary files off this machine and PUT them wherever
|
|
284
|
+
// it likes. Validated per response: each platform gets its own upload URLs
|
|
285
|
+
// for the same files.
|
|
286
|
+
const resolvedUploads = (await Promise.all(uploadUrls.map(({ uploadRequests }) => (0, assets_1.resolveUploadRequests)({
|
|
287
|
+
uploadRequests,
|
|
288
|
+
exportDir: path_1.default.join(projectDir, outputDir),
|
|
289
|
+
manifest: files,
|
|
290
|
+
})))).flat();
|
|
291
|
+
await Promise.all(resolvedUploads.map(async ({ item: itm, absolutePath, manifestEntry }) => {
|
|
271
292
|
const isLocalBucketFileUpload = itm.requestUploadUrl.startsWith(`${serverUrl}/${appId}/uploadLocalFile`);
|
|
272
|
-
const formData = new form_data_1.default();
|
|
273
|
-
let file;
|
|
274
|
-
try {
|
|
275
|
-
file = fs_extra_1.default.createReadStream(path_1.default.join(projectDir, outputDir, itm.filePath));
|
|
276
|
-
}
|
|
277
|
-
catch {
|
|
278
|
-
throw new Error(`Failed to read file ${itm.filePath}`);
|
|
279
|
-
}
|
|
280
|
-
formData.append(itm.fileName, file);
|
|
281
293
|
if (isLocalBucketFileUpload) {
|
|
282
|
-
const
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
294
|
+
const formData = new form_data_1.default();
|
|
295
|
+
const file = fs_extra_1.default.createReadStream(absolutePath);
|
|
296
|
+
formData.append(itm.fileName, file);
|
|
297
|
+
try {
|
|
298
|
+
const response = await (0, fetch_1.fetchWithRetries)(itm.requestUploadUrl, {
|
|
299
|
+
method: 'PUT',
|
|
300
|
+
headers: {
|
|
301
|
+
...formData.getHeaders(),
|
|
302
|
+
...(0, auth_1.getAuthHeaders)(credentials),
|
|
303
|
+
},
|
|
304
|
+
body: formData,
|
|
305
|
+
// The URL was validated as a string; following a redirect would
|
|
306
|
+
// send these bytes to an origin nothing ever checked.
|
|
307
|
+
redirect: 'error',
|
|
308
|
+
});
|
|
309
|
+
if (!response.ok) {
|
|
310
|
+
log_1.default.error('Failed to upload file', await response.text());
|
|
311
|
+
throw new Error('Failed to upload file');
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
finally {
|
|
315
|
+
file.close();
|
|
293
316
|
}
|
|
294
|
-
file.close();
|
|
295
317
|
return;
|
|
296
318
|
}
|
|
297
|
-
|
|
298
|
-
if (!findFile) {
|
|
299
|
-
log_1.default.error(`File ${itm.filePath} not found`);
|
|
300
|
-
throw new Error(`File ${itm.filePath} not found`);
|
|
301
|
-
}
|
|
302
|
-
let contentType = mime_1.default.getType(findFile.ext);
|
|
319
|
+
let contentType = mime_1.default.getType(manifestEntry.ext);
|
|
303
320
|
if (!contentType) {
|
|
304
321
|
contentType = 'application/octet-stream';
|
|
305
322
|
}
|
|
306
|
-
const buffer = await fs_extra_1.default.readFile(
|
|
323
|
+
const buffer = await fs_extra_1.default.readFile(absolutePath);
|
|
307
324
|
const response = await (0, fetch_1.fetchWithRetries)(itm.requestUploadUrl, {
|
|
308
325
|
method: 'PUT',
|
|
309
326
|
headers: {
|
|
@@ -312,12 +329,17 @@ class Publish extends core_1.Command {
|
|
|
312
329
|
...(itm.headers ?? {}),
|
|
313
330
|
},
|
|
314
331
|
body: buffer,
|
|
332
|
+
// Only the URL string was validated. node-fetch follows up to 20
|
|
333
|
+
// redirects by default with no protocol or host check, so a server
|
|
334
|
+
// handing back a valid https URL that 302s to http://internal-host
|
|
335
|
+
// would exfiltrate the bundle in cleartext to an origin of its
|
|
336
|
+
// choosing. Nothing legitimate redirects an upload PUT.
|
|
337
|
+
redirect: 'error',
|
|
315
338
|
});
|
|
316
339
|
if (!response.ok) {
|
|
317
340
|
log_1.default.error('❌ File upload failed', await response.text());
|
|
318
341
|
process.exit(1);
|
|
319
342
|
}
|
|
320
|
-
file.close();
|
|
321
343
|
}));
|
|
322
344
|
uploadFilesSpinner.succeed('✅ Files uploaded successfully');
|
|
323
345
|
}
|
|
@@ -385,6 +407,15 @@ class Publish extends core_1.Command {
|
|
|
385
407
|
if (hasSuccess) {
|
|
386
408
|
log_1.default.withInfo(`🌿 Branch: \`${branch}\``);
|
|
387
409
|
log_1.default.withInfo(`⏳ Deployed at: \`${new Date().toUTCString()}\`\n`);
|
|
410
|
+
const groupAcknowledged = uploadUrls.every(u => u.publishGroup === publishGroupId);
|
|
411
|
+
if (groupAcknowledged) {
|
|
412
|
+
log_1.default.withInfo(`📦 Publish group: \`${publishGroupId}\``);
|
|
413
|
+
}
|
|
414
|
+
else if (uploadUrls.length > 1) {
|
|
415
|
+
// Only worth a note when several platforms were published: a single
|
|
416
|
+
// update has nothing to group anyway.
|
|
417
|
+
log_1.default.withInfo('ℹ️ Platform updates were published without grouping (publish groups require a server in control plane mode).');
|
|
418
|
+
}
|
|
388
419
|
log_1.default.withInfo('🔥 Your users will receive the latest update automatically!');
|
|
389
420
|
}
|
|
390
421
|
}
|
|
@@ -6,6 +6,7 @@ export default class Publish extends Command {
|
|
|
6
6
|
static flags: {
|
|
7
7
|
branch: import("@oclif/core/lib/interfaces").OptionFlag<string, import("@oclif/core/lib/interfaces").CustomOptions>;
|
|
8
8
|
platform: import("@oclif/core/lib/interfaces").OptionFlag<string, import("@oclif/core/lib/interfaces").CustomOptions>;
|
|
9
|
+
serverUrl: import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
|
|
9
10
|
};
|
|
10
11
|
private sanitizeFlags;
|
|
11
12
|
run(): Promise<void>;
|
|
@@ -2,13 +2,14 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
const tslib_1 = require("tslib");
|
|
4
4
|
const core_1 = require("@oclif/core");
|
|
5
|
-
const ora_1 = tslib_1.__importDefault(require("ora"));
|
|
6
5
|
const auth_1 = require("../lib/auth");
|
|
7
6
|
const expoConfig_1 = require("../lib/expoConfig");
|
|
8
7
|
const fetch_1 = require("../lib/fetch");
|
|
9
8
|
const log_1 = tslib_1.__importDefault(require("../lib/log"));
|
|
9
|
+
const ora_1 = require("../lib/ora");
|
|
10
10
|
const package_1 = require("../lib/package");
|
|
11
11
|
const prompts_1 = require("../lib/prompts");
|
|
12
|
+
const serverUpdates_1 = require("../lib/serverUpdates");
|
|
12
13
|
const vcs_1 = require("../lib/vcs");
|
|
13
14
|
class Publish extends core_1.Command {
|
|
14
15
|
static args = {};
|
|
@@ -25,11 +26,16 @@ class Publish extends core_1.Command {
|
|
|
25
26
|
default: 'all',
|
|
26
27
|
required: true,
|
|
27
28
|
}),
|
|
29
|
+
serverUrl: core_1.Flags.string({
|
|
30
|
+
description: 'URL of the self-hosted update server to republish on. Defaults to the origin of updates.url from your Expo config',
|
|
31
|
+
required: false,
|
|
32
|
+
}),
|
|
28
33
|
};
|
|
29
34
|
sanitizeFlags(flags) {
|
|
30
35
|
return {
|
|
31
36
|
branch: flags.branch,
|
|
32
37
|
platform: flags.platform,
|
|
38
|
+
customServerUrl: flags.serverUrl,
|
|
33
39
|
};
|
|
34
40
|
}
|
|
35
41
|
async run() {
|
|
@@ -39,7 +45,7 @@ class Publish extends core_1.Command {
|
|
|
39
45
|
process.exit(1);
|
|
40
46
|
}
|
|
41
47
|
const { flags } = await this.parse(Publish);
|
|
42
|
-
const { branch, platform } = this.sanitizeFlags(flags);
|
|
48
|
+
const { branch, platform, customServerUrl } = this.sanitizeFlags(flags);
|
|
43
49
|
if (!branch) {
|
|
44
50
|
log_1.default.error('Branch name is required');
|
|
45
51
|
process.exit(1);
|
|
@@ -50,7 +56,6 @@ class Publish extends core_1.Command {
|
|
|
50
56
|
}
|
|
51
57
|
const vcsClient = (0, vcs_1.resolveVcsClient)(true);
|
|
52
58
|
await vcsClient.ensureRepoExistsAsync();
|
|
53
|
-
// const commitHash = await vcsClient.getCommitHashAsync();
|
|
54
59
|
const projectDir = process.cwd();
|
|
55
60
|
const hasExpo = (0, package_1.isExpoInstalled)(projectDir);
|
|
56
61
|
if (!hasExpo) {
|
|
@@ -60,39 +65,24 @@ class Publish extends core_1.Command {
|
|
|
60
65
|
const privateConfig = await (0, expoConfig_1.getPrivateExpoConfigAsync)(projectDir, {
|
|
61
66
|
env: process.env,
|
|
62
67
|
});
|
|
63
|
-
const
|
|
64
|
-
|
|
65
|
-
log_1.default.error("Update url is not setup in your config. Please run 'eoas init' to setup the update url");
|
|
68
|
+
const baseUrl = await (0, expoConfig_1.resolveServerUrl)(privateConfig, customServerUrl).catch(e => {
|
|
69
|
+
log_1.default.error(e.message);
|
|
66
70
|
process.exit(1);
|
|
67
|
-
}
|
|
71
|
+
});
|
|
68
72
|
const appId = (0, expoConfig_1.requireExpoAppId)(privateConfig);
|
|
69
|
-
let
|
|
73
|
+
let runtimeVersions;
|
|
70
74
|
try {
|
|
71
|
-
|
|
72
|
-
baseUrl = parsedUrl.origin;
|
|
75
|
+
runtimeVersions = await (0, serverUpdates_1.fetchRuntimeVersions)({ baseUrl, appId, branch, credentials });
|
|
73
76
|
}
|
|
74
77
|
catch (e) {
|
|
75
|
-
log_1.default.error(
|
|
76
|
-
process.exit(1);
|
|
77
|
-
}
|
|
78
|
-
const runtimeVersionsEndpoint = `${baseUrl}/api/apps/${appId}/branch/${branch}/runtimeVersions`;
|
|
79
|
-
const response = await (0, fetch_1.fetchWithRetries)(runtimeVersionsEndpoint, {
|
|
80
|
-
headers: {
|
|
81
|
-
...(0, auth_1.getAuthHeaders)(credentials),
|
|
82
|
-
'use-cli-auth': 'true',
|
|
83
|
-
},
|
|
84
|
-
});
|
|
85
|
-
if (!response.ok) {
|
|
86
|
-
log_1.default.error(`Failed to fetch runtime versions: ${await response.text()}`);
|
|
78
|
+
log_1.default.error(e instanceof Error ? e.message : e);
|
|
87
79
|
process.exit(1);
|
|
88
80
|
}
|
|
89
|
-
const runtimeVersions = (await response.json());
|
|
90
81
|
const filteredRuntimeVersions = runtimeVersions.filter(runtimeVersion => runtimeVersion.numberOfUpdates > 1);
|
|
91
82
|
if (filteredRuntimeVersions.length === 0) {
|
|
92
83
|
log_1.default.error('No runtime versions found');
|
|
93
84
|
process.exit(1);
|
|
94
85
|
}
|
|
95
|
-
// Ask the user to select a runtime version
|
|
96
86
|
const selectedRuntimeVersion = await (0, prompts_1.promptAsync)({
|
|
97
87
|
type: 'select',
|
|
98
88
|
name: 'runtimeVersion',
|
|
@@ -103,21 +93,86 @@ class Publish extends core_1.Command {
|
|
|
103
93
|
})),
|
|
104
94
|
});
|
|
105
95
|
log_1.default.log(`Selected runtime version: ${selectedRuntimeVersion.runtimeVersion}`);
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
96
|
+
let allUpdates;
|
|
97
|
+
try {
|
|
98
|
+
allUpdates = await (0, serverUpdates_1.fetchUpdates)({
|
|
99
|
+
baseUrl,
|
|
100
|
+
appId,
|
|
101
|
+
branch,
|
|
102
|
+
runtimeVersion: selectedRuntimeVersion.runtimeVersion,
|
|
103
|
+
credentials,
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
catch (e) {
|
|
107
|
+
log_1.default.error(e instanceof Error ? e.message : e);
|
|
115
108
|
process.exit(1);
|
|
116
109
|
}
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
});
|
|
110
|
+
// Rollback markers have no files to republish.
|
|
111
|
+
const updates = allUpdates.filter(u => u.updateUUID !== 'Rollback to embedded');
|
|
120
112
|
if (updates.length === 0) {
|
|
113
|
+
log_1.default.error(`No republishable updates found for runtime version ${selectedRuntimeVersion.runtimeVersion}.`);
|
|
114
|
+
process.exit(1);
|
|
115
|
+
}
|
|
116
|
+
const { groups } = (0, serverUpdates_1.groupPublishedUpdates)(updates);
|
|
117
|
+
// Offer the group mode only when there is something to group and the user
|
|
118
|
+
// did not already narrow the run to one platform.
|
|
119
|
+
let mode = 'single';
|
|
120
|
+
if (platform === 'all' && groups.length > 0) {
|
|
121
|
+
const selectedMode = await (0, prompts_1.promptAsync)({
|
|
122
|
+
type: 'select',
|
|
123
|
+
name: 'mode',
|
|
124
|
+
message: 'What do you want to republish?',
|
|
125
|
+
choices: [
|
|
126
|
+
{
|
|
127
|
+
title: 'A full publish (all its platforms together)',
|
|
128
|
+
description: 'Only for servers in control plane mode',
|
|
129
|
+
value: 'group',
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
title: 'A single platform update',
|
|
133
|
+
description: 'Pick one iOS or Android update',
|
|
134
|
+
value: 'single',
|
|
135
|
+
},
|
|
136
|
+
],
|
|
137
|
+
});
|
|
138
|
+
mode = selectedMode.mode;
|
|
139
|
+
}
|
|
140
|
+
if (mode === 'group') {
|
|
141
|
+
const selectedGroup = await (0, prompts_1.promptAsync)({
|
|
142
|
+
type: 'select',
|
|
143
|
+
name: 'group',
|
|
144
|
+
message: 'Select a publish to republish',
|
|
145
|
+
choices: groups.map(group => ({
|
|
146
|
+
...(0, serverUpdates_1.describePublishGroup)(group),
|
|
147
|
+
value: group,
|
|
148
|
+
})),
|
|
149
|
+
});
|
|
150
|
+
const group = selectedGroup.group;
|
|
151
|
+
const republishUrl = new URL(`${baseUrl}/${appId}/republish/${branch}`);
|
|
152
|
+
republishUrl.searchParams.set('runtimeVersion', selectedRuntimeVersion.runtimeVersion);
|
|
153
|
+
republishUrl.searchParams.set('publishGroup', group.publishGroup);
|
|
154
|
+
const republishSpinner = (0, ora_1.ora)(`🔄 Republishing ${group.platforms.join(' + ')} updates...`).start();
|
|
155
|
+
const response = await (0, fetch_1.fetchWithRetries)(republishUrl.toString(), {
|
|
156
|
+
method: 'POST',
|
|
157
|
+
headers: {
|
|
158
|
+
...(0, auth_1.getAuthHeaders)(credentials),
|
|
159
|
+
'use-cli-auth': 'true',
|
|
160
|
+
'Content-Type': 'application/json',
|
|
161
|
+
},
|
|
162
|
+
});
|
|
163
|
+
if (!response.ok) {
|
|
164
|
+
republishSpinner.fail('❌ Republish failed');
|
|
165
|
+
log_1.default.error(`Failed to republish publish group: ${await response.text()}`);
|
|
166
|
+
process.exit(1);
|
|
167
|
+
}
|
|
168
|
+
const result = (await response.json());
|
|
169
|
+
republishSpinner.succeed(result.publishGroup
|
|
170
|
+
? `✅ Republished ${group.platforms.join(' + ')} as publish group ${result.publishGroup}`
|
|
171
|
+
: `✅ Republished ${group.platforms.join(' + ')}`);
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
const platformUpdates = updates.filter(u => platform === 'all' || u.platform === platform);
|
|
175
|
+
if (platformUpdates.length === 0) {
|
|
121
176
|
log_1.default.error(`No republishable updates found for runtime version ${selectedRuntimeVersion.runtimeVersion} on platform ${platform}.`);
|
|
122
177
|
process.exit(1);
|
|
123
178
|
}
|
|
@@ -125,7 +180,7 @@ class Publish extends core_1.Command {
|
|
|
125
180
|
type: 'select',
|
|
126
181
|
name: 'update',
|
|
127
182
|
message: 'Select an update to republish',
|
|
128
|
-
choices:
|
|
183
|
+
choices: platformUpdates.map(update => ({
|
|
129
184
|
title: update.updateUUID,
|
|
130
185
|
value: update,
|
|
131
186
|
description: `Created at: ${update.createdAt}, Platform: ${update.platform}, Commit hash: ${update.commitHash}`,
|
|
@@ -137,7 +192,7 @@ class Publish extends core_1.Command {
|
|
|
137
192
|
republishUrl.searchParams.set('runtimeVersion', selectedRuntimeVersion.runtimeVersion);
|
|
138
193
|
republishUrl.searchParams.set('updateId', selectedUpdated.update.updateId);
|
|
139
194
|
republishUrl.searchParams.set('commitHash', selectedUpdated.update.commitHash);
|
|
140
|
-
const republishSpinner = (0, ora_1.
|
|
195
|
+
const republishSpinner = (0, ora_1.ora)('🔄 Republishing update...').start();
|
|
141
196
|
const republishResponse = await (0, fetch_1.fetchWithRetries)(republishUrl.toString(), {
|
|
142
197
|
method: 'POST',
|
|
143
198
|
headers: {
|
|
@@ -6,6 +6,7 @@ export default class Publish extends Command {
|
|
|
6
6
|
static flags: {
|
|
7
7
|
platform: import("@oclif/core/lib/interfaces").OptionFlag<string, import("@oclif/core/lib/interfaces").CustomOptions>;
|
|
8
8
|
branch: import("@oclif/core/lib/interfaces").OptionFlag<string, import("@oclif/core/lib/interfaces").CustomOptions>;
|
|
9
|
+
serverUrl: import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
|
|
9
10
|
nonInteractive: import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
|
|
10
11
|
};
|
|
11
12
|
private sanitizeFlags;
|
|
@@ -28,6 +28,10 @@ class Publish extends core_1.Command {
|
|
|
28
28
|
description: 'Name of the branch to point to',
|
|
29
29
|
required: true,
|
|
30
30
|
}),
|
|
31
|
+
serverUrl: core_1.Flags.string({
|
|
32
|
+
description: 'URL of the self-hosted update server to roll back on. Defaults to the origin of updates.url from your Expo config',
|
|
33
|
+
required: false,
|
|
34
|
+
}),
|
|
31
35
|
nonInteractive: core_1.Flags.boolean({
|
|
32
36
|
description: 'Run command in non-interactive mode',
|
|
33
37
|
default: false,
|
|
@@ -37,6 +41,7 @@ class Publish extends core_1.Command {
|
|
|
37
41
|
return {
|
|
38
42
|
platform: flags.platform,
|
|
39
43
|
branch: flags.branch,
|
|
44
|
+
customServerUrl: flags.serverUrl,
|
|
40
45
|
nonInteractive: flags.nonInteractive,
|
|
41
46
|
};
|
|
42
47
|
}
|
|
@@ -47,7 +52,7 @@ class Publish extends core_1.Command {
|
|
|
47
52
|
process.exit(1);
|
|
48
53
|
}
|
|
49
54
|
const { flags } = await this.parse(Publish);
|
|
50
|
-
const { platform, branch, nonInteractive } = this.sanitizeFlags(flags);
|
|
55
|
+
const { platform, branch, customServerUrl, nonInteractive } = this.sanitizeFlags(flags);
|
|
51
56
|
if (!branch) {
|
|
52
57
|
log_1.default.error('Branch name is required');
|
|
53
58
|
process.exit(1);
|
|
@@ -79,21 +84,11 @@ class Publish extends core_1.Command {
|
|
|
79
84
|
log_1.default.error('When using disableAntiBrickingMeasures, expo-updates is ignoring the embeded update of the app, please use republish command instead');
|
|
80
85
|
process.exit(1);
|
|
81
86
|
}
|
|
82
|
-
const
|
|
83
|
-
|
|
84
|
-
log_1.default.error("Update url is not setup in your config. Please run 'eoas init' to setup the update url");
|
|
87
|
+
const baseUrl = await (0, expoConfig_1.resolveServerUrl)(privateConfig, customServerUrl).catch(e => {
|
|
88
|
+
log_1.default.error(e.message);
|
|
85
89
|
process.exit(1);
|
|
86
|
-
}
|
|
90
|
+
});
|
|
87
91
|
const appId = (0, expoConfig_1.requireExpoAppId)(privateConfig);
|
|
88
|
-
let baseUrl;
|
|
89
|
-
try {
|
|
90
|
-
const parsedUrl = new URL(updateUrl);
|
|
91
|
-
baseUrl = parsedUrl.origin;
|
|
92
|
-
}
|
|
93
|
-
catch (e) {
|
|
94
|
-
log_1.default.error('Invalid URL', e);
|
|
95
|
-
process.exit(1);
|
|
96
|
-
}
|
|
97
92
|
const runtimeSpinner = (0, ora_1.ora)('🔄 Resolving runtime version...').start();
|
|
98
93
|
const runtimeVersions = [
|
|
99
94
|
...(!platform || platform === expoConfig_1.RequestedPlatform.All || platform === expoConfig_1.RequestedPlatform.Ios
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { Command } from '@oclif/core';
|
|
2
|
+
import { type Deployment } from '../../lib/serverConfig/choices';
|
|
3
|
+
/** A master key found in the file the wizard is about to replace. `unreadable`
|
|
4
|
+
* distinguishes "the file has no key" from "the file could not be parsed". */
|
|
5
|
+
type ExistingMasterKey = {
|
|
6
|
+
key?: string;
|
|
7
|
+
unreadable: boolean;
|
|
8
|
+
};
|
|
9
|
+
export declare function readExistingMasterKey(deployment: Deployment): Promise<ExistingMasterKey>;
|
|
10
|
+
export default class ServerInit extends Command {
|
|
11
|
+
static args: {};
|
|
12
|
+
static description: string;
|
|
13
|
+
static examples: string[];
|
|
14
|
+
static flags: {};
|
|
15
|
+
run(): Promise<void>;
|
|
16
|
+
}
|
|
17
|
+
export {};
|