@scalebun-release/cli 2.5.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/bin/scalebun.js +2 -0
- package/lib/api/client.d.ts +52 -0
- package/lib/api/client.d.ts.map +1 -0
- package/lib/api/client.js +335 -0
- package/lib/api/client.js.map +1 -0
- package/lib/commands/ci-init.d.ts +3 -0
- package/lib/commands/ci-init.d.ts.map +1 -0
- package/lib/commands/ci-init.js +348 -0
- package/lib/commands/ci-init.js.map +1 -0
- package/lib/commands/doctor.d.ts +31 -0
- package/lib/commands/doctor.d.ts.map +1 -0
- package/lib/commands/doctor.js +559 -0
- package/lib/commands/doctor.js.map +1 -0
- package/lib/commands/launch.d.ts +3 -0
- package/lib/commands/launch.d.ts.map +1 -0
- package/lib/commands/launch.js +23 -0
- package/lib/commands/launch.js.map +1 -0
- package/lib/commands/link.d.ts +16 -0
- package/lib/commands/link.d.ts.map +1 -0
- package/lib/commands/link.js +164 -0
- package/lib/commands/link.js.map +1 -0
- package/lib/commands/login.d.ts +12 -0
- package/lib/commands/login.d.ts.map +1 -0
- package/lib/commands/login.js +124 -0
- package/lib/commands/login.js.map +1 -0
- package/lib/commands/migrate-codepush.d.ts +13 -0
- package/lib/commands/migrate-codepush.d.ts.map +1 -0
- package/lib/commands/migrate-codepush.js +98 -0
- package/lib/commands/migrate-codepush.js.map +1 -0
- package/lib/commands/ota-bundle.d.ts +22 -0
- package/lib/commands/ota-bundle.d.ts.map +1 -0
- package/lib/commands/ota-bundle.js +253 -0
- package/lib/commands/ota-bundle.js.map +1 -0
- package/lib/commands/ota-channels.d.ts +3 -0
- package/lib/commands/ota-channels.d.ts.map +1 -0
- package/lib/commands/ota-channels.js +231 -0
- package/lib/commands/ota-channels.js.map +1 -0
- package/lib/commands/ota-delete-bundle.d.ts +16 -0
- package/lib/commands/ota-delete-bundle.d.ts.map +1 -0
- package/lib/commands/ota-delete-bundle.js +96 -0
- package/lib/commands/ota-delete-bundle.js.map +1 -0
- package/lib/commands/ota-inspect.d.ts +3 -0
- package/lib/commands/ota-inspect.d.ts.map +1 -0
- package/lib/commands/ota-inspect.js +258 -0
- package/lib/commands/ota-inspect.js.map +1 -0
- package/lib/commands/ota-keys.d.ts +91 -0
- package/lib/commands/ota-keys.d.ts.map +1 -0
- package/lib/commands/ota-keys.js +765 -0
- package/lib/commands/ota-keys.js.map +1 -0
- package/lib/commands/ota-promote.d.ts +3 -0
- package/lib/commands/ota-promote.d.ts.map +1 -0
- package/lib/commands/ota-promote.js +194 -0
- package/lib/commands/ota-promote.js.map +1 -0
- package/lib/commands/ota-publish.d.ts +26 -0
- package/lib/commands/ota-publish.d.ts.map +1 -0
- package/lib/commands/ota-publish.js +892 -0
- package/lib/commands/ota-publish.js.map +1 -0
- package/lib/commands/ota-rollout.d.ts +3 -0
- package/lib/commands/ota-rollout.d.ts.map +1 -0
- package/lib/commands/ota-rollout.js +265 -0
- package/lib/commands/ota-rollout.js.map +1 -0
- package/lib/commands/ota-sign.d.ts +19 -0
- package/lib/commands/ota-sign.d.ts.map +1 -0
- package/lib/commands/ota-sign.js +245 -0
- package/lib/commands/ota-sign.js.map +1 -0
- package/lib/commands/push-apns.d.ts +3 -0
- package/lib/commands/push-apns.d.ts.map +1 -0
- package/lib/commands/push-apns.js +166 -0
- package/lib/commands/push-apns.js.map +1 -0
- package/lib/commands/quickstart.d.ts +4 -0
- package/lib/commands/quickstart.d.ts.map +1 -0
- package/lib/commands/quickstart.js +672 -0
- package/lib/commands/quickstart.js.map +1 -0
- package/lib/commands/shell.d.ts +28 -0
- package/lib/commands/shell.d.ts.map +1 -0
- package/lib/commands/shell.js +512 -0
- package/lib/commands/shell.js.map +1 -0
- package/lib/commands/tls-pins.d.ts +27 -0
- package/lib/commands/tls-pins.d.ts.map +1 -0
- package/lib/commands/tls-pins.js +209 -0
- package/lib/commands/tls-pins.js.map +1 -0
- package/lib/index.d.ts +2 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +233 -0
- package/lib/index.js.map +1 -0
- package/lib/ui/banner.d.ts +23 -0
- package/lib/ui/banner.d.ts.map +1 -0
- package/lib/ui/banner.js +162 -0
- package/lib/ui/banner.js.map +1 -0
- package/lib/ui/graphics.d.ts +18 -0
- package/lib/ui/graphics.d.ts.map +1 -0
- package/lib/ui/graphics.js +134 -0
- package/lib/ui/graphics.js.map +1 -0
- package/lib/ui/mark.d.ts +4 -0
- package/lib/ui/mark.d.ts.map +1 -0
- package/lib/ui/mark.js +21 -0
- package/lib/ui/mark.js.map +1 -0
- package/lib/ui/menu.d.ts +109 -0
- package/lib/ui/menu.d.ts.map +1 -0
- package/lib/ui/menu.js +314 -0
- package/lib/ui/menu.js.map +1 -0
- package/lib/ui/sixel.d.ts +6 -0
- package/lib/ui/sixel.d.ts.map +1 -0
- package/lib/ui/sixel.js +9 -0
- package/lib/ui/sixel.js.map +1 -0
- package/lib/utils/apps.d.ts +29 -0
- package/lib/utils/apps.d.ts.map +1 -0
- package/lib/utils/apps.js +19 -0
- package/lib/utils/apps.js.map +1 -0
- package/lib/utils/browser.d.ts +9 -0
- package/lib/utils/browser.d.ts.map +1 -0
- package/lib/utils/browser.js +39 -0
- package/lib/utils/browser.js.map +1 -0
- package/lib/utils/bundle.d.ts +153 -0
- package/lib/utils/bundle.d.ts.map +1 -0
- package/lib/utils/bundle.js +459 -0
- package/lib/utils/bundle.js.map +1 -0
- package/lib/utils/bundleStore.d.ts +128 -0
- package/lib/utils/bundleStore.d.ts.map +1 -0
- package/lib/utils/bundleStore.js +250 -0
- package/lib/utils/bundleStore.js.map +1 -0
- package/lib/utils/context.d.ts +88 -0
- package/lib/utils/context.d.ts.map +1 -0
- package/lib/utils/context.js +247 -0
- package/lib/utils/context.js.map +1 -0
- package/lib/utils/credentials.d.ts +59 -0
- package/lib/utils/credentials.d.ts.map +1 -0
- package/lib/utils/credentials.js +147 -0
- package/lib/utils/credentials.js.map +1 -0
- package/lib/utils/guide.d.ts +63 -0
- package/lib/utils/guide.d.ts.map +1 -0
- package/lib/utils/guide.js +191 -0
- package/lib/utils/guide.js.map +1 -0
- package/lib/utils/hash.d.ts +3 -0
- package/lib/utils/hash.d.ts.map +1 -0
- package/lib/utils/hash.js +44 -0
- package/lib/utils/hash.js.map +1 -0
- package/lib/utils/native-id.d.ts +17 -0
- package/lib/utils/native-id.d.ts.map +1 -0
- package/lib/utils/native-id.js +105 -0
- package/lib/utils/native-id.js.map +1 -0
- package/lib/utils/output.d.ts +96 -0
- package/lib/utils/output.d.ts.map +1 -0
- package/lib/utils/output.js +135 -0
- package/lib/utils/output.js.map +1 -0
- package/lib/utils/prompt.d.ts +106 -0
- package/lib/utils/prompt.d.ts.map +1 -0
- package/lib/utils/prompt.js +457 -0
- package/lib/utils/prompt.js.map +1 -0
- package/lib/utils/provenance.d.ts +30 -0
- package/lib/utils/provenance.d.ts.map +1 -0
- package/lib/utils/provenance.js +157 -0
- package/lib/utils/provenance.js.map +1 -0
- package/lib/utils/resolve.d.ts +81 -0
- package/lib/utils/resolve.d.ts.map +1 -0
- package/lib/utils/resolve.js +139 -0
- package/lib/utils/resolve.js.map +1 -0
- package/lib/utils/sdkBin.d.ts +18 -0
- package/lib/utils/sdkBin.d.ts.map +1 -0
- package/lib/utils/sdkBin.js +76 -0
- package/lib/utils/sdkBin.js.map +1 -0
- package/lib/utils/signer.d.ts +94 -0
- package/lib/utils/signer.d.ts.map +1 -0
- package/lib/utils/signer.js +230 -0
- package/lib/utils/signer.js.map +1 -0
- package/lib/utils/signing.d.ts +102 -0
- package/lib/utils/signing.d.ts.map +1 -0
- package/lib/utils/signing.js +229 -0
- package/lib/utils/signing.js.map +1 -0
- package/lib/utils/transportSecurity.d.ts +36 -0
- package/lib/utils/transportSecurity.d.ts.map +1 -0
- package/lib/utils/transportSecurity.js +74 -0
- package/lib/utils/transportSecurity.js.map +1 -0
- package/lib/utils/trustAnchor.d.ts +109 -0
- package/lib/utils/trustAnchor.d.ts.map +1 -0
- package/lib/utils/trustAnchor.js +169 -0
- package/lib/utils/trustAnchor.js.map +1 -0
- package/lib/utils/ui.d.ts +12 -0
- package/lib/utils/ui.d.ts.map +1 -0
- package/lib/utils/ui.js +60 -0
- package/lib/utils/ui.js.map +1 -0
- package/lib/utils/validate.d.ts +62 -0
- package/lib/utils/validate.d.ts.map +1 -0
- package/lib/utils/validate.js +189 -0
- package/lib/utils/validate.js.map +1 -0
- package/lib/vendor/ota-protocol/base64.d.ts +11 -0
- package/lib/vendor/ota-protocol/base64.d.ts.map +1 -0
- package/lib/vendor/ota-protocol/base64.js +39 -0
- package/lib/vendor/ota-protocol/base64.js.map +1 -0
- package/lib/vendor/ota-protocol/codec.d.ts +26 -0
- package/lib/vendor/ota-protocol/codec.d.ts.map +1 -0
- package/lib/vendor/ota-protocol/codec.js +349 -0
- package/lib/vendor/ota-protocol/codec.js.map +1 -0
- package/lib/vendor/ota-protocol/hex.d.ts +8 -0
- package/lib/vendor/ota-protocol/hex.d.ts.map +1 -0
- package/lib/vendor/ota-protocol/hex.js +31 -0
- package/lib/vendor/ota-protocol/hex.js.map +1 -0
- package/lib/vendor/ota-protocol/index.d.ts +17 -0
- package/lib/vendor/ota-protocol/index.d.ts.map +1 -0
- package/lib/vendor/ota-protocol/index.js +41 -0
- package/lib/vendor/ota-protocol/index.js.map +1 -0
- package/lib/vendor/ota-protocol/limits.d.ts +39 -0
- package/lib/vendor/ota-protocol/limits.d.ts.map +1 -0
- package/lib/vendor/ota-protocol/limits.js +42 -0
- package/lib/vendor/ota-protocol/limits.js.map +1 -0
- package/lib/vendor/ota-protocol/types.d.ts +73 -0
- package/lib/vendor/ota-protocol/types.d.ts.map +1 -0
- package/lib/vendor/ota-protocol/types.js +24 -0
- package/lib/vendor/ota-protocol/types.js.map +1 -0
- package/package.json +37 -0
|
@@ -0,0 +1,892 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
36
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
|
+
};
|
|
38
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
+
exports.parseRolloutPercent = parseRolloutPercent;
|
|
40
|
+
exports.registerOtaPublishCommand = registerOtaPublishCommand;
|
|
41
|
+
const fs = __importStar(require("fs"));
|
|
42
|
+
const path = __importStar(require("path"));
|
|
43
|
+
const crypto = __importStar(require("crypto"));
|
|
44
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
45
|
+
const credentials_1 = require("../utils/credentials");
|
|
46
|
+
const client_1 = require("../api/client");
|
|
47
|
+
const validate_1 = require("../utils/validate");
|
|
48
|
+
const resolve_1 = require("../utils/resolve");
|
|
49
|
+
const context_1 = require("../utils/context");
|
|
50
|
+
const output_1 = require("../utils/output");
|
|
51
|
+
const bundle_1 = require("../utils/bundle");
|
|
52
|
+
const hash_1 = require("../utils/hash");
|
|
53
|
+
const native_id_1 = require("../utils/native-id");
|
|
54
|
+
const provenance_1 = require("../utils/provenance");
|
|
55
|
+
const ota_keys_1 = require("./ota-keys");
|
|
56
|
+
const signing_1 = require("../utils/signing");
|
|
57
|
+
const signer_1 = require("../utils/signer");
|
|
58
|
+
const apps_1 = require("../utils/apps");
|
|
59
|
+
const prompt_1 = require("../utils/prompt");
|
|
60
|
+
const browser_1 = require("../utils/browser");
|
|
61
|
+
const login_1 = require("./login");
|
|
62
|
+
const DASHBOARD_OTA_URL = 'https://dash.scalebun.com/ota/publish';
|
|
63
|
+
/**
|
|
64
|
+
* `scalebun ota publish` — the flagship CLI command.
|
|
65
|
+
*
|
|
66
|
+
* Flow:
|
|
67
|
+
* 1. Build the RN JS bundle (npx react-native bundle, Hermes default)
|
|
68
|
+
* 2. Compute SHA-256 of the output
|
|
69
|
+
* 3. POST /saas/apps/:appId/ota/bundles/presign → presigned PUT URL
|
|
70
|
+
* 4. PUT the bytes directly to storage (R2/S3)
|
|
71
|
+
* 5. POST /saas/apps/:appId/ota/bundles/:id/finalize → version assigned
|
|
72
|
+
* 6. POST /saas/apps/:appId/ota/bundles/:id/promote → configure installMode & rollout
|
|
73
|
+
* 7. Print success
|
|
74
|
+
*/
|
|
75
|
+
/**
|
|
76
|
+
* Parse `--rollout`, refusing anything that isn't 0-100.
|
|
77
|
+
*
|
|
78
|
+
* This was `parseInt(x, 10) || 100`, which turned BOTH `--rollout 0` and any
|
|
79
|
+
* typo into a 100% rollout — the opposite of what the operator asked for. A
|
|
80
|
+
* dark release (0%) is a documented, legitimate way to promote a bundle without
|
|
81
|
+
* exposing it, so silently inverting it exposes every production device to an
|
|
82
|
+
* untested bundle. Exits rather than guessing.
|
|
83
|
+
*/
|
|
84
|
+
function parseRolloutPercent(raw) {
|
|
85
|
+
// Delegates so this flag, the promote flag, and the interactive prompt all
|
|
86
|
+
// accept the same things — including a trailing `%`, which Number() reads as
|
|
87
|
+
// NaN and which is the obvious thing to type for a percentage.
|
|
88
|
+
return (0, validate_1.checkedPercent)('--rollout', raw, 100);
|
|
89
|
+
}
|
|
90
|
+
function registerOtaPublishCommand(ota) {
|
|
91
|
+
ota
|
|
92
|
+
.command('publish')
|
|
93
|
+
.description('Bundle, hash, upload, and finalize an OTA update release')
|
|
94
|
+
// Not requiredOption: when it (and other flags) are missing in a TTY, the
|
|
95
|
+
// interactive wizard fills them in. Non-interactive runs still require it.
|
|
96
|
+
.option('--app-id <id>', 'Scalebun application ID')
|
|
97
|
+
.option('--channel <nameOrId>', 'OTA channel name (e.g. default, beta, production) or channel ID', 'default')
|
|
98
|
+
.option('--channel-id <id>', 'OTA channel ID (legacy alias)')
|
|
99
|
+
.option('--platform <platform>', 'Target platform: android | ios', 'android')
|
|
100
|
+
.option('--install-mode <mode>', 'IMMEDIATE (SDK restarts the app right after install; also marked mandatory) | ' +
|
|
101
|
+
'ON_NEXT_RESTART (default: activates on next cold boot) | ' +
|
|
102
|
+
'ON_NEXT_RESUME (SDK restarts when the app next returns to the foreground)', 'ON_NEXT_RESTART')
|
|
103
|
+
.option('--target-version <semver>', 'Target app native semver range (e.g. ">=1.0.0", "1.0.0")', '>=1.0.0')
|
|
104
|
+
.option('--rollout <percent>', 'Target rollout percentage (1-100)', '100')
|
|
105
|
+
.option('--entry-file <path>', 'JS entry file', 'index.js')
|
|
106
|
+
.option('--release-note <text>', 'Release note for dashboard display and device changelog')
|
|
107
|
+
.option('--hermes', 'Compile to Hermes bytecode (the default — flag kept for compatibility)')
|
|
108
|
+
.option('--hermes-disabled', 'Ship plain JS instead of Hermes bytecode (~20-30% slower cold start)')
|
|
109
|
+
.option('--allow-plain-js', 'Publish uncompiled JS if hermesc fails, instead of aborting')
|
|
110
|
+
.option('--key-dir <dir>', 'Directory holding the RSA signing key (default: ./.scalebun)')
|
|
111
|
+
.option('--key-id <id>', 'Signing key id (defaults to $SCALEBUN_KEY_ID or the local ./.scalebun key id)')
|
|
112
|
+
.option('--passphrase <pass>', 'Passphrase for an encrypted signing key (defaults to $SCALEBUN_SIGNING_KEY_PASSPHRASE)')
|
|
113
|
+
.option('--require-signing', 'Fail if the release cannot be signed (also via $SCALEBUN_REQUIRE_SIGNING). Prevents silently shipping an unsigned release a signature-enforcing device would reject.')
|
|
114
|
+
.option('--skip-bundle', 'Skip react-native bundle (provide --bundle-path instead)')
|
|
115
|
+
.option('--bundle-path <path>', 'Path to a pre-built bundle file')
|
|
116
|
+
.option('--custom-bundle-path <path>', 'Alias for --bundle-path (explicit naming)')
|
|
117
|
+
.option('--sourcemap', 'Generate and upload sourcemap for crash symbolication')
|
|
118
|
+
.option('--keep-artifacts', 'Keep build artifacts (bundle + sourcemap) after upload')
|
|
119
|
+
.option('--skip-promote', 'Upload and finalize only — do not make the release live')
|
|
120
|
+
.option('--yes', 'Skip the interactive wizard\'s final confirmation')
|
|
121
|
+
.action(async (opts, cmd) => {
|
|
122
|
+
// Interactive wizard: in a terminal with no --app-id (and no pre-built
|
|
123
|
+
// bundle), guide the user through the flags. The Dashboard and Cancel
|
|
124
|
+
// paths exit inside the wizard; the CLI path fills `opts` and returns.
|
|
125
|
+
if (process.stdout.isTTY &&
|
|
126
|
+
!opts.appId &&
|
|
127
|
+
!opts.skipBundle &&
|
|
128
|
+
!opts.bundlePath &&
|
|
129
|
+
!opts.customBundlePath) {
|
|
130
|
+
await runPublishWizard(opts, cmd);
|
|
131
|
+
}
|
|
132
|
+
// publish was the ONE command that never adopted the resolver. It
|
|
133
|
+
// demanded --app-id even with a scalebun.json sitting beside it, so the
|
|
134
|
+
// flagship command was the only one that failed on a correctly linked
|
|
135
|
+
// project. The wizard above still fills opts.appId when it runs; this
|
|
136
|
+
// covers every other path, including CI.
|
|
137
|
+
opts.appId = await (0, resolve_1.resolveAppId)({ appId: opts.appId, nonInteractive: opts.yes });
|
|
138
|
+
// commander cannot tell "--channel default" from the option's own
|
|
139
|
+
// default, so ask it: only a value that came from the CLI outranks the
|
|
140
|
+
// project's configured channel.
|
|
141
|
+
const explicitChannel = cmd?.getOptionValueSource?.('channel') === 'cli' ? opts.channel : undefined;
|
|
142
|
+
const creds = (0, credentials_1.readCredentials)();
|
|
143
|
+
if (!creds) {
|
|
144
|
+
console.error('❌ Not logged in. Run `scalebun login` first.');
|
|
145
|
+
process.exit(1);
|
|
146
|
+
}
|
|
147
|
+
const installMode = (0, validate_1.checkedEnum)('--install-mode', opts.installMode || 'ON_NEXT_RESTART', ['IMMEDIATE', 'ON_NEXT_RESTART', 'ON_NEXT_RESUME'], { caseInsensitive: true });
|
|
148
|
+
// Validate BEFORE building: a bad --rollout should cost a second, not a
|
|
149
|
+
// full Hermes compile.
|
|
150
|
+
const rolloutPercent = parseRolloutPercent(opts.rollout);
|
|
151
|
+
// The channel default was a hardcoded 'default' while `scalebun link`
|
|
152
|
+
// had already written the project's real channel to scalebun.json.
|
|
153
|
+
// Publishing on a linked project therefore aimed at a channel that did
|
|
154
|
+
// not exist — and said so only after a full Hermes build.
|
|
155
|
+
// explicit flag > legacy --channel-id > project config > 'default'.
|
|
156
|
+
const projectChannel = (0, context_1.getContext)({ appId: opts.appId }).channel;
|
|
157
|
+
const targetChannel = opts.channelId || explicitChannel || projectChannel || 'default';
|
|
158
|
+
const client = new client_1.ApiClient(creds);
|
|
159
|
+
// Was a bare cast, so `--platform windows` reached the bundler and
|
|
160
|
+
// failed later with an error about a missing entry file.
|
|
161
|
+
const platform = (0, validate_1.checkedEnum)('--platform', opts.platform, ['android', 'ios'], {
|
|
162
|
+
caseInsensitive: true,
|
|
163
|
+
});
|
|
164
|
+
// Fail closed BEFORE the expensive bundle: if this release must be signed
|
|
165
|
+
// (--require-signing / SCALEBUN_REQUIRE_SIGNING / a configured key id) but
|
|
166
|
+
// no signing key can be resolved, stop now rather than after a full Hermes
|
|
167
|
+
// compile. A signature-enforcing device rejects an unsigned release, so
|
|
168
|
+
// shipping unsigned here would be a broken deploy.
|
|
169
|
+
{
|
|
170
|
+
const early = (0, ota_keys_1.resolveSigningMaterial)({
|
|
171
|
+
keyDir: opts.keyDir,
|
|
172
|
+
keyId: opts.keyId,
|
|
173
|
+
passphrase: opts.passphrase,
|
|
174
|
+
});
|
|
175
|
+
if ((0, ota_keys_1.signingRequired)({ requireSigning: opts.requireSigning }, early) &&
|
|
176
|
+
!early.privatePem) {
|
|
177
|
+
(0, output_1.fail)('Signing is required for this release, but no signing key is available.', [
|
|
178
|
+
'Provide the key via --key-dir, the SCALEBUN_SIGNING_PRIVATE_KEY secret,',
|
|
179
|
+
'or ./.scalebun/ota-signing-key.pem.',
|
|
180
|
+
'To publish unsigned (SHA-256 only), drop --require-signing / --key-id and',
|
|
181
|
+
'unset SCALEBUN_REQUIRE_SIGNING / SCALEBUN_KEY_ID.',
|
|
182
|
+
], output_1.ExitCode.Usage);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
// Check the channel EXISTS before building anything.
|
|
186
|
+
//
|
|
187
|
+
// The presign call is what rejects an unknown channel, and it happens
|
|
188
|
+
// after Metro and Hermes have run — so a name that was never going to
|
|
189
|
+
// work cost a full 50-second compile before saying so. Every flag in
|
|
190
|
+
// this command already validates before the network; the channel is the
|
|
191
|
+
// one piece of state that needs a round trip to check, and one cheap GET
|
|
192
|
+
// is worth far less than the build it saves.
|
|
193
|
+
//
|
|
194
|
+
// Best-effort: a listing that fails for any other reason must not block
|
|
195
|
+
// a publish that would have succeeded, so this only ever fires on a
|
|
196
|
+
// definite "that channel is not there".
|
|
197
|
+
//
|
|
198
|
+
// The try/catch wraps ONLY the network listing. The abort below is
|
|
199
|
+
// deliberately OUTSIDE it: inside the interactive shell `fail()` ->
|
|
200
|
+
// process.exit is replaced by a throw (so one command can't kill the
|
|
201
|
+
// session), and a `fail()` raised inside this catch was swallowed as if
|
|
202
|
+
// it were a listing error — the guard printed "no bundle was compiled"
|
|
203
|
+
// and then built anyway. Deciding after the try keeps the abort real.
|
|
204
|
+
let knownChannels = null;
|
|
205
|
+
try {
|
|
206
|
+
const known = await client.get(`/saas/apps/${opts.appId}/ota/channels`);
|
|
207
|
+
knownChannels = Array.isArray(known) ? known : [];
|
|
208
|
+
}
|
|
209
|
+
catch {
|
|
210
|
+
// Listing failed for some other reason. Let the publish proceed and
|
|
211
|
+
// let presign be the authority, exactly as before.
|
|
212
|
+
knownChannels = null;
|
|
213
|
+
}
|
|
214
|
+
if (knownChannels && knownChannels.length) {
|
|
215
|
+
const hit = knownChannels.find((c) => c.id === targetChannel || c.name.toLowerCase() === targetChannel.toLowerCase());
|
|
216
|
+
if (!hit) {
|
|
217
|
+
(0, output_1.fail)(`No OTA channel named "${targetChannel}" on this app.`, [
|
|
218
|
+
`Existing: ${knownChannels.map((c) => c.name).join(', ')}`,
|
|
219
|
+
`Pass --channel <name>, or create it: scalebun ota channels create --name ${targetChannel}`,
|
|
220
|
+
'Checked before building — no bundle was compiled.',
|
|
221
|
+
], output_1.ExitCode.Usage);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
// Runtime binding. `targetAppVersion` gates on a marketing string; these
|
|
225
|
+
// are the actual contract between the bundle and the binary that runs
|
|
226
|
+
// it. The backend withholds a bundle from a device whose runtime does
|
|
227
|
+
// not match — see runtime-compat.ts.
|
|
228
|
+
const rnVersion = (0, bundle_1.detectProjectRnVersion)();
|
|
229
|
+
// Generated here, before the build, so it can be compiled into the JS.
|
|
230
|
+
// The SDK reads it back at boot to prove the bundle it installed is the
|
|
231
|
+
// one actually executing.
|
|
232
|
+
const identityToken = crypto.randomBytes(16).toString('hex');
|
|
233
|
+
// Sourcemap outcome tracking — the publish summary must report what
|
|
234
|
+
// actually happened, not what was requested.
|
|
235
|
+
let sourcemapStored = false;
|
|
236
|
+
let sourcemapRegistered = false;
|
|
237
|
+
// Step 1: Build the bundle (or use pre-built)
|
|
238
|
+
let bundlePath;
|
|
239
|
+
let hermesBytecodeVersion = null;
|
|
240
|
+
let bundleCarriesMarker = false;
|
|
241
|
+
// The `--assets-dest` directory from buildBundle, walked into the asset
|
|
242
|
+
// manifest. Undefined for a pre-built bundle (no assets to enumerate).
|
|
243
|
+
let assetsDir;
|
|
244
|
+
if (opts.skipBundle && (opts.bundlePath || opts.customBundlePath)) {
|
|
245
|
+
bundlePath = opts.bundlePath || opts.customBundlePath;
|
|
246
|
+
if (!fs.existsSync(bundlePath)) {
|
|
247
|
+
console.error(`❌ Bundle file not found: ${bundlePath}`);
|
|
248
|
+
process.exit(1);
|
|
249
|
+
}
|
|
250
|
+
console.log(`📦 Using pre-built bundle: ${bundlePath}`);
|
|
251
|
+
// A pre-built artifact is taken as-is: injecting a marker would mean
|
|
252
|
+
// rewriting someone else's build output, and if it is already Hermes
|
|
253
|
+
// bytecode a text prefix would corrupt it outright.
|
|
254
|
+
hermesBytecodeVersion = (0, bundle_1.readHermesBytecodeVersion)(bundlePath);
|
|
255
|
+
console.log(' ℹ️ Pre-built bundle — no identity marker injected. Install verification ' +
|
|
256
|
+
'will be unavailable for this release.');
|
|
257
|
+
}
|
|
258
|
+
else {
|
|
259
|
+
// Hermes is the DEFAULT. The docs have always said "compiled to Hermes
|
|
260
|
+
// bytecode by default (--hermes)" while the flag was actually opt-in —
|
|
261
|
+
// so every publish that didn't know the flag shipped plain JS with a
|
|
262
|
+
// slower cold start and no HBC version for the runtime gate to check.
|
|
263
|
+
// Making the code match the docs is the right direction: bytecode is
|
|
264
|
+
// what production wants, and `--hermes-disabled` remains the opt-out.
|
|
265
|
+
const useHermes = opts.hermesDisabled ? false : true;
|
|
266
|
+
console.log(`📦 Building ${platform} bundle${useHermes ? ' (Hermes bytecode)' : ''}…`);
|
|
267
|
+
const result = (0, bundle_1.buildBundle)({
|
|
268
|
+
platform,
|
|
269
|
+
entryFile: opts.entryFile,
|
|
270
|
+
dev: false,
|
|
271
|
+
hermes: useHermes,
|
|
272
|
+
allowPlainJs: opts.allowPlainJs ?? false,
|
|
273
|
+
sourcemap: opts.sourcemap,
|
|
274
|
+
identityToken,
|
|
275
|
+
});
|
|
276
|
+
bundlePath = result.bundlePath;
|
|
277
|
+
hermesBytecodeVersion = result.hermesBytecodeVersion;
|
|
278
|
+
bundleCarriesMarker = true;
|
|
279
|
+
assetsDir = result.assetsPath;
|
|
280
|
+
console.log(` ✅ Bundle built: ${bundlePath}`);
|
|
281
|
+
// ASSETS DO NOT SHIP OVER OTA.
|
|
282
|
+
//
|
|
283
|
+
// An update is one artifact: the JS/bytecode bundle. Images and fonts
|
|
284
|
+
// are resolved from what the INSTALLED BINARY already contains, so an
|
|
285
|
+
// asset added since the last store build is referenced by the new JS
|
|
286
|
+
// and is not on the device. The update installs cleanly, reports
|
|
287
|
+
// success, and renders a missing image — the worst kind of failure.
|
|
288
|
+
// Metro writes those files next to the bundle and the publish flow
|
|
289
|
+
// has always discarded them without a word. Now it says so.
|
|
290
|
+
if (result.assets.length) {
|
|
291
|
+
const shown = result.assets.slice(0, 5);
|
|
292
|
+
console.log(` ℹ️ This bundle references ${result.assets.length} asset(s). ` +
|
|
293
|
+
'OTA ships JS only — assets resolve from the installed binary.');
|
|
294
|
+
console.log(` ${shown.join(', ')}${result.assets.length > shown.length ? `, +${result.assets.length - shown.length} more` : ''}`);
|
|
295
|
+
console.log(' Any image or font ADDED since your last store build will be ' +
|
|
296
|
+
'missing on device. Changing which existing asset you use is fine; ' +
|
|
297
|
+
'a new one needs a native release (fonts always do).');
|
|
298
|
+
}
|
|
299
|
+
// You asked for bytecode and didn't get it: that is a failed build,
|
|
300
|
+
// not a warning. This degradation has shipped silently twice now —
|
|
301
|
+
// first via `npx hermesc` (not a real package), then via spawnSync's
|
|
302
|
+
// 1MB maxBuffer killing the compiler — and a plain-JS bundle both
|
|
303
|
+
// starts ~20-30% slower and carries no HBC version for the runtime
|
|
304
|
+
// gate to check. If plain JS is what you want, say so.
|
|
305
|
+
if (useHermes && !result.hermesCompiled) {
|
|
306
|
+
console.error('❌ --hermes was requested but the bundle is NOT Hermes bytecode (see the hermesc error above).\n' +
|
|
307
|
+
' To intentionally publish plain JS, use --hermes-disabled.');
|
|
308
|
+
process.exit(1);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
console.log(` Runtime: RN ${rnVersion ?? 'unknown'}` +
|
|
312
|
+
(hermesBytecodeVersion !== null
|
|
313
|
+
? `, Hermes HBC v${hermesBytecodeVersion}`
|
|
314
|
+
: ', plain JS'));
|
|
315
|
+
if (!rnVersion) {
|
|
316
|
+
console.warn(' ⚠️ React Native version could not be detected — this bundle will not be ' +
|
|
317
|
+
'runtime-gated, and can reach binaries that cannot run it.');
|
|
318
|
+
}
|
|
319
|
+
// The sourcemap must be located BEFORE presign, because sourcemaps are
|
|
320
|
+
// uploaded through a presigned URL requested alongside the bundle's.
|
|
321
|
+
const sourcemapPath = opts.sourcemap && fs.existsSync(bundlePath + '.map')
|
|
322
|
+
? bundlePath + '.map'
|
|
323
|
+
: null;
|
|
324
|
+
if (opts.sourcemap && !sourcemapPath) {
|
|
325
|
+
console.warn(' ⚠️ --sourcemap was passed but no .map file was produced — continuing without one.');
|
|
326
|
+
}
|
|
327
|
+
// Step 2: Hash the bundle
|
|
328
|
+
console.log('🔒 Computing SHA-256…');
|
|
329
|
+
const sha256 = (0, hash_1.sha256File)(bundlePath);
|
|
330
|
+
const sizeBytes = fs.statSync(bundlePath).size;
|
|
331
|
+
console.log(` Hash: ${sha256}`);
|
|
332
|
+
console.log(` Size: ${(sizeBytes / 1024 / 1024).toFixed(2)} MB`);
|
|
333
|
+
if (sizeBytes > 50000000) {
|
|
334
|
+
console.error(`❌ Bundle is ${(sizeBytes / 1024 / 1024).toFixed(1)} MB — exceeds 50 MB limit`);
|
|
335
|
+
process.exit(1);
|
|
336
|
+
}
|
|
337
|
+
// Provenance (git + CI) and the asset manifest — best-effort metadata so
|
|
338
|
+
// the dashboard can show Git SHA / PR / CI build and per-asset size diffs.
|
|
339
|
+
// Both are optional on the backend; sending them changes nothing about
|
|
340
|
+
// delivery, only what the Bundles page can display.
|
|
341
|
+
const provenance = (0, provenance_1.collectProvenance)();
|
|
342
|
+
const assetManifest = (0, provenance_1.collectAssetManifest)(bundlePath, sha256, sizeBytes, assetsDir);
|
|
343
|
+
if (provenance.gitSha) {
|
|
344
|
+
console.log(` Source: ${provenance.gitSha.slice(0, 7)}${provenance.gitBranch ? ` (${provenance.gitBranch})` : ''}` +
|
|
345
|
+
`${provenance.ciProvider ? ` · ${provenance.ciProvider}` : ''}`);
|
|
346
|
+
}
|
|
347
|
+
console.log(` Assets: ${assetManifest.length} (bundle + ${assetManifest.length - 1} files)`);
|
|
348
|
+
// The binary's native application id (Android applicationId / iOS bundle
|
|
349
|
+
// id). Signed releases are identified by it and the device verifies it
|
|
350
|
+
// against its own package name. Sending it lets the server adopt it as the
|
|
351
|
+
// app's bundleId when that field is still empty (the common case), so the
|
|
352
|
+
// first signed publish just works instead of failing with an empty
|
|
353
|
+
// applicationId.
|
|
354
|
+
const nativeApplicationId = (0, native_id_1.detectNativeApplicationId)(platform);
|
|
355
|
+
if (nativeApplicationId) {
|
|
356
|
+
console.log(` Native app id: ${nativeApplicationId}`);
|
|
357
|
+
}
|
|
358
|
+
// Step 3: Get presigned upload URL
|
|
359
|
+
console.log('🔗 Requesting presigned upload URL…');
|
|
360
|
+
let presigned;
|
|
361
|
+
try {
|
|
362
|
+
presigned = await client.post(`/saas/apps/${opts.appId}/ota/bundles/presign`, {
|
|
363
|
+
channelId: targetChannel,
|
|
364
|
+
platform,
|
|
365
|
+
nativeApplicationId,
|
|
366
|
+
sha256,
|
|
367
|
+
sizeBytes,
|
|
368
|
+
releaseNote: opts.releaseNote,
|
|
369
|
+
rnVersion: rnVersion ?? undefined,
|
|
370
|
+
hermes: hermesBytecodeVersion !== null,
|
|
371
|
+
hermesBytecodeVersion: hermesBytecodeVersion ?? undefined,
|
|
372
|
+
identityToken: bundleCarriesMarker ? identityToken : undefined,
|
|
373
|
+
// Provenance + asset manifest — optional; the backend stores them
|
|
374
|
+
// for the dashboard and ignores them if omitted.
|
|
375
|
+
...provenance,
|
|
376
|
+
assets: assetManifest,
|
|
377
|
+
// Sourcemaps ride the presign flow: declaring them here returns a
|
|
378
|
+
// second presigned PUT URL. This CLI used to skip these fields and
|
|
379
|
+
// instead POST base64 JSON to /bundles/:id/sourcemap after promote
|
|
380
|
+
// — an endpoint that DOES NOT EXIST and never has. The error was
|
|
381
|
+
// caught and printed as a "non-fatal" warning, so every
|
|
382
|
+
// `--sourcemap` publish ever made silently uploaded nothing while
|
|
383
|
+
// the summary implied symbolication would work.
|
|
384
|
+
...(sourcemapPath
|
|
385
|
+
? {
|
|
386
|
+
sourcemapSha256: (0, hash_1.sha256File)(sourcemapPath),
|
|
387
|
+
sourcemapSizeBytes: fs.statSync(sourcemapPath).size,
|
|
388
|
+
}
|
|
389
|
+
: {}),
|
|
390
|
+
},
|
|
391
|
+
// Presign is safe to retry: it never got an answer if the socket
|
|
392
|
+
// dropped, and a stray UPLOADING row is superseded by the retry. A
|
|
393
|
+
// single "socket hang up" must not sink an already-built bundle.
|
|
394
|
+
{ retry: true });
|
|
395
|
+
}
|
|
396
|
+
catch (err) {
|
|
397
|
+
console.error(`❌ Presign failed: ${err.message}`);
|
|
398
|
+
process.exit(1);
|
|
399
|
+
}
|
|
400
|
+
console.log(` Bundle ID: ${presigned.bundleId}`);
|
|
401
|
+
console.log(` Upload URL expires in ${presigned.expiresIn}s`);
|
|
402
|
+
// Step 4: Upload to R2/S3
|
|
403
|
+
console.log('⬆️ Uploading bundle to storage…');
|
|
404
|
+
const bundleData = fs.readFileSync(bundlePath);
|
|
405
|
+
try {
|
|
406
|
+
await client.putBytes(presigned.uploadUrl, bundleData, 'application/octet-stream');
|
|
407
|
+
}
|
|
408
|
+
catch (err) {
|
|
409
|
+
console.error(`❌ Upload failed: ${err.message}`);
|
|
410
|
+
process.exit(1);
|
|
411
|
+
}
|
|
412
|
+
console.log(' ✅ Upload complete');
|
|
413
|
+
// Step 5: Finalize — server-side SHA-256 verification + version assignment
|
|
414
|
+
console.log('🔍 Finalizing (server-side hash verification)…');
|
|
415
|
+
let finalized;
|
|
416
|
+
try {
|
|
417
|
+
// Sign under SB-OTA-RSA-SHA256-V1 with the locally-held RSA private key,
|
|
418
|
+
// if this project has adopted signing. The key never leaves this machine:
|
|
419
|
+
// we build the binary signed-release payload from the identity the server
|
|
420
|
+
// RESERVED at presign (releaseId/bundleId/releaseSequence/project/app) plus
|
|
421
|
+
// the artifact's own size + SHA-256, RSA-sign it, self-verify, and upload
|
|
422
|
+
// only { signedPayload, signature }. ScaleBun serves a signature it could
|
|
423
|
+
// not itself have produced. Unsigned publishes stay valid; the device only
|
|
424
|
+
// enforces once the app embeds a trusted public key.
|
|
425
|
+
const signing = (0, ota_keys_1.resolveSigningMaterial)({
|
|
426
|
+
keyDir: opts.keyDir,
|
|
427
|
+
keyId: opts.keyId,
|
|
428
|
+
passphrase: opts.passphrase,
|
|
429
|
+
});
|
|
430
|
+
let finalizeBody = undefined;
|
|
431
|
+
// Fail closed: a release that must be signed but has no key must not
|
|
432
|
+
// silently ship unsigned — a signature-enforcing device rejects it, so
|
|
433
|
+
// "unsigned" here is a broken deploy, not a lenient fallback.
|
|
434
|
+
if ((0, ota_keys_1.signingRequired)({ requireSigning: opts.requireSigning }, signing) && !signing.privatePem) {
|
|
435
|
+
(0, output_1.fail)('Signing is required for this release, but no signing key is available.', [
|
|
436
|
+
'Provide the key via --key-dir, the SCALEBUN_SIGNING_PRIVATE_KEY secret,',
|
|
437
|
+
'or ./.scalebun/ota-signing-key.pem.',
|
|
438
|
+
'To publish unsigned (SHA-256 only), drop --require-signing / --key-id and',
|
|
439
|
+
'unset SCALEBUN_REQUIRE_SIGNING / SCALEBUN_KEY_ID.',
|
|
440
|
+
], output_1.ExitCode.Usage);
|
|
441
|
+
}
|
|
442
|
+
if (signing.privatePem) {
|
|
443
|
+
const keyId = signing.keyId;
|
|
444
|
+
if (!keyId) {
|
|
445
|
+
(0, output_1.fail)('A signing key is present but its key id is unknown.', [
|
|
446
|
+
'Pass --key-id <id> or set SCALEBUN_KEY_ID, or regenerate with',
|
|
447
|
+
'`scalebun ota keys generate` which writes the key id alongside the key.',
|
|
448
|
+
], output_1.ExitCode.Usage);
|
|
449
|
+
}
|
|
450
|
+
// Distinguish the two failure modes so the message points at the
|
|
451
|
+
// real cause instead of always blaming the backend version.
|
|
452
|
+
if (!presigned.applicationId) {
|
|
453
|
+
// The app has no bundleId, and the server could not adopt one.
|
|
454
|
+
(0, output_1.fail)("This app has no bundleId (native application id), so a signed release can't be identified.", nativeApplicationId
|
|
455
|
+
? [
|
|
456
|
+
`The CLI detected '${nativeApplicationId}' and sent it, but the backend did not`,
|
|
457
|
+
'adopt it — the deployed backend predates bundleId self-heal. Set the app',
|
|
458
|
+
"bundleId in the dashboard (app settings), then re-run publish.",
|
|
459
|
+
]
|
|
460
|
+
: [
|
|
461
|
+
'Could not read the native applicationId from this project (looked for',
|
|
462
|
+
'android/app/build.gradle applicationId / the iOS bundle identifier).',
|
|
463
|
+
'Set the app bundleId in the dashboard (app settings), or run publish from',
|
|
464
|
+
'the app root so the id can be detected, then re-run.',
|
|
465
|
+
], output_1.ExitCode.Rejected);
|
|
466
|
+
}
|
|
467
|
+
if (!presigned.projectId ||
|
|
468
|
+
!presigned.releaseId ||
|
|
469
|
+
presigned.releaseSequence === undefined ||
|
|
470
|
+
presigned.releaseSequence === null) {
|
|
471
|
+
(0, output_1.fail)('This ScaleBun backend did not return SB-OTA-RSA-SHA256-V1 release identity.', [
|
|
472
|
+
'The RSA signing protocol needs the server to reserve releaseId + releaseSequence',
|
|
473
|
+
'at presign. Update the ScaleBun backend, or publish without a local signing key',
|
|
474
|
+
'to ship on SHA-256 integrity only.',
|
|
475
|
+
], output_1.ExitCode.Rejected);
|
|
476
|
+
}
|
|
477
|
+
const payload = {
|
|
478
|
+
signatureAlgorithm: 'SB-OTA-RSA-SHA256-V1',
|
|
479
|
+
hashAlgorithm: 'SHA-256',
|
|
480
|
+
keyId: keyId,
|
|
481
|
+
projectId: presigned.projectId,
|
|
482
|
+
applicationId: presigned.applicationId,
|
|
483
|
+
releaseId: presigned.releaseId,
|
|
484
|
+
bundleId: presigned.bundleId,
|
|
485
|
+
platform,
|
|
486
|
+
artifactType: 'full',
|
|
487
|
+
targetAppVersion: opts.targetVersion || '>=1.0.0',
|
|
488
|
+
targetRuntimeVersion: rnVersion ?? '',
|
|
489
|
+
minimumSdkVersion: presigned.minimumSdkVersion ?? '',
|
|
490
|
+
releaseSequence: presigned.releaseSequence,
|
|
491
|
+
issuedAtEpochSeconds: Math.floor(Date.now() / 1000),
|
|
492
|
+
expiresAtEpochSeconds: 0,
|
|
493
|
+
artifactSize: sizeBytes,
|
|
494
|
+
artifactSha256: sha256,
|
|
495
|
+
baseBundleId: '',
|
|
496
|
+
baseArtifactSha256: '',
|
|
497
|
+
// A full download IS the final bundle, so the installed result hash
|
|
498
|
+
// equals the downloaded artifact hash.
|
|
499
|
+
resultArtifactSha256: sha256,
|
|
500
|
+
};
|
|
501
|
+
// An external signer is used only when one is configured. With
|
|
502
|
+
// nothing set this is the same local-PEM path as before, byte for
|
|
503
|
+
// byte — the point of the change is that a customer who has not
|
|
504
|
+
// asked for an HSM cannot tell it exists.
|
|
505
|
+
const externalSigner = process.env.SCALEBUN_SIGNER_COMMAND?.trim();
|
|
506
|
+
const displayMetadata = {
|
|
507
|
+
releaseId: presigned.releaseId,
|
|
508
|
+
releaseNote: opts.releaseNote,
|
|
509
|
+
};
|
|
510
|
+
const envelope = externalSigner
|
|
511
|
+
? await (0, signing_1.buildReleaseEnvelopeWith)(payload, (0, signer_1.resolveSigner)({ privateKeyPem: signing.privatePem }), { displayMetadata })
|
|
512
|
+
: (0, signing_1.buildReleaseEnvelope)(payload, signing.privatePem, {
|
|
513
|
+
passphrase: signing.passphrase,
|
|
514
|
+
displayMetadata,
|
|
515
|
+
});
|
|
516
|
+
const via = externalSigner
|
|
517
|
+
? ' (external signer)'
|
|
518
|
+
: signing.privateSource === 'env'
|
|
519
|
+
? ' (key from SCALEBUN_SIGNING_PRIVATE_KEY)'
|
|
520
|
+
: signing.privateSource === 'flag'
|
|
521
|
+
? ' (key from --key-dir)'
|
|
522
|
+
: '';
|
|
523
|
+
console.log(` 🔏 Signed with RSA key (SB-OTA-RSA-SHA256-V1)${via}`);
|
|
524
|
+
finalizeBody = {
|
|
525
|
+
protocol: envelope.protocol,
|
|
526
|
+
keyId: envelope.keyId,
|
|
527
|
+
signedPayload: envelope.signedPayload,
|
|
528
|
+
signature: envelope.signature,
|
|
529
|
+
};
|
|
530
|
+
}
|
|
531
|
+
finalized = await client.post(`/saas/apps/${opts.appId}/ota/bundles/${presigned.bundleId}/finalize`, finalizeBody);
|
|
532
|
+
}
|
|
533
|
+
catch (err) {
|
|
534
|
+
console.error(`❌ Finalize failed: ${err.message}`);
|
|
535
|
+
process.exit(1);
|
|
536
|
+
}
|
|
537
|
+
// Step 6: Promote release to ACTIVE status with installMode
|
|
538
|
+
if (opts.skipPromote) {
|
|
539
|
+
console.log('⏭️ Skipping promotion (--skip-promote) — bundle is uploaded but NOT live.');
|
|
540
|
+
console.log(` Promote later from the dashboard, or re-run without --skip-promote.`);
|
|
541
|
+
}
|
|
542
|
+
else {
|
|
543
|
+
console.log('🚀 Promoting release to active target channel…');
|
|
544
|
+
try {
|
|
545
|
+
await client.post(`/saas/apps/${opts.appId}/ota/bundles/${presigned.bundleId}/promote`, {
|
|
546
|
+
targetAppVersion: opts.targetVersion || '>=1.0.0',
|
|
547
|
+
rolloutPercent,
|
|
548
|
+
isMandatory: installMode === 'IMMEDIATE',
|
|
549
|
+
installMode,
|
|
550
|
+
releaseNote: opts.releaseNote,
|
|
551
|
+
});
|
|
552
|
+
}
|
|
553
|
+
catch (err) {
|
|
554
|
+
// This used to warn and exit 0 — so a CI pipeline printed
|
|
555
|
+
// "published successfully" while no device would ever receive the
|
|
556
|
+
// update (promotion is what makes a bundle live). A pipeline must
|
|
557
|
+
// fail when its purpose failed.
|
|
558
|
+
console.error(`❌ Promotion failed: ${err.message}`);
|
|
559
|
+
console.error(` The bundle IS uploaded (v pending, id ${presigned.bundleId}) but is NOT live.\n` +
|
|
560
|
+
' Fix the error and promote from the dashboard, or publish again.\n' +
|
|
561
|
+
' To upload without promoting on purpose, use --skip-promote.');
|
|
562
|
+
process.exit(1);
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
// Step 7: Upload the sourcemap through its presigned URL.
|
|
566
|
+
// (The old code POSTed base64 JSON to /bundles/:id/sourcemap — a route
|
|
567
|
+
// that has never existed — and downgraded the 404 to a "non-fatal"
|
|
568
|
+
// warning, so symbolication silently never worked for OTA bundles.)
|
|
569
|
+
if (sourcemapPath && presigned.sourcemapUploadUrl) {
|
|
570
|
+
console.log('🗺️ Uploading sourcemap for crash symbolication…');
|
|
571
|
+
try {
|
|
572
|
+
await client.putBytes(presigned.sourcemapUploadUrl, fs.readFileSync(sourcemapPath), 'application/json');
|
|
573
|
+
console.log(' ✅ Sourcemap uploaded');
|
|
574
|
+
sourcemapStored = true;
|
|
575
|
+
// Cleanup happens AFTER symbolication registration below — the
|
|
576
|
+
// symbols upload reads this same file.
|
|
577
|
+
}
|
|
578
|
+
catch (err) {
|
|
579
|
+
console.warn(` ⚠️ Sourcemap upload failed: ${err.message}\n` +
|
|
580
|
+
' Stack traces from this bundle will not symbolicate. Re-publish to retry.');
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
else if (sourcemapPath && !presigned.sourcemapUploadUrl) {
|
|
584
|
+
console.warn(' ⚠️ Backend did not return a sourcemap upload URL — is it up to date?');
|
|
585
|
+
}
|
|
586
|
+
// D6: register the map with SYMBOLICATION, not just storage. The
|
|
587
|
+
// presigned PUT above lands the map in the OTA bundle bucket —
|
|
588
|
+
// `OtaBundle.sourcemapKey` — which NOTHING reads: the symbolication
|
|
589
|
+
// worker only consumes `symbolUpload` rows, written by
|
|
590
|
+
// POST /apps/:appId/symbols. Without this second upload, every
|
|
591
|
+
// `--sourcemap` publish paid to store a map no crash could ever use.
|
|
592
|
+
// That endpoint authenticates with the app's SDK SECRET key (a server
|
|
593
|
+
// credential, deliberately not the client key baked into the binary),
|
|
594
|
+
// so this step is opt-in via SCALEBUN_SDK_SECRET.
|
|
595
|
+
if (sourcemapPath && fs.existsSync(sourcemapPath)) {
|
|
596
|
+
const sdkSecret = process.env.SCALEBUN_SDK_SECRET;
|
|
597
|
+
if (!sdkSecret) {
|
|
598
|
+
console.warn(' ⚠️ SCALEBUN_SDK_SECRET not set — the sourcemap was stored but NOT ' +
|
|
599
|
+
'registered for crash symbolication. Set the app\'s SDK secret key ' +
|
|
600
|
+
'in that env var to enable symbolicated stack traces.');
|
|
601
|
+
}
|
|
602
|
+
else {
|
|
603
|
+
console.log('🔣 Registering sourcemap for crash symbolication…');
|
|
604
|
+
try {
|
|
605
|
+
const boundary = `----scalebun${Date.now().toString(16)}`;
|
|
606
|
+
const mapBytes = fs.readFileSync(sourcemapPath);
|
|
607
|
+
const field = (name, value) => `--${boundary}\r\nContent-Disposition: form-data; name="${name}"\r\n\r\n${value}\r\n`;
|
|
608
|
+
const head = Buffer.from(field('platform', 'js') +
|
|
609
|
+
(rnVersion ? field('version', opts.targetVersion ?? '') : '') +
|
|
610
|
+
field('buildUuid', sha256) +
|
|
611
|
+
`--${boundary}\r\nContent-Disposition: form-data; name="file"; filename="bundle.jsbundle.map"\r\nContent-Type: application/json\r\n\r\n`, 'utf-8');
|
|
612
|
+
const tail = Buffer.from(`\r\n--${boundary}--\r\n`, 'utf-8');
|
|
613
|
+
await client.postMultipart(`/apps/${opts.appId}/symbols`, Buffer.concat([head, mapBytes, tail]), `multipart/form-data; boundary=${boundary}`, { 'x-scalebun-secret-key': sdkSecret });
|
|
614
|
+
console.log(' ✅ Sourcemap registered — crashes from this bundle will symbolicate');
|
|
615
|
+
sourcemapRegistered = true;
|
|
616
|
+
}
|
|
617
|
+
catch (err) {
|
|
618
|
+
console.warn(` ⚠️ Symbolication registration failed: ${err.message}`);
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
if (!opts.keepArtifacts) {
|
|
622
|
+
try {
|
|
623
|
+
fs.unlinkSync(sourcemapPath);
|
|
624
|
+
}
|
|
625
|
+
catch {
|
|
626
|
+
/* non-fatal */
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
// Step 8: Print detailed success summary
|
|
631
|
+
const installModeLabel = installMode === 'IMMEDIATE'
|
|
632
|
+
? '⚡ IMMEDIATE (SDK restarts the app right after install)'
|
|
633
|
+
: installMode === 'ON_NEXT_RESUME'
|
|
634
|
+
? '🔄 ON_NEXT_RESUME (SDK restarts on next return to foreground)'
|
|
635
|
+
: '🌙 ON_NEXT_RESTART (activates silently on next launch)';
|
|
636
|
+
console.log('');
|
|
637
|
+
console.log(`✅ Bundle v${finalized.version} published successfully!`);
|
|
638
|
+
console.log(` Channel: ${finalized.channelName} (${finalized.environmentName})`);
|
|
639
|
+
console.log(` Platform: ${finalized.platform}`);
|
|
640
|
+
console.log(` Install Mode: ${installModeLabel}`);
|
|
641
|
+
console.log(` SHA-256: ${finalized.sha256}`);
|
|
642
|
+
console.log(` Size: ${(finalized.sizeBytes / 1024).toFixed(1)} KB`);
|
|
643
|
+
if (opts.sourcemap) {
|
|
644
|
+
// Report what actually happened. This line used to print success
|
|
645
|
+
// whenever --sourcemap was passed — even when no map was produced,
|
|
646
|
+
// the upload failed, or SCALEBUN_SDK_SECRET was unset so nothing was
|
|
647
|
+
// registered for symbolication. CI logs then certified symbolication
|
|
648
|
+
// that would not happen, and the first production crash arrived
|
|
649
|
+
// minified.
|
|
650
|
+
const smState = sourcemapRegistered
|
|
651
|
+
? 'Uploaded and registered ✓ (stack traces will symbolicate)'
|
|
652
|
+
: sourcemapStored
|
|
653
|
+
? 'Uploaded, NOT registered — set SCALEBUN_SDK_SECRET to symbolicate'
|
|
654
|
+
: 'NOT uploaded — stack traces from this bundle stay minified';
|
|
655
|
+
console.log(` Sourcemap: ${smState}`);
|
|
656
|
+
}
|
|
657
|
+
const rolloutNow = opts.skipPromote ? 0 : rolloutPercent;
|
|
658
|
+
// The --app-id on every line here was load-bearing before `link` and
|
|
659
|
+
// the resolver existed. It is now noise: the id resolves from
|
|
660
|
+
// scalebun.json, and a suggestion carrying a 25-character cuid reads as
|
|
661
|
+
// something you must paste rather than something you can just run.
|
|
662
|
+
(0, output_1.printNextSteps)([
|
|
663
|
+
{ label: 'See live state', argv: ['ota', 'status'] },
|
|
664
|
+
...(!opts.skipPromote && rolloutNow < 100
|
|
665
|
+
? [{ label: 'Widen the rollout', argv: ['ota', 'rollout', '--percent', '100'] }]
|
|
666
|
+
: []),
|
|
667
|
+
...(opts.skipPromote
|
|
668
|
+
? [{ label: 'Make it live', argv: ['ota', 'promote'] }]
|
|
669
|
+
: []),
|
|
670
|
+
{ label: 'Stop new deliveries', argv: ['ota', 'pause'] },
|
|
671
|
+
{ label: 'Revert every device', argv: ['ota', 'rollback'] },
|
|
672
|
+
], chalk_1.default.cyan);
|
|
673
|
+
if (!opts.skipPromote && rolloutNow === 100) {
|
|
674
|
+
console.log('');
|
|
675
|
+
console.log(' Tip: production releases usually start staged — `--rollout 10`, watch the');
|
|
676
|
+
console.log(' health page, then `ota rollout --percent 100` once it looks clean.');
|
|
677
|
+
}
|
|
678
|
+
// Clean up bundle unless --keep-artifacts or using pre-built
|
|
679
|
+
if (!opts.keepArtifacts && !opts.skipBundle) {
|
|
680
|
+
try {
|
|
681
|
+
fs.unlinkSync(bundlePath);
|
|
682
|
+
}
|
|
683
|
+
catch {
|
|
684
|
+
// non-fatal
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
});
|
|
688
|
+
}
|
|
689
|
+
/**
|
|
690
|
+
* Fill `opts` interactively. Picks the publish method first: the Dashboard and
|
|
691
|
+
* Cancel branches finish inside here (process.exit); the CLI branch mutates
|
|
692
|
+
* `opts` and returns so the normal publish flow runs.
|
|
693
|
+
*/
|
|
694
|
+
async function runPublishWizard(opts, cmd) {
|
|
695
|
+
console.log(chalk_1.default.bold.cyan('\n⚡ ScaleBun — publish an OTA update\n'));
|
|
696
|
+
const method = await (0, prompt_1.select)('How do you want to publish?', [
|
|
697
|
+
{ label: 'Publish now (CLI)', value: 'cli', hint: 'bundle, upload & promote this release' },
|
|
698
|
+
{ label: 'Build a bundle for the Dashboard', value: 'dashboard', hint: `drag & drop at ${DASHBOARD_OTA_URL}` },
|
|
699
|
+
{ label: 'Cancel', value: 'cancel' },
|
|
700
|
+
]);
|
|
701
|
+
if (method === 'cancel') {
|
|
702
|
+
console.log('Cancelled — nothing published.');
|
|
703
|
+
process.exit(0);
|
|
704
|
+
}
|
|
705
|
+
if (method === 'dashboard') {
|
|
706
|
+
await runDashboardBundleFlow(opts);
|
|
707
|
+
process.exit(0);
|
|
708
|
+
}
|
|
709
|
+
// ── CLI path ────────────────────────────────────────────────────────────────
|
|
710
|
+
let creds = (0, credentials_1.readCredentials)();
|
|
711
|
+
if (!creds) {
|
|
712
|
+
console.log('\nYou need to sign in first.\n');
|
|
713
|
+
await (0, login_1.runLogin)({});
|
|
714
|
+
creds = (0, credentials_1.readCredentials)();
|
|
715
|
+
if (!creds)
|
|
716
|
+
process.exit(1);
|
|
717
|
+
}
|
|
718
|
+
const client = new client_1.ApiClient(creds);
|
|
719
|
+
opts.appId = await pickApp(client);
|
|
720
|
+
opts.platform = await (0, prompt_1.select)('Platform', [
|
|
721
|
+
{ label: 'Android', value: 'android' },
|
|
722
|
+
{ label: 'iOS', value: 'ios' },
|
|
723
|
+
], { default: opts.platform === 'ios' ? 1 : 0 });
|
|
724
|
+
opts.channel = await pickChannel(client, opts.appId, opts.channel);
|
|
725
|
+
// The wizard fills opts by mutation, but the action decides whether to trust
|
|
726
|
+
// opts.channel by asking commander where the value came from
|
|
727
|
+
// (getOptionValueSource === 'cli'). A bare `ota publish` leaves that source at
|
|
728
|
+
// 'default', so a wizard-picked channel was silently dropped in favour of
|
|
729
|
+
// scalebun.json's channel. Register the pick through commander so the single
|
|
730
|
+
// resolution path sees it as explicit — every other wizard answer is read off
|
|
731
|
+
// opts directly, only the channel is gated on the source.
|
|
732
|
+
cmd?.setOptionValueWithSource('channel', opts.channel, 'cli');
|
|
733
|
+
opts.installMode = await (0, prompt_1.select)('Install mode', [
|
|
734
|
+
{ label: 'ON_NEXT_RESTART', value: 'ON_NEXT_RESTART', hint: 'applies on the next cold launch (default)' },
|
|
735
|
+
{ label: 'ON_NEXT_RESUME', value: 'ON_NEXT_RESUME', hint: 'applies when the app next returns to the foreground' },
|
|
736
|
+
{ label: 'IMMEDIATE', value: 'IMMEDIATE', hint: 'reloads the app right away — for hotfixes' },
|
|
737
|
+
]);
|
|
738
|
+
opts.rollout = String(await (0, prompt_1.number)('Rollout %', { default: 100, min: 0, max: 100 }));
|
|
739
|
+
opts.targetVersion = await (0, prompt_1.text)('Target native app version range', {
|
|
740
|
+
default: opts.targetVersion || '>=1.0.0',
|
|
741
|
+
});
|
|
742
|
+
const note = await (0, prompt_1.text)('Release note (optional)', {
|
|
743
|
+
default: opts.releaseNote || '',
|
|
744
|
+
allowEmpty: true,
|
|
745
|
+
});
|
|
746
|
+
opts.releaseNote = note || undefined;
|
|
747
|
+
const useHermes = await (0, prompt_1.confirm)('Compile to Hermes bytecode? (smaller, faster cold start)', {
|
|
748
|
+
default: true,
|
|
749
|
+
});
|
|
750
|
+
if (useHermes)
|
|
751
|
+
opts.hermes = true;
|
|
752
|
+
else
|
|
753
|
+
opts.hermesDisabled = true;
|
|
754
|
+
if (await (0, prompt_1.confirm)('Configure advanced options?', { default: false })) {
|
|
755
|
+
opts.entryFile = await (0, prompt_1.text)('JS entry file', { default: opts.entryFile || 'index.js' });
|
|
756
|
+
opts.sourcemap = await (0, prompt_1.confirm)('Generate & upload a sourcemap (crash symbolication)?', {
|
|
757
|
+
default: !!opts.sourcemap,
|
|
758
|
+
});
|
|
759
|
+
opts.keepArtifacts = await (0, prompt_1.confirm)('Keep build artifacts after upload?', {
|
|
760
|
+
default: !!opts.keepArtifacts,
|
|
761
|
+
});
|
|
762
|
+
opts.skipPromote = await (0, prompt_1.confirm)('Upload only — do NOT make it live yet (skip promote)?', {
|
|
763
|
+
default: !!opts.skipPromote,
|
|
764
|
+
});
|
|
765
|
+
}
|
|
766
|
+
printReview(opts);
|
|
767
|
+
if (!opts.yes) {
|
|
768
|
+
// No default: the wizard has just collected every answer, and Enter
|
|
769
|
+
// meaning "throw it away" is the same trap as the promote confirmation.
|
|
770
|
+
const go = await (0, prompt_1.confirm)('Publish now?');
|
|
771
|
+
if (!go) {
|
|
772
|
+
(0, prompt_1.closePrompts)();
|
|
773
|
+
console.log('Aborted — nothing published.');
|
|
774
|
+
process.exit(0);
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
(0, prompt_1.closePrompts)();
|
|
778
|
+
console.log('');
|
|
779
|
+
}
|
|
780
|
+
/** App pick-list from the account, with a manual-entry fallback. */
|
|
781
|
+
async function pickApp(client) {
|
|
782
|
+
let entries;
|
|
783
|
+
try {
|
|
784
|
+
entries = await (0, apps_1.fetchAllApps)(client);
|
|
785
|
+
}
|
|
786
|
+
catch {
|
|
787
|
+
entries = [];
|
|
788
|
+
}
|
|
789
|
+
if (!entries.length) {
|
|
790
|
+
console.log(chalk_1.default.yellow(' Could not list your apps — enter the app id manually.'));
|
|
791
|
+
// Was: `if (!id) process.exit(1)` — a silent exit 1 from a prompt that had
|
|
792
|
+
// just asked for an app id, with nothing said about why it stopped.
|
|
793
|
+
return (0, prompt_1.text)('App id', {
|
|
794
|
+
help: 'Find it in the dashboard URL, or run `scalebun apps` from another terminal.',
|
|
795
|
+
validate: (v) => (/\s/.test(v) ? 'An app id has no spaces.' : null),
|
|
796
|
+
});
|
|
797
|
+
}
|
|
798
|
+
return (0, prompt_1.select)('App', entries.map((e) => ({
|
|
799
|
+
label: e.app.name,
|
|
800
|
+
value: e.app.id,
|
|
801
|
+
hint: `${e.app.id} · ${e.orgName}/${e.projectName}${e.app.platform ? ` · ${e.app.platform}` : ''}`,
|
|
802
|
+
})));
|
|
803
|
+
}
|
|
804
|
+
/** Channel pick-list for the app (falls back to typing a name). */
|
|
805
|
+
async function pickChannel(client, appId, current) {
|
|
806
|
+
let channels = [];
|
|
807
|
+
try {
|
|
808
|
+
channels = (await client.get(`/saas/apps/${appId}/ota/channels`)) ?? [];
|
|
809
|
+
}
|
|
810
|
+
catch {
|
|
811
|
+
channels = [];
|
|
812
|
+
}
|
|
813
|
+
const choices = channels.map((c) => ({
|
|
814
|
+
label: c.name,
|
|
815
|
+
value: c.name,
|
|
816
|
+
hint: c.environment?.name ? `env: ${c.environment.name}` : undefined,
|
|
817
|
+
}));
|
|
818
|
+
if (!choices.some((c) => c.value === 'default')) {
|
|
819
|
+
choices.unshift({ label: 'default', value: 'default', hint: 'created automatically on first publish' });
|
|
820
|
+
}
|
|
821
|
+
const OTHER = '__other__';
|
|
822
|
+
choices.push({ label: 'Other (type a name)…', value: OTHER, hint: undefined });
|
|
823
|
+
const defaultIdx = current ? Math.max(0, choices.findIndex((c) => c.value === current)) : 0;
|
|
824
|
+
const picked = await (0, prompt_1.select)('Channel', choices, { default: defaultIdx });
|
|
825
|
+
if (picked === OTHER)
|
|
826
|
+
return (await (0, prompt_1.text)('Channel name', { default: 'default' })) || 'default';
|
|
827
|
+
return picked;
|
|
828
|
+
}
|
|
829
|
+
/** The Dashboard path: build a standalone bundle and point the user at the dashboard. */
|
|
830
|
+
async function runDashboardBundleFlow(opts) {
|
|
831
|
+
const platform = await (0, prompt_1.select)('Platform', [
|
|
832
|
+
{ label: 'Android', value: 'android' },
|
|
833
|
+
{ label: 'iOS', value: 'ios' },
|
|
834
|
+
], { default: opts.platform === 'ios' ? 1 : 0 });
|
|
835
|
+
const useHermes = await (0, prompt_1.confirm)('Compile to Hermes bytecode? (smaller, faster cold start)', {
|
|
836
|
+
default: true,
|
|
837
|
+
});
|
|
838
|
+
const entryFile = await (0, prompt_1.text)('JS entry file', { default: opts.entryFile || 'index.js' });
|
|
839
|
+
const sourcemap = await (0, prompt_1.confirm)('Generate a sourcemap alongside the bundle?', { default: false });
|
|
840
|
+
// Release stdin BEFORE spawning `react-native bundle`. buildBundle runs Metro
|
|
841
|
+
// with stdio:'inherit'; on Windows an inherited child sharing a still-open
|
|
842
|
+
// readline on the console gets a corrupt stdin handle and is killed (exit code
|
|
843
|
+
// null). The CLI-publish path already closes prompts before building — do the
|
|
844
|
+
// same here.
|
|
845
|
+
(0, prompt_1.closePrompts)();
|
|
846
|
+
console.log(`\n📦 Building ${platform} bundle${useHermes ? ' (Hermes bytecode)' : ''}…`);
|
|
847
|
+
const result = (0, bundle_1.buildBundle)({
|
|
848
|
+
platform,
|
|
849
|
+
entryFile,
|
|
850
|
+
dev: false,
|
|
851
|
+
hermes: useHermes,
|
|
852
|
+
allowPlainJs: false,
|
|
853
|
+
sourcemap,
|
|
854
|
+
});
|
|
855
|
+
const outPath = path.resolve(`./index.${platform}.bundle`);
|
|
856
|
+
fs.copyFileSync(result.bundlePath, outPath);
|
|
857
|
+
const sizeMb = (fs.statSync(outPath).size / (1024 * 1024)).toFixed(2);
|
|
858
|
+
console.log(`\n✅ Bundle created: ${chalk_1.default.cyan(outPath)} (${sizeMb} MB)`);
|
|
859
|
+
console.log(chalk_1.default.dim(` (equivalent: scalebun ota bundle --platform ${platform} ${useHermes ? '--hermes' : '--hermes-disabled'}${sourcemap ? ' --sourcemap' : ''})`));
|
|
860
|
+
console.log(`\n👉 Drag & drop it into the Scalebun Dashboard: ${chalk_1.default.cyan(DASHBOARD_OTA_URL)}`);
|
|
861
|
+
const open = await (0, prompt_1.confirm)('Open the dashboard now?', { default: true });
|
|
862
|
+
(0, prompt_1.closePrompts)();
|
|
863
|
+
if (open)
|
|
864
|
+
(0, browser_1.openUrl)(DASHBOARD_OTA_URL);
|
|
865
|
+
}
|
|
866
|
+
/** Show the equivalent command before the irreversible publish. */
|
|
867
|
+
function printReview(opts) {
|
|
868
|
+
const parts = [
|
|
869
|
+
'scalebun ota publish',
|
|
870
|
+
`--app-id ${opts.appId}`,
|
|
871
|
+
`--platform ${opts.platform}`,
|
|
872
|
+
`--channel ${opts.channel}`,
|
|
873
|
+
`--install-mode ${opts.installMode}`,
|
|
874
|
+
`--rollout ${opts.rollout}`,
|
|
875
|
+
`--target-version "${opts.targetVersion}"`,
|
|
876
|
+
];
|
|
877
|
+
if (opts.releaseNote)
|
|
878
|
+
parts.push(`--release-note "${opts.releaseNote}"`);
|
|
879
|
+
parts.push(opts.hermesDisabled ? '--hermes-disabled' : '--hermes');
|
|
880
|
+
if (opts.sourcemap)
|
|
881
|
+
parts.push('--sourcemap');
|
|
882
|
+
if (opts.keepArtifacts)
|
|
883
|
+
parts.push('--keep-artifacts');
|
|
884
|
+
if (opts.skipPromote)
|
|
885
|
+
parts.push('--skip-promote');
|
|
886
|
+
if (opts.entryFile && opts.entryFile !== 'index.js')
|
|
887
|
+
parts.push(`--entry-file ${opts.entryFile}`);
|
|
888
|
+
console.log('\n' + chalk_1.default.bold('Review'));
|
|
889
|
+
console.log(' ' + chalk_1.default.green(parts.join(' \\\n ')));
|
|
890
|
+
console.log('');
|
|
891
|
+
}
|
|
892
|
+
//# sourceMappingURL=ota-publish.js.map
|