@mondaydotcomorg/z2h-cli 0.10.1 → 0.11.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/__tests__/edit.test.d.ts +2 -0
- package/dist/commands/__tests__/edit.test.d.ts.map +1 -0
- package/dist/commands/create.d.ts +2 -2
- package/dist/commands/create.d.ts.map +1 -1
- package/dist/commands/create.js +14 -34
- package/dist/commands/deploy.d.ts +3 -0
- package/dist/commands/deploy.d.ts.map +1 -1
- package/dist/commands/deploy.js +120 -21
- package/dist/commands/edit.d.ts +5 -0
- package/dist/commands/edit.d.ts.map +1 -0
- package/dist/commands/edit.js +61 -0
- package/dist/commands/grant.d.ts +12 -0
- package/dist/commands/grant.d.ts.map +1 -0
- package/dist/commands/grant.js +94 -0
- package/dist/constants.d.ts +1 -0
- package/dist/constants.d.ts.map +1 -1
- package/dist/constants.js +7 -0
- package/dist/esm/commands/__tests__/edit.test.d.ts +2 -0
- package/dist/esm/commands/__tests__/edit.test.d.ts.map +1 -0
- package/dist/esm/commands/create.d.ts +2 -2
- package/dist/esm/commands/create.d.ts.map +1 -1
- package/dist/esm/commands/create.mjs +16 -35
- package/dist/esm/commands/deploy.d.ts +3 -0
- package/dist/esm/commands/deploy.d.ts.map +1 -1
- package/dist/esm/commands/deploy.mjs +122 -23
- package/dist/esm/commands/edit.d.ts +5 -0
- package/dist/esm/commands/edit.d.ts.map +1 -0
- package/dist/esm/commands/edit.mjs +55 -0
- package/dist/esm/commands/grant.d.ts +12 -0
- package/dist/esm/commands/grant.d.ts.map +1 -0
- package/dist/esm/commands/grant.mjs +90 -0
- package/dist/esm/constants.d.ts +1 -0
- package/dist/esm/constants.d.ts.map +1 -1
- package/dist/esm/constants.mjs +7 -1
- package/dist/esm/index.mjs +67 -10
- package/dist/esm/shadow/prepare.d.ts +1 -1
- package/dist/esm/shadow/prepare.d.ts.map +1 -1
- package/dist/esm/shadow/prepare.mjs +2 -2
- package/dist/esm/util/app-name.d.ts +1 -0
- package/dist/esm/util/app-name.d.ts.map +1 -1
- package/dist/esm/util/app-name.mjs +11 -1
- package/dist/esm/util/auth/broker-credential-provider.d.ts +10 -2
- package/dist/esm/util/auth/broker-credential-provider.d.ts.map +1 -1
- package/dist/esm/util/auth/broker-credential-provider.mjs +35 -13
- package/dist/esm/util/broker/app.d.ts +14 -0
- package/dist/esm/util/broker/app.d.ts.map +1 -0
- package/dist/esm/util/broker/app.mjs +27 -0
- package/dist/esm/util/git/git-env.d.ts +9 -0
- package/dist/esm/util/git/git-env.d.ts.map +1 -0
- package/dist/esm/util/git/git-env.mjs +22 -0
- package/dist/esm/util/git/remote-options.d.ts +17 -0
- package/dist/esm/util/git/remote-options.d.ts.map +1 -0
- package/dist/esm/util/git/remote-options.mjs +30 -0
- package/dist/esm/util/git/repo.d.ts +21 -3
- package/dist/esm/util/git/repo.d.ts.map +1 -1
- package/dist/esm/util/git/repo.mjs +96 -48
- package/dist/esm/util/s3/client.d.ts +2 -7
- package/dist/esm/util/s3/client.d.ts.map +1 -1
- package/dist/esm/util/s3/client.mjs +3 -11
- package/dist/index.js +69 -12
- package/dist/shadow/prepare.d.ts +1 -1
- package/dist/shadow/prepare.d.ts.map +1 -1
- package/dist/shadow/prepare.js +2 -2
- package/dist/util/app-name.d.ts +1 -0
- package/dist/util/app-name.d.ts.map +1 -1
- package/dist/util/app-name.js +11 -0
- package/dist/util/auth/broker-credential-provider.d.ts +10 -2
- package/dist/util/auth/broker-credential-provider.d.ts.map +1 -1
- package/dist/util/auth/broker-credential-provider.js +35 -11
- package/dist/util/broker/app.d.ts +14 -0
- package/dist/util/broker/app.d.ts.map +1 -0
- package/dist/util/broker/app.js +29 -0
- package/dist/util/git/git-env.d.ts +9 -0
- package/dist/util/git/git-env.d.ts.map +1 -0
- package/dist/util/git/git-env.js +24 -0
- package/dist/util/git/remote-options.d.ts +17 -0
- package/dist/util/git/remote-options.d.ts.map +1 -0
- package/dist/util/git/remote-options.js +34 -0
- package/dist/util/git/repo.d.ts +21 -3
- package/dist/util/git/repo.d.ts.map +1 -1
- package/dist/util/git/repo.js +97 -48
- package/dist/util/s3/client.d.ts +2 -7
- package/dist/util/s3/client.d.ts.map +1 -1
- package/dist/util/s3/client.js +3 -11
- package/package.json +1 -1
- package/src/commands/__tests__/create.test.ts +9 -7
- package/src/commands/__tests__/edit.test.ts +184 -0
- package/src/commands/create.ts +15 -33
- package/src/commands/deploy.ts +145 -31
- package/src/commands/edit.ts +63 -0
- package/src/commands/grant.ts +115 -0
- package/src/constants.ts +7 -0
- package/src/index.ts +83 -13
- package/src/shadow/prepare.ts +2 -2
- package/src/templates/app/CLAUDE.md +15 -0
- package/src/util/app-name.ts +10 -0
- package/src/util/auth/__tests__/broker-credential-provider.test.ts +34 -2
- package/src/util/auth/broker-credential-provider.ts +43 -15
- package/src/util/broker/app.ts +34 -0
- package/src/util/git/__tests__/repo.test.ts +114 -90
- package/src/util/git/git-env.ts +23 -0
- package/src/util/git/remote-options.ts +37 -0
- package/src/util/git/repo.ts +109 -48
- package/src/util/s3/client.ts +4 -11
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
|
|
2
|
+
|
|
3
|
+
const util_auth_brokerCredentialProvider = require('../auth/broker-credential-provider.js');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Builds the AWS_* environment that `git-remote-s3` needs to talk to an S3-backed
|
|
7
|
+
* git remote, by minting short-lived broker credentials scoped to the app.
|
|
8
|
+
*
|
|
9
|
+
* Returns `undefined` for non-S3 remotes (GitHub apps ride the user's own git
|
|
10
|
+
* credential helper) and when there is no remote at all.
|
|
11
|
+
*/
|
|
12
|
+
async function resolveGitEnv(appName, gitRemote) {
|
|
13
|
+
if (!gitRemote?.startsWith('s3://')) {
|
|
14
|
+
return undefined;
|
|
15
|
+
}
|
|
16
|
+
const creds = await util_auth_brokerCredentialProvider.brokerCredentialProvider({ appName })();
|
|
17
|
+
return {
|
|
18
|
+
AWS_ACCESS_KEY_ID: creds.accessKeyId,
|
|
19
|
+
AWS_SECRET_ACCESS_KEY: creds.secretAccessKey,
|
|
20
|
+
AWS_SESSION_TOKEN: creds.sessionToken ?? '',
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
exports.resolveGitEnv = resolveGitEnv;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export declare const REMOTE_TYPES: readonly ["s3", "github"];
|
|
2
|
+
export type RemoteType = (typeof REMOTE_TYPES)[number];
|
|
3
|
+
export interface RemoteOptions {
|
|
4
|
+
remote?: RemoteType;
|
|
5
|
+
repoUrl?: string;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Validates --remote and --repo-url option values.
|
|
9
|
+
* Pass `requireRemote: true` when the flag is mandatory (e.g. create).
|
|
10
|
+
*/
|
|
11
|
+
export declare function validateRemoteOptions(opts: RemoteOptions, requireRemote?: boolean): void;
|
|
12
|
+
/**
|
|
13
|
+
* Resolves the git remote URL from --remote / --repo-url options.
|
|
14
|
+
* Call after validateRemoteOptions.
|
|
15
|
+
*/
|
|
16
|
+
export declare function resolveRemoteUrl(remote: RemoteType, appName: string, repoUrl?: string): string;
|
|
17
|
+
//# sourceMappingURL=remote-options.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"remote-options.d.ts","sourceRoot":"","sources":["../../../src/util/git/remote-options.ts"],"names":[],"mappings":"AAEA,eAAO,MAAM,YAAY,2BAA4B,CAAC;AAEtD,MAAM,MAAM,UAAU,GAAG,CAAC,OAAO,YAAY,CAAC,CAAC,MAAM,CAAC,CAAC;AAEvD,MAAM,WAAW,aAAa;IAC5B,MAAM,CAAC,EAAE,UAAU,CAAC;IACpB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED;;;GAGG;AACH,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,aAAa,EAAE,aAAa,UAAQ,GAAG,IAAI,CAatF;AAED;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,UAAU,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,GAAG,MAAM,CAE9F"}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
|
|
2
|
+
|
|
3
|
+
const util_git_repo = require('./repo.js');
|
|
4
|
+
|
|
5
|
+
const REMOTE_TYPES = ['s3', 'github'];
|
|
6
|
+
/**
|
|
7
|
+
* Validates --remote and --repo-url option values.
|
|
8
|
+
* Pass `requireRemote: true` when the flag is mandatory (e.g. create).
|
|
9
|
+
*/
|
|
10
|
+
function validateRemoteOptions(opts, requireRemote = false) {
|
|
11
|
+
if (!opts.remote) {
|
|
12
|
+
if (requireRemote) {
|
|
13
|
+
throw new Error(`--remote is required. Use --remote=s3 (shared storage) or --remote=github (your GitHub repo).`);
|
|
14
|
+
}
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
if (!REMOTE_TYPES.includes(opts.remote)) {
|
|
18
|
+
throw new Error(`Unknown remote "${opts.remote}". Use ${REMOTE_TYPES.join(' or ')}.`);
|
|
19
|
+
}
|
|
20
|
+
if (opts.remote === 'github' && !opts.repoUrl) {
|
|
21
|
+
throw new Error('--repo-url is required when using --remote=github.');
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Resolves the git remote URL from --remote / --repo-url options.
|
|
26
|
+
* Call after validateRemoteOptions.
|
|
27
|
+
*/
|
|
28
|
+
function resolveRemoteUrl(remote, appName, repoUrl) {
|
|
29
|
+
return remote === 's3' ? util_git_repo.defaultS3Remote(appName) : repoUrl;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
exports.REMOTE_TYPES = REMOTE_TYPES;
|
|
33
|
+
exports.resolveRemoteUrl = resolveRemoteUrl;
|
|
34
|
+
exports.validateRemoteOptions = validateRemoteOptions;
|
package/dist/util/git/repo.d.ts
CHANGED
|
@@ -1,13 +1,31 @@
|
|
|
1
1
|
export declare class GitConflictError extends Error {
|
|
2
2
|
constructor(message: string);
|
|
3
3
|
}
|
|
4
|
+
export interface ConflictInfo {
|
|
5
|
+
localCommits: string[];
|
|
6
|
+
remoteCommits: string[];
|
|
7
|
+
conflictingFiles: string[];
|
|
8
|
+
}
|
|
4
9
|
export declare function defaultS3Remote(app: string): string;
|
|
5
10
|
export declare function initRepo(dir: string, remote: string): Promise<void>;
|
|
6
11
|
export declare function getRemoteUrl(dir: string): Promise<string | undefined>;
|
|
7
|
-
|
|
12
|
+
/**
|
|
13
|
+
* Throws if the working tree has uncommitted changes. Call only on a known git
|
|
14
|
+
* repo (e.g. guarded by a resolved remote) — a failing `git status` propagates
|
|
15
|
+
* rather than being silently treated as "clean".
|
|
16
|
+
*/
|
|
17
|
+
export declare function guardCleanTree(dir: string): Promise<void>;
|
|
18
|
+
export declare function cloneRepo(remote: string, dir: string, env?: Record<string, string>): Promise<void>;
|
|
8
19
|
export declare function safeBackupIfDirty(dir: string): Promise<void>;
|
|
9
|
-
export declare function pullAndResolve(dir: string): Promise<void>;
|
|
20
|
+
export declare function pullAndResolve(dir: string, env?: Record<string, string>): Promise<void>;
|
|
21
|
+
/**
|
|
22
|
+
* Pushes the local master branch to origin. On a rejected (non-fast-forward) push —
|
|
23
|
+
* i.e. a deploy landed while we were building — gathers structured conflict info and
|
|
24
|
+
* throws GitConflictError so the deploy stops before anything goes live (spec: push
|
|
25
|
+
* must stop the deploy before the bundle upload). The skill re-resolves and re-runs,
|
|
26
|
+
* which rebuilds against the latest. Non-conflict push errors re-throw as-is.
|
|
27
|
+
*/
|
|
28
|
+
export declare function pushToRemote(dir: string, env?: Record<string, string>): Promise<void>;
|
|
10
29
|
export declare function remoteHead(dir: string): Promise<string>;
|
|
11
30
|
export declare function localHead(dir: string): Promise<string>;
|
|
12
|
-
export declare function commitDetailedAndPush(dir: string, version: number): Promise<void>;
|
|
13
31
|
//# sourceMappingURL=repo.d.ts.map
|
|
@@ -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":"AAkBA,qBAAa,gBAAiB,SAAQ,KAAK;gBAC7B,OAAO,EAAE,MAAM;CAI5B;AAED,MAAM,WAAW,YAAY;IAC3B,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB,aAAa,EAAE,MAAM,EAAE,CAAC;IACxB,gBAAgB,EAAE,MAAM,EAAE,CAAC;CAC5B;AAKD,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,CAOzE;AAED,wBAAsB,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAI3E;AAED;;;;GAIG;AACH,wBAAsB,cAAc,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAK/D;AAED,wBAAsB,SAAS,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,GAAG,GAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAG5G;AAED,wBAAsB,iBAAiB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAclE;AAED,wBAAsB,cAAc,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAe7F;AAED;;;;;;GAMG;AACH,wBAAsB,YAAY,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAqB3F;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"}
|
package/dist/util/git/repo.js
CHANGED
|
@@ -11,6 +11,14 @@ const _interopDefault = e => e && e.__esModule ? e : { default: e };
|
|
|
11
11
|
const path__default = /*#__PURE__*/_interopDefault(path);
|
|
12
12
|
const fs__default = /*#__PURE__*/_interopDefault(fs);
|
|
13
13
|
|
|
14
|
+
function execGit(args, dir, env) {
|
|
15
|
+
return execa.execa('git', args, { cwd: dir, env });
|
|
16
|
+
}
|
|
17
|
+
async function execGitLines(args, dir, env) {
|
|
18
|
+
return execGit(args, dir, env)
|
|
19
|
+
.then(r => r.stdout.split('\n').filter(Boolean))
|
|
20
|
+
.catch(() => []);
|
|
21
|
+
}
|
|
14
22
|
class GitConflictError extends Error {
|
|
15
23
|
constructor(message) {
|
|
16
24
|
super(message);
|
|
@@ -18,28 +26,42 @@ class GitConflictError extends Error {
|
|
|
18
26
|
}
|
|
19
27
|
}
|
|
20
28
|
const REQUIRED_GITIGNORE_ENTRIES = ['node_modules/', '.zth/', 'build/'];
|
|
29
|
+
const LOCAL_HOOKS_DIR = '.zth/hooks';
|
|
21
30
|
function defaultS3Remote(app) {
|
|
22
31
|
return `s3://${constants.BUCKET_WRITE_NAME}/${util_appName.mfAppName(app)}/git`;
|
|
23
32
|
}
|
|
24
33
|
async function initRepo(dir, remote) {
|
|
25
34
|
await ensureGitignore(dir);
|
|
26
|
-
await
|
|
27
|
-
await
|
|
28
|
-
await
|
|
29
|
-
await
|
|
35
|
+
await execGit(['init', '-b', 'master'], dir);
|
|
36
|
+
await disableLocalHooks(dir);
|
|
37
|
+
await execGit(['add', '.'], dir);
|
|
38
|
+
await execGit(['commit', '-m', 'Initial commit'], dir);
|
|
39
|
+
await execGit(['remote', 'add', 'origin', remote], dir);
|
|
30
40
|
}
|
|
31
41
|
async function getRemoteUrl(dir) {
|
|
32
|
-
return
|
|
42
|
+
return execGit(['remote', 'get-url', 'origin'], dir)
|
|
33
43
|
.then(r => r.stdout.trim() || undefined)
|
|
34
44
|
.catch(() => undefined);
|
|
35
45
|
}
|
|
36
|
-
|
|
37
|
-
|
|
46
|
+
/**
|
|
47
|
+
* Throws if the working tree has uncommitted changes. Call only on a known git
|
|
48
|
+
* repo (e.g. guarded by a resolved remote) — a failing `git status` propagates
|
|
49
|
+
* rather than being silently treated as "clean".
|
|
50
|
+
*/
|
|
51
|
+
async function guardCleanTree(dir) {
|
|
52
|
+
const { stdout } = await execGit(['status', '--porcelain'], dir);
|
|
53
|
+
if (stdout.trim()) {
|
|
54
|
+
throw new Error('You have uncommitted changes — commit your changes before deploying.');
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
async function cloneRepo(remote, dir, env = {}) {
|
|
58
|
+
await execa.execa('git', ['clone', remote, dir], { env: { ...process.env, ...env } });
|
|
59
|
+
await disableLocalHooks(dir);
|
|
38
60
|
}
|
|
39
61
|
async function safeBackupIfDirty(dir) {
|
|
40
62
|
let status;
|
|
41
63
|
try {
|
|
42
|
-
({ stdout: status } = await
|
|
64
|
+
({ stdout: status } = await execGit(['status', '--porcelain'], dir));
|
|
43
65
|
}
|
|
44
66
|
catch {
|
|
45
67
|
return;
|
|
@@ -52,17 +74,55 @@ async function safeBackupIfDirty(dir) {
|
|
|
52
74
|
const backupDir = path__default.default.join(path__default.default.dirname(dir), `${appName}.backup-${sha}`);
|
|
53
75
|
await fs__default.default.copy(dir, backupDir, { overwrite: false });
|
|
54
76
|
}
|
|
55
|
-
async function pullAndResolve(dir) {
|
|
77
|
+
async function pullAndResolve(dir, env) {
|
|
78
|
+
const gitEnv = env ? { ...process.env, ...env } : undefined;
|
|
56
79
|
try {
|
|
57
|
-
await
|
|
80
|
+
await execGit(['pull', '--ff-only'], dir, gitEnv);
|
|
58
81
|
}
|
|
59
|
-
catch {
|
|
60
|
-
//
|
|
61
|
-
|
|
82
|
+
catch (err) {
|
|
83
|
+
// First deploy: remote exists but has no commits yet — not a conflict.
|
|
84
|
+
const e = err;
|
|
85
|
+
const msg = `${e.message ?? ''} ${e.stderr ?? ''}`;
|
|
86
|
+
if (/couldn't find remote ref|no tracking information|does not appear to be a git repository/i.test(msg)) {
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
await safeBackupIfDirty(dir);
|
|
90
|
+
const info = await gatherConflictInfo(dir, gitEnv);
|
|
91
|
+
throw new GitConflictError(JSON.stringify(info));
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Pushes the local master branch to origin. On a rejected (non-fast-forward) push —
|
|
96
|
+
* i.e. a deploy landed while we were building — gathers structured conflict info and
|
|
97
|
+
* throws GitConflictError so the deploy stops before anything goes live (spec: push
|
|
98
|
+
* must stop the deploy before the bundle upload). The skill re-resolves and re-runs,
|
|
99
|
+
* which rebuilds against the latest. Non-conflict push errors re-throw as-is.
|
|
100
|
+
*/
|
|
101
|
+
async function pushToRemote(dir, env) {
|
|
102
|
+
const gitEnv = env ? { ...process.env, ...env } : undefined;
|
|
103
|
+
try {
|
|
104
|
+
await execGit(['push', '-u', 'origin', 'master'], dir, gitEnv);
|
|
105
|
+
}
|
|
106
|
+
catch (err) {
|
|
107
|
+
// execa puts the command in message but stderr in a separate field.
|
|
108
|
+
const e = err;
|
|
109
|
+
const msg = `${e.message ?? ''} ${e.stderr ?? ''}`;
|
|
110
|
+
const isRejected = /\[rejected\]|non-fast-forward/.test(msg);
|
|
111
|
+
// git-remote-s3 emits EPIPE when git closes its stdin after a completed
|
|
112
|
+
// push (new commits or already-up-to-date). Treat EPIPE without a
|
|
113
|
+
// rejection marker as success — the push went through.
|
|
114
|
+
if (/EPIPE/.test(msg) && !isRejected) {
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
if (isRejected) {
|
|
118
|
+
const info = await gatherConflictInfo(dir, gitEnv);
|
|
119
|
+
throw new GitConflictError(JSON.stringify(info));
|
|
120
|
+
}
|
|
121
|
+
throw err;
|
|
62
122
|
}
|
|
63
123
|
}
|
|
64
124
|
async function remoteHead(dir) {
|
|
65
|
-
const { stdout } = await
|
|
125
|
+
const { stdout } = await execGit(['ls-remote', 'origin', 'refs/heads/master'], dir);
|
|
66
126
|
const sha = stdout.split('\t')[0]?.trim();
|
|
67
127
|
if (!sha) {
|
|
68
128
|
throw new Error('could not read remote head — is the remote reachable?');
|
|
@@ -70,42 +130,30 @@ async function remoteHead(dir) {
|
|
|
70
130
|
return sha;
|
|
71
131
|
}
|
|
72
132
|
async function localHead(dir) {
|
|
73
|
-
const { stdout } = await
|
|
133
|
+
const { stdout } = await execGit(['rev-parse', 'HEAD'], dir);
|
|
74
134
|
return stdout.trim();
|
|
75
135
|
}
|
|
76
|
-
async function
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
throw new Error('Nothing new to deploy — no files have changed since the last deploy.');
|
|
85
|
-
}
|
|
86
|
-
const message = await buildCommitMessage(dir, version);
|
|
87
|
-
await execa.execa('git', ['commit', '-m', message], { cwd: dir });
|
|
88
|
-
try {
|
|
89
|
-
await execa.execa('git', ['push', '-u', 'origin', 'master'], { cwd: dir });
|
|
90
|
-
}
|
|
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;
|
|
96
|
-
}
|
|
136
|
+
async function gatherConflictInfo(dir, gitEnv) {
|
|
137
|
+
await execGit(['fetch', 'origin'], dir, gitEnv).catch(() => { });
|
|
138
|
+
const [localCommits, remoteCommits, conflictingFiles] = await Promise.all([
|
|
139
|
+
execGitLines(['log', 'origin/master..HEAD', '--oneline'], dir, gitEnv),
|
|
140
|
+
execGitLines(['log', 'HEAD..origin/master', '--oneline'], dir, gitEnv),
|
|
141
|
+
execGitLines(['diff', '--name-only', 'HEAD', 'origin/master'], dir, gitEnv),
|
|
142
|
+
]);
|
|
143
|
+
return { localCommits, remoteCommits, conflictingFiles };
|
|
97
144
|
}
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
145
|
+
/**
|
|
146
|
+
* Z2H repos are agent-driven and non-interactive — git hooks (whether from
|
|
147
|
+
* global core.hooksPath, init.templateDir, or manually placed .git/hooks/*)
|
|
148
|
+
* serve no purpose and break non-interactive pushes. Point core.hooksPath to an
|
|
149
|
+
* empty local directory so all hook sources are bypassed for this repo only.
|
|
150
|
+
*/
|
|
151
|
+
async function disableLocalHooks(dir) {
|
|
152
|
+
if (await fs__default.default.pathExists(path__default.default.join(dir, LOCAL_HOOKS_DIR))) {
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
await fs__default.default.ensureDir(path__default.default.join(dir, LOCAL_HOOKS_DIR));
|
|
156
|
+
await execGit(['config', '--local', 'core.hooksPath', LOCAL_HOOKS_DIR], dir);
|
|
109
157
|
}
|
|
110
158
|
async function ensureGitignore(dir) {
|
|
111
159
|
const gitignorePath = path__default.default.join(dir, '.gitignore');
|
|
@@ -127,11 +175,12 @@ async function ensureGitignore(dir) {
|
|
|
127
175
|
|
|
128
176
|
exports.GitConflictError = GitConflictError;
|
|
129
177
|
exports.cloneRepo = cloneRepo;
|
|
130
|
-
exports.commitDetailedAndPush = commitDetailedAndPush;
|
|
131
178
|
exports.defaultS3Remote = defaultS3Remote;
|
|
132
179
|
exports.getRemoteUrl = getRemoteUrl;
|
|
180
|
+
exports.guardCleanTree = guardCleanTree;
|
|
133
181
|
exports.initRepo = initRepo;
|
|
134
182
|
exports.localHead = localHead;
|
|
135
183
|
exports.pullAndResolve = pullAndResolve;
|
|
184
|
+
exports.pushToRemote = pushToRemote;
|
|
136
185
|
exports.remoteHead = remoteHead;
|
|
137
186
|
exports.safeBackupIfDirty = safeBackupIfDirty;
|
package/dist/util/s3/client.d.ts
CHANGED
|
@@ -1,10 +1,5 @@
|
|
|
1
1
|
import { S3Client } from '@aws-sdk/client-s3';
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
* to the given app's S3 prefix. Must be called once before any `getS3Client`.
|
|
5
|
-
*/
|
|
6
|
-
export declare function setupS3Client({ appName }: {
|
|
7
|
-
appName: string;
|
|
8
|
-
}): void;
|
|
2
|
+
import type { AwsCredentialIdentity } from '@aws-sdk/types';
|
|
3
|
+
export declare function setupS3Client(credentials: AwsCredentialIdentity): void;
|
|
9
4
|
export declare function getS3Client(): S3Client;
|
|
10
5
|
//# sourceMappingURL=client.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../../src/util/s3/client.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAC;
|
|
1
|
+
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../../src/util/s3/client.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAC;AAE9C,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,gBAAgB,CAAC;AAI5D,wBAAgB,aAAa,CAAC,WAAW,EAAE,qBAAqB,GAAG,IAAI,CAEtE;AAED,wBAAgB,WAAW,IAAI,QAAQ,CAKtC"}
|
package/dist/util/s3/client.js
CHANGED
|
@@ -1,23 +1,15 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
|
|
2
2
|
|
|
3
3
|
const clientS3 = require('@aws-sdk/client-s3');
|
|
4
|
-
const util_auth_brokerCredentialProvider = require('../auth/broker-credential-provider.js');
|
|
5
4
|
const constants = require('../../constants.js');
|
|
6
5
|
|
|
7
6
|
let client = null;
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
* to the given app's S3 prefix. Must be called once before any `getS3Client`.
|
|
11
|
-
*/
|
|
12
|
-
function setupS3Client({ appName }) {
|
|
13
|
-
client = new clientS3.S3Client({
|
|
14
|
-
region: constants.REGION,
|
|
15
|
-
credentials: util_auth_brokerCredentialProvider.brokerCredentialProvider({ appName }),
|
|
16
|
-
});
|
|
7
|
+
function setupS3Client(credentials) {
|
|
8
|
+
client = new clientS3.S3Client({ region: constants.REGION, credentials });
|
|
17
9
|
}
|
|
18
10
|
function getS3Client() {
|
|
19
11
|
if (!client) {
|
|
20
|
-
throw new Error('S3 client not initialized — call setupS3Client(
|
|
12
|
+
throw new Error('S3 client not initialized — call setupS3Client(credentials) first.');
|
|
21
13
|
}
|
|
22
14
|
return client;
|
|
23
15
|
}
|
package/package.json
CHANGED
|
@@ -5,9 +5,8 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
|
5
5
|
|
|
6
6
|
import { runYarnInstall } from '../../util/npm/install';
|
|
7
7
|
import { initRepo, defaultS3Remote } from '../../util/git/repo';
|
|
8
|
-
import {
|
|
8
|
+
import { prepareShadow } from '../../shadow/prepare';
|
|
9
9
|
import { createCommand } from '../create';
|
|
10
|
-
import * as manifest from '../../util/manifest';
|
|
11
10
|
|
|
12
11
|
// Mock the install helper so `yarn install` (with its broker preflight) doesn't
|
|
13
12
|
// actually run. Token-injection + 401 re-auth is covered in
|
|
@@ -21,12 +20,15 @@ vi.mock('../../util/git/repo', () => ({
|
|
|
21
20
|
defaultS3Remote: vi.fn((app: string) => `s3://prod-use1-bigbrain-zth-mf-assets/mf-${app}/git`),
|
|
22
21
|
}));
|
|
23
22
|
|
|
24
|
-
// Mock
|
|
25
|
-
vi.mock('
|
|
26
|
-
|
|
23
|
+
// Mock shadow prep so the fs scaffolding doesn't run in tests.
|
|
24
|
+
vi.mock('../../shadow/prepare', () => ({
|
|
25
|
+
prepareShadow: vi.fn().mockResolvedValue(undefined),
|
|
27
26
|
}));
|
|
28
27
|
|
|
29
|
-
|
|
28
|
+
// Mock broker app lookup — no token needed in tests; default to null (app does not exist).
|
|
29
|
+
vi.mock('../../util/broker/app', () => ({
|
|
30
|
+
fetchApp: vi.fn().mockResolvedValue(null),
|
|
31
|
+
}));
|
|
30
32
|
|
|
31
33
|
async function makeWorkspace(tmp: string): Promise<void> {
|
|
32
34
|
await ensureDir(tmp);
|
|
@@ -47,7 +49,7 @@ describe('create', () => {
|
|
|
47
49
|
vi.mocked(runYarnInstall).mockClear();
|
|
48
50
|
vi.mocked(initRepo).mockClear();
|
|
49
51
|
vi.mocked(defaultS3Remote).mockClear();
|
|
50
|
-
vi.mocked(
|
|
52
|
+
vi.mocked(prepareShadow).mockClear();
|
|
51
53
|
});
|
|
52
54
|
|
|
53
55
|
afterEach(async () => {
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import { mkdtemp, rm, ensureDir, writeJson } from 'fs-extra';
|
|
4
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
5
|
+
|
|
6
|
+
import { runYarnInstall } from '../../util/npm/install';
|
|
7
|
+
import { prepareShadow } from '../../shadow/prepare';
|
|
8
|
+
import { editCommand } from '../edit';
|
|
9
|
+
import * as brokerApp from '../../util/broker/app';
|
|
10
|
+
import * as repo from '../../util/git/repo';
|
|
11
|
+
import * as brokerProvider from '../../util/auth/broker-credential-provider';
|
|
12
|
+
|
|
13
|
+
vi.mock('../../util/npm/install', () => ({
|
|
14
|
+
runYarnInstall: vi.fn().mockResolvedValue(undefined),
|
|
15
|
+
}));
|
|
16
|
+
|
|
17
|
+
vi.mock('../../shadow/prepare', () => ({
|
|
18
|
+
prepareShadow: vi.fn().mockResolvedValue(undefined),
|
|
19
|
+
}));
|
|
20
|
+
|
|
21
|
+
vi.mock('../../util/git/repo', () => ({
|
|
22
|
+
cloneRepo: vi.fn().mockResolvedValue(undefined),
|
|
23
|
+
pullAndResolve: vi.fn().mockResolvedValue(undefined),
|
|
24
|
+
GitConflictError: class GitConflictError extends Error {},
|
|
25
|
+
}));
|
|
26
|
+
|
|
27
|
+
const FAKE_CREDS = { accessKeyId: 'AKID', secretAccessKey: 'SECRET', sessionToken: 'TOKEN' };
|
|
28
|
+
const FAKE_GIT_ENV = { AWS_ACCESS_KEY_ID: 'AKID', AWS_SECRET_ACCESS_KEY: 'SECRET', AWS_SESSION_TOKEN: 'TOKEN' };
|
|
29
|
+
|
|
30
|
+
vi.mock('../../util/auth/broker-credential-provider', () => ({
|
|
31
|
+
brokerCredentialProvider: vi.fn(() => vi.fn().mockResolvedValue(FAKE_CREDS)),
|
|
32
|
+
}));
|
|
33
|
+
|
|
34
|
+
const S3_REMOTE = 's3://prod-use1-bigbrain-zth-mf-assets/mf-demo/git';
|
|
35
|
+
const GITHUB_REMOTE = 'https://github.com/org/demo-app.git';
|
|
36
|
+
|
|
37
|
+
async function makeWorkspace(tmp: string): Promise<void> {
|
|
38
|
+
await ensureDir(tmp);
|
|
39
|
+
await writeJson(path.join(tmp, 'package.json'), {
|
|
40
|
+
name: '@mondaydotcomorg/zth-projects-workspace',
|
|
41
|
+
private: true,
|
|
42
|
+
z2hWorkspace: true,
|
|
43
|
+
workspaces: ['*', '*/.zth'],
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
describe('edit', () => {
|
|
48
|
+
let tmp: string;
|
|
49
|
+
|
|
50
|
+
beforeEach(async () => {
|
|
51
|
+
tmp = await mkdtemp(path.join(os.tmpdir(), 'z2h-e-'));
|
|
52
|
+
vi.stubEnv('Z2H_WORKSPACE_HOME', tmp);
|
|
53
|
+
vi.mocked(runYarnInstall).mockClear();
|
|
54
|
+
vi.mocked(prepareShadow).mockClear();
|
|
55
|
+
vi.mocked(repo.cloneRepo).mockClear();
|
|
56
|
+
vi.mocked(repo.pullAndResolve).mockClear();
|
|
57
|
+
vi.mocked(brokerProvider.brokerCredentialProvider).mockClear();
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
afterEach(async () => {
|
|
61
|
+
vi.unstubAllEnvs();
|
|
62
|
+
await rm(tmp, { recursive: true, force: true });
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('skips cleanly when the app has no gitRemote', async () => {
|
|
66
|
+
await makeWorkspace(tmp);
|
|
67
|
+
vi.spyOn(brokerApp, 'fetchApp').mockResolvedValue(null);
|
|
68
|
+
|
|
69
|
+
await expect(editCommand('demo')).resolves.toBeUndefined();
|
|
70
|
+
expect(repo.cloneRepo).not.toHaveBeenCalled();
|
|
71
|
+
expect(repo.pullAndResolve).not.toHaveBeenCalled();
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('clones when the app dir does not exist locally', async () => {
|
|
75
|
+
await makeWorkspace(tmp);
|
|
76
|
+
vi.spyOn(brokerApp, 'fetchApp').mockResolvedValue({
|
|
77
|
+
appName: 'mf-demo',
|
|
78
|
+
version: 3,
|
|
79
|
+
gitRemote: S3_REMOTE,
|
|
80
|
+
entry: {},
|
|
81
|
+
deployedBy: 'u@x.com',
|
|
82
|
+
createdBy: 'u@x.com',
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
await editCommand('demo');
|
|
86
|
+
|
|
87
|
+
expect(repo.cloneRepo).toHaveBeenCalledWith(
|
|
88
|
+
S3_REMOTE,
|
|
89
|
+
path.join(tmp, 'demo'),
|
|
90
|
+
expect.objectContaining(FAKE_GIT_ENV)
|
|
91
|
+
);
|
|
92
|
+
expect(repo.pullAndResolve).not.toHaveBeenCalled();
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('pulls when the app dir already exists locally', async () => {
|
|
96
|
+
await makeWorkspace(tmp);
|
|
97
|
+
await ensureDir(path.join(tmp, 'demo'));
|
|
98
|
+
vi.spyOn(brokerApp, 'fetchApp').mockResolvedValue({
|
|
99
|
+
appName: 'mf-demo',
|
|
100
|
+
version: 3,
|
|
101
|
+
gitRemote: S3_REMOTE,
|
|
102
|
+
entry: {},
|
|
103
|
+
deployedBy: 'u@x.com',
|
|
104
|
+
createdBy: 'u@x.com',
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
await editCommand('demo');
|
|
108
|
+
|
|
109
|
+
expect(repo.pullAndResolve).toHaveBeenCalledWith(path.join(tmp, 'demo'), expect.objectContaining(FAKE_GIT_ENV));
|
|
110
|
+
expect(repo.cloneRepo).not.toHaveBeenCalled();
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it('injects AWS credentials for S3 remotes', async () => {
|
|
114
|
+
await makeWorkspace(tmp);
|
|
115
|
+
vi.spyOn(brokerApp, 'fetchApp').mockResolvedValue({
|
|
116
|
+
appName: 'mf-demo',
|
|
117
|
+
version: 3,
|
|
118
|
+
gitRemote: S3_REMOTE,
|
|
119
|
+
entry: {},
|
|
120
|
+
deployedBy: 'u@x.com',
|
|
121
|
+
createdBy: 'u@x.com',
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
await editCommand('demo');
|
|
125
|
+
|
|
126
|
+
expect(brokerProvider.brokerCredentialProvider).toHaveBeenCalledWith({ appName: 'mf-demo' });
|
|
127
|
+
expect(repo.cloneRepo).toHaveBeenCalledWith(S3_REMOTE, expect.any(String), expect.objectContaining(FAKE_GIT_ENV));
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it('does not inject AWS credentials for GitHub remotes', async () => {
|
|
131
|
+
await makeWorkspace(tmp);
|
|
132
|
+
vi.spyOn(brokerApp, 'fetchApp').mockResolvedValue({
|
|
133
|
+
appName: 'mf-demo',
|
|
134
|
+
version: 3,
|
|
135
|
+
gitRemote: GITHUB_REMOTE,
|
|
136
|
+
entry: {},
|
|
137
|
+
deployedBy: 'u@x.com',
|
|
138
|
+
createdBy: 'u@x.com',
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
await editCommand('demo');
|
|
142
|
+
|
|
143
|
+
expect(brokerProvider.brokerCredentialProvider).not.toHaveBeenCalled();
|
|
144
|
+
expect(repo.cloneRepo).toHaveBeenCalledWith(GITHUB_REMOTE, expect.any(String), undefined);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it('re-wires the workspace after a fresh clone', async () => {
|
|
148
|
+
await makeWorkspace(tmp);
|
|
149
|
+
vi.spyOn(brokerApp, 'fetchApp').mockResolvedValue({
|
|
150
|
+
appName: 'mf-demo',
|
|
151
|
+
version: 3,
|
|
152
|
+
gitRemote: S3_REMOTE,
|
|
153
|
+
entry: {},
|
|
154
|
+
deployedBy: 'u@x.com',
|
|
155
|
+
createdBy: 'u@x.com',
|
|
156
|
+
});
|
|
157
|
+
vi.mocked(repo.cloneRepo).mockImplementationOnce(async (_remote, dir) => {
|
|
158
|
+
await ensureDir(dir);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
await editCommand('demo');
|
|
162
|
+
|
|
163
|
+
expect(runYarnInstall).toHaveBeenCalledWith(expect.objectContaining({ cwd: tmp }));
|
|
164
|
+
expect(prepareShadow).toHaveBeenCalledWith(path.join(tmp, 'demo'));
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it('does not re-wire the workspace on a pull', async () => {
|
|
168
|
+
await makeWorkspace(tmp);
|
|
169
|
+
await ensureDir(path.join(tmp, 'demo'));
|
|
170
|
+
vi.spyOn(brokerApp, 'fetchApp').mockResolvedValue({
|
|
171
|
+
appName: 'mf-demo',
|
|
172
|
+
version: 3,
|
|
173
|
+
gitRemote: S3_REMOTE,
|
|
174
|
+
entry: {},
|
|
175
|
+
deployedBy: 'u@x.com',
|
|
176
|
+
createdBy: 'u@x.com',
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
await editCommand('demo');
|
|
180
|
+
|
|
181
|
+
expect(runYarnInstall).not.toHaveBeenCalled();
|
|
182
|
+
expect(prepareShadow).not.toHaveBeenCalled();
|
|
183
|
+
});
|
|
184
|
+
});
|