@mondaydotcomorg/z2h-cli 0.21.1 → 0.21.2
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/dist/commands/deploy.d.ts +0 -1
- package/dist/commands/deploy.d.ts.map +1 -1
- package/dist/commands/deploy.js +9 -2
- package/dist/esm/commands/deploy.d.ts +0 -1
- package/dist/esm/commands/deploy.d.ts.map +1 -1
- package/dist/esm/commands/deploy.mjs +10 -3
- package/dist/esm/index.mjs +2 -6
- package/dist/esm/shadow/prepare.d.ts +8 -1
- package/dist/esm/shadow/prepare.d.ts.map +1 -1
- package/dist/esm/shadow/prepare.mjs +15 -7
- package/dist/esm/types.d.ts +1 -0
- package/dist/esm/types.d.ts.map +1 -1
- package/dist/index.js +2 -6
- package/dist/shadow/prepare.d.ts +8 -1
- package/dist/shadow/prepare.d.ts.map +1 -1
- package/dist/shadow/prepare.js +15 -6
- package/dist/types.d.ts +1 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/commands/__tests__/deploy.test.ts +38 -27
- package/src/commands/deploy.ts +12 -7
- package/src/index.ts +3 -8
- package/src/shadow/prepare.ts +18 -6
- package/src/types.ts +1 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"deploy.d.ts","sourceRoot":"","sources":["../../src/commands/deploy.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"deploy.d.ts","sourceRoot":"","sources":["../../src/commands/deploy.ts"],"names":[],"mappings":"AAkBA,OAAO,EAAE,KAAK,UAAU,EAA2C,MAAM,4BAA4B,CAAC;AAWtG,MAAM,WAAW,aAAa;IAC5B,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,MAAM,CAAC,EAAE,UAAU,CAAC;IACpB,OAAO,CAAC,EAAE,MAAM,CAAC;IAEjB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAWD,wBAAsB,aAAa,CAAC,IAAI,GAAE,aAAkB,GAAG,OAAO,CAAC,IAAI,CAAC,CAoB3E"}
|
package/dist/commands/deploy.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
|
|
2
2
|
|
|
3
3
|
const os = require('node:os');
|
|
4
|
-
const crypto = require('node:crypto');
|
|
5
4
|
const fs = require('fs-extra');
|
|
6
5
|
const util_logger = require('../util/logger.js');
|
|
7
6
|
const shadow_paths = require('../shadow/paths.js');
|
|
@@ -18,6 +17,7 @@ const util_s3_client = require('../util/s3/client.js');
|
|
|
18
17
|
const util_machineIdentity = require('../util/machine-identity.js');
|
|
19
18
|
const util_broker_app = require('../util/broker/app.js');
|
|
20
19
|
const util_openBrowser = require('../util/open-browser.js');
|
|
20
|
+
const shadow_prepare = require('../shadow/prepare.js');
|
|
21
21
|
const commands_build = require('./build.js');
|
|
22
22
|
const commands_tag = require('./tag.js');
|
|
23
23
|
|
|
@@ -63,7 +63,8 @@ async function buildAndStamp(opts) {
|
|
|
63
63
|
};
|
|
64
64
|
}
|
|
65
65
|
async function deployPreview({ paths, consumerName, appName, token, opts }) {
|
|
66
|
-
|
|
66
|
+
await fs.ensureDir(paths.shadowDir);
|
|
67
|
+
const { previewId } = await shadow_prepare.resolvePortAndMetadata(paths, util_appName.cleanAppName(consumerName));
|
|
67
68
|
const previewSlug = util_appName.buildPreviewSlug(consumerName, previewId);
|
|
68
69
|
const previewAppName = util_appName.mfAppName(previewSlug);
|
|
69
70
|
const keyPrefix = `${constants.S3_PREVIEWS_FOLDER}/${previewAppName}/MF`;
|
|
@@ -73,10 +74,12 @@ async function deployPreview({ paths, consumerName, appName, token, opts }) {
|
|
|
73
74
|
const nextVersion = (await util_auth_brokerCredentialProvider.fetchDeployVersion(previewAppName)) + 1;
|
|
74
75
|
util_logger.info('building');
|
|
75
76
|
const entry = await buildAndStamp({ paths, publicUrl: url, nextVersion, gitRemote: '' });
|
|
77
|
+
util_logger.info('getting sts credentials');
|
|
76
78
|
const creds = await util_auth_brokerCredentialProvider.brokerCredentialProvider({ appName: previewAppName, previewId })();
|
|
77
79
|
util_s3_client.setupS3Client(creds);
|
|
78
80
|
util_logger.info(`uploading preview build → s3://${constants.BUCKET_WRITE_NAME}/${keyPrefix}/`);
|
|
79
81
|
await util_s3_upload.uploadDir(paths.shadowBuild, keyPrefix);
|
|
82
|
+
util_logger.info('updating manifest');
|
|
80
83
|
await util_broker_app.registerOrUpdateApp({ appName: previewAppName, token, nextVersion, entry, gitRemote: '' });
|
|
81
84
|
const previewUrl = `https://bigbrain.me/bigbrain-vibe/${previewSlug}`;
|
|
82
85
|
util_logger.output(`[z2h-cli] preview ready ✅\n app: ${appName}\n previewAppName: ${previewAppName}\n open: ${previewUrl}`, {
|
|
@@ -151,8 +154,10 @@ async function deployLive({ paths, consumerName, appName, token, opts }) {
|
|
|
151
154
|
// Acquire the deploy lock explicitly. Done after the build
|
|
152
155
|
// so the lock hold stays tiny — it wraps only push → upload → manifest.
|
|
153
156
|
// A 409 here means someone else is mid-deploy;
|
|
157
|
+
util_logger.info('getting sts credentials');
|
|
154
158
|
const creds = await util_auth_brokerCredentialProvider.brokerCredentialProvider({ appName })();
|
|
155
159
|
util_s3_client.setupS3Client(creds);
|
|
160
|
+
util_logger.info('acquiring deploy lock');
|
|
156
161
|
await acquireDeployLock(appName, token);
|
|
157
162
|
// Push before any S3 write so a rejected push (someone deployed during our
|
|
158
163
|
// build) stops the deploy before the bundle/manifest go live
|
|
@@ -162,9 +167,11 @@ async function deployLive({ paths, consumerName, appName, token, opts }) {
|
|
|
162
167
|
}
|
|
163
168
|
util_logger.info(`uploading build → s3://${constants.BUCKET_WRITE_NAME}/${keyPrefix}/`);
|
|
164
169
|
await util_s3_upload.uploadDir(paths.shadowBuild, keyPrefix);
|
|
170
|
+
util_logger.info('updating manifest');
|
|
165
171
|
await util_broker_app.registerOrUpdateApp({ appName, token, nextVersion, entry, gitRemote, description: opts.description });
|
|
166
172
|
}
|
|
167
173
|
finally {
|
|
174
|
+
util_logger.info('releasing deploy lock');
|
|
168
175
|
await releaseLockBestEffort(appName, token);
|
|
169
176
|
}
|
|
170
177
|
// Tags are a post-deploy concern: the S3 upload has already succeeded, so a
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"deploy.d.ts","sourceRoot":"","sources":["../../../src/commands/deploy.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"deploy.d.ts","sourceRoot":"","sources":["../../../src/commands/deploy.ts"],"names":[],"mappings":"AAkBA,OAAO,EAAE,KAAK,UAAU,EAA2C,MAAM,4BAA4B,CAAC;AAWtG,MAAM,WAAW,aAAa;IAC5B,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,MAAM,CAAC,EAAE,UAAU,CAAC;IACpB,OAAO,CAAC,EAAE,MAAM,CAAC;IAEjB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAWD,wBAAsB,aAAa,CAAC,IAAI,GAAE,aAAkB,GAAG,OAAO,CAAC,IAAI,CAAC,CAoB3E"}
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import os from 'node:os';
|
|
2
|
-
import {
|
|
3
|
-
import { readJson } from 'fs-extra';
|
|
2
|
+
import { readJson, ensureDir } from 'fs-extra';
|
|
4
3
|
import { info, output, getOutputMode, warn } from '../util/logger.mjs';
|
|
5
4
|
import { resolveConsumerPaths } from '../shadow/paths.mjs';
|
|
6
5
|
import { fetchAppTags, fetchTagCatalog, setAppTags } from '../util/tags.mjs';
|
|
@@ -16,6 +15,7 @@ import { setupS3Client } from '../util/s3/client.mjs';
|
|
|
16
15
|
import { getMachineAppName } from '../util/machine-identity.mjs';
|
|
17
16
|
import { registerOrUpdateApp } from '../util/broker/app.mjs';
|
|
18
17
|
import { openInBrowser } from '../util/open-browser.mjs';
|
|
18
|
+
import { resolvePortAndMetadata } from '../shadow/prepare.mjs';
|
|
19
19
|
import { buildCommand } from './build.mjs';
|
|
20
20
|
import { promptForTags } from './tag.mjs';
|
|
21
21
|
|
|
@@ -57,7 +57,8 @@ async function buildAndStamp(opts) {
|
|
|
57
57
|
};
|
|
58
58
|
}
|
|
59
59
|
async function deployPreview({ paths, consumerName, appName, token, opts }) {
|
|
60
|
-
|
|
60
|
+
await ensureDir(paths.shadowDir);
|
|
61
|
+
const { previewId } = await resolvePortAndMetadata(paths, cleanAppName(consumerName));
|
|
61
62
|
const previewSlug = buildPreviewSlug(consumerName, previewId);
|
|
62
63
|
const previewAppName = mfAppName(previewSlug);
|
|
63
64
|
const keyPrefix = `${S3_PREVIEWS_FOLDER}/${previewAppName}/MF`;
|
|
@@ -67,10 +68,12 @@ async function deployPreview({ paths, consumerName, appName, token, opts }) {
|
|
|
67
68
|
const nextVersion = (await fetchDeployVersion(previewAppName)) + 1;
|
|
68
69
|
info('building');
|
|
69
70
|
const entry = await buildAndStamp({ paths, publicUrl: url, nextVersion, gitRemote: '' });
|
|
71
|
+
info('getting sts credentials');
|
|
70
72
|
const creds = await brokerCredentialProvider({ appName: previewAppName, previewId })();
|
|
71
73
|
setupS3Client(creds);
|
|
72
74
|
info(`uploading preview build → s3://${BUCKET_WRITE_NAME}/${keyPrefix}/`);
|
|
73
75
|
await uploadDir(paths.shadowBuild, keyPrefix);
|
|
76
|
+
info('updating manifest');
|
|
74
77
|
await registerOrUpdateApp({ appName: previewAppName, token, nextVersion, entry, gitRemote: '' });
|
|
75
78
|
const previewUrl = `https://bigbrain.me/bigbrain-vibe/${previewSlug}`;
|
|
76
79
|
output(`[z2h-cli] preview ready ✅\n app: ${appName}\n previewAppName: ${previewAppName}\n open: ${previewUrl}`, {
|
|
@@ -145,8 +148,10 @@ async function deployLive({ paths, consumerName, appName, token, opts }) {
|
|
|
145
148
|
// Acquire the deploy lock explicitly. Done after the build
|
|
146
149
|
// so the lock hold stays tiny — it wraps only push → upload → manifest.
|
|
147
150
|
// A 409 here means someone else is mid-deploy;
|
|
151
|
+
info('getting sts credentials');
|
|
148
152
|
const creds = await brokerCredentialProvider({ appName })();
|
|
149
153
|
setupS3Client(creds);
|
|
154
|
+
info('acquiring deploy lock');
|
|
150
155
|
await acquireDeployLock(appName, token);
|
|
151
156
|
// Push before any S3 write so a rejected push (someone deployed during our
|
|
152
157
|
// build) stops the deploy before the bundle/manifest go live
|
|
@@ -156,9 +161,11 @@ async function deployLive({ paths, consumerName, appName, token, opts }) {
|
|
|
156
161
|
}
|
|
157
162
|
info(`uploading build → s3://${BUCKET_WRITE_NAME}/${keyPrefix}/`);
|
|
158
163
|
await uploadDir(paths.shadowBuild, keyPrefix);
|
|
164
|
+
info('updating manifest');
|
|
159
165
|
await registerOrUpdateApp({ appName, token, nextVersion, entry, gitRemote, description: opts.description });
|
|
160
166
|
}
|
|
161
167
|
finally {
|
|
168
|
+
info('releasing deploy lock');
|
|
162
169
|
await releaseLockBestEffort(appName, token);
|
|
163
170
|
}
|
|
164
171
|
// Tags are a post-deploy concern: the S3 upload has already succeeded, so a
|
package/dist/esm/index.mjs
CHANGED
|
@@ -93,18 +93,14 @@ async function main() {
|
|
|
93
93
|
.command('deploy')
|
|
94
94
|
.description('Build, upload to S3, and register the app with the Z2H backend')
|
|
95
95
|
.option('--dry-run', 'print what would happen without uploading anything')
|
|
96
|
-
.option('--preview
|
|
96
|
+
.option('--preview', 'build an ephemeral, shareable preview instead of deploying — no git required, no deploy lock, never touches the live version; reuses the same preview URL across runs (stable link)')
|
|
97
97
|
.option('--open', 'open the resulting app/preview URL in the browser on success')
|
|
98
98
|
.option('--remote <type>', 'First deploy only: source control remote to initialise — s3 (shared storage) or github (your repo). Error if the app already has a git remote.')
|
|
99
99
|
.option('--repo-url <url>', 'First deploy only: GitHub repo URL — required when --remote=github. Error if the app already has a git remote.')
|
|
100
100
|
.option('--description <text>', 'short (<=200 char) description of the app')
|
|
101
101
|
.action(async (opts) => {
|
|
102
102
|
try {
|
|
103
|
-
await trackDuration(opts.preview ? 'z2h_app_preview_success' : 'z2h_app_deploy_success', () => deployCommand({
|
|
104
|
-
...opts,
|
|
105
|
-
preview: Boolean(opts.preview),
|
|
106
|
-
previewId: typeof opts.preview === 'string' ? opts.preview : undefined,
|
|
107
|
-
}), {
|
|
103
|
+
await trackDuration(opts.preview ? 'z2h_app_preview_success' : 'z2h_app_deploy_success', () => deployCommand(opts), {
|
|
108
104
|
app_name: getCwdAppName(),
|
|
109
105
|
});
|
|
110
106
|
}
|
|
@@ -1,3 +1,10 @@
|
|
|
1
|
-
import type { PreparedShadow } from '../types';
|
|
1
|
+
import type { ConsumerPaths, PreparedShadow, ShadowMetadata } from '../types';
|
|
2
2
|
export declare function prepareShadow(consumerDir?: string): Promise<PreparedShadow>;
|
|
3
|
+
/**
|
|
4
|
+
* Resolve the dev server port. Prefer the persisted port if it's still free
|
|
5
|
+
* (URL stays stable across restarts). Otherwise pick a fresh one. The
|
|
6
|
+
* orphaned process — if there is one — keeps running on its old port,
|
|
7
|
+
* harmless. No process killing, no PID files.
|
|
8
|
+
*/
|
|
9
|
+
export declare function resolvePortAndMetadata(paths: ConsumerPaths, cleanAppName: string): Promise<ShadowMetadata>;
|
|
3
10
|
//# sourceMappingURL=prepare.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"prepare.d.ts","sourceRoot":"","sources":["../../../src/shadow/prepare.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"prepare.d.ts","sourceRoot":"","sources":["../../../src/shadow/prepare.ts"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAE,aAAa,EAAe,cAAc,EAAE,cAAc,EAAE,MAAM,UAAU,CAAC;AAE3F,wBAAsB,aAAa,CAAC,WAAW,GAAE,MAAsB,GAAG,OAAO,CAAC,cAAc,CAAC,CAgChG;AAMD;;;;;GAKG;AACH,wBAAsB,sBAAsB,CAAC,KAAK,EAAE,aAAa,EAAE,YAAY,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,CAAC,CAoBhH"}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
1
2
|
import { pathExists, readJson, ensureDir } from 'fs-extra';
|
|
2
3
|
import { info } from '../util/logger.mjs';
|
|
3
4
|
import { cleanAppName } from '../util/app-name.mjs';
|
|
@@ -35,24 +36,31 @@ async function prepareShadow(consumerDir = process.cwd()) {
|
|
|
35
36
|
await syncShadow(paths, consumerPkg);
|
|
36
37
|
return { appName, port, paths };
|
|
37
38
|
}
|
|
39
|
+
function generatePreviewId() {
|
|
40
|
+
return randomUUID().slice(0, 8);
|
|
41
|
+
}
|
|
38
42
|
/**
|
|
39
43
|
* Resolve the dev server port. Prefer the persisted port if it's still free
|
|
40
44
|
* (URL stays stable across restarts). Otherwise pick a fresh one. The
|
|
41
45
|
* orphaned process — if there is one — keeps running on its old port,
|
|
42
46
|
* harmless. No process killing, no PID files.
|
|
43
47
|
*/
|
|
44
|
-
async function resolvePortAndMetadata(paths,
|
|
48
|
+
async function resolvePortAndMetadata(paths, cleanAppName) {
|
|
45
49
|
const existing = await readMetadata(paths);
|
|
46
|
-
|
|
47
|
-
|
|
50
|
+
let port = existing?.port;
|
|
51
|
+
if (!port || !(await isPortFree(port))) {
|
|
52
|
+
port = await pickPort();
|
|
48
53
|
}
|
|
49
54
|
const next = {
|
|
50
55
|
createdAt: existing?.createdAt ?? new Date().toISOString(),
|
|
51
|
-
appName,
|
|
52
|
-
port
|
|
56
|
+
appName: cleanAppName,
|
|
57
|
+
port,
|
|
58
|
+
previewId: existing?.previewId ?? generatePreviewId(),
|
|
53
59
|
};
|
|
54
|
-
|
|
60
|
+
if (JSON.stringify(next) !== JSON.stringify(existing)) {
|
|
61
|
+
await writeMetadata(paths, next);
|
|
62
|
+
}
|
|
55
63
|
return next;
|
|
56
64
|
}
|
|
57
65
|
|
|
58
|
-
export { prepareShadow };
|
|
66
|
+
export { prepareShadow, resolvePortAndMetadata };
|
package/dist/esm/types.d.ts
CHANGED
package/dist/esm/types.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/types.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAGH,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,UAAU,CAAC,EAAE,MAAM,EAAE,GAAG;QAAE,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAA;KAAE,CAAC;IAChD,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACtC,eAAe,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACzC,gBAAgB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC1C,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACjC,CAAC,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC;CACtB;AAOD,MAAM,WAAW,SAAS;IACxB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,IAAI,CAAC;IACd,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAChC,eAAe,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACzC;AAGD,MAAM,WAAW,cAAc;IAC7B,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/types.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAGH,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,UAAU,CAAC,EAAE,MAAM,EAAE,GAAG;QAAE,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAA;KAAE,CAAC;IAChD,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACtC,eAAe,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACzC,gBAAgB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC1C,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACjC,CAAC,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC;CACtB;AAOD,MAAM,WAAW,SAAS;IACxB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,IAAI,CAAC;IACd,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAChC,eAAe,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACzC;AAGD,MAAM,WAAW,cAAc;IAC7B,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,CAAC;CACnB;AAGD,MAAM,WAAW,aAAa;IAC5B,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,MAAM,CAAC;IACpB,gBAAgB,EAAE,MAAM,CAAC;IACzB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,aAAa,EAAE,MAAM,CAAC;IACtB,eAAe,EAAE,MAAM,CAAC;IACxB,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,MAAM,CAAC;IAClB,cAAc,EAAE,MAAM,CAAC;IACvB,WAAW,EAAE,MAAM,CAAC;IACpB,cAAc,EAAE,MAAM,CAAC;IACvB,cAAc,EAAE,MAAM,CAAC;CACxB;AAGD,MAAM,WAAW,cAAc;IAC7B,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,aAAa,CAAC;CACtB"}
|
package/dist/index.js
CHANGED
|
@@ -99,18 +99,14 @@ async function main() {
|
|
|
99
99
|
.command('deploy')
|
|
100
100
|
.description('Build, upload to S3, and register the app with the Z2H backend')
|
|
101
101
|
.option('--dry-run', 'print what would happen without uploading anything')
|
|
102
|
-
.option('--preview
|
|
102
|
+
.option('--preview', 'build an ephemeral, shareable preview instead of deploying — no git required, no deploy lock, never touches the live version; reuses the same preview URL across runs (stable link)')
|
|
103
103
|
.option('--open', 'open the resulting app/preview URL in the browser on success')
|
|
104
104
|
.option('--remote <type>', 'First deploy only: source control remote to initialise — s3 (shared storage) or github (your repo). Error if the app already has a git remote.')
|
|
105
105
|
.option('--repo-url <url>', 'First deploy only: GitHub repo URL — required when --remote=github. Error if the app already has a git remote.')
|
|
106
106
|
.option('--description <text>', 'short (<=200 char) description of the app')
|
|
107
107
|
.action(async (opts) => {
|
|
108
108
|
try {
|
|
109
|
-
await util_tracker.trackDuration(opts.preview ? 'z2h_app_preview_success' : 'z2h_app_deploy_success', () => commands_deploy.deployCommand({
|
|
110
|
-
...opts,
|
|
111
|
-
preview: Boolean(opts.preview),
|
|
112
|
-
previewId: typeof opts.preview === 'string' ? opts.preview : undefined,
|
|
113
|
-
}), {
|
|
109
|
+
await util_tracker.trackDuration(opts.preview ? 'z2h_app_preview_success' : 'z2h_app_deploy_success', () => commands_deploy.deployCommand(opts), {
|
|
114
110
|
app_name: util_appName.getCwdAppName(),
|
|
115
111
|
});
|
|
116
112
|
}
|
package/dist/shadow/prepare.d.ts
CHANGED
|
@@ -1,3 +1,10 @@
|
|
|
1
|
-
import type { PreparedShadow } from '../types';
|
|
1
|
+
import type { ConsumerPaths, PreparedShadow, ShadowMetadata } from '../types';
|
|
2
2
|
export declare function prepareShadow(consumerDir?: string): Promise<PreparedShadow>;
|
|
3
|
+
/**
|
|
4
|
+
* Resolve the dev server port. Prefer the persisted port if it's still free
|
|
5
|
+
* (URL stays stable across restarts). Otherwise pick a fresh one. The
|
|
6
|
+
* orphaned process — if there is one — keeps running on its old port,
|
|
7
|
+
* harmless. No process killing, no PID files.
|
|
8
|
+
*/
|
|
9
|
+
export declare function resolvePortAndMetadata(paths: ConsumerPaths, cleanAppName: string): Promise<ShadowMetadata>;
|
|
3
10
|
//# sourceMappingURL=prepare.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"prepare.d.ts","sourceRoot":"","sources":["../../src/shadow/prepare.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"prepare.d.ts","sourceRoot":"","sources":["../../src/shadow/prepare.ts"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAE,aAAa,EAAe,cAAc,EAAE,cAAc,EAAE,MAAM,UAAU,CAAC;AAE3F,wBAAsB,aAAa,CAAC,WAAW,GAAE,MAAsB,GAAG,OAAO,CAAC,cAAc,CAAC,CAgChG;AAMD;;;;;GAKG;AACH,wBAAsB,sBAAsB,CAAC,KAAK,EAAE,aAAa,EAAE,YAAY,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,CAAC,CAoBhH"}
|
package/dist/shadow/prepare.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
|
|
2
2
|
|
|
3
|
+
const crypto = require('node:crypto');
|
|
3
4
|
const fs = require('fs-extra');
|
|
4
5
|
const util_logger = require('../util/logger.js');
|
|
5
6
|
const util_appName = require('../util/app-name.js');
|
|
@@ -37,24 +38,32 @@ async function prepareShadow(consumerDir = process.cwd()) {
|
|
|
37
38
|
await shadow_sync.syncShadow(paths, consumerPkg);
|
|
38
39
|
return { appName, port, paths };
|
|
39
40
|
}
|
|
41
|
+
function generatePreviewId() {
|
|
42
|
+
return crypto.randomUUID().slice(0, 8);
|
|
43
|
+
}
|
|
40
44
|
/**
|
|
41
45
|
* Resolve the dev server port. Prefer the persisted port if it's still free
|
|
42
46
|
* (URL stays stable across restarts). Otherwise pick a fresh one. The
|
|
43
47
|
* orphaned process — if there is one — keeps running on its old port,
|
|
44
48
|
* harmless. No process killing, no PID files.
|
|
45
49
|
*/
|
|
46
|
-
async function resolvePortAndMetadata(paths,
|
|
50
|
+
async function resolvePortAndMetadata(paths, cleanAppName) {
|
|
47
51
|
const existing = await shadow_metadata.readMetadata(paths);
|
|
48
|
-
|
|
49
|
-
|
|
52
|
+
let port = existing?.port;
|
|
53
|
+
if (!port || !(await shadow_port.isPortFree(port))) {
|
|
54
|
+
port = await shadow_port.pickPort();
|
|
50
55
|
}
|
|
51
56
|
const next = {
|
|
52
57
|
createdAt: existing?.createdAt ?? new Date().toISOString(),
|
|
53
|
-
appName,
|
|
54
|
-
port
|
|
58
|
+
appName: cleanAppName,
|
|
59
|
+
port,
|
|
60
|
+
previewId: existing?.previewId ?? generatePreviewId(),
|
|
55
61
|
};
|
|
56
|
-
|
|
62
|
+
if (JSON.stringify(next) !== JSON.stringify(existing)) {
|
|
63
|
+
await shadow_metadata.writeMetadata(paths, next);
|
|
64
|
+
}
|
|
57
65
|
return next;
|
|
58
66
|
}
|
|
59
67
|
|
|
60
68
|
exports.prepareShadow = prepareShadow;
|
|
69
|
+
exports.resolvePortAndMetadata = resolvePortAndMetadata;
|
package/dist/types.d.ts
CHANGED
package/dist/types.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAGH,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,UAAU,CAAC,EAAE,MAAM,EAAE,GAAG;QAAE,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAA;KAAE,CAAC;IAChD,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACtC,eAAe,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACzC,gBAAgB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC1C,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACjC,CAAC,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC;CACtB;AAOD,MAAM,WAAW,SAAS;IACxB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,IAAI,CAAC;IACd,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAChC,eAAe,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACzC;AAGD,MAAM,WAAW,cAAc;IAC7B,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAGH,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,UAAU,CAAC,EAAE,MAAM,EAAE,GAAG;QAAE,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAA;KAAE,CAAC;IAChD,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACtC,eAAe,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACzC,gBAAgB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC1C,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACjC,CAAC,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC;CACtB;AAOD,MAAM,WAAW,SAAS;IACxB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,IAAI,CAAC;IACd,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAChC,eAAe,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACzC;AAGD,MAAM,WAAW,cAAc;IAC7B,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,CAAC;CACnB;AAGD,MAAM,WAAW,aAAa;IAC5B,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,MAAM,CAAC;IACpB,gBAAgB,EAAE,MAAM,CAAC;IACzB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,aAAa,EAAE,MAAM,CAAC;IACtB,eAAe,EAAE,MAAM,CAAC;IACxB,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,MAAM,CAAC;IAClB,cAAc,EAAE,MAAM,CAAC;IACvB,WAAW,EAAE,MAAM,CAAC;IACpB,cAAc,EAAE,MAAM,CAAC;IACvB,cAAc,EAAE,MAAM,CAAC;CACxB;AAGD,MAAM,WAAW,cAAc;IAC7B,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,aAAa,CAAC;CACtB"}
|
package/package.json
CHANGED
|
@@ -57,6 +57,8 @@ const ASSET_MANIFEST = {
|
|
|
57
57
|
entrypoints: ['main.js'],
|
|
58
58
|
};
|
|
59
59
|
|
|
60
|
+
const STUB_PREVIEW_ID = 'existing-preview-id' as const;
|
|
61
|
+
|
|
60
62
|
describe('deploy --preview', () => {
|
|
61
63
|
let tmp: string;
|
|
62
64
|
let cwd: string;
|
|
@@ -99,6 +101,14 @@ describe('deploy --preview', () => {
|
|
|
99
101
|
await rm(tmp, { recursive: true, force: true });
|
|
100
102
|
});
|
|
101
103
|
|
|
104
|
+
const writeMetadata = (previewId: string) =>
|
|
105
|
+
writeJson(path.join(tmp, '.zth', 'metadata.json'), {
|
|
106
|
+
createdAt: '2026-01-01T00:00:00.000Z',
|
|
107
|
+
appName: 'my-app',
|
|
108
|
+
port: 13000,
|
|
109
|
+
previewId,
|
|
110
|
+
});
|
|
111
|
+
|
|
102
112
|
it('throws when the consumer package.json has no name', async () => {
|
|
103
113
|
await writeJson(path.join(tmp, 'package.json'), {});
|
|
104
114
|
await expect(deployCommand({ preview: true })).rejects.toThrow(/must have a "name" field/);
|
|
@@ -118,49 +128,48 @@ describe('deploy --preview', () => {
|
|
|
118
128
|
expect(uploadDir).toHaveBeenCalledWith(path.join(tmp, '.zth', 'build'), `previews/${previewAppName}/MF`);
|
|
119
129
|
});
|
|
120
130
|
|
|
121
|
-
it('
|
|
122
|
-
|
|
131
|
+
it('reuses the sticky previewId from metadata', async () => {
|
|
132
|
+
await writeMetadata(STUB_PREVIEW_ID);
|
|
133
|
+
vi.mocked(brokerProvider.fetchDeployVersion).mockResolvedValue(2);
|
|
123
134
|
|
|
124
|
-
await deployCommand({ preview: true
|
|
135
|
+
await deployCommand({ preview: true });
|
|
125
136
|
|
|
126
|
-
const
|
|
127
|
-
|
|
137
|
+
const call = vi.mocked(brokerProvider.brokerCredentialProvider).mock.calls[0][0];
|
|
138
|
+
expect(call.previewId).toBe(STUB_PREVIEW_ID);
|
|
139
|
+
|
|
140
|
+
const previewAppName = `mf-${buildPreviewSlug('my-app', STUB_PREVIEW_ID)}`;
|
|
141
|
+
expect(uploadDir).toHaveBeenCalledWith(path.join(tmp, '.zth', 'build'), `previews/${previewAppName}/MF`);
|
|
128
142
|
|
|
129
143
|
expect(brokerApp.registerOrUpdateApp).toHaveBeenCalledWith({
|
|
130
144
|
appName: previewAppName,
|
|
131
145
|
token: 'jwt-stub',
|
|
132
|
-
nextVersion:
|
|
133
|
-
entry: expect.objectContaining({ ...ASSET_MANIFEST, version:
|
|
146
|
+
nextVersion: 3,
|
|
147
|
+
entry: expect.objectContaining({ ...ASSET_MANIFEST, version: 3, gitRemote: '' }),
|
|
134
148
|
gitRemote: '',
|
|
135
149
|
});
|
|
136
150
|
});
|
|
137
151
|
|
|
138
|
-
it('
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
await deployCommand({ preview: true, previewId: 'existing-preview-id' });
|
|
142
|
-
|
|
143
|
-
const call = vi.mocked(brokerProvider.brokerCredentialProvider).mock.calls[0][0];
|
|
144
|
-
expect(call.previewId).toBe('existing-preview-id');
|
|
145
|
-
|
|
146
|
-
const previewSlug = buildPreviewSlug('my-app', 'existing-preview-id');
|
|
147
|
-
const previewAppName = `mf-${previewSlug}`;
|
|
152
|
+
it('calls registerOrUpdateApp for a brand-new preview at version 1 with an empty gitRemote', async () => {
|
|
153
|
+
await writeMetadata(STUB_PREVIEW_ID);
|
|
154
|
+
vi.mocked(brokerProvider.fetchDeployVersion).mockResolvedValue(0);
|
|
148
155
|
|
|
149
|
-
|
|
156
|
+
await deployCommand({ preview: true });
|
|
150
157
|
|
|
158
|
+
const previewAppName = `mf-${buildPreviewSlug('my-app', STUB_PREVIEW_ID)}`;
|
|
151
159
|
expect(brokerApp.registerOrUpdateApp).toHaveBeenCalledWith({
|
|
152
160
|
appName: previewAppName,
|
|
153
161
|
token: 'jwt-stub',
|
|
154
|
-
nextVersion:
|
|
155
|
-
entry: expect.objectContaining({ ...ASSET_MANIFEST, version:
|
|
162
|
+
nextVersion: 1,
|
|
163
|
+
entry: expect.objectContaining({ ...ASSET_MANIFEST, version: 1, gitRemote: '' }),
|
|
156
164
|
gitRemote: '',
|
|
157
165
|
});
|
|
158
166
|
});
|
|
159
167
|
|
|
160
168
|
it('passes a full CDN URL (not a relative path) as publicUrl to the build', async () => {
|
|
161
|
-
await
|
|
169
|
+
await writeMetadata(STUB_PREVIEW_ID);
|
|
170
|
+
await deployCommand({ preview: true });
|
|
162
171
|
|
|
163
|
-
const previewAppName = `mf-${buildPreviewSlug('my-app',
|
|
172
|
+
const previewAppName = `mf-${buildPreviewSlug('my-app', STUB_PREVIEW_ID)}`;
|
|
164
173
|
expect(buildCommand).toHaveBeenCalledWith(
|
|
165
174
|
expect.objectContaining({
|
|
166
175
|
publicUrl: `https://bigbrain-zth-mf-assets.bigbrain.me/previews/${previewAppName}/MF`,
|
|
@@ -178,9 +187,10 @@ describe('deploy --preview', () => {
|
|
|
178
187
|
});
|
|
179
188
|
|
|
180
189
|
it('--open opens the previewUrl in the browser on success', async () => {
|
|
181
|
-
await
|
|
190
|
+
await writeMetadata(STUB_PREVIEW_ID);
|
|
191
|
+
await deployCommand({ preview: true, open: true });
|
|
182
192
|
|
|
183
|
-
const previewSlug = buildPreviewSlug('my-app',
|
|
193
|
+
const previewSlug = buildPreviewSlug('my-app', STUB_PREVIEW_ID);
|
|
184
194
|
expect(openInBrowser).toHaveBeenCalledWith(`https://bigbrain.me/bigbrain-vibe/${previewSlug}`);
|
|
185
195
|
});
|
|
186
196
|
|
|
@@ -195,19 +205,20 @@ describe('deploy --preview', () => {
|
|
|
195
205
|
});
|
|
196
206
|
|
|
197
207
|
it('--json emits a machine-readable payload with an app-shaped previewUrl (not ?z2h_preview=)', async () => {
|
|
208
|
+
await writeMetadata(STUB_PREVIEW_ID);
|
|
198
209
|
const writeSpy = vi.spyOn(process.stdout, 'write').mockImplementation(() => true);
|
|
199
210
|
setOutputMode('json');
|
|
200
211
|
|
|
201
|
-
await deployCommand({ preview: true
|
|
212
|
+
await deployCommand({ preview: true });
|
|
202
213
|
|
|
203
214
|
const jsonLine = writeSpy.mock.calls.map(c => c[0]).find(line => typeof line === 'string' && line.includes('"ok"'));
|
|
204
215
|
expect(jsonLine).toBeDefined();
|
|
205
216
|
const payload = JSON.parse(jsonLine as string);
|
|
206
|
-
const previewSlug = buildPreviewSlug('my-app',
|
|
217
|
+
const previewSlug = buildPreviewSlug('my-app', STUB_PREVIEW_ID);
|
|
207
218
|
expect(payload).toMatchObject({
|
|
208
219
|
ok: true,
|
|
209
220
|
appName: 'mf-my-app',
|
|
210
|
-
previewId:
|
|
221
|
+
previewId: STUB_PREVIEW_ID,
|
|
211
222
|
previewAppName: `mf-${previewSlug}`,
|
|
212
223
|
previewUrl: `https://bigbrain.me/bigbrain-vibe/${previewSlug}`,
|
|
213
224
|
});
|
package/src/commands/deploy.ts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import os from 'node:os';
|
|
2
|
-
import {
|
|
3
|
-
import { readJson } from 'fs-extra';
|
|
2
|
+
import { ensureDir, readJson } from 'fs-extra';
|
|
4
3
|
import { info, warn, output, getOutputMode } from '../util/logger';
|
|
5
4
|
import { resolveConsumerPaths } from '../shadow/paths';
|
|
6
5
|
import { fetchAppTags, fetchTagCatalog, setAppTags } from '../util/tags';
|
|
@@ -22,6 +21,7 @@ import { setupS3Client } from '../util/s3/client';
|
|
|
22
21
|
import { getMachineAppName } from '../util/machine-identity';
|
|
23
22
|
import { registerOrUpdateApp } from '../util/broker/app';
|
|
24
23
|
import { openInBrowser } from '../util/open-browser';
|
|
24
|
+
import { resolvePortAndMetadata } from '../shadow/prepare';
|
|
25
25
|
import { buildCommand } from './build';
|
|
26
26
|
import { promptForTags } from './tag';
|
|
27
27
|
import type { ConsumerPaths } from '../types';
|
|
@@ -31,11 +31,8 @@ export interface DeployOptions {
|
|
|
31
31
|
dryRun?: boolean;
|
|
32
32
|
remote?: RemoteType;
|
|
33
33
|
repoUrl?: string;
|
|
34
|
-
// Registers a real, disposable `apps` row under a synthetic name instead of
|
|
35
|
-
// deploying — no git, no deploy lock (each previewId's slug is unique).
|
|
34
|
+
// Registers a real, disposable `apps` row under a synthetic name instead of deploying as a live app
|
|
36
35
|
preview?: boolean;
|
|
37
|
-
// With --preview: update an existing preview instead of minting a new one.
|
|
38
|
-
previewId?: string;
|
|
39
36
|
open?: boolean;
|
|
40
37
|
description?: string;
|
|
41
38
|
}
|
|
@@ -96,7 +93,9 @@ async function buildAndStamp(opts: {
|
|
|
96
93
|
}
|
|
97
94
|
|
|
98
95
|
async function deployPreview({ paths, consumerName, appName, token, opts }: DeployContext): Promise<void> {
|
|
99
|
-
|
|
96
|
+
await ensureDir(paths.shadowDir);
|
|
97
|
+
const { previewId } = await resolvePortAndMetadata(paths, cleanAppName(consumerName));
|
|
98
|
+
|
|
100
99
|
const previewSlug = buildPreviewSlug(consumerName, previewId);
|
|
101
100
|
const previewAppName = mfAppName(previewSlug);
|
|
102
101
|
const keyPrefix = `${S3_PREVIEWS_FOLDER}/${previewAppName}/MF`;
|
|
@@ -109,12 +108,14 @@ async function deployPreview({ paths, consumerName, appName, token, opts }: Depl
|
|
|
109
108
|
info('building');
|
|
110
109
|
const entry = await buildAndStamp({ paths, publicUrl: url, nextVersion, gitRemote: '' });
|
|
111
110
|
|
|
111
|
+
info('getting sts credentials');
|
|
112
112
|
const creds = await brokerCredentialProvider({ appName: previewAppName, previewId })();
|
|
113
113
|
setupS3Client(creds);
|
|
114
114
|
|
|
115
115
|
info(`uploading preview build → s3://${BUCKET_WRITE_NAME}/${keyPrefix}/`);
|
|
116
116
|
await uploadDir(paths.shadowBuild, keyPrefix);
|
|
117
117
|
|
|
118
|
+
info('updating manifest');
|
|
118
119
|
await registerOrUpdateApp({ appName: previewAppName, token, nextVersion, entry, gitRemote: '' });
|
|
119
120
|
|
|
120
121
|
const previewUrl = `https://bigbrain.me/bigbrain-vibe/${previewSlug}`;
|
|
@@ -206,8 +207,10 @@ async function deployLive({ paths, consumerName, appName, token, opts }: DeployC
|
|
|
206
207
|
// Acquire the deploy lock explicitly. Done after the build
|
|
207
208
|
// so the lock hold stays tiny — it wraps only push → upload → manifest.
|
|
208
209
|
// A 409 here means someone else is mid-deploy;
|
|
210
|
+
info('getting sts credentials');
|
|
209
211
|
const creds = await brokerCredentialProvider({ appName })();
|
|
210
212
|
setupS3Client(creds);
|
|
213
|
+
info('acquiring deploy lock');
|
|
211
214
|
await acquireDeployLock(appName, token);
|
|
212
215
|
|
|
213
216
|
// Push before any S3 write so a rejected push (someone deployed during our
|
|
@@ -220,8 +223,10 @@ async function deployLive({ paths, consumerName, appName, token, opts }: DeployC
|
|
|
220
223
|
info(`uploading build → s3://${BUCKET_WRITE_NAME}/${keyPrefix}/`);
|
|
221
224
|
await uploadDir(paths.shadowBuild, keyPrefix);
|
|
222
225
|
|
|
226
|
+
info('updating manifest');
|
|
223
227
|
await registerOrUpdateApp({ appName, token, nextVersion, entry, gitRemote, description: opts.description });
|
|
224
228
|
} finally {
|
|
229
|
+
info('releasing deploy lock');
|
|
225
230
|
await releaseLockBestEffort(appName, token);
|
|
226
231
|
}
|
|
227
232
|
|
package/src/index.ts
CHANGED
|
@@ -103,8 +103,8 @@ async function main(): Promise<void> {
|
|
|
103
103
|
.description('Build, upload to S3, and register the app with the Z2H backend')
|
|
104
104
|
.option('--dry-run', 'print what would happen without uploading anything')
|
|
105
105
|
.option(
|
|
106
|
-
'--preview
|
|
107
|
-
'build an ephemeral, shareable preview instead of deploying — no git required, no deploy lock, never touches the live version;
|
|
106
|
+
'--preview',
|
|
107
|
+
'build an ephemeral, shareable preview instead of deploying — no git required, no deploy lock, never touches the live version; reuses the same preview URL across runs (stable link)'
|
|
108
108
|
)
|
|
109
109
|
.option('--open', 'open the resulting app/preview URL in the browser on success')
|
|
110
110
|
.option(
|
|
@@ -128,12 +128,7 @@ async function main(): Promise<void> {
|
|
|
128
128
|
try {
|
|
129
129
|
await trackDuration(
|
|
130
130
|
opts.preview ? 'z2h_app_preview_success' : 'z2h_app_deploy_success',
|
|
131
|
-
() =>
|
|
132
|
-
deployCommand({
|
|
133
|
-
...opts,
|
|
134
|
-
preview: Boolean(opts.preview),
|
|
135
|
-
previewId: typeof opts.preview === 'string' ? opts.preview : undefined,
|
|
136
|
-
} as Parameters<typeof deployCommand>[0]),
|
|
131
|
+
() => deployCommand(opts as Parameters<typeof deployCommand>[0]),
|
|
137
132
|
{
|
|
138
133
|
app_name: getCwdAppName(),
|
|
139
134
|
}
|
package/src/shadow/prepare.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
1
2
|
import { ensureDir, pathExists, readJson } from 'fs-extra';
|
|
2
3
|
import { info } from '../util/logger';
|
|
3
4
|
import { cleanAppName } from '../util/app-name';
|
|
@@ -44,23 +45,34 @@ export async function prepareShadow(consumerDir: string = process.cwd()): Promis
|
|
|
44
45
|
return { appName, port, paths };
|
|
45
46
|
}
|
|
46
47
|
|
|
48
|
+
function generatePreviewId(): string {
|
|
49
|
+
return randomUUID().slice(0, 8);
|
|
50
|
+
}
|
|
51
|
+
|
|
47
52
|
/**
|
|
48
53
|
* Resolve the dev server port. Prefer the persisted port if it's still free
|
|
49
54
|
* (URL stays stable across restarts). Otherwise pick a fresh one. The
|
|
50
55
|
* orphaned process — if there is one — keeps running on its old port,
|
|
51
56
|
* harmless. No process killing, no PID files.
|
|
52
57
|
*/
|
|
53
|
-
async function resolvePortAndMetadata(paths: ConsumerPaths,
|
|
58
|
+
export async function resolvePortAndMetadata(paths: ConsumerPaths, cleanAppName: string): Promise<ShadowMetadata> {
|
|
54
59
|
const existing = await readMetadata(paths);
|
|
55
|
-
|
|
56
|
-
|
|
60
|
+
|
|
61
|
+
let port = existing?.port;
|
|
62
|
+
if (!port || !(await isPortFree(port))) {
|
|
63
|
+
port = await pickPort();
|
|
57
64
|
}
|
|
58
65
|
|
|
59
66
|
const next: ShadowMetadata = {
|
|
60
67
|
createdAt: existing?.createdAt ?? new Date().toISOString(),
|
|
61
|
-
appName,
|
|
62
|
-
port
|
|
68
|
+
appName: cleanAppName,
|
|
69
|
+
port,
|
|
70
|
+
previewId: existing?.previewId ?? generatePreviewId(),
|
|
63
71
|
};
|
|
64
|
-
|
|
72
|
+
|
|
73
|
+
if (JSON.stringify(next) !== JSON.stringify(existing)) {
|
|
74
|
+
await writeMetadata(paths, next);
|
|
75
|
+
}
|
|
76
|
+
|
|
65
77
|
return next;
|
|
66
78
|
}
|