@aspruyt/xfg 6.0.3 → 6.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/lifecycle-report-builder.d.ts +2 -2
- package/dist/cli/lifecycle-report-builder.js +3 -11
- package/dist/cli/program.d.ts +2 -1
- package/dist/cli/program.js +2 -3
- package/dist/cli/repo-sync-runner.d.ts +24 -0
- package/dist/cli/repo-sync-runner.js +156 -0
- package/dist/cli/results-collector.d.ts +1 -1
- package/dist/cli/results-collector.js +2 -2
- package/dist/cli/settings-factories.d.ts +7 -0
- package/dist/cli/settings-factories.js +27 -0
- package/dist/cli/settings-report-builder.d.ts +1 -1
- package/dist/cli/settings-report-builder.js +12 -23
- package/dist/cli/settings-runner.d.ts +2 -0
- package/dist/cli/settings-runner.js +87 -0
- package/dist/cli/sync-command.d.ts +1 -1
- package/dist/cli/sync-command.js +31 -372
- package/dist/cli/sync-report-builder.d.ts +1 -1
- package/dist/cli/sync-utils.d.ts +8 -0
- package/dist/cli/sync-utils.js +36 -0
- package/dist/cli/types.d.ts +5 -7
- package/dist/cli/unified-summary.d.ts +1 -3
- package/dist/cli/unified-summary.js +7 -5
- package/dist/cli.js +2 -1
- package/dist/{shared → config}/env.js +2 -2
- package/dist/config/extends-resolver.js +4 -3
- package/dist/config/file-reference-resolver.js +4 -2
- package/dist/config/formatter.js +18 -1
- package/dist/config/index.d.ts +1 -1
- package/dist/config/loader.js +30 -6
- package/dist/config/merge.d.ts +11 -1
- package/dist/config/merge.js +78 -6
- package/dist/config/normalizer.js +53 -38
- package/dist/config/validator.d.ts +1 -4
- package/dist/config/validator.js +13 -599
- package/dist/config/validators/file-validator.d.ts +2 -1
- package/dist/config/validators/file-validator.js +9 -1
- package/dist/config/validators/group-validator.d.ts +3 -0
- package/dist/config/validators/group-validator.js +167 -0
- package/dist/config/validators/repo-entry-validator.d.ts +2 -0
- package/dist/config/validators/repo-entry-validator.js +165 -0
- package/dist/config/validators/repo-settings-validator.js +18 -7
- package/dist/config/validators/ruleset-validator.js +2 -5
- package/dist/config/validators/shared.d.ts +11 -0
- package/dist/config/validators/shared.js +242 -0
- package/dist/lifecycle/ado-migration-source.js +2 -4
- package/dist/lifecycle/github-lifecycle-provider.d.ts +7 -11
- package/dist/lifecycle/github-lifecycle-provider.js +125 -136
- package/dist/lifecycle/{lifecycle-helpers.d.ts → helpers.d.ts} +5 -1
- package/dist/lifecycle/{lifecycle-helpers.js → helpers.js} +9 -8
- package/dist/lifecycle/index.d.ts +2 -2
- package/dist/lifecycle/index.js +1 -1
- package/dist/lifecycle/repo-lifecycle-factory.d.ts +2 -2
- package/dist/output/github-summary.js +2 -3
- package/dist/output/index.d.ts +4 -0
- package/dist/output/index.js +4 -0
- package/dist/output/lifecycle-report.d.ts +1 -1
- package/dist/output/lifecycle-report.js +5 -0
- package/dist/output/sync-report.d.ts +25 -3
- package/dist/output/sync-report.js +11 -11
- package/dist/settings/base-processor.d.ts +18 -7
- package/dist/settings/base-processor.js +26 -5
- package/dist/settings/code-scanning/diff.js +2 -2
- package/dist/settings/code-scanning/formatter.d.ts +2 -6
- package/dist/settings/code-scanning/formatter.js +2 -25
- package/dist/settings/code-scanning/github-code-scanning-strategy.d.ts +3 -7
- package/dist/settings/code-scanning/github-code-scanning-strategy.js +2 -2
- package/dist/settings/code-scanning/processor.js +6 -4
- package/dist/settings/code-scanning/types.d.ts +10 -8
- package/dist/settings/labels/github-labels-strategy.d.ts +3 -11
- package/dist/settings/labels/types.d.ts +12 -10
- package/dist/settings/repo-settings/diff.d.ts +1 -1
- package/dist/settings/repo-settings/diff.js +1 -1
- package/dist/settings/repo-settings/formatter.d.ts +2 -6
- package/dist/settings/repo-settings/formatter.js +4 -23
- package/dist/settings/repo-settings/github-repo-settings-strategy.d.ts +2 -2
- package/dist/settings/repo-settings/github-repo-settings-strategy.js +8 -7
- package/dist/settings/repo-settings/processor.js +11 -11
- package/dist/settings/repo-settings/types.d.ts +2 -2
- package/dist/settings/rulesets/diff-algorithm.js +4 -2
- package/dist/settings/rulesets/diff.js +2 -51
- package/dist/settings/rulesets/formatter.js +4 -0
- package/dist/settings/rulesets/github-ruleset-strategy.d.ts +3 -3
- package/dist/settings/rulesets/github-ruleset-strategy.js +4 -6
- package/dist/settings/rulesets/index.d.ts +1 -1
- package/dist/settings/rulesets/index.js +0 -2
- package/dist/settings/rulesets/processor.js +1 -1
- package/dist/settings/rulesets/types.d.ts +6 -2
- package/dist/shared/command-executor.d.ts +4 -4
- package/dist/shared/command-executor.js +9 -7
- package/dist/shared/diff-format.d.ts +1 -0
- package/dist/shared/diff-format.js +10 -0
- package/dist/shared/errors.d.ts +7 -4
- package/dist/shared/errors.js +8 -8
- package/dist/shared/gh-api-utils.d.ts +3 -34
- package/dist/shared/gh-api-utils.js +23 -53
- package/dist/shared/gh-token-utils.d.ts +26 -0
- package/dist/shared/gh-token-utils.js +32 -0
- package/dist/shared/json-utils.js +1 -1
- package/dist/shared/regex-utils.d.ts +1 -0
- package/dist/shared/regex-utils.js +3 -0
- package/dist/shared/retry-utils.d.ts +1 -0
- package/dist/shared/retry-utils.js +13 -7
- package/dist/sync/auth-options-builder.js +1 -1
- package/dist/sync/branch-manager.js +5 -3
- package/dist/sync/commit-push-manager.js +2 -3
- package/dist/sync/diff-utils.d.ts +0 -1
- package/dist/sync/diff-utils.js +5 -10
- package/dist/sync/file-sync-orchestrator.js +0 -2
- package/dist/sync/file-writer.d.ts +3 -0
- package/dist/sync/file-writer.js +84 -81
- package/dist/sync/index.d.ts +0 -1
- package/dist/sync/index.js +0 -1
- package/dist/sync/manifest.js +1 -1
- package/dist/sync/pr-merge-handler.js +6 -6
- package/dist/sync/sync-workflow.js +1 -1
- package/dist/sync/types.d.ts +2 -2
- package/dist/vcs/ado-pr-strategy.d.ts +3 -5
- package/dist/vcs/ado-pr-strategy.js +131 -33
- package/dist/vcs/authenticated-git-ops.js +45 -23
- package/dist/vcs/git-commit-strategy.js +10 -6
- package/dist/vcs/git-ops.js +30 -24
- package/dist/vcs/github-pr-strategy.d.ts +3 -2
- package/dist/vcs/github-pr-strategy.js +80 -30
- package/dist/vcs/gitlab-pr-strategy.d.ts +2 -5
- package/dist/vcs/gitlab-pr-strategy.js +88 -87
- package/dist/vcs/graphql-commit-strategy.d.ts +1 -5
- package/dist/vcs/graphql-commit-strategy.js +21 -37
- package/dist/vcs/pr-creator.js +9 -2
- package/dist/vcs/pr-strategy.d.ts +2 -3
- package/dist/vcs/pr-strategy.js +0 -1
- package/dist/vcs/types.d.ts +9 -5
- package/package.json +5 -5
- package/dist/config/validators/index.d.ts +0 -3
- package/dist/config/validators/index.js +0 -6
- package/dist/output/types.d.ts +0 -20
- package/dist/output/types.js +0 -1
- package/dist/shared/shell-utils.d.ts +0 -6
- package/dist/shared/shell-utils.js +0 -17
- /package/dist/{shared → config}/env.d.ts +0 -0
- /package/dist/lifecycle/{lifecycle-formatter.d.ts → formatter.d.ts} +0 -0
- /package/dist/lifecycle/{lifecycle-formatter.js → formatter.js} +0 -0
- /package/dist/{vcs → shared}/sanitize-utils.d.ts +0 -0
- /package/dist/{vcs → shared}/sanitize-utils.js +0 -0
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
import { existsSync, writeFileSync, unlinkSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
|
-
import {
|
|
3
|
+
import { escapeRegExp } from "../shared/regex-utils.js";
|
|
4
4
|
import { assertGitHubRepo } from "../repo/index.js";
|
|
5
5
|
import { BasePRStrategy } from "./pr-strategy.js";
|
|
6
6
|
import { withRetry, isPermanentError } from "../shared/retry-utils.js";
|
|
7
|
-
import { sanitizeCredentials } from "
|
|
7
|
+
import { sanitizeCredentials } from "../shared/sanitize-utils.js";
|
|
8
8
|
import { toErrorMessage } from "../shared/type-guards.js";
|
|
9
9
|
import { safeCleanup } from "../shared/cleanup-utils.js";
|
|
10
10
|
import { NO_OP_DEBUG_LOG } from "../shared/logger.js";
|
|
11
11
|
import { getStderr } from "../shared/command-executor.js";
|
|
12
|
-
import { buildTokenEnv,
|
|
12
|
+
import { buildTokenEnv, buildHostnameArgs } from "../shared/gh-api-utils.js";
|
|
13
13
|
import { SyncError } from "../shared/errors.js";
|
|
14
14
|
/**
|
|
15
15
|
* Get the repo flag value for gh CLI commands.
|
|
@@ -26,14 +26,26 @@ function buildPRUrlRegex(host) {
|
|
|
26
26
|
return new RegExp(`https://${escapedHost}/[\\w-]+/[\\w.-]+/pull/\\d+`);
|
|
27
27
|
}
|
|
28
28
|
export class GitHubPRStrategy extends BasePRStrategy {
|
|
29
|
+
bodyFilePath = ".pr-body.md";
|
|
29
30
|
async findExistingPRUrl(options) {
|
|
30
31
|
const { repoInfo, branchName, workDir, retries = 3, token } = options;
|
|
31
32
|
assertGitHubRepo(repoInfo, "GitHub PR strategy");
|
|
32
33
|
const repoFlag = getRepoFlag(repoInfo);
|
|
33
34
|
const tokenEnv = buildTokenEnv(token);
|
|
34
|
-
const
|
|
35
|
+
const args = [
|
|
36
|
+
"pr",
|
|
37
|
+
"list",
|
|
38
|
+
"--repo",
|
|
39
|
+
repoFlag,
|
|
40
|
+
"--head",
|
|
41
|
+
branchName,
|
|
42
|
+
"--json",
|
|
43
|
+
"url",
|
|
44
|
+
"--jq",
|
|
45
|
+
".[0].url",
|
|
46
|
+
];
|
|
35
47
|
try {
|
|
36
|
-
const existingPR = await withRetry(() => this.executor.exec(
|
|
48
|
+
const existingPR = await withRetry(() => this.executor.exec("gh", args, workDir, { env: tokenEnv }), { retries, log: this.log });
|
|
37
49
|
return existingPR || null;
|
|
38
50
|
}
|
|
39
51
|
catch (error) {
|
|
@@ -50,7 +62,6 @@ export class GitHubPRStrategy extends BasePRStrategy {
|
|
|
50
62
|
async closeExistingPR(options) {
|
|
51
63
|
const { repoInfo, branchName, baseBranch, workDir, retries = 3, token, } = options;
|
|
52
64
|
assertGitHubRepo(repoInfo, "GitHub PR strategy");
|
|
53
|
-
// First check if there's an existing PR (pass token through)
|
|
54
65
|
const existingUrl = await this.findExistingPRUrl({
|
|
55
66
|
repoInfo,
|
|
56
67
|
branchName,
|
|
@@ -60,25 +71,33 @@ export class GitHubPRStrategy extends BasePRStrategy {
|
|
|
60
71
|
token,
|
|
61
72
|
});
|
|
62
73
|
if (!existingUrl) {
|
|
63
|
-
return
|
|
74
|
+
return { status: "no_pr" };
|
|
64
75
|
}
|
|
65
|
-
// Extract PR number from URL
|
|
66
76
|
const prNumber = existingUrl.match(/\/pull\/(\d+)/)?.[1];
|
|
67
77
|
if (!prNumber) {
|
|
68
|
-
|
|
69
|
-
|
|
78
|
+
return {
|
|
79
|
+
status: "close_failed",
|
|
80
|
+
message: `Could not extract PR number from URL: ${existingUrl}`,
|
|
81
|
+
};
|
|
70
82
|
}
|
|
71
83
|
const repoFlag = getRepoFlag(repoInfo);
|
|
72
84
|
const tokenEnv = buildTokenEnv(token);
|
|
73
|
-
const
|
|
85
|
+
const args = [
|
|
86
|
+
"pr",
|
|
87
|
+
"close",
|
|
88
|
+
prNumber,
|
|
89
|
+
"--repo",
|
|
90
|
+
repoFlag,
|
|
91
|
+
"--delete-branch",
|
|
92
|
+
];
|
|
74
93
|
try {
|
|
75
|
-
await withRetry(() => this.executor.exec(
|
|
76
|
-
return
|
|
94
|
+
await withRetry(() => this.executor.exec("gh", args, workDir, { env: tokenEnv }), { retries, log: this.log });
|
|
95
|
+
return { status: "closed" };
|
|
77
96
|
}
|
|
78
97
|
catch (error) {
|
|
79
98
|
const message = toErrorMessage(error);
|
|
80
99
|
this.log?.warn(`Failed to close existing PR #${prNumber}: ${message}`);
|
|
81
|
-
return
|
|
100
|
+
return { status: "close_failed", message };
|
|
82
101
|
}
|
|
83
102
|
}
|
|
84
103
|
async create(options) {
|
|
@@ -89,18 +108,28 @@ export class GitHubPRStrategy extends BasePRStrategy {
|
|
|
89
108
|
writeFileSync(bodyFile, body, "utf-8");
|
|
90
109
|
}
|
|
91
110
|
catch (err) {
|
|
92
|
-
throw new SyncError(`Failed to write PR description to ${bodyFile}: ${toErrorMessage(err)}
|
|
111
|
+
throw new SyncError(`Failed to write PR description to ${bodyFile}: ${toErrorMessage(err)}`, { cause: err });
|
|
93
112
|
}
|
|
94
113
|
const tokenEnv = buildTokenEnv(token);
|
|
95
|
-
|
|
96
|
-
|
|
114
|
+
const args = [
|
|
115
|
+
"pr",
|
|
116
|
+
"create",
|
|
117
|
+
"--title",
|
|
118
|
+
title,
|
|
119
|
+
"--body-file",
|
|
120
|
+
bodyFile,
|
|
121
|
+
"--base",
|
|
122
|
+
baseBranch,
|
|
123
|
+
"--head",
|
|
124
|
+
branchName,
|
|
125
|
+
];
|
|
97
126
|
if (labels && labels.length > 0) {
|
|
98
127
|
for (const label of labels) {
|
|
99
|
-
|
|
128
|
+
args.push("--label", label);
|
|
100
129
|
}
|
|
101
130
|
}
|
|
102
131
|
try {
|
|
103
|
-
const result = await withRetry(() => this.executor.exec(
|
|
132
|
+
const result = await withRetry(() => this.executor.exec("gh", args, workDir, { env: tokenEnv }), { retries, log: this.log });
|
|
104
133
|
// Extract URL from output - use strict regex for valid PR URLs only
|
|
105
134
|
const host = repoInfo.host;
|
|
106
135
|
const urlRegex = buildPRUrlRegex(host);
|
|
@@ -125,12 +154,17 @@ export class GitHubPRStrategy extends BasePRStrategy {
|
|
|
125
154
|
* Check if auto-merge is enabled on the repository.
|
|
126
155
|
*/
|
|
127
156
|
async checkAutoMergeEnabled(repoInfo, workDir, retries = 3, token) {
|
|
128
|
-
const
|
|
129
|
-
const hostnamePart = hostnameFlag ? `${hostnameFlag} ` : "";
|
|
157
|
+
const hostnameArgs = buildHostnameArgs(repoInfo);
|
|
130
158
|
const tokenEnv = buildTokenEnv(token);
|
|
131
|
-
const
|
|
159
|
+
const args = [
|
|
160
|
+
"api",
|
|
161
|
+
...hostnameArgs,
|
|
162
|
+
`repos/${repoInfo.owner}/${repoInfo.repo}`,
|
|
163
|
+
"--jq",
|
|
164
|
+
".allow_auto_merge // false",
|
|
165
|
+
];
|
|
132
166
|
try {
|
|
133
|
-
const result = await withRetry(() => this.executor.exec(
|
|
167
|
+
const result = await withRetry(() => this.executor.exec("gh", args, workDir, { env: tokenEnv }), { retries, log: this.log });
|
|
134
168
|
return result.trim() === "true";
|
|
135
169
|
}
|
|
136
170
|
catch (error) {
|
|
@@ -149,8 +183,12 @@ export class GitHubPRStrategy extends BasePRStrategy {
|
|
|
149
183
|
case "rebase":
|
|
150
184
|
return "--rebase";
|
|
151
185
|
case "merge":
|
|
152
|
-
|
|
186
|
+
case undefined:
|
|
153
187
|
return "--merge";
|
|
188
|
+
default: {
|
|
189
|
+
const _exhaustive = strategy;
|
|
190
|
+
throw new Error(`Unexpected merge strategy: ${_exhaustive}`);
|
|
191
|
+
}
|
|
154
192
|
}
|
|
155
193
|
}
|
|
156
194
|
async merge(options) {
|
|
@@ -163,7 +201,6 @@ export class GitHubPRStrategy extends BasePRStrategy {
|
|
|
163
201
|
};
|
|
164
202
|
}
|
|
165
203
|
const strategyFlag = this.getMergeStrategyFlag(config.strategy);
|
|
166
|
-
const deleteBranchFlag = config.deleteBranch ? "--delete-branch" : "";
|
|
167
204
|
const tokenEnv = buildTokenEnv(token);
|
|
168
205
|
if (config.mode === "auto") {
|
|
169
206
|
// Check if auto-merge is enabled on the repo
|
|
@@ -179,9 +216,15 @@ export class GitHubPRStrategy extends BasePRStrategy {
|
|
|
179
216
|
autoMergeEnabled: false,
|
|
180
217
|
};
|
|
181
218
|
}
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
219
|
+
const autoArgs = [
|
|
220
|
+
"pr",
|
|
221
|
+
"merge",
|
|
222
|
+
prUrl,
|
|
223
|
+
"--auto",
|
|
224
|
+
strategyFlag,
|
|
225
|
+
...(config.deleteBranch ? ["--delete-branch"] : []),
|
|
226
|
+
];
|
|
227
|
+
return this.executeMergeCommand(() => this.executor.exec("gh", autoArgs, workDir, { env: tokenEnv }), retries, {
|
|
185
228
|
success: true,
|
|
186
229
|
message: "Auto-merge enabled. PR will merge when checks pass.",
|
|
187
230
|
merged: false,
|
|
@@ -190,8 +233,15 @@ export class GitHubPRStrategy extends BasePRStrategy {
|
|
|
190
233
|
}
|
|
191
234
|
if (config.mode === "force") {
|
|
192
235
|
this.log?.warn(`Force-merging PR ${prUrl} using admin privileges (bypasses branch protection)`);
|
|
193
|
-
const
|
|
194
|
-
|
|
236
|
+
const forceArgs = [
|
|
237
|
+
"pr",
|
|
238
|
+
"merge",
|
|
239
|
+
prUrl,
|
|
240
|
+
"--admin",
|
|
241
|
+
strategyFlag,
|
|
242
|
+
...(config.deleteBranch ? ["--delete-branch"] : []),
|
|
243
|
+
];
|
|
244
|
+
return this.executeMergeCommand(() => this.executor.exec("gh", forceArgs, workDir, { env: tokenEnv }), retries, {
|
|
195
245
|
success: true,
|
|
196
246
|
message: "PR merged successfully using admin privileges.",
|
|
197
247
|
merged: true,
|
|
@@ -1,10 +1,7 @@
|
|
|
1
1
|
import type { PRResult } from "./types.js";
|
|
2
2
|
import { BasePRStrategy } from "./pr-strategy.js";
|
|
3
|
-
import type {
|
|
4
|
-
import type { PRStrategyOptions, CloseExistingPROptions, MergeOptions, MergeResult } from "./types.js";
|
|
5
|
-
import { type ICommandExecutor } from "../shared/command-executor.js";
|
|
3
|
+
import type { PRStrategyOptions, CloseExistingPROptions, ClosePRResult, MergeOptions, MergeResult } from "./types.js";
|
|
6
4
|
export declare class GitLabPRStrategy extends BasePRStrategy {
|
|
7
|
-
constructor(executor: ICommandExecutor, log?: IPRStrategyLogger);
|
|
8
5
|
/**
|
|
9
6
|
* Build the repo flag for glab commands.
|
|
10
7
|
* Format: namespace/repo (supports nested groups)
|
|
@@ -23,7 +20,7 @@ export declare class GitLabPRStrategy extends BasePRStrategy {
|
|
|
23
20
|
*/
|
|
24
21
|
private getMergeStrategyFlag;
|
|
25
22
|
findExistingPRUrl(options: CloseExistingPROptions): Promise<string | null>;
|
|
26
|
-
closeExistingPR(options: CloseExistingPROptions): Promise<
|
|
23
|
+
closeExistingPR(options: CloseExistingPROptions): Promise<ClosePRResult>;
|
|
27
24
|
create(options: PRStrategyOptions): Promise<PRResult>;
|
|
28
25
|
merge(options: MergeOptions): Promise<MergeResult>;
|
|
29
26
|
}
|
|
@@ -1,21 +1,13 @@
|
|
|
1
|
-
import { existsSync, writeFileSync, unlinkSync } from "node:fs";
|
|
2
|
-
import { join } from "node:path";
|
|
3
|
-
import { escapeShellArg } from "../shared/shell-utils.js";
|
|
4
1
|
import { assertGitLabRepo } from "../repo/index.js";
|
|
5
2
|
import { BasePRStrategy } from "./pr-strategy.js";
|
|
6
3
|
import { withRetry, isPermanentError } from "../shared/retry-utils.js";
|
|
7
|
-
import { getStderr
|
|
4
|
+
import { getStderr } from "../shared/command-executor.js";
|
|
8
5
|
import { parseApiJson } from "../shared/json-utils.js";
|
|
9
|
-
import { sanitizeCredentials } from "
|
|
6
|
+
import { sanitizeCredentials } from "../shared/sanitize-utils.js";
|
|
10
7
|
import { toErrorMessage } from "../shared/type-guards.js";
|
|
11
|
-
import { safeCleanup } from "../shared/cleanup-utils.js";
|
|
12
|
-
import { NO_OP_DEBUG_LOG } from "../shared/logger.js";
|
|
13
8
|
import { SyncError } from "../shared/errors.js";
|
|
9
|
+
const MR_CREATED_MSG = "MR created successfully";
|
|
14
10
|
export class GitLabPRStrategy extends BasePRStrategy {
|
|
15
|
-
constructor(executor, log) {
|
|
16
|
-
super(executor, log);
|
|
17
|
-
this.bodyFilePath = ".mr-description.md";
|
|
18
|
-
}
|
|
19
11
|
/**
|
|
20
12
|
* Build the repo flag for glab commands.
|
|
21
13
|
* Format: namespace/repo (supports nested groups)
|
|
@@ -61,19 +53,30 @@ export class GitLabPRStrategy extends BasePRStrategy {
|
|
|
61
53
|
case "rebase":
|
|
62
54
|
return "--rebase";
|
|
63
55
|
case "merge":
|
|
64
|
-
|
|
56
|
+
case undefined:
|
|
65
57
|
return "";
|
|
58
|
+
/* c8 ignore next 4 */
|
|
59
|
+
default: {
|
|
60
|
+
const _exhaustive = strategy;
|
|
61
|
+
throw new Error(`Unexpected merge strategy: ${_exhaustive}`);
|
|
62
|
+
}
|
|
66
63
|
}
|
|
67
64
|
}
|
|
68
65
|
async findExistingPRUrl(options) {
|
|
69
66
|
const { repoInfo, branchName, workDir, retries = 3 } = options;
|
|
70
67
|
assertGitLabRepo(repoInfo, "GitLab PR strategy");
|
|
71
68
|
const repoFlag = this.getRepoFlag(repoInfo);
|
|
72
|
-
// Use glab mr list with JSON output for reliable parsing
|
|
73
|
-
// Note: glab mr list returns open MRs by default (use -c for closed, -M for merged)
|
|
74
|
-
const command = `glab mr list --source-branch ${escapeShellArg(branchName)} -R ${escapeShellArg(repoFlag)} -F json`;
|
|
75
69
|
try {
|
|
76
|
-
const result = await withRetry(() => this.executor.exec(
|
|
70
|
+
const result = await withRetry(() => this.executor.exec("glab", [
|
|
71
|
+
"mr",
|
|
72
|
+
"list",
|
|
73
|
+
"--source-branch",
|
|
74
|
+
branchName,
|
|
75
|
+
"-R",
|
|
76
|
+
repoFlag,
|
|
77
|
+
"-F",
|
|
78
|
+
"json",
|
|
79
|
+
], workDir), { retries, log: this.log });
|
|
77
80
|
if (!result || result.trim() === "" || result.trim() === "[]") {
|
|
78
81
|
return null;
|
|
79
82
|
}
|
|
@@ -97,7 +100,6 @@ export class GitLabPRStrategy extends BasePRStrategy {
|
|
|
97
100
|
async closeExistingPR(options) {
|
|
98
101
|
const { repoInfo, branchName, baseBranch, workDir, retries = 3 } = options;
|
|
99
102
|
assertGitLabRepo(repoInfo, "GitLab PR strategy");
|
|
100
|
-
// First check if there's an existing MR
|
|
101
103
|
const existingUrl = await this.findExistingPRUrl({
|
|
102
104
|
repoInfo,
|
|
103
105
|
branchName,
|
|
@@ -106,84 +108,74 @@ export class GitLabPRStrategy extends BasePRStrategy {
|
|
|
106
108
|
retries,
|
|
107
109
|
});
|
|
108
110
|
if (!existingUrl) {
|
|
109
|
-
return
|
|
111
|
+
return { status: "no_pr" };
|
|
110
112
|
}
|
|
111
|
-
// Extract MR IID from URL
|
|
112
113
|
const mrInfo = this.parseMRUrl(existingUrl);
|
|
113
114
|
if (!mrInfo) {
|
|
114
|
-
|
|
115
|
-
|
|
115
|
+
return {
|
|
116
|
+
status: "close_failed",
|
|
117
|
+
message: `Could not extract MR IID from URL: ${existingUrl}`,
|
|
118
|
+
};
|
|
116
119
|
}
|
|
117
120
|
const repoFlag = this.getRepoFlag(repoInfo);
|
|
118
|
-
// Close the MR
|
|
119
|
-
const closeCommand = `glab mr close ${escapeShellArg(mrInfo.mrIid)} -R ${escapeShellArg(repoFlag)}`;
|
|
120
121
|
try {
|
|
121
|
-
await withRetry(() => this.executor.exec(
|
|
122
|
-
retries,
|
|
123
|
-
log: this.log,
|
|
124
|
-
});
|
|
122
|
+
await withRetry(() => this.executor.exec("glab", ["mr", "close", mrInfo.mrIid, "-R", repoFlag], workDir), { retries, log: this.log });
|
|
125
123
|
}
|
|
126
124
|
catch (error) {
|
|
127
125
|
const message = toErrorMessage(error);
|
|
128
126
|
this.log?.warn(`Failed to close existing MR !${mrInfo.mrIid}: ${message}`);
|
|
129
|
-
return
|
|
127
|
+
return { status: "close_failed", message };
|
|
130
128
|
}
|
|
131
|
-
const deleteBranchCommand = `git push origin --delete ${escapeShellArg(branchName)}`;
|
|
132
129
|
try {
|
|
133
|
-
await withRetry(() => this.executor.exec(
|
|
134
|
-
retries,
|
|
135
|
-
log: this.log,
|
|
136
|
-
});
|
|
130
|
+
await withRetry(() => this.executor.exec("git", ["push", "origin", "--delete", branchName], workDir), { retries, log: this.log });
|
|
137
131
|
}
|
|
138
132
|
catch (error) {
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
133
|
+
const message = `MR !${mrInfo.mrIid} closed but branch ${branchName} deletion failed: ${toErrorMessage(error)}`;
|
|
134
|
+
this.log?.warn(message);
|
|
135
|
+
return { status: "close_failed", message };
|
|
142
136
|
}
|
|
143
|
-
return
|
|
137
|
+
return { status: "closed" };
|
|
144
138
|
}
|
|
145
139
|
async create(options) {
|
|
146
140
|
const { repoInfo, title, body, branchName, baseBranch, workDir, retries = 3, } = options;
|
|
147
141
|
assertGitLabRepo(repoInfo, "GitLab PR strategy");
|
|
148
142
|
const repoFlag = this.getRepoFlag(repoInfo);
|
|
149
|
-
const
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
url: this.buildMRUrl(repoInfo, mrMatch[1]),
|
|
175
|
-
success: true,
|
|
176
|
-
message: "MR created successfully",
|
|
177
|
-
};
|
|
178
|
-
}
|
|
179
|
-
throw new SyncError(`Could not parse MR URL from output: ${result}`);
|
|
143
|
+
const args = [
|
|
144
|
+
"mr",
|
|
145
|
+
"create",
|
|
146
|
+
"--source-branch",
|
|
147
|
+
branchName,
|
|
148
|
+
"--target-branch",
|
|
149
|
+
baseBranch,
|
|
150
|
+
"--title",
|
|
151
|
+
title,
|
|
152
|
+
"--description",
|
|
153
|
+
body,
|
|
154
|
+
"--yes",
|
|
155
|
+
"-R",
|
|
156
|
+
repoFlag,
|
|
157
|
+
];
|
|
158
|
+
const result = await withRetry(() => this.executor.exec("glab", args, workDir), { retries, log: this.log });
|
|
159
|
+
// Extract MR URL from output
|
|
160
|
+
// glab typically outputs the URL directly
|
|
161
|
+
const urlMatch = result.match(/https:\/\/[^\s]+\/-\/merge_requests\/\d+/);
|
|
162
|
+
if (urlMatch) {
|
|
163
|
+
return {
|
|
164
|
+
url: urlMatch[0],
|
|
165
|
+
success: true,
|
|
166
|
+
message: MR_CREATED_MSG,
|
|
167
|
+
};
|
|
180
168
|
}
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
169
|
+
// Fallback: extract MR number and build URL
|
|
170
|
+
const mrMatch = result.match(/!(\d+)/);
|
|
171
|
+
if (mrMatch) {
|
|
172
|
+
return {
|
|
173
|
+
url: this.buildMRUrl(repoInfo, mrMatch[1]),
|
|
174
|
+
success: true,
|
|
175
|
+
message: MR_CREATED_MSG,
|
|
176
|
+
};
|
|
186
177
|
}
|
|
178
|
+
throw new SyncError(`Could not parse MR URL from output: ${result}`);
|
|
187
179
|
}
|
|
188
180
|
async merge(options) {
|
|
189
181
|
const { prUrl, config, workDir, retries = 3 } = options;
|
|
@@ -205,18 +197,19 @@ export class GitLabPRStrategy extends BasePRStrategy {
|
|
|
205
197
|
}
|
|
206
198
|
const repoFlag = `${mrInfo.namespace}/${mrInfo.repo}`;
|
|
207
199
|
const strategyFlag = this.getMergeStrategyFlag(config.strategy);
|
|
208
|
-
const deleteBranchFlag = config.deleteBranch
|
|
209
|
-
? "--remove-source-branch"
|
|
210
|
-
: "";
|
|
211
200
|
if (config.mode === "auto") {
|
|
212
|
-
|
|
213
|
-
|
|
201
|
+
const args = [
|
|
202
|
+
"mr",
|
|
203
|
+
"merge",
|
|
204
|
+
mrInfo.mrIid,
|
|
214
205
|
"--when-pipeline-succeeds",
|
|
215
|
-
strategyFlag,
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
206
|
+
...(strategyFlag ? [strategyFlag] : []),
|
|
207
|
+
...(config.deleteBranch ? ["--remove-source-branch"] : []),
|
|
208
|
+
"-R",
|
|
209
|
+
repoFlag,
|
|
210
|
+
"-y",
|
|
211
|
+
];
|
|
212
|
+
return this.executeMergeCommand(() => this.executor.exec("glab", args, workDir), retries, {
|
|
220
213
|
success: true,
|
|
221
214
|
message: "Auto-merge enabled. MR will merge when pipeline succeeds.",
|
|
222
215
|
merged: false,
|
|
@@ -224,10 +217,18 @@ export class GitLabPRStrategy extends BasePRStrategy {
|
|
|
224
217
|
}, "Failed to enable auto-merge");
|
|
225
218
|
}
|
|
226
219
|
if (config.mode === "force") {
|
|
227
|
-
|
|
228
|
-
const
|
|
229
|
-
|
|
230
|
-
|
|
220
|
+
this.log?.warn(`Force-merging MR ${mrInfo.mrIid} immediately (bypasses pipeline requirements)`);
|
|
221
|
+
const args = [
|
|
222
|
+
"mr",
|
|
223
|
+
"merge",
|
|
224
|
+
mrInfo.mrIid,
|
|
225
|
+
...(strategyFlag ? [strategyFlag] : []),
|
|
226
|
+
...(config.deleteBranch ? ["--remove-source-branch"] : []),
|
|
227
|
+
"-R",
|
|
228
|
+
repoFlag,
|
|
229
|
+
"-y",
|
|
230
|
+
];
|
|
231
|
+
return this.executeMergeCommand(() => this.executor.exec("glab", args, workDir), retries, {
|
|
231
232
|
success: true,
|
|
232
233
|
message: "MR merged successfully.",
|
|
233
234
|
merged: true,
|
|
@@ -76,11 +76,7 @@ export declare class GraphQLCommitStrategy implements ICommitStrategy {
|
|
|
76
76
|
* This happens when the branch was updated between getting HEAD and making the commit.
|
|
77
77
|
*/
|
|
78
78
|
private isHeadOidMismatchError;
|
|
79
|
-
|
|
80
|
-
* Execute a GraphQL query or mutation for ref operations.
|
|
81
|
-
* Handles command construction, retry, error sanitization, and response parsing.
|
|
82
|
-
* Uses gh CLI's --input flag to pass GraphQL via stdin (same pattern as executeGraphQLMutation).
|
|
83
|
-
*/
|
|
79
|
+
private execGraphQL;
|
|
84
80
|
private executeGraphQLRefOp;
|
|
85
81
|
/**
|
|
86
82
|
* Query the remote for a repository's Node ID and a ref's Node ID.
|
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import { isGitHubRepo } from "../repo/index.js";
|
|
2
|
-
import { escapeShellArg } from "../shared/shell-utils.js";
|
|
3
2
|
import { withRetry, CORE_PERMANENT_ERROR_PATTERNS, DEFAULT_PERMANENT_ERROR_PATTERNS, } from "../shared/retry-utils.js";
|
|
4
3
|
import { toErrorMessage } from "../shared/type-guards.js";
|
|
5
4
|
import { parseApiJson } from "../shared/json-utils.js";
|
|
6
|
-
import { buildTokenEnv } from "../shared/gh-api-utils.js";
|
|
5
|
+
import { buildHostnameArgs, buildTokenEnv } from "../shared/gh-api-utils.js";
|
|
7
6
|
import { ValidationError, GraphQLApiError } from "../shared/errors.js";
|
|
8
7
|
/**
|
|
9
8
|
* Maximum payload size for GitHub GraphQL API (50MB).
|
|
@@ -104,15 +103,11 @@ export class GraphQLCommitStrategy {
|
|
|
104
103
|
let lastError = null;
|
|
105
104
|
for (let attempt = 0; attempt <= retries; attempt++) {
|
|
106
105
|
try {
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
await gitOps.fetchBranch(branchName);
|
|
106
|
+
if (!gitOps) {
|
|
107
|
+
throw new ValidationError("gitOps is required for GraphQL commit strategy");
|
|
110
108
|
}
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
}
|
|
114
|
-
// Get the remote HEAD SHA for this branch (not local HEAD)
|
|
115
|
-
const headSha = await this.executor.exec(`git rev-parse origin/${safeBranch}`, workDir);
|
|
109
|
+
await gitOps.fetchBranch(branchName);
|
|
110
|
+
const headSha = await this.executor.exec("git", ["rev-parse", `origin/${branchName}`], workDir);
|
|
116
111
|
const result = await this.executeGraphQLMutation(repoInfo, branchName, message, headSha.trim(), additions, deletions, workDir, token);
|
|
117
112
|
return result;
|
|
118
113
|
}
|
|
@@ -125,6 +120,7 @@ export class GraphQLCommitStrategy {
|
|
|
125
120
|
throw lastError;
|
|
126
121
|
}
|
|
127
122
|
}
|
|
123
|
+
// Defensive — loop always exits via return or throw, but TS needs this for exhaustiveness
|
|
128
124
|
throw (lastError ?? new GraphQLApiError("Unexpected error in GraphQL commit"));
|
|
129
125
|
}
|
|
130
126
|
/**
|
|
@@ -164,19 +160,12 @@ export class GraphQLCommitStrategy {
|
|
|
164
160
|
query: mutation,
|
|
165
161
|
variables,
|
|
166
162
|
});
|
|
167
|
-
const hostnameArg = repoInfo.host !== "github.com"
|
|
168
|
-
? `--hostname ${escapeShellArg(repoInfo.host)}`
|
|
169
|
-
: "";
|
|
170
|
-
const tokenEnv = buildTokenEnv(token);
|
|
171
|
-
const command = `echo ${escapeShellArg(requestBody)} | gh api graphql ${hostnameArg} --input -`;
|
|
172
163
|
let response;
|
|
173
164
|
try {
|
|
174
|
-
response = await
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
],
|
|
179
|
-
});
|
|
165
|
+
response = await this.execGraphQL(requestBody, repoInfo, workDir, token, [
|
|
166
|
+
...DEFAULT_PERMANENT_ERROR_PATTERNS,
|
|
167
|
+
...OID_MISMATCH_PATTERNS,
|
|
168
|
+
]);
|
|
180
169
|
}
|
|
181
170
|
catch (error) {
|
|
182
171
|
throw this.sanitizeCommandError(error, repositoryNameWithOwner);
|
|
@@ -215,7 +204,7 @@ export class GraphQLCommitStrategy {
|
|
|
215
204
|
if (refId && force) {
|
|
216
205
|
// Branch exists + force: delete then recreate from local HEAD
|
|
217
206
|
await this.deleteRemoteRef(refId, workDir, repoInfo, token);
|
|
218
|
-
const sha = (await this.executor.exec("git rev-parse HEAD", workDir)).trim();
|
|
207
|
+
const sha = (await this.executor.exec("git", ["rev-parse", "HEAD"], workDir)).trim();
|
|
219
208
|
await this.createRemoteRef(repositoryId, branchName, sha, workDir, repoInfo, token);
|
|
220
209
|
}
|
|
221
210
|
else if (!refId) {
|
|
@@ -223,7 +212,7 @@ export class GraphQLCommitStrategy {
|
|
|
223
212
|
// Race condition: on newly created forks, queryRemoteRef may return null
|
|
224
213
|
// due to eventual consistency, but the branch may exist by the time we
|
|
225
214
|
// try to create it. Treat "already exists" as success.
|
|
226
|
-
const sha = (await this.executor.exec("git rev-parse HEAD", workDir)).trim();
|
|
215
|
+
const sha = (await this.executor.exec("git", ["rev-parse", "HEAD"], workDir)).trim();
|
|
227
216
|
try {
|
|
228
217
|
await this.createRemoteRef(repositoryId, branchName, sha, workDir, repoInfo, token);
|
|
229
218
|
}
|
|
@@ -261,7 +250,9 @@ export class GraphQLCommitStrategy {
|
|
|
261
250
|
if (cleanMessage.length > 2000) {
|
|
262
251
|
cleanMessage = cleanMessage.substring(0, 2000) + "... (truncated)";
|
|
263
252
|
}
|
|
264
|
-
return new GraphQLApiError(`Commit failed for ${repo}: ${cleanMessage}
|
|
253
|
+
return new GraphQLApiError(`Commit failed for ${repo}: ${cleanMessage}`, {
|
|
254
|
+
cause: error,
|
|
255
|
+
});
|
|
265
256
|
}
|
|
266
257
|
/**
|
|
267
258
|
* Check if an error is due to expectedHeadOid mismatch (optimistic locking failure).
|
|
@@ -271,23 +262,16 @@ export class GraphQLCommitStrategy {
|
|
|
271
262
|
const message = error.message.toLowerCase();
|
|
272
263
|
return OID_MISMATCH_PATTERNS.some((pattern) => pattern.test(message));
|
|
273
264
|
}
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
265
|
+
async execGraphQL(requestBody, repoInfo, workDir, token, permanentErrorPatterns) {
|
|
266
|
+
const hostnameArgs = buildHostnameArgs(repoInfo);
|
|
267
|
+
const tokenEnv = buildTokenEnv(token);
|
|
268
|
+
return withRetry(() => this.executor.exec("gh", ["api", "graphql", ...hostnameArgs, "--input", "-"], workDir, { env: tokenEnv, input: requestBody }), { permanentErrorPatterns });
|
|
269
|
+
}
|
|
279
270
|
async executeGraphQLRefOp(queryOrMutation, repoInfo, workDir, token) {
|
|
280
271
|
const requestBody = JSON.stringify({ query: queryOrMutation });
|
|
281
|
-
const hostnameArg = repoInfo.host !== "github.com"
|
|
282
|
-
? `--hostname ${escapeShellArg(repoInfo.host)}`
|
|
283
|
-
: "";
|
|
284
|
-
const tokenEnv = buildTokenEnv(token);
|
|
285
|
-
const command = `echo ${escapeShellArg(requestBody)} | gh api graphql ${hostnameArg} --input -`;
|
|
286
272
|
let response;
|
|
287
273
|
try {
|
|
288
|
-
response = await
|
|
289
|
-
permanentErrorPatterns: GraphQLCommitStrategy.GRAPHQL_PERMANENT_ERROR_PATTERNS,
|
|
290
|
-
});
|
|
274
|
+
response = await this.execGraphQL(requestBody, repoInfo, workDir, token, GraphQLCommitStrategy.GRAPHQL_PERMANENT_ERROR_PATTERNS);
|
|
291
275
|
}
|
|
292
276
|
catch (error) {
|
|
293
277
|
throw this.sanitizeCommandError(error, `${repoInfo.owner}/${repoInfo.repo}`);
|
package/dist/vcs/pr-creator.js
CHANGED
|
@@ -47,8 +47,15 @@ function formatFileChanges(files) {
|
|
|
47
47
|
case "delete":
|
|
48
48
|
actionText = "Deleted";
|
|
49
49
|
break;
|
|
50
|
-
|
|
51
|
-
|
|
50
|
+
/* c8 ignore next 3 -- filtered out by line 77 */
|
|
51
|
+
case "skip":
|
|
52
|
+
actionText = "Skipped";
|
|
53
|
+
break;
|
|
54
|
+
/* c8 ignore next 4 */
|
|
55
|
+
default: {
|
|
56
|
+
const _exhaustive = f.action;
|
|
57
|
+
throw new Error(`Unexpected action: ${_exhaustive}`);
|
|
58
|
+
}
|
|
52
59
|
}
|
|
53
60
|
return `- ${actionText} \`${f.fileName}\``;
|
|
54
61
|
})
|