@mondaydotcomorg/z2h-cli 0.9.1 → 0.10.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commands/create.d.ts +4 -0
- package/dist/commands/create.d.ts.map +1 -1
- package/dist/commands/create.js +24 -0
- package/dist/commands/deploy.d.ts.map +1 -1
- package/dist/commands/deploy.js +5 -1
- package/dist/esm/commands/create.d.ts +4 -0
- package/dist/esm/commands/create.d.ts.map +1 -1
- package/dist/esm/commands/create.mjs +24 -1
- package/dist/esm/commands/deploy.d.ts.map +1 -1
- package/dist/esm/commands/deploy.mjs +5 -1
- package/dist/esm/index.mjs +3 -0
- package/dist/esm/util/auth/auth-store.d.ts.map +1 -1
- package/dist/esm/util/auth/auth-store.mjs +10 -0
- package/dist/esm/util/git/repo.d.ts +2 -1
- package/dist/esm/util/git/repo.d.ts.map +1 -1
- package/dist/esm/util/git/repo.mjs +24 -8
- package/dist/esm/util/machine-identity.d.ts +9 -0
- package/dist/esm/util/machine-identity.d.ts.map +1 -0
- package/dist/esm/util/machine-identity.mjs +12 -0
- package/dist/esm/util/manifest.d.ts +1 -1
- package/dist/esm/util/manifest.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/util/auth/auth-store.d.ts.map +1 -1
- package/dist/util/auth/auth-store.js +10 -0
- package/dist/util/git/repo.d.ts +2 -1
- package/dist/util/git/repo.d.ts.map +1 -1
- package/dist/util/git/repo.js +25 -8
- package/dist/util/machine-identity.d.ts +9 -0
- package/dist/util/machine-identity.d.ts.map +1 -0
- package/dist/util/machine-identity.js +14 -0
- package/dist/util/manifest.d.ts +1 -1
- package/dist/util/manifest.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/commands/__tests__/create.test.ts +45 -6
- package/src/commands/create.ts +28 -0
- package/src/commands/deploy.ts +5 -1
- package/src/index.ts +18 -10
- package/src/util/auth/auth-store.ts +12 -0
- package/src/util/git/__tests__/repo.test.ts +57 -18
- package/src/util/git/repo.ts +24 -7
- package/src/util/machine-identity.ts +10 -0
- package/src/util/manifest.ts +1 -1
|
@@ -1,6 +1,10 @@
|
|
|
1
|
+
export declare const REMOTE_TYPES: readonly ["s3", "github"];
|
|
2
|
+
export type RemoteType = (typeof REMOTE_TYPES)[number];
|
|
1
3
|
export interface CreateOptions {
|
|
2
4
|
template?: string;
|
|
3
5
|
htmlFile?: string;
|
|
6
|
+
remote?: RemoteType;
|
|
7
|
+
repoUrl?: string;
|
|
4
8
|
json?: boolean;
|
|
5
9
|
}
|
|
6
10
|
export declare function createCommand(appName: string, opts?: CreateOptions): Promise<void>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"create.d.ts","sourceRoot":"","sources":["../../src/commands/create.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"create.d.ts","sourceRoot":"","sources":["../../src/commands/create.ts"],"names":[],"mappings":"AAaA,eAAO,MAAM,YAAY,2BAA4B,CAAC;AACtD,MAAM,MAAM,UAAU,GAAG,CAAC,OAAO,YAAY,CAAC,CAAC,MAAM,CAAC,CAAC;AAEvD,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE,UAAU,CAAC;IACpB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,IAAI,CAAC,EAAE,OAAO,CAAC;CAChB;AA6BD,wBAAsB,aAAa,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,GAAE,aAAkB,GAAG,OAAO,CAAC,IAAI,CAAC,CAoH5F"}
|
package/dist/commands/create.js
CHANGED
|
@@ -10,12 +10,14 @@ const util_manifest = require('../util/manifest.js');
|
|
|
10
10
|
const util_appName = require('../util/app-name.js');
|
|
11
11
|
const constants = require('../constants.js');
|
|
12
12
|
const util_npm_install = require('../util/npm/install.js');
|
|
13
|
+
const util_git_repo = require('../util/git/repo.js');
|
|
13
14
|
const commands_build = require('./build.js');
|
|
14
15
|
|
|
15
16
|
const _interopDefault = e => e && e.__esModule ? e : { default: e };
|
|
16
17
|
|
|
17
18
|
const path__default = /*#__PURE__*/_interopDefault(path);
|
|
18
19
|
|
|
20
|
+
const REMOTE_TYPES = ['s3', 'github'];
|
|
19
21
|
// Names reserved by the host MF (mf-bigbrain-zth) for platform pages under
|
|
20
22
|
// /bigbrain-vibe/<name>. Creating an app with one of these would shadow the
|
|
21
23
|
// platform page in the URL, so we refuse upfront.
|
|
@@ -54,6 +56,16 @@ async function createCommand(appName, opts = {}) {
|
|
|
54
56
|
throw new Error(`App name "${appName}" is invalid. Use lowercase kebab-case (a-z, 0-9, -), ` +
|
|
55
57
|
`2-50 chars, starting with a letter.`);
|
|
56
58
|
}
|
|
59
|
+
// Validate --remote up front before any file I/O.
|
|
60
|
+
if (!opts.remote) {
|
|
61
|
+
throw new Error('--remote is required. Use --remote=s3 (shared storage) or --remote=github (your GitHub repo).');
|
|
62
|
+
}
|
|
63
|
+
if (!REMOTE_TYPES.includes(opts.remote)) {
|
|
64
|
+
throw new Error(`Unknown remote "${opts.remote}". Use ${REMOTE_TYPES.join(' or ')}.`);
|
|
65
|
+
}
|
|
66
|
+
if (opts.remote === 'github' && !opts.repoUrl) {
|
|
67
|
+
throw new Error('--repo-url is required when using --remote=github.');
|
|
68
|
+
}
|
|
57
69
|
// Resolve template.
|
|
58
70
|
// `--html-file` implies `--template html-embed` unless the user already picked one.
|
|
59
71
|
const template = (opts.template ?? (opts.htmlFile ? 'html-embed' : 'default'));
|
|
@@ -96,6 +108,15 @@ async function createCommand(appName, opts = {}) {
|
|
|
96
108
|
if (template === 'html-embed' && opts.htmlFile) {
|
|
97
109
|
await fs.copy(opts.htmlFile, path__default.default.join(appDir, 'src', 'page.html'));
|
|
98
110
|
}
|
|
111
|
+
util_logger.info(`initializing git repo for ${appName}...`);
|
|
112
|
+
let gitRemote;
|
|
113
|
+
if (opts.remote === 's3') {
|
|
114
|
+
gitRemote = util_git_repo.defaultS3Remote(appName);
|
|
115
|
+
}
|
|
116
|
+
else {
|
|
117
|
+
gitRemote = opts.repoUrl;
|
|
118
|
+
}
|
|
119
|
+
await util_git_repo.initRepo(appDir, gitRemote);
|
|
99
120
|
util_logger.info(`installing dependencies for ${appName}...`);
|
|
100
121
|
await util_npm_install.runYarnInstall({ cwd: workspace });
|
|
101
122
|
// build/dev/deploy all locate the consumer via process.cwd() — chdir before calling them.
|
|
@@ -116,7 +137,10 @@ async function createCommand(appName, opts = {}) {
|
|
|
116
137
|
appDir,
|
|
117
138
|
appName,
|
|
118
139
|
template,
|
|
140
|
+
remote: opts.remote,
|
|
141
|
+
gitRemote,
|
|
119
142
|
});
|
|
120
143
|
}
|
|
121
144
|
|
|
145
|
+
exports.REMOTE_TYPES = REMOTE_TYPES;
|
|
122
146
|
exports.createCommand = createCommand;
|
|
@@ -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":"AAsBA,MAAM,WAAW,aAAa;IAC5B,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,IAAI,CAAC,EAAE,OAAO,CAAC;CAChB;AAGD,wBAAsB,aAAa,CAAC,IAAI,GAAE,aAAkB,GAAG,OAAO,CAAC,IAAI,CAAC,CAyF3E"}
|
package/dist/commands/deploy.js
CHANGED
|
@@ -11,6 +11,8 @@ const constants = require('../constants.js');
|
|
|
11
11
|
const util_s3_client = require('../util/s3/client.js');
|
|
12
12
|
const util_auth_authStore = require('../util/auth/auth-store.js');
|
|
13
13
|
const util_appName = require('../util/app-name.js');
|
|
14
|
+
const util_machineIdentity = require('../util/machine-identity.js');
|
|
15
|
+
const util_git_repo = require('../util/git/repo.js');
|
|
14
16
|
const commands_build = require('./build.js');
|
|
15
17
|
|
|
16
18
|
const _interopDefault = e => e && e.__esModule ? e : { default: e };
|
|
@@ -49,12 +51,14 @@ async function deployCommand(opts = {}) {
|
|
|
49
51
|
throw new Error(`Missing asset-manifest.json at ${assetManifestPath}.`);
|
|
50
52
|
}
|
|
51
53
|
const assetManifest = (await fs.readJson(assetManifestPath));
|
|
52
|
-
const deployedBy = os__default.default.userInfo().username;
|
|
54
|
+
const deployedBy = util_machineIdentity.getMachineAppName() ?? os__default.default.userInfo().username;
|
|
55
|
+
const gitRemote = await util_git_repo.getRemoteUrl(process.cwd());
|
|
53
56
|
const entry = {
|
|
54
57
|
...assetManifest,
|
|
55
58
|
version,
|
|
56
59
|
url,
|
|
57
60
|
deployedBy,
|
|
61
|
+
gitRemote,
|
|
58
62
|
};
|
|
59
63
|
if (opts.dryRun) {
|
|
60
64
|
util_logger.output(`[z2h-cli] dry-run: would upload to s3://${constants.BUCKET_WRITE_NAME}/${keyPrefix}/ as v${version}`, {
|
|
@@ -1,6 +1,10 @@
|
|
|
1
|
+
export declare const REMOTE_TYPES: readonly ["s3", "github"];
|
|
2
|
+
export type RemoteType = (typeof REMOTE_TYPES)[number];
|
|
1
3
|
export interface CreateOptions {
|
|
2
4
|
template?: string;
|
|
3
5
|
htmlFile?: string;
|
|
6
|
+
remote?: RemoteType;
|
|
7
|
+
repoUrl?: string;
|
|
4
8
|
json?: boolean;
|
|
5
9
|
}
|
|
6
10
|
export declare function createCommand(appName: string, opts?: CreateOptions): Promise<void>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"create.d.ts","sourceRoot":"","sources":["../../../src/commands/create.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"create.d.ts","sourceRoot":"","sources":["../../../src/commands/create.ts"],"names":[],"mappings":"AAaA,eAAO,MAAM,YAAY,2BAA4B,CAAC;AACtD,MAAM,MAAM,UAAU,GAAG,CAAC,OAAO,YAAY,CAAC,CAAC,MAAM,CAAC,CAAC;AAEvD,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE,UAAU,CAAC;IACpB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,IAAI,CAAC,EAAE,OAAO,CAAC;CAChB;AA6BD,wBAAsB,aAAa,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,GAAE,aAAkB,GAAG,OAAO,CAAC,IAAI,CAAC,CAoH5F"}
|
|
@@ -8,8 +8,10 @@ import { readManifest } from '../util/manifest.mjs';
|
|
|
8
8
|
import { mfAppName } from '../util/app-name.mjs';
|
|
9
9
|
import { APP_NAME_PATTERN } from '../constants.mjs';
|
|
10
10
|
import { runYarnInstall } from '../util/npm/install.mjs';
|
|
11
|
+
import { defaultS3Remote, initRepo } from '../util/git/repo.mjs';
|
|
11
12
|
import { buildCommand } from './build.mjs';
|
|
12
13
|
|
|
14
|
+
const REMOTE_TYPES = ['s3', 'github'];
|
|
13
15
|
// Names reserved by the host MF (mf-bigbrain-zth) for platform pages under
|
|
14
16
|
// /bigbrain-vibe/<name>. Creating an app with one of these would shadow the
|
|
15
17
|
// platform page in the URL, so we refuse upfront.
|
|
@@ -48,6 +50,16 @@ async function createCommand(appName, opts = {}) {
|
|
|
48
50
|
throw new Error(`App name "${appName}" is invalid. Use lowercase kebab-case (a-z, 0-9, -), ` +
|
|
49
51
|
`2-50 chars, starting with a letter.`);
|
|
50
52
|
}
|
|
53
|
+
// Validate --remote up front before any file I/O.
|
|
54
|
+
if (!opts.remote) {
|
|
55
|
+
throw new Error('--remote is required. Use --remote=s3 (shared storage) or --remote=github (your GitHub repo).');
|
|
56
|
+
}
|
|
57
|
+
if (!REMOTE_TYPES.includes(opts.remote)) {
|
|
58
|
+
throw new Error(`Unknown remote "${opts.remote}". Use ${REMOTE_TYPES.join(' or ')}.`);
|
|
59
|
+
}
|
|
60
|
+
if (opts.remote === 'github' && !opts.repoUrl) {
|
|
61
|
+
throw new Error('--repo-url is required when using --remote=github.');
|
|
62
|
+
}
|
|
51
63
|
// Resolve template.
|
|
52
64
|
// `--html-file` implies `--template html-embed` unless the user already picked one.
|
|
53
65
|
const template = (opts.template ?? (opts.htmlFile ? 'html-embed' : 'default'));
|
|
@@ -90,6 +102,15 @@ async function createCommand(appName, opts = {}) {
|
|
|
90
102
|
if (template === 'html-embed' && opts.htmlFile) {
|
|
91
103
|
await copy(opts.htmlFile, path.join(appDir, 'src', 'page.html'));
|
|
92
104
|
}
|
|
105
|
+
info(`initializing git repo for ${appName}...`);
|
|
106
|
+
let gitRemote;
|
|
107
|
+
if (opts.remote === 's3') {
|
|
108
|
+
gitRemote = defaultS3Remote(appName);
|
|
109
|
+
}
|
|
110
|
+
else {
|
|
111
|
+
gitRemote = opts.repoUrl;
|
|
112
|
+
}
|
|
113
|
+
await initRepo(appDir, gitRemote);
|
|
93
114
|
info(`installing dependencies for ${appName}...`);
|
|
94
115
|
await runYarnInstall({ cwd: workspace });
|
|
95
116
|
// build/dev/deploy all locate the consumer via process.cwd() — chdir before calling them.
|
|
@@ -110,7 +131,9 @@ async function createCommand(appName, opts = {}) {
|
|
|
110
131
|
appDir,
|
|
111
132
|
appName,
|
|
112
133
|
template,
|
|
134
|
+
remote: opts.remote,
|
|
135
|
+
gitRemote,
|
|
113
136
|
});
|
|
114
137
|
}
|
|
115
138
|
|
|
116
|
-
export { createCommand };
|
|
139
|
+
export { REMOTE_TYPES, createCommand };
|
|
@@ -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":"AAsBA,MAAM,WAAW,aAAa;IAC5B,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,IAAI,CAAC,EAAE,OAAO,CAAC;CAChB;AAGD,wBAAsB,aAAa,CAAC,IAAI,GAAE,aAAkB,GAAG,OAAO,CAAC,IAAI,CAAC,CAyF3E"}
|
|
@@ -9,6 +9,8 @@ import { BUCKET_WRITE_NAME, brokerBaseUrl, BUCKET_READ_DNS_URL } from '../consta
|
|
|
9
9
|
import { setupS3Client } from '../util/s3/client.mjs';
|
|
10
10
|
import { readZthAuth } from '../util/auth/auth-store.mjs';
|
|
11
11
|
import { mfAppName, cleanAppName } from '../util/app-name.mjs';
|
|
12
|
+
import { getMachineAppName } from '../util/machine-identity.mjs';
|
|
13
|
+
import { getRemoteUrl } from '../util/git/repo.mjs';
|
|
12
14
|
import { buildCommand } from './build.mjs';
|
|
13
15
|
|
|
14
16
|
// Should be run from the consumer app directory — uses process.cwd() to locate it.
|
|
@@ -42,12 +44,14 @@ async function deployCommand(opts = {}) {
|
|
|
42
44
|
throw new Error(`Missing asset-manifest.json at ${assetManifestPath}.`);
|
|
43
45
|
}
|
|
44
46
|
const assetManifest = (await readJson(assetManifestPath));
|
|
45
|
-
const deployedBy = os.userInfo().username;
|
|
47
|
+
const deployedBy = getMachineAppName() ?? os.userInfo().username;
|
|
48
|
+
const gitRemote = await getRemoteUrl(process.cwd());
|
|
46
49
|
const entry = {
|
|
47
50
|
...assetManifest,
|
|
48
51
|
version,
|
|
49
52
|
url,
|
|
50
53
|
deployedBy,
|
|
54
|
+
gitRemote,
|
|
51
55
|
};
|
|
52
56
|
if (opts.dryRun) {
|
|
53
57
|
output(`[z2h-cli] dry-run: would upload to s3://${BUCKET_WRITE_NAME}/${keyPrefix}/ as v${version}`, {
|
package/dist/esm/index.mjs
CHANGED
|
@@ -135,12 +135,15 @@ async function main() {
|
|
|
135
135
|
.description('Scaffold a new Z2H consumer app inside ~/zth-projects/')
|
|
136
136
|
.option('--template <name>', 'Template name (default | html-embed)')
|
|
137
137
|
.option('--html-file <path>', 'Path to an HTML file to embed (implies --template html-embed)')
|
|
138
|
+
.option('--remote <type>', 'Source control remote: s3 (shared storage) or github (your repo)')
|
|
139
|
+
.option('--repo-url <url>', 'GitHub repo URL — required when --remote=github')
|
|
138
140
|
.option('--json', 'emit structured JSON on stdout')
|
|
139
141
|
.action(async (appName, opts) => {
|
|
140
142
|
try {
|
|
141
143
|
await trackDuration('z2h_app_created', () => createCommand(appName, opts), {
|
|
142
144
|
app_name: appName,
|
|
143
145
|
template: opts.template ?? 'default',
|
|
146
|
+
remote: opts.remote ?? 'unknown',
|
|
144
147
|
});
|
|
145
148
|
}
|
|
146
149
|
catch (err) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"auth-store.d.ts","sourceRoot":"","sources":["../../../../src/util/auth/auth-store.ts"],"names":[],"mappings":"AAOA,eAAO,MAAM,YAAY,QAAkC,CAAC;AAC5D,eAAO,MAAM,aAAa,QAAuC,CAAC;AAElE,MAAM,WAAW,OAAO;IACtB,KAAK,EAAE,MAAM,CAAC;CACf;AAED,qBAAa,mBAAoB,SAAQ,KAAK;aAChB,YAAY,EAAE,MAAM;gBAApB,YAAY,EAAE,MAAM;CAIjD;AAED,wBAAsB,WAAW,IAAI,OAAO,CAAC,OAAO,CAAC,
|
|
1
|
+
{"version":3,"file":"auth-store.d.ts","sourceRoot":"","sources":["../../../../src/util/auth/auth-store.ts"],"names":[],"mappings":"AAOA,eAAO,MAAM,YAAY,QAAkC,CAAC;AAC5D,eAAO,MAAM,aAAa,QAAuC,CAAC;AAElE,MAAM,WAAW,OAAO;IACtB,KAAK,EAAE,MAAM,CAAC;CACf;AAED,qBAAa,mBAAoB,SAAQ,KAAK;aAChB,YAAY,EAAE,MAAM;gBAApB,YAAY,EAAE,MAAM;CAIjD;AAED,wBAAsB,WAAW,IAAI,OAAO,CAAC,OAAO,CAAC,CAkBpD;AAED;;;;;;;;GAQG;AACH,wBAAgB,oBAAoB,IAAI,MAAM,GAAG,IAAI,CAUpD"}
|
|
@@ -16,6 +16,13 @@ class Z2hAuthMissingError extends Error {
|
|
|
16
16
|
}
|
|
17
17
|
}
|
|
18
18
|
async function readZthAuth() {
|
|
19
|
+
if (process.env.Z2H_AUTH_TOKEN) {
|
|
20
|
+
const token = process.env.Z2H_AUTH_TOKEN;
|
|
21
|
+
if (!/^[\w-]+\.[\w-]+\.[\w-]+$/.test(token)) {
|
|
22
|
+
throw new Error('Z2H_AUTH_TOKEN is not a valid JWT (expected three dot-separated segments). Please re-set the environment variable.');
|
|
23
|
+
}
|
|
24
|
+
return { token };
|
|
25
|
+
}
|
|
19
26
|
if (!(await pathExists(Z2H_AUTH_FILE))) {
|
|
20
27
|
throw new Z2hAuthMissingError(Z2H_AUTH_FILE);
|
|
21
28
|
}
|
|
@@ -35,6 +42,9 @@ async function readZthAuth() {
|
|
|
35
42
|
* blow up — and where an inline sync read keeps the spawn call site simple.
|
|
36
43
|
*/
|
|
37
44
|
function tryReadAuthTokenSync() {
|
|
45
|
+
if (process.env.Z2H_AUTH_TOKEN) {
|
|
46
|
+
return process.env.Z2H_AUTH_TOKEN;
|
|
47
|
+
}
|
|
38
48
|
try {
|
|
39
49
|
const parsed = readJsonSync(Z2H_AUTH_FILE);
|
|
40
50
|
return parsed?.token ?? null;
|
|
@@ -2,7 +2,8 @@ export declare class GitConflictError extends Error {
|
|
|
2
2
|
constructor(message: string);
|
|
3
3
|
}
|
|
4
4
|
export declare function defaultS3Remote(app: string): string;
|
|
5
|
-
export declare function
|
|
5
|
+
export declare function initRepo(dir: string, remote: string): Promise<void>;
|
|
6
|
+
export declare function getRemoteUrl(dir: string): Promise<string | undefined>;
|
|
6
7
|
export declare function cloneRepo(remote: string, dir: string): Promise<void>;
|
|
7
8
|
export declare function safeBackupIfDirty(dir: string): Promise<void>;
|
|
8
9
|
export declare function pullAndResolve(dir: string): Promise<void>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"repo.d.ts","sourceRoot":"","sources":["../../../../src/util/git/repo.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"repo.d.ts","sourceRoot":"","sources":["../../../../src/util/git/repo.ts"],"names":[],"mappings":"AAMA,qBAAa,gBAAiB,SAAQ,KAAK;gBAC7B,OAAO,EAAE,MAAM;CAI5B;AAID,wBAAgB,eAAe,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAEnD;AAED,wBAAsB,QAAQ,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAMzE;AAED,wBAAsB,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAI3E;AAED,wBAAsB,SAAS,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAE1E;AAED,wBAAsB,iBAAiB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAclE;AAED,wBAAsB,cAAc,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAS/D;AAED,wBAAsB,UAAU,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAO7D;AAED,wBAAsB,SAAS,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAG5D;AAED,wBAAsB,qBAAqB,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAoBvF"}
|
|
@@ -2,6 +2,7 @@ import path from 'node:path';
|
|
|
2
2
|
import fs from 'fs-extra';
|
|
3
3
|
import { execa } from 'execa';
|
|
4
4
|
import { BUCKET_WRITE_NAME } from '../../constants.mjs';
|
|
5
|
+
import { mfAppName } from '../app-name.mjs';
|
|
5
6
|
|
|
6
7
|
class GitConflictError extends Error {
|
|
7
8
|
constructor(message) {
|
|
@@ -11,15 +12,19 @@ class GitConflictError extends Error {
|
|
|
11
12
|
}
|
|
12
13
|
const REQUIRED_GITIGNORE_ENTRIES = ['node_modules/', '.zth/', 'build/'];
|
|
13
14
|
function defaultS3Remote(app) {
|
|
14
|
-
return `s3://${BUCKET_WRITE_NAME}/${app}/git`;
|
|
15
|
+
return `s3://${BUCKET_WRITE_NAME}/${mfAppName(app)}/git`;
|
|
15
16
|
}
|
|
16
|
-
async function
|
|
17
|
+
async function initRepo(dir, remote) {
|
|
17
18
|
await ensureGitignore(dir);
|
|
18
19
|
await execa('git', ['init', '-b', 'master'], { cwd: dir });
|
|
19
20
|
await execa('git', ['add', '.'], { cwd: dir });
|
|
20
21
|
await execa('git', ['commit', '-m', 'Initial commit'], { cwd: dir });
|
|
21
22
|
await execa('git', ['remote', 'add', 'origin', remote], { cwd: dir });
|
|
22
|
-
|
|
23
|
+
}
|
|
24
|
+
async function getRemoteUrl(dir) {
|
|
25
|
+
return execa('git', ['remote', 'get-url', 'origin'], { cwd: dir })
|
|
26
|
+
.then(r => r.stdout.trim() || undefined)
|
|
27
|
+
.catch(() => undefined);
|
|
23
28
|
}
|
|
24
29
|
async function cloneRepo(remote, dir) {
|
|
25
30
|
await execa('git', ['clone', remote, dir]);
|
|
@@ -62,14 +67,25 @@ async function localHead(dir) {
|
|
|
62
67
|
return stdout.trim();
|
|
63
68
|
}
|
|
64
69
|
async function commitDetailedAndPush(dir, version) {
|
|
65
|
-
const
|
|
70
|
+
const { stdout: branch } = await execa('git', ['rev-parse', '--abbrev-ref', 'HEAD'], { cwd: dir });
|
|
71
|
+
if (branch.trim() !== 'master') {
|
|
72
|
+
throw new Error(`expected branch "master" but you are on "${branch.trim()}" — switch back before deploying.`);
|
|
73
|
+
}
|
|
66
74
|
await execa('git', ['add', '.'], { cwd: dir });
|
|
75
|
+
const { stdout: staged } = await execa('git', ['diff', '--cached', '--name-only'], { cwd: dir });
|
|
76
|
+
if (!staged.trim()) {
|
|
77
|
+
throw new Error('Nothing new to deploy — no files have changed since the last deploy.');
|
|
78
|
+
}
|
|
79
|
+
const message = await buildCommitMessage(dir, version);
|
|
67
80
|
await execa('git', ['commit', '-m', message], { cwd: dir });
|
|
68
81
|
try {
|
|
69
|
-
await execa('git', ['push'], { cwd: dir });
|
|
82
|
+
await execa('git', ['push', '-u', 'origin', 'master'], { cwd: dir });
|
|
70
83
|
}
|
|
71
|
-
catch {
|
|
72
|
-
|
|
84
|
+
catch (err) {
|
|
85
|
+
if (/\[rejected\]|non-fast-forward/.test(err.message)) {
|
|
86
|
+
throw new GitConflictError('push rejected — remote has new commits');
|
|
87
|
+
}
|
|
88
|
+
throw err;
|
|
73
89
|
}
|
|
74
90
|
}
|
|
75
91
|
async function buildCommitMessage(dir, version) {
|
|
@@ -102,4 +118,4 @@ async function ensureGitignore(dir) {
|
|
|
102
118
|
await fs.outputFile(gitignorePath, content + separator + missing.join('\n') + '\n');
|
|
103
119
|
}
|
|
104
120
|
|
|
105
|
-
export { GitConflictError, cloneRepo, commitDetailedAndPush, defaultS3Remote,
|
|
121
|
+
export { GitConflictError, cloneRepo, commitDetailedAndPush, defaultS3Remote, getRemoteUrl, initRepo, localHead, pullAndResolve, remoteHead, safeBackupIfDirty };
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Returns the machine app name when running as a service (e.g. agent-runner).
|
|
3
|
+
* Available when the process is a monday.com microservice with `APP_NAME` set
|
|
4
|
+
* by the platform runtime.
|
|
5
|
+
*
|
|
6
|
+
* Returns `undefined` for interactive human users (fallback to os.userInfo).
|
|
7
|
+
*/
|
|
8
|
+
export declare function getMachineAppName(): string | undefined;
|
|
9
|
+
//# sourceMappingURL=machine-identity.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"machine-identity.d.ts","sourceRoot":"","sources":["../../../src/util/machine-identity.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,wBAAgB,iBAAiB,IAAI,MAAM,GAAG,SAAS,CAEtD"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Returns the machine app name when running as a service (e.g. agent-runner).
|
|
3
|
+
* Available when the process is a monday.com microservice with `APP_NAME` set
|
|
4
|
+
* by the platform runtime.
|
|
5
|
+
*
|
|
6
|
+
* Returns `undefined` for interactive human users (fallback to os.userInfo).
|
|
7
|
+
*/
|
|
8
|
+
function getMachineAppName() {
|
|
9
|
+
return process.env.APP_NAME || undefined;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export { getMachineAppName };
|
|
@@ -18,12 +18,12 @@ export interface AssetManifestFile {
|
|
|
18
18
|
nonExternalsEntrypoints?: string[];
|
|
19
19
|
scriptType?: 'module' | 'classic';
|
|
20
20
|
mfs?: string[];
|
|
21
|
-
gitRemote: string;
|
|
22
21
|
}
|
|
23
22
|
export interface ManifestEntry extends AssetManifestFile {
|
|
24
23
|
version: number;
|
|
25
24
|
url: string;
|
|
26
25
|
deployedBy: string;
|
|
26
|
+
gitRemote?: string;
|
|
27
27
|
}
|
|
28
28
|
export type Manifest = Record<string, ManifestEntry>;
|
|
29
29
|
export declare function readManifest(): Promise<Manifest>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"manifest.d.ts","sourceRoot":"","sources":["../../../src/util/manifest.ts"],"names":[],"mappings":"AAIA,MAAM,WAAW,SAAS;IACxB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,UAAU,EAAE,MAAM,CAAC;IACnB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACtC;AAED;;;;GAIG;AACH,MAAM,WAAW,iBAAiB;IAChC,UAAU,EAAE,OAAO,CAAC;IACpB,oBAAoB,EAAE,OAAO,CAAC;IAC9B,MAAM,EAAE,SAAS,EAAE,CAAC;IACpB,WAAW,EAAE,MAAM,EAAE,CAAC;IACtB,uBAAuB,CAAC,EAAE,MAAM,EAAE,CAAC;IACnC,UAAU,CAAC,EAAE,QAAQ,GAAG,SAAS,CAAC;IAClC,GAAG,CAAC,EAAE,MAAM,EAAE,CAAC;
|
|
1
|
+
{"version":3,"file":"manifest.d.ts","sourceRoot":"","sources":["../../../src/util/manifest.ts"],"names":[],"mappings":"AAIA,MAAM,WAAW,SAAS;IACxB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,UAAU,EAAE,MAAM,CAAC;IACnB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACtC;AAED;;;;GAIG;AACH,MAAM,WAAW,iBAAiB;IAChC,UAAU,EAAE,OAAO,CAAC;IACpB,oBAAoB,EAAE,OAAO,CAAC;IAC9B,MAAM,EAAE,SAAS,EAAE,CAAC;IACpB,WAAW,EAAE,MAAM,EAAE,CAAC;IACtB,uBAAuB,CAAC,EAAE,MAAM,EAAE,CAAC;IACnC,UAAU,CAAC,EAAE,QAAQ,GAAG,SAAS,CAAC;IAClC,GAAG,CAAC,EAAE,MAAM,EAAE,CAAC;CAChB;AAED,MAAM,WAAW,aAAc,SAAQ,iBAAiB;IACtD,OAAO,EAAE,MAAM,CAAC;IAChB,GAAG,EAAE,MAAM,CAAC;IACZ,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,MAAM,QAAQ,GAAG,MAAM,CAAC,MAAM,EAAE,aAAa,CAAC,CAAC;AAErD,wBAAsB,YAAY,IAAI,OAAO,CAAC,QAAQ,CAAC,CAQtD;AAED,wBAAgB,WAAW,CAAC,QAAQ,EAAE,QAAQ,EAAE,GAAG,EAAE,MAAM,UAI1D;AAED,wBAAgB,WAAW,CAAC,QAAQ,EAAE,QAAQ,EAAE,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,aAAa,GAAG,QAAQ,CAE3F;AAED,wBAAsB,aAAa,CAAC,QAAQ,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,CAWrE"}
|
package/dist/index.js
CHANGED
|
@@ -141,12 +141,15 @@ async function main() {
|
|
|
141
141
|
.description('Scaffold a new Z2H consumer app inside ~/zth-projects/')
|
|
142
142
|
.option('--template <name>', 'Template name (default | html-embed)')
|
|
143
143
|
.option('--html-file <path>', 'Path to an HTML file to embed (implies --template html-embed)')
|
|
144
|
+
.option('--remote <type>', 'Source control remote: s3 (shared storage) or github (your repo)')
|
|
145
|
+
.option('--repo-url <url>', 'GitHub repo URL — required when --remote=github')
|
|
144
146
|
.option('--json', 'emit structured JSON on stdout')
|
|
145
147
|
.action(async (appName, opts) => {
|
|
146
148
|
try {
|
|
147
149
|
await util_tracker.trackDuration('z2h_app_created', () => commands_create.createCommand(appName, opts), {
|
|
148
150
|
app_name: appName,
|
|
149
151
|
template: opts.template ?? 'default',
|
|
152
|
+
remote: opts.remote ?? 'unknown',
|
|
150
153
|
});
|
|
151
154
|
}
|
|
152
155
|
catch (err) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"auth-store.d.ts","sourceRoot":"","sources":["../../../src/util/auth/auth-store.ts"],"names":[],"mappings":"AAOA,eAAO,MAAM,YAAY,QAAkC,CAAC;AAC5D,eAAO,MAAM,aAAa,QAAuC,CAAC;AAElE,MAAM,WAAW,OAAO;IACtB,KAAK,EAAE,MAAM,CAAC;CACf;AAED,qBAAa,mBAAoB,SAAQ,KAAK;aAChB,YAAY,EAAE,MAAM;gBAApB,YAAY,EAAE,MAAM;CAIjD;AAED,wBAAsB,WAAW,IAAI,OAAO,CAAC,OAAO,CAAC,
|
|
1
|
+
{"version":3,"file":"auth-store.d.ts","sourceRoot":"","sources":["../../../src/util/auth/auth-store.ts"],"names":[],"mappings":"AAOA,eAAO,MAAM,YAAY,QAAkC,CAAC;AAC5D,eAAO,MAAM,aAAa,QAAuC,CAAC;AAElE,MAAM,WAAW,OAAO;IACtB,KAAK,EAAE,MAAM,CAAC;CACf;AAED,qBAAa,mBAAoB,SAAQ,KAAK;aAChB,YAAY,EAAE,MAAM;gBAApB,YAAY,EAAE,MAAM;CAIjD;AAED,wBAAsB,WAAW,IAAI,OAAO,CAAC,OAAO,CAAC,CAkBpD;AAED;;;;;;;;GAQG;AACH,wBAAgB,oBAAoB,IAAI,MAAM,GAAG,IAAI,CAUpD"}
|
|
@@ -23,6 +23,13 @@ class Z2hAuthMissingError extends Error {
|
|
|
23
23
|
}
|
|
24
24
|
}
|
|
25
25
|
async function readZthAuth() {
|
|
26
|
+
if (process.env.Z2H_AUTH_TOKEN) {
|
|
27
|
+
const token = process.env.Z2H_AUTH_TOKEN;
|
|
28
|
+
if (!/^[\w-]+\.[\w-]+\.[\w-]+$/.test(token)) {
|
|
29
|
+
throw new Error('Z2H_AUTH_TOKEN is not a valid JWT (expected three dot-separated segments). Please re-set the environment variable.');
|
|
30
|
+
}
|
|
31
|
+
return { token };
|
|
32
|
+
}
|
|
26
33
|
if (!(await fs.pathExists(Z2H_AUTH_FILE))) {
|
|
27
34
|
throw new Z2hAuthMissingError(Z2H_AUTH_FILE);
|
|
28
35
|
}
|
|
@@ -42,6 +49,9 @@ async function readZthAuth() {
|
|
|
42
49
|
* blow up — and where an inline sync read keeps the spawn call site simple.
|
|
43
50
|
*/
|
|
44
51
|
function tryReadAuthTokenSync() {
|
|
52
|
+
if (process.env.Z2H_AUTH_TOKEN) {
|
|
53
|
+
return process.env.Z2H_AUTH_TOKEN;
|
|
54
|
+
}
|
|
45
55
|
try {
|
|
46
56
|
const parsed = fs.readJsonSync(Z2H_AUTH_FILE);
|
|
47
57
|
return parsed?.token ?? null;
|
package/dist/util/git/repo.d.ts
CHANGED
|
@@ -2,7 +2,8 @@ export declare class GitConflictError extends Error {
|
|
|
2
2
|
constructor(message: string);
|
|
3
3
|
}
|
|
4
4
|
export declare function defaultS3Remote(app: string): string;
|
|
5
|
-
export declare function
|
|
5
|
+
export declare function initRepo(dir: string, remote: string): Promise<void>;
|
|
6
|
+
export declare function getRemoteUrl(dir: string): Promise<string | undefined>;
|
|
6
7
|
export declare function cloneRepo(remote: string, dir: string): Promise<void>;
|
|
7
8
|
export declare function safeBackupIfDirty(dir: string): Promise<void>;
|
|
8
9
|
export declare function pullAndResolve(dir: string): Promise<void>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"repo.d.ts","sourceRoot":"","sources":["../../../src/util/git/repo.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"repo.d.ts","sourceRoot":"","sources":["../../../src/util/git/repo.ts"],"names":[],"mappings":"AAMA,qBAAa,gBAAiB,SAAQ,KAAK;gBAC7B,OAAO,EAAE,MAAM;CAI5B;AAID,wBAAgB,eAAe,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAEnD;AAED,wBAAsB,QAAQ,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAMzE;AAED,wBAAsB,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAI3E;AAED,wBAAsB,SAAS,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAE1E;AAED,wBAAsB,iBAAiB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAclE;AAED,wBAAsB,cAAc,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAS/D;AAED,wBAAsB,UAAU,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAO7D;AAED,wBAAsB,SAAS,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAG5D;AAED,wBAAsB,qBAAqB,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAoBvF"}
|
package/dist/util/git/repo.js
CHANGED
|
@@ -4,6 +4,7 @@ const path = require('node:path');
|
|
|
4
4
|
const fs = require('fs-extra');
|
|
5
5
|
const execa = require('execa');
|
|
6
6
|
const constants = require('../../constants.js');
|
|
7
|
+
const util_appName = require('../app-name.js');
|
|
7
8
|
|
|
8
9
|
const _interopDefault = e => e && e.__esModule ? e : { default: e };
|
|
9
10
|
|
|
@@ -18,15 +19,19 @@ class GitConflictError extends Error {
|
|
|
18
19
|
}
|
|
19
20
|
const REQUIRED_GITIGNORE_ENTRIES = ['node_modules/', '.zth/', 'build/'];
|
|
20
21
|
function defaultS3Remote(app) {
|
|
21
|
-
return `s3://${constants.BUCKET_WRITE_NAME}/${app}/git`;
|
|
22
|
+
return `s3://${constants.BUCKET_WRITE_NAME}/${util_appName.mfAppName(app)}/git`;
|
|
22
23
|
}
|
|
23
|
-
async function
|
|
24
|
+
async function initRepo(dir, remote) {
|
|
24
25
|
await ensureGitignore(dir);
|
|
25
26
|
await execa.execa('git', ['init', '-b', 'master'], { cwd: dir });
|
|
26
27
|
await execa.execa('git', ['add', '.'], { cwd: dir });
|
|
27
28
|
await execa.execa('git', ['commit', '-m', 'Initial commit'], { cwd: dir });
|
|
28
29
|
await execa.execa('git', ['remote', 'add', 'origin', remote], { cwd: dir });
|
|
29
|
-
|
|
30
|
+
}
|
|
31
|
+
async function getRemoteUrl(dir) {
|
|
32
|
+
return execa.execa('git', ['remote', 'get-url', 'origin'], { cwd: dir })
|
|
33
|
+
.then(r => r.stdout.trim() || undefined)
|
|
34
|
+
.catch(() => undefined);
|
|
30
35
|
}
|
|
31
36
|
async function cloneRepo(remote, dir) {
|
|
32
37
|
await execa.execa('git', ['clone', remote, dir]);
|
|
@@ -69,14 +74,25 @@ async function localHead(dir) {
|
|
|
69
74
|
return stdout.trim();
|
|
70
75
|
}
|
|
71
76
|
async function commitDetailedAndPush(dir, version) {
|
|
72
|
-
const
|
|
77
|
+
const { stdout: branch } = await execa.execa('git', ['rev-parse', '--abbrev-ref', 'HEAD'], { cwd: dir });
|
|
78
|
+
if (branch.trim() !== 'master') {
|
|
79
|
+
throw new Error(`expected branch "master" but you are on "${branch.trim()}" — switch back before deploying.`);
|
|
80
|
+
}
|
|
73
81
|
await execa.execa('git', ['add', '.'], { cwd: dir });
|
|
82
|
+
const { stdout: staged } = await execa.execa('git', ['diff', '--cached', '--name-only'], { cwd: dir });
|
|
83
|
+
if (!staged.trim()) {
|
|
84
|
+
throw new Error('Nothing new to deploy — no files have changed since the last deploy.');
|
|
85
|
+
}
|
|
86
|
+
const message = await buildCommitMessage(dir, version);
|
|
74
87
|
await execa.execa('git', ['commit', '-m', message], { cwd: dir });
|
|
75
88
|
try {
|
|
76
|
-
await execa.execa('git', ['push'], { cwd: dir });
|
|
89
|
+
await execa.execa('git', ['push', '-u', 'origin', 'master'], { cwd: dir });
|
|
77
90
|
}
|
|
78
|
-
catch {
|
|
79
|
-
|
|
91
|
+
catch (err) {
|
|
92
|
+
if (/\[rejected\]|non-fast-forward/.test(err.message)) {
|
|
93
|
+
throw new GitConflictError('push rejected — remote has new commits');
|
|
94
|
+
}
|
|
95
|
+
throw err;
|
|
80
96
|
}
|
|
81
97
|
}
|
|
82
98
|
async function buildCommitMessage(dir, version) {
|
|
@@ -113,7 +129,8 @@ exports.GitConflictError = GitConflictError;
|
|
|
113
129
|
exports.cloneRepo = cloneRepo;
|
|
114
130
|
exports.commitDetailedAndPush = commitDetailedAndPush;
|
|
115
131
|
exports.defaultS3Remote = defaultS3Remote;
|
|
116
|
-
exports.
|
|
132
|
+
exports.getRemoteUrl = getRemoteUrl;
|
|
133
|
+
exports.initRepo = initRepo;
|
|
117
134
|
exports.localHead = localHead;
|
|
118
135
|
exports.pullAndResolve = pullAndResolve;
|
|
119
136
|
exports.remoteHead = remoteHead;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Returns the machine app name when running as a service (e.g. agent-runner).
|
|
3
|
+
* Available when the process is a monday.com microservice with `APP_NAME` set
|
|
4
|
+
* by the platform runtime.
|
|
5
|
+
*
|
|
6
|
+
* Returns `undefined` for interactive human users (fallback to os.userInfo).
|
|
7
|
+
*/
|
|
8
|
+
export declare function getMachineAppName(): string | undefined;
|
|
9
|
+
//# sourceMappingURL=machine-identity.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"machine-identity.d.ts","sourceRoot":"","sources":["../../src/util/machine-identity.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,wBAAgB,iBAAiB,IAAI,MAAM,GAAG,SAAS,CAEtD"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Returns the machine app name when running as a service (e.g. agent-runner).
|
|
5
|
+
* Available when the process is a monday.com microservice with `APP_NAME` set
|
|
6
|
+
* by the platform runtime.
|
|
7
|
+
*
|
|
8
|
+
* Returns `undefined` for interactive human users (fallback to os.userInfo).
|
|
9
|
+
*/
|
|
10
|
+
function getMachineAppName() {
|
|
11
|
+
return process.env.APP_NAME || undefined;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
exports.getMachineAppName = getMachineAppName;
|
package/dist/util/manifest.d.ts
CHANGED
|
@@ -18,12 +18,12 @@ export interface AssetManifestFile {
|
|
|
18
18
|
nonExternalsEntrypoints?: string[];
|
|
19
19
|
scriptType?: 'module' | 'classic';
|
|
20
20
|
mfs?: string[];
|
|
21
|
-
gitRemote: string;
|
|
22
21
|
}
|
|
23
22
|
export interface ManifestEntry extends AssetManifestFile {
|
|
24
23
|
version: number;
|
|
25
24
|
url: string;
|
|
26
25
|
deployedBy: string;
|
|
26
|
+
gitRemote?: string;
|
|
27
27
|
}
|
|
28
28
|
export type Manifest = Record<string, ManifestEntry>;
|
|
29
29
|
export declare function readManifest(): Promise<Manifest>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"manifest.d.ts","sourceRoot":"","sources":["../../src/util/manifest.ts"],"names":[],"mappings":"AAIA,MAAM,WAAW,SAAS;IACxB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,UAAU,EAAE,MAAM,CAAC;IACnB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACtC;AAED;;;;GAIG;AACH,MAAM,WAAW,iBAAiB;IAChC,UAAU,EAAE,OAAO,CAAC;IACpB,oBAAoB,EAAE,OAAO,CAAC;IAC9B,MAAM,EAAE,SAAS,EAAE,CAAC;IACpB,WAAW,EAAE,MAAM,EAAE,CAAC;IACtB,uBAAuB,CAAC,EAAE,MAAM,EAAE,CAAC;IACnC,UAAU,CAAC,EAAE,QAAQ,GAAG,SAAS,CAAC;IAClC,GAAG,CAAC,EAAE,MAAM,EAAE,CAAC;
|
|
1
|
+
{"version":3,"file":"manifest.d.ts","sourceRoot":"","sources":["../../src/util/manifest.ts"],"names":[],"mappings":"AAIA,MAAM,WAAW,SAAS;IACxB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,UAAU,EAAE,MAAM,CAAC;IACnB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACtC;AAED;;;;GAIG;AACH,MAAM,WAAW,iBAAiB;IAChC,UAAU,EAAE,OAAO,CAAC;IACpB,oBAAoB,EAAE,OAAO,CAAC;IAC9B,MAAM,EAAE,SAAS,EAAE,CAAC;IACpB,WAAW,EAAE,MAAM,EAAE,CAAC;IACtB,uBAAuB,CAAC,EAAE,MAAM,EAAE,CAAC;IACnC,UAAU,CAAC,EAAE,QAAQ,GAAG,SAAS,CAAC;IAClC,GAAG,CAAC,EAAE,MAAM,EAAE,CAAC;CAChB;AAED,MAAM,WAAW,aAAc,SAAQ,iBAAiB;IACtD,OAAO,EAAE,MAAM,CAAC;IAChB,GAAG,EAAE,MAAM,CAAC;IACZ,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,MAAM,QAAQ,GAAG,MAAM,CAAC,MAAM,EAAE,aAAa,CAAC,CAAC;AAErD,wBAAsB,YAAY,IAAI,OAAO,CAAC,QAAQ,CAAC,CAQtD;AAED,wBAAgB,WAAW,CAAC,QAAQ,EAAE,QAAQ,EAAE,GAAG,EAAE,MAAM,UAI1D;AAED,wBAAgB,WAAW,CAAC,QAAQ,EAAE,QAAQ,EAAE,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,aAAa,GAAG,QAAQ,CAE3F;AAED,wBAAsB,aAAa,CAAC,QAAQ,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,CAWrE"}
|
package/package.json
CHANGED
|
@@ -4,6 +4,7 @@ import { mkdtemp, rm, writeJson, readJson, ensureDir, pathExists, writeFile } fr
|
|
|
4
4
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
5
5
|
|
|
6
6
|
import { runYarnInstall } from '../../util/npm/install';
|
|
7
|
+
import { initRepo, defaultS3Remote } from '../../util/git/repo';
|
|
7
8
|
import { buildCommand } from '../build';
|
|
8
9
|
import { createCommand } from '../create';
|
|
9
10
|
import * as manifest from '../../util/manifest';
|
|
@@ -15,6 +16,11 @@ vi.mock('../../util/npm/install', () => ({
|
|
|
15
16
|
runYarnInstall: vi.fn().mockResolvedValue(undefined),
|
|
16
17
|
}));
|
|
17
18
|
|
|
19
|
+
vi.mock('../../util/git/repo', () => ({
|
|
20
|
+
initRepo: vi.fn().mockResolvedValue(undefined),
|
|
21
|
+
defaultS3Remote: vi.fn((app: string) => `s3://prod-use1-bigbrain-zth-mf-assets/mf-${app}/git`),
|
|
22
|
+
}));
|
|
23
|
+
|
|
18
24
|
// Mock build so tsc/yarn build don't run in tests.
|
|
19
25
|
vi.mock('../build', () => ({
|
|
20
26
|
buildCommand: vi.fn().mockResolvedValue(undefined),
|
|
@@ -39,6 +45,8 @@ describe('create', () => {
|
|
|
39
45
|
tmp = await mkdtemp(path.join(os.tmpdir(), 'z2h-c-'));
|
|
40
46
|
process.env.Z2H_WORKSPACE_HOME = tmp;
|
|
41
47
|
vi.mocked(runYarnInstall).mockClear();
|
|
48
|
+
vi.mocked(initRepo).mockClear();
|
|
49
|
+
vi.mocked(defaultS3Remote).mockClear();
|
|
42
50
|
vi.mocked(buildCommand).mockClear();
|
|
43
51
|
});
|
|
44
52
|
|
|
@@ -63,12 +71,12 @@ describe('create', () => {
|
|
|
63
71
|
const appDir = path.join(tmp, 'demo-app');
|
|
64
72
|
await ensureDir(appDir);
|
|
65
73
|
await writeFile(path.join(appDir, 'something.txt'), 'hi');
|
|
66
|
-
await expect(createCommand('demo-app')).rejects.toThrow(/non-empty/);
|
|
74
|
+
await expect(createCommand('demo-app', { remote: 's3' })).rejects.toThrow(/non-empty/);
|
|
67
75
|
});
|
|
68
76
|
|
|
69
77
|
it('scaffolds an app with the default React template', async () => {
|
|
70
78
|
await makeWorkspace(tmp);
|
|
71
|
-
await createCommand('demo-app');
|
|
79
|
+
await createCommand('demo-app', { remote: 's3' });
|
|
72
80
|
|
|
73
81
|
const appDir = path.join(tmp, 'demo-app');
|
|
74
82
|
expect(await pathExists(path.join(appDir, 'package.json'))).toBe(true);
|
|
@@ -94,7 +102,7 @@ describe('create', () => {
|
|
|
94
102
|
const htmlPath = path.join(tmp, 'page.html');
|
|
95
103
|
await writeFile(htmlPath, '<html><body>Hi</body></html>');
|
|
96
104
|
|
|
97
|
-
await createCommand('html-app', { template: 'html-embed', htmlFile: htmlPath });
|
|
105
|
+
await createCommand('html-app', { template: 'html-embed', htmlFile: htmlPath, remote: 's3' });
|
|
98
106
|
|
|
99
107
|
const appDir = path.join(tmp, 'html-app');
|
|
100
108
|
expect(await pathExists(path.join(appDir, 'src', 'page.html'))).toBe(true);
|
|
@@ -107,13 +115,44 @@ describe('create', () => {
|
|
|
107
115
|
|
|
108
116
|
it('rejects unknown templates', async () => {
|
|
109
117
|
await makeWorkspace(tmp);
|
|
110
|
-
await expect(createCommand('demo', { template: 'nonexistent' })).rejects.toThrow(/unknown template/i);
|
|
118
|
+
await expect(createCommand('demo', { remote: 's3', template: 'nonexistent' })).rejects.toThrow(/unknown template/i);
|
|
111
119
|
});
|
|
112
120
|
|
|
113
121
|
it('rejects --html-file when the file does not exist', async () => {
|
|
114
122
|
await makeWorkspace(tmp);
|
|
115
|
-
await expect(
|
|
116
|
-
/html
|
|
123
|
+
await expect(
|
|
124
|
+
createCommand('demo', { remote: 's3', template: 'html-embed', htmlFile: '/no/such/file.html' })
|
|
125
|
+
).rejects.toThrow(/html file/i);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it('rejects when --remote is missing', async () => {
|
|
129
|
+
await makeWorkspace(tmp);
|
|
130
|
+
await expect(createCommand('demo-app', {})).rejects.toThrow(/--remote is required/);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it('rejects an unknown --remote value', async () => {
|
|
134
|
+
await makeWorkspace(tmp);
|
|
135
|
+
await expect(createCommand('demo-app', { remote: 'bitbucket' as 's3' })).rejects.toThrow(/Unknown remote/);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it('rejects --remote=github without --repo-url', async () => {
|
|
139
|
+
await makeWorkspace(tmp);
|
|
140
|
+
await expect(createCommand('demo-app', { remote: 'github' })).rejects.toThrow(/--repo-url is required/);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it('calls initRepo with the S3 remote for --remote=s3', async () => {
|
|
144
|
+
await makeWorkspace(tmp);
|
|
145
|
+
await createCommand('my-app', { remote: 's3' });
|
|
146
|
+
expect(vi.mocked(defaultS3Remote)).toHaveBeenCalledWith('my-app');
|
|
147
|
+
expect(vi.mocked(initRepo)).toHaveBeenCalledWith(
|
|
148
|
+
path.join(tmp, 'my-app'),
|
|
149
|
+
's3://prod-use1-bigbrain-zth-mf-assets/mf-my-app/git'
|
|
117
150
|
);
|
|
118
151
|
});
|
|
152
|
+
|
|
153
|
+
it('calls initRepo with the repo URL for --remote=github', async () => {
|
|
154
|
+
await makeWorkspace(tmp);
|
|
155
|
+
await createCommand('gh-app', { remote: 'github', repoUrl: 'https://github.com/acme/gh-app' });
|
|
156
|
+
expect(vi.mocked(initRepo)).toHaveBeenCalledWith(path.join(tmp, 'gh-app'), 'https://github.com/acme/gh-app');
|
|
157
|
+
});
|
|
119
158
|
});
|
package/src/commands/create.ts
CHANGED
|
@@ -8,11 +8,17 @@ import { readManifest } from '../util/manifest';
|
|
|
8
8
|
import { mfAppName } from '../util/app-name';
|
|
9
9
|
import { APP_NAME_PATTERN } from '../constants';
|
|
10
10
|
import { runYarnInstall } from '../util/npm/install';
|
|
11
|
+
import { initRepo, defaultS3Remote } from '../util/git/repo';
|
|
11
12
|
import { buildCommand } from './build';
|
|
12
13
|
|
|
14
|
+
export const REMOTE_TYPES = ['s3', 'github'] as const;
|
|
15
|
+
export type RemoteType = (typeof REMOTE_TYPES)[number];
|
|
16
|
+
|
|
13
17
|
export interface CreateOptions {
|
|
14
18
|
template?: string;
|
|
15
19
|
htmlFile?: string;
|
|
20
|
+
remote?: RemoteType;
|
|
21
|
+
repoUrl?: string;
|
|
16
22
|
json?: boolean;
|
|
17
23
|
}
|
|
18
24
|
|
|
@@ -64,6 +70,17 @@ export async function createCommand(appName: string, opts: CreateOptions = {}):
|
|
|
64
70
|
);
|
|
65
71
|
}
|
|
66
72
|
|
|
73
|
+
// Validate --remote up front before any file I/O.
|
|
74
|
+
if (!opts.remote) {
|
|
75
|
+
throw new Error('--remote is required. Use --remote=s3 (shared storage) or --remote=github (your GitHub repo).');
|
|
76
|
+
}
|
|
77
|
+
if (!(REMOTE_TYPES as readonly string[]).includes(opts.remote)) {
|
|
78
|
+
throw new Error(`Unknown remote "${opts.remote}". Use ${REMOTE_TYPES.join(' or ')}.`);
|
|
79
|
+
}
|
|
80
|
+
if (opts.remote === 'github' && !opts.repoUrl) {
|
|
81
|
+
throw new Error('--repo-url is required when using --remote=github.');
|
|
82
|
+
}
|
|
83
|
+
|
|
67
84
|
// Resolve template.
|
|
68
85
|
// `--html-file` implies `--template html-embed` unless the user already picked one.
|
|
69
86
|
const template: AppTemplate = (opts.template ?? (opts.htmlFile ? 'html-embed' : 'default')) as AppTemplate;
|
|
@@ -116,6 +133,15 @@ export async function createCommand(appName: string, opts: CreateOptions = {}):
|
|
|
116
133
|
await copy(opts.htmlFile, path.join(appDir, 'src', 'page.html'));
|
|
117
134
|
}
|
|
118
135
|
|
|
136
|
+
info(`initializing git repo for ${appName}...`);
|
|
137
|
+
let gitRemote: string;
|
|
138
|
+
if (opts.remote === 's3') {
|
|
139
|
+
gitRemote = defaultS3Remote(appName);
|
|
140
|
+
} else {
|
|
141
|
+
gitRemote = opts.repoUrl as string;
|
|
142
|
+
}
|
|
143
|
+
await initRepo(appDir, gitRemote);
|
|
144
|
+
|
|
119
145
|
info(`installing dependencies for ${appName}...`);
|
|
120
146
|
await runYarnInstall({ cwd: workspace });
|
|
121
147
|
|
|
@@ -136,5 +162,7 @@ export async function createCommand(appName: string, opts: CreateOptions = {}):
|
|
|
136
162
|
appDir,
|
|
137
163
|
appName,
|
|
138
164
|
template,
|
|
165
|
+
remote: opts.remote,
|
|
166
|
+
gitRemote,
|
|
139
167
|
});
|
|
140
168
|
}
|
package/src/commands/deploy.ts
CHANGED
|
@@ -16,6 +16,8 @@ import { BUCKET_WRITE_NAME, BUCKET_READ_DNS_URL, brokerBaseUrl } from '../consta
|
|
|
16
16
|
import { setupS3Client } from '../util/s3/client';
|
|
17
17
|
import { readZthAuth } from '../util/auth/auth-store';
|
|
18
18
|
import { cleanAppName, mfAppName } from '../util/app-name';
|
|
19
|
+
import { getMachineAppName } from '../util/machine-identity';
|
|
20
|
+
import { getRemoteUrl } from '../util/git/repo';
|
|
19
21
|
import { buildCommand } from './build';
|
|
20
22
|
|
|
21
23
|
export interface DeployOptions {
|
|
@@ -61,12 +63,14 @@ export async function deployCommand(opts: DeployOptions = {}): Promise<void> {
|
|
|
61
63
|
}
|
|
62
64
|
const assetManifest = (await readJson(assetManifestPath)) as AssetManifestFile;
|
|
63
65
|
|
|
64
|
-
const deployedBy = os.userInfo().username;
|
|
66
|
+
const deployedBy = getMachineAppName() ?? os.userInfo().username;
|
|
67
|
+
const gitRemote = await getRemoteUrl(process.cwd());
|
|
65
68
|
const entry: ManifestEntry = {
|
|
66
69
|
...assetManifest,
|
|
67
70
|
version,
|
|
68
71
|
url,
|
|
69
72
|
deployedBy,
|
|
73
|
+
gitRemote,
|
|
70
74
|
};
|
|
71
75
|
|
|
72
76
|
if (opts.dryRun) {
|
package/src/index.ts
CHANGED
|
@@ -9,7 +9,7 @@ import { buildCommand } from './commands/build';
|
|
|
9
9
|
import { cleanCommand } from './commands/clean';
|
|
10
10
|
import { deployCommand } from './commands/deploy';
|
|
11
11
|
import { createWorkspaceCommand } from './commands/create-workspace';
|
|
12
|
-
import { createCommand } from './commands/create';
|
|
12
|
+
import { createCommand, CreateOptions } from './commands/create';
|
|
13
13
|
import { generateZ2HTokenCommand } from './commands/generate-z2h-token';
|
|
14
14
|
import { migrateCommand } from './commands/migrate';
|
|
15
15
|
import { errorLog, fail, getOutputMode } from './util/logger';
|
|
@@ -142,17 +142,25 @@ async function main(): Promise<void> {
|
|
|
142
142
|
.description('Scaffold a new Z2H consumer app inside ~/zth-projects/')
|
|
143
143
|
.option('--template <name>', 'Template name (default | html-embed)')
|
|
144
144
|
.option('--html-file <path>', 'Path to an HTML file to embed (implies --template html-embed)')
|
|
145
|
+
.option('--remote <type>', 'Source control remote: s3 (shared storage) or github (your repo)')
|
|
146
|
+
.option('--repo-url <url>', 'GitHub repo URL — required when --remote=github')
|
|
145
147
|
.option('--json', 'emit structured JSON on stdout')
|
|
146
|
-
.action(
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
148
|
+
.action(
|
|
149
|
+
async (
|
|
150
|
+
appName: string,
|
|
151
|
+
opts: { template?: string; htmlFile?: string; remote?: string; repoUrl?: string; json?: boolean }
|
|
152
|
+
) => {
|
|
153
|
+
try {
|
|
154
|
+
await trackDuration('z2h_app_created', () => createCommand(appName, opts as CreateOptions), {
|
|
155
|
+
app_name: appName,
|
|
156
|
+
template: opts.template ?? 'default',
|
|
157
|
+
remote: opts.remote ?? 'unknown',
|
|
158
|
+
});
|
|
159
|
+
} catch (err) {
|
|
160
|
+
await fail(`create failed: ${(err as Error).message}`, { command: 'create' }, 1);
|
|
161
|
+
}
|
|
154
162
|
}
|
|
155
|
-
|
|
163
|
+
);
|
|
156
164
|
|
|
157
165
|
program
|
|
158
166
|
.command('migrate')
|
|
@@ -20,6 +20,15 @@ export class Z2hAuthMissingError extends Error {
|
|
|
20
20
|
}
|
|
21
21
|
|
|
22
22
|
export async function readZthAuth(): Promise<ZthAuth> {
|
|
23
|
+
if (process.env.Z2H_AUTH_TOKEN) {
|
|
24
|
+
const token = process.env.Z2H_AUTH_TOKEN;
|
|
25
|
+
if (!/^[\w-]+\.[\w-]+\.[\w-]+$/.test(token)) {
|
|
26
|
+
throw new Error(
|
|
27
|
+
'Z2H_AUTH_TOKEN is not a valid JWT (expected three dot-separated segments). Please re-set the environment variable.'
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
return { token };
|
|
31
|
+
}
|
|
23
32
|
if (!(await pathExists(Z2H_AUTH_FILE))) {
|
|
24
33
|
throw new Z2hAuthMissingError(Z2H_AUTH_FILE);
|
|
25
34
|
}
|
|
@@ -40,6 +49,9 @@ export async function readZthAuth(): Promise<ZthAuth> {
|
|
|
40
49
|
* blow up — and where an inline sync read keeps the spawn call site simple.
|
|
41
50
|
*/
|
|
42
51
|
export function tryReadAuthTokenSync(): string | null {
|
|
52
|
+
if (process.env.Z2H_AUTH_TOKEN) {
|
|
53
|
+
return process.env.Z2H_AUTH_TOKEN;
|
|
54
|
+
}
|
|
43
55
|
try {
|
|
44
56
|
const parsed = readJsonSync(Z2H_AUTH_FILE) as Partial<ZthAuth> | null;
|
|
45
57
|
return parsed?.token ?? null;
|
|
@@ -6,13 +6,13 @@ import fs from 'fs-extra';
|
|
|
6
6
|
import {
|
|
7
7
|
GitConflictError,
|
|
8
8
|
defaultS3Remote,
|
|
9
|
+
initRepo,
|
|
9
10
|
cloneRepo,
|
|
10
11
|
safeBackupIfDirty,
|
|
11
12
|
pullAndResolve,
|
|
12
13
|
remoteHead,
|
|
13
14
|
localHead,
|
|
14
15
|
commitDetailedAndPush,
|
|
15
|
-
initAndPush,
|
|
16
16
|
} from '../repo';
|
|
17
17
|
|
|
18
18
|
vi.mock('execa', () => ({ execa: vi.fn() }));
|
|
@@ -42,7 +42,7 @@ beforeEach(() => {
|
|
|
42
42
|
|
|
43
43
|
describe('defaultS3Remote', () => {
|
|
44
44
|
it('returns the expected S3 URL', () => {
|
|
45
|
-
expect(defaultS3Remote('my-app')).toBe('s3://prod-use1-bigbrain-zth-mf-assets/my-app/git');
|
|
45
|
+
expect(defaultS3Remote('my-app')).toBe('s3://prod-use1-bigbrain-zth-mf-assets/mf-my-app/git');
|
|
46
46
|
});
|
|
47
47
|
});
|
|
48
48
|
|
|
@@ -56,27 +56,27 @@ describe('cloneRepo', () => {
|
|
|
56
56
|
});
|
|
57
57
|
});
|
|
58
58
|
|
|
59
|
-
// ──
|
|
59
|
+
// ── initRepo ─────────────────────────────────────────────────────────────────
|
|
60
60
|
|
|
61
|
-
describe('
|
|
62
|
-
it('runs the correct git command sequence', async () => {
|
|
61
|
+
describe('initRepo', () => {
|
|
62
|
+
it('runs the correct git command sequence without pushing', async () => {
|
|
63
63
|
mockExeca.mockResolvedValue(execaOk());
|
|
64
64
|
|
|
65
|
-
await
|
|
65
|
+
await initRepo(DIR, 's3://bucket/my-app/git');
|
|
66
66
|
|
|
67
67
|
const calls = mockExeca.mock.calls.map(c => c.slice(0, 2));
|
|
68
68
|
expect(calls).toContainEqual(['git', ['init', '-b', 'master']]);
|
|
69
69
|
expect(calls).toContainEqual(['git', ['add', '.']]);
|
|
70
70
|
expect(calls).toContainEqual(['git', ['commit', '-m', 'Initial commit']]);
|
|
71
71
|
expect(calls).toContainEqual(['git', ['remote', 'add', 'origin', 's3://bucket/my-app/git']]);
|
|
72
|
-
expect(calls).toContainEqual(['git', ['push'
|
|
72
|
+
expect(calls).not.toContainEqual(['git', expect.arrayContaining(['push'])]);
|
|
73
73
|
});
|
|
74
74
|
|
|
75
75
|
it('writes missing gitignore entries', async () => {
|
|
76
76
|
mockFs.readFile.mockResolvedValueOnce('' as never);
|
|
77
77
|
mockExeca.mockResolvedValue(execaOk());
|
|
78
78
|
|
|
79
|
-
await
|
|
79
|
+
await initRepo(DIR, 's3://bucket/my-app/git');
|
|
80
80
|
|
|
81
81
|
expect(mockFs.outputFile).toHaveBeenCalledWith(
|
|
82
82
|
path.join(DIR, '.gitignore'),
|
|
@@ -88,7 +88,7 @@ describe('initAndPush', () => {
|
|
|
88
88
|
mockFs.readFile.mockResolvedValueOnce('node_modules/\n.zth/\nbuild/\n' as never);
|
|
89
89
|
mockExeca.mockResolvedValue(execaOk());
|
|
90
90
|
|
|
91
|
-
await
|
|
91
|
+
await initRepo(DIR, 's3://bucket/my-app/git');
|
|
92
92
|
|
|
93
93
|
expect(mockFs.outputFile).not.toHaveBeenCalled();
|
|
94
94
|
});
|
|
@@ -165,12 +165,20 @@ describe('localHead', () => {
|
|
|
165
165
|
// ── commitDetailedAndPush ────────────────────────────────────────────────────
|
|
166
166
|
|
|
167
167
|
describe('commitDetailedAndPush', () => {
|
|
168
|
+
it('throws when not on master branch', async () => {
|
|
169
|
+
mockExeca.mockResolvedValueOnce(execaOk('feature/oops\n')); // git rev-parse --abbrev-ref HEAD
|
|
170
|
+
|
|
171
|
+
await expect(commitDetailedAndPush(DIR, 1)).rejects.toThrow(/expected branch "master"/);
|
|
172
|
+
});
|
|
173
|
+
|
|
168
174
|
it('commits with a version-prefixed message and pushes', async () => {
|
|
169
175
|
mockExeca
|
|
170
|
-
.mockResolvedValueOnce(execaOk('
|
|
176
|
+
.mockResolvedValueOnce(execaOk('master\n')) // git rev-parse --abbrev-ref HEAD
|
|
171
177
|
.mockResolvedValueOnce(execaOk()) // git add .
|
|
178
|
+
.mockResolvedValueOnce(execaOk('src/index.ts\nnew-file.ts')) // git diff --cached --name-only
|
|
179
|
+
.mockResolvedValueOnce(execaOk('M src/index.ts\nA new-file.ts')) // git status --short
|
|
172
180
|
.mockResolvedValueOnce(execaOk()) // git commit
|
|
173
|
-
.mockResolvedValueOnce(execaOk()); // git push
|
|
181
|
+
.mockResolvedValueOnce(execaOk()); // git push -u origin master
|
|
174
182
|
|
|
175
183
|
await commitDetailedAndPush(DIR, 3);
|
|
176
184
|
|
|
@@ -179,25 +187,56 @@ describe('commitDetailedAndPush', () => {
|
|
|
179
187
|
const msg = (commitCall![1] as string[])[2];
|
|
180
188
|
expect(msg).toMatch(/^v3:/);
|
|
181
189
|
expect(msg).toContain('src/index.ts');
|
|
190
|
+
|
|
191
|
+
const pushCall = mockExeca.mock.calls.find(c => c[0] === 'git' && (c[1] as string[])[0] === 'push');
|
|
192
|
+
expect(pushCall![1]).toEqual(['push', '-u', 'origin', 'master']);
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
it('throws when there is nothing to commit', async () => {
|
|
196
|
+
mockExeca
|
|
197
|
+
.mockResolvedValueOnce(execaOk('master\n')) // git rev-parse --abbrev-ref HEAD
|
|
198
|
+
.mockResolvedValueOnce(execaOk()) // git add .
|
|
199
|
+
.mockResolvedValueOnce(execaOk('')); // git diff --cached --name-only (empty)
|
|
200
|
+
|
|
201
|
+
await expect(commitDetailedAndPush(DIR, 4)).rejects.toThrow(/Nothing new to deploy/);
|
|
182
202
|
});
|
|
183
203
|
|
|
184
204
|
it('throws GitConflictError when push is rejected', async () => {
|
|
185
205
|
mockExeca
|
|
186
|
-
.mockResolvedValueOnce(execaOk('')) // git
|
|
206
|
+
.mockResolvedValueOnce(execaOk('master\n')) // git rev-parse --abbrev-ref HEAD
|
|
187
207
|
.mockResolvedValueOnce(execaOk()) // git add .
|
|
208
|
+
.mockResolvedValueOnce(execaOk('src/index.ts')) // git diff --cached --name-only
|
|
209
|
+
.mockResolvedValueOnce(execaOk(' M src/index.ts')) // git status --short
|
|
188
210
|
.mockResolvedValueOnce(execaOk()) // git commit
|
|
189
|
-
.mockRejectedValueOnce(new Error('[rejected]')); // git push
|
|
211
|
+
.mockRejectedValueOnce(new Error('[rejected]')); // git push -u origin master
|
|
190
212
|
|
|
191
213
|
await expect(commitDetailedAndPush(DIR, 4)).rejects.toThrow(GitConflictError);
|
|
192
214
|
});
|
|
193
215
|
|
|
216
|
+
it('re-throws non-rejection push errors as-is', async () => {
|
|
217
|
+
mockExeca
|
|
218
|
+
.mockResolvedValueOnce(execaOk('master\n')) // git rev-parse --abbrev-ref HEAD
|
|
219
|
+
.mockResolvedValueOnce(execaOk()) // git add .
|
|
220
|
+
.mockResolvedValueOnce(execaOk('src/index.ts')) // git diff --cached --name-only
|
|
221
|
+
.mockResolvedValueOnce(execaOk(' M src/index.ts')) // git status --short
|
|
222
|
+
.mockResolvedValueOnce(execaOk()) // git commit
|
|
223
|
+
.mockRejectedValueOnce(new Error('Connection refused')); // git push — auth/network error
|
|
224
|
+
|
|
225
|
+
const err = await commitDetailedAndPush(DIR, 4).catch(e => e);
|
|
226
|
+
expect(err).not.toBeInstanceOf(GitConflictError);
|
|
227
|
+
expect(err.message).toMatch(/Connection refused/);
|
|
228
|
+
});
|
|
229
|
+
|
|
194
230
|
it('truncates long file lists to 5 entries', async () => {
|
|
195
|
-
const manyFiles = Array.from({ length: 8 }, (_, i) => `
|
|
231
|
+
const manyFiles = Array.from({ length: 8 }, (_, i) => `M file${i}.ts`).join('\n');
|
|
232
|
+
const manyNames = Array.from({ length: 8 }, (_, i) => `file${i}.ts`).join('\n');
|
|
196
233
|
mockExeca
|
|
197
|
-
.mockResolvedValueOnce(execaOk(
|
|
198
|
-
.mockResolvedValueOnce(execaOk())
|
|
199
|
-
.mockResolvedValueOnce(execaOk())
|
|
200
|
-
.mockResolvedValueOnce(execaOk())
|
|
234
|
+
.mockResolvedValueOnce(execaOk('master\n')) // git rev-parse --abbrev-ref HEAD
|
|
235
|
+
.mockResolvedValueOnce(execaOk()) // git add .
|
|
236
|
+
.mockResolvedValueOnce(execaOk(manyNames)) // git diff --cached --name-only
|
|
237
|
+
.mockResolvedValueOnce(execaOk(manyFiles)) // git status --short
|
|
238
|
+
.mockResolvedValueOnce(execaOk()) // git commit
|
|
239
|
+
.mockResolvedValueOnce(execaOk()); // git push
|
|
201
240
|
|
|
202
241
|
await commitDetailedAndPush(DIR, 1);
|
|
203
242
|
|
package/src/util/git/repo.ts
CHANGED
|
@@ -2,6 +2,7 @@ import path from 'node:path';
|
|
|
2
2
|
import fs from 'fs-extra';
|
|
3
3
|
import { execa } from 'execa';
|
|
4
4
|
import { BUCKET_WRITE_NAME } from '../../constants';
|
|
5
|
+
import { mfAppName } from '../app-name';
|
|
5
6
|
|
|
6
7
|
export class GitConflictError extends Error {
|
|
7
8
|
constructor(message: string) {
|
|
@@ -13,16 +14,21 @@ export class GitConflictError extends Error {
|
|
|
13
14
|
const REQUIRED_GITIGNORE_ENTRIES = ['node_modules/', '.zth/', 'build/'];
|
|
14
15
|
|
|
15
16
|
export function defaultS3Remote(app: string): string {
|
|
16
|
-
return `s3://${BUCKET_WRITE_NAME}/${app}/git`;
|
|
17
|
+
return `s3://${BUCKET_WRITE_NAME}/${mfAppName(app)}/git`;
|
|
17
18
|
}
|
|
18
19
|
|
|
19
|
-
export async function
|
|
20
|
+
export async function initRepo(dir: string, remote: string): Promise<void> {
|
|
20
21
|
await ensureGitignore(dir);
|
|
21
22
|
await execa('git', ['init', '-b', 'master'], { cwd: dir });
|
|
22
23
|
await execa('git', ['add', '.'], { cwd: dir });
|
|
23
24
|
await execa('git', ['commit', '-m', 'Initial commit'], { cwd: dir });
|
|
24
25
|
await execa('git', ['remote', 'add', 'origin', remote], { cwd: dir });
|
|
25
|
-
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export async function getRemoteUrl(dir: string): Promise<string | undefined> {
|
|
29
|
+
return execa('git', ['remote', 'get-url', 'origin'], { cwd: dir })
|
|
30
|
+
.then(r => r.stdout.trim() || undefined)
|
|
31
|
+
.catch(() => undefined);
|
|
26
32
|
}
|
|
27
33
|
|
|
28
34
|
export async function cloneRepo(remote: string, dir: string): Promise<void> {
|
|
@@ -71,13 +77,24 @@ export async function localHead(dir: string): Promise<string> {
|
|
|
71
77
|
}
|
|
72
78
|
|
|
73
79
|
export async function commitDetailedAndPush(dir: string, version: number): Promise<void> {
|
|
74
|
-
const
|
|
80
|
+
const { stdout: branch } = await execa('git', ['rev-parse', '--abbrev-ref', 'HEAD'], { cwd: dir });
|
|
81
|
+
if (branch.trim() !== 'master') {
|
|
82
|
+
throw new Error(`expected branch "master" but you are on "${branch.trim()}" — switch back before deploying.`);
|
|
83
|
+
}
|
|
75
84
|
await execa('git', ['add', '.'], { cwd: dir });
|
|
85
|
+
const { stdout: staged } = await execa('git', ['diff', '--cached', '--name-only'], { cwd: dir });
|
|
86
|
+
if (!staged.trim()) {
|
|
87
|
+
throw new Error('Nothing new to deploy — no files have changed since the last deploy.');
|
|
88
|
+
}
|
|
89
|
+
const message = await buildCommitMessage(dir, version);
|
|
76
90
|
await execa('git', ['commit', '-m', message], { cwd: dir });
|
|
77
91
|
try {
|
|
78
|
-
await execa('git', ['push'], { cwd: dir });
|
|
79
|
-
} catch {
|
|
80
|
-
|
|
92
|
+
await execa('git', ['push', '-u', 'origin', 'master'], { cwd: dir });
|
|
93
|
+
} catch (err) {
|
|
94
|
+
if (/\[rejected\]|non-fast-forward/.test((err as Error).message)) {
|
|
95
|
+
throw new GitConflictError('push rejected — remote has new commits');
|
|
96
|
+
}
|
|
97
|
+
throw err;
|
|
81
98
|
}
|
|
82
99
|
}
|
|
83
100
|
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Returns the machine app name when running as a service (e.g. agent-runner).
|
|
3
|
+
* Available when the process is a monday.com microservice with `APP_NAME` set
|
|
4
|
+
* by the platform runtime.
|
|
5
|
+
*
|
|
6
|
+
* Returns `undefined` for interactive human users (fallback to os.userInfo).
|
|
7
|
+
*/
|
|
8
|
+
export function getMachineAppName(): string | undefined {
|
|
9
|
+
return process.env.APP_NAME || undefined;
|
|
10
|
+
}
|
package/src/util/manifest.ts
CHANGED
|
@@ -23,13 +23,13 @@ export interface AssetManifestFile {
|
|
|
23
23
|
nonExternalsEntrypoints?: string[];
|
|
24
24
|
scriptType?: 'module' | 'classic';
|
|
25
25
|
mfs?: string[];
|
|
26
|
-
gitRemote: string;
|
|
27
26
|
}
|
|
28
27
|
|
|
29
28
|
export interface ManifestEntry extends AssetManifestFile {
|
|
30
29
|
version: number;
|
|
31
30
|
url: string;
|
|
32
31
|
deployedBy: string;
|
|
32
|
+
gitRemote?: string;
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
export type Manifest = Record<string, ManifestEntry>;
|