@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,6 +1,5 @@
|
|
|
1
1
|
import { existsSync, writeFileSync, unlinkSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
|
-
import { escapeShellArg } from "../shared/shell-utils.js";
|
|
4
3
|
import { assertAzureDevOpsRepo, } from "../repo/index.js";
|
|
5
4
|
import { SyncError } from "../shared/errors.js";
|
|
6
5
|
import { BasePRStrategy } from "./pr-strategy.js";
|
|
@@ -8,13 +7,10 @@ import { withRetry, isPermanentError } from "../shared/retry-utils.js";
|
|
|
8
7
|
import { toErrorMessage } from "../shared/type-guards.js";
|
|
9
8
|
import { safeCleanup } from "../shared/cleanup-utils.js";
|
|
10
9
|
import { NO_OP_DEBUG_LOG } from "../shared/logger.js";
|
|
11
|
-
import { sanitizeCredentials } from "
|
|
10
|
+
import { sanitizeCredentials } from "../shared/sanitize-utils.js";
|
|
12
11
|
import { getStderr } from "../shared/command-executor.js";
|
|
13
12
|
export class AdoPRStrategy extends BasePRStrategy {
|
|
14
|
-
|
|
15
|
-
super(executor, log);
|
|
16
|
-
this.bodyFilePath = ".pr-description.md";
|
|
17
|
-
}
|
|
13
|
+
bodyFilePath = ".pr-description.md";
|
|
18
14
|
getOrgUrl(repoInfo) {
|
|
19
15
|
return `https://dev.azure.com/${encodeURIComponent(repoInfo.organization)}`;
|
|
20
16
|
}
|
|
@@ -27,9 +23,27 @@ export class AdoPRStrategy extends BasePRStrategy {
|
|
|
27
23
|
*/
|
|
28
24
|
async findExistingPRId(azureRepoInfo, branchName, baseBranch, workDir, retries) {
|
|
29
25
|
const orgUrl = this.getOrgUrl(azureRepoInfo);
|
|
30
|
-
const
|
|
26
|
+
const args = [
|
|
27
|
+
"repos",
|
|
28
|
+
"pr",
|
|
29
|
+
"list",
|
|
30
|
+
"--repository",
|
|
31
|
+
azureRepoInfo.repo,
|
|
32
|
+
"--source-branch",
|
|
33
|
+
branchName,
|
|
34
|
+
"--target-branch",
|
|
35
|
+
baseBranch,
|
|
36
|
+
"--org",
|
|
37
|
+
orgUrl,
|
|
38
|
+
"--project",
|
|
39
|
+
azureRepoInfo.project,
|
|
40
|
+
"--query",
|
|
41
|
+
"[0].pullRequestId",
|
|
42
|
+
"-o",
|
|
43
|
+
"tsv",
|
|
44
|
+
];
|
|
31
45
|
try {
|
|
32
|
-
const existingPRId = await withRetry(() => this.executor.exec(
|
|
46
|
+
const existingPRId = await withRetry(() => this.executor.exec("az", args, workDir), { retries, log: this.log });
|
|
33
47
|
return existingPRId ? existingPRId.trim() : null;
|
|
34
48
|
}
|
|
35
49
|
catch (error) {
|
|
@@ -57,12 +71,21 @@ export class AdoPRStrategy extends BasePRStrategy {
|
|
|
57
71
|
const orgUrl = this.getOrgUrl(azureRepoInfo);
|
|
58
72
|
const prId = await this.findExistingPRId(azureRepoInfo, branchName, baseBranch, workDir, retries);
|
|
59
73
|
if (!prId) {
|
|
60
|
-
return
|
|
74
|
+
return { status: "no_pr" };
|
|
61
75
|
}
|
|
62
|
-
|
|
63
|
-
|
|
76
|
+
const abandonArgs = [
|
|
77
|
+
"repos",
|
|
78
|
+
"pr",
|
|
79
|
+
"update",
|
|
80
|
+
"--id",
|
|
81
|
+
prId,
|
|
82
|
+
"--status",
|
|
83
|
+
"abandoned",
|
|
84
|
+
"--org",
|
|
85
|
+
orgUrl,
|
|
86
|
+
];
|
|
64
87
|
try {
|
|
65
|
-
await withRetry(() => this.executor.exec(
|
|
88
|
+
await withRetry(() => this.executor.exec("az", abandonArgs, workDir), {
|
|
66
89
|
retries,
|
|
67
90
|
log: this.log,
|
|
68
91
|
});
|
|
@@ -70,22 +93,52 @@ export class AdoPRStrategy extends BasePRStrategy {
|
|
|
70
93
|
catch (error) {
|
|
71
94
|
const message = toErrorMessage(error);
|
|
72
95
|
this.log?.warn(`Failed to abandon PR #${prId}: ${message}`);
|
|
73
|
-
return
|
|
96
|
+
return { status: "close_failed", message };
|
|
74
97
|
}
|
|
75
98
|
try {
|
|
76
|
-
const
|
|
77
|
-
|
|
99
|
+
const getRefArgs = [
|
|
100
|
+
"repos",
|
|
101
|
+
"ref",
|
|
102
|
+
"list",
|
|
103
|
+
"--repository",
|
|
104
|
+
azureRepoInfo.repo,
|
|
105
|
+
"--org",
|
|
106
|
+
orgUrl,
|
|
107
|
+
"--project",
|
|
108
|
+
azureRepoInfo.project,
|
|
109
|
+
"--filter",
|
|
110
|
+
`heads/${branchName}`,
|
|
111
|
+
"--query",
|
|
112
|
+
"[0].objectId",
|
|
113
|
+
"-o",
|
|
114
|
+
"tsv",
|
|
115
|
+
];
|
|
116
|
+
const objectId = await withRetry(() => this.executor.exec("az", getRefArgs, workDir), { retries, log: this.log });
|
|
78
117
|
if (objectId) {
|
|
79
|
-
const
|
|
80
|
-
|
|
118
|
+
const deleteBranchArgs = [
|
|
119
|
+
"repos",
|
|
120
|
+
"ref",
|
|
121
|
+
"delete",
|
|
122
|
+
"--name",
|
|
123
|
+
`refs/heads/${branchName}`,
|
|
124
|
+
"--repository",
|
|
125
|
+
azureRepoInfo.repo,
|
|
126
|
+
"--org",
|
|
127
|
+
orgUrl,
|
|
128
|
+
"--project",
|
|
129
|
+
azureRepoInfo.project,
|
|
130
|
+
"--object-id",
|
|
131
|
+
objectId,
|
|
132
|
+
];
|
|
133
|
+
await withRetry(() => this.executor.exec("az", deleteBranchArgs, workDir), { retries, log: this.log });
|
|
81
134
|
}
|
|
82
135
|
}
|
|
83
136
|
catch (error) {
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
137
|
+
const message = `PR #${prId} abandoned but branch ${branchName} deletion failed: ${toErrorMessage(error)}`;
|
|
138
|
+
this.log?.warn(message);
|
|
139
|
+
return { status: "close_failed", message };
|
|
87
140
|
}
|
|
88
|
-
return
|
|
141
|
+
return { status: "closed" };
|
|
89
142
|
}
|
|
90
143
|
async create(options) {
|
|
91
144
|
const { repoInfo, title, body, branchName, baseBranch, workDir, retries = 3, } = options;
|
|
@@ -97,12 +150,33 @@ export class AdoPRStrategy extends BasePRStrategy {
|
|
|
97
150
|
writeFileSync(descFile, body, "utf-8");
|
|
98
151
|
}
|
|
99
152
|
catch (err) {
|
|
100
|
-
throw new SyncError(`Failed to write PR description to ${descFile}: ${toErrorMessage(err)}
|
|
153
|
+
throw new SyncError(`Failed to write PR description to ${descFile}: ${toErrorMessage(err)}`, { cause: err });
|
|
101
154
|
}
|
|
102
|
-
|
|
103
|
-
|
|
155
|
+
const args = [
|
|
156
|
+
"repos",
|
|
157
|
+
"pr",
|
|
158
|
+
"create",
|
|
159
|
+
"--repository",
|
|
160
|
+
azureRepoInfo.repo,
|
|
161
|
+
"--source-branch",
|
|
162
|
+
branchName,
|
|
163
|
+
"--target-branch",
|
|
164
|
+
baseBranch,
|
|
165
|
+
"--title",
|
|
166
|
+
title,
|
|
167
|
+
"--description",
|
|
168
|
+
`@${descFile}`,
|
|
169
|
+
"--org",
|
|
170
|
+
orgUrl,
|
|
171
|
+
"--project",
|
|
172
|
+
azureRepoInfo.project,
|
|
173
|
+
"--query",
|
|
174
|
+
"pullRequestId",
|
|
175
|
+
"-o",
|
|
176
|
+
"tsv",
|
|
177
|
+
];
|
|
104
178
|
try {
|
|
105
|
-
const prId = await withRetry(() => this.executor.exec(
|
|
179
|
+
const prId = await withRetry(() => this.executor.exec("az", args, workDir), {
|
|
106
180
|
retries,
|
|
107
181
|
log: this.log,
|
|
108
182
|
});
|
|
@@ -152,13 +226,21 @@ export class AdoPRStrategy extends BasePRStrategy {
|
|
|
152
226
|
};
|
|
153
227
|
}
|
|
154
228
|
const orgUrl = `https://dev.azure.com/${encodeURIComponent(prInfo.organization)}`;
|
|
155
|
-
const squashFlag = config.strategy === "squash" ? "--squash true" : "";
|
|
156
|
-
const deleteBranchFlag = config.deleteBranch
|
|
157
|
-
? "--delete-source-branch true"
|
|
158
|
-
: "";
|
|
159
229
|
if (config.mode === "auto") {
|
|
160
|
-
const
|
|
161
|
-
|
|
230
|
+
const autoArgs = [
|
|
231
|
+
"repos",
|
|
232
|
+
"pr",
|
|
233
|
+
"update",
|
|
234
|
+
"--id",
|
|
235
|
+
prInfo.prId,
|
|
236
|
+
"--auto-complete",
|
|
237
|
+
"true",
|
|
238
|
+
...(config.strategy === "squash" ? ["--squash", "true"] : []),
|
|
239
|
+
...(config.deleteBranch ? ["--delete-source-branch", "true"] : []),
|
|
240
|
+
"--org",
|
|
241
|
+
orgUrl,
|
|
242
|
+
];
|
|
243
|
+
return this.executeMergeCommand(() => this.executor.exec("az", autoArgs, workDir), retries, {
|
|
162
244
|
success: true,
|
|
163
245
|
message: "Auto-complete enabled. PR will merge when all policies pass.",
|
|
164
246
|
merged: false,
|
|
@@ -168,8 +250,24 @@ export class AdoPRStrategy extends BasePRStrategy {
|
|
|
168
250
|
if (config.mode === "force") {
|
|
169
251
|
const bypassReason = config.bypassReason ?? "Automated config sync via xfg";
|
|
170
252
|
this.log?.warn(`Bypassing policies for PR ${prInfo.prId} (reason: ${bypassReason})`);
|
|
171
|
-
const
|
|
172
|
-
|
|
253
|
+
const forceArgs = [
|
|
254
|
+
"repos",
|
|
255
|
+
"pr",
|
|
256
|
+
"update",
|
|
257
|
+
"--id",
|
|
258
|
+
prInfo.prId,
|
|
259
|
+
"--bypass-policy",
|
|
260
|
+
"true",
|
|
261
|
+
"--bypass-policy-reason",
|
|
262
|
+
bypassReason,
|
|
263
|
+
"--status",
|
|
264
|
+
"completed",
|
|
265
|
+
...(config.strategy === "squash" ? ["--squash", "true"] : []),
|
|
266
|
+
...(config.deleteBranch ? ["--delete-source-branch", "true"] : []),
|
|
267
|
+
"--org",
|
|
268
|
+
orgUrl,
|
|
269
|
+
];
|
|
270
|
+
return this.executeMergeCommand(() => this.executor.exec("az", forceArgs, workDir), retries, {
|
|
173
271
|
success: true,
|
|
174
272
|
message: "PR completed by bypassing policies.",
|
|
175
273
|
merged: true,
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { escapeShellArg } from "../shared/shell-utils.js";
|
|
2
1
|
import { withRetry } from "../shared/retry-utils.js";
|
|
3
2
|
import { toErrorMessage } from "../shared/type-guards.js";
|
|
4
3
|
import { SyncError } from "../shared/errors.js";
|
|
@@ -17,8 +16,8 @@ export class AuthenticatedGitOps {
|
|
|
17
16
|
this.auth = options.auth;
|
|
18
17
|
this.log = options.log;
|
|
19
18
|
}
|
|
20
|
-
execWithRetry(
|
|
21
|
-
return withRetry(() => this.executor.exec(
|
|
19
|
+
execWithRetry(executable, args) {
|
|
20
|
+
return withRetry(() => this.executor.exec(executable, args, this.workDir), {
|
|
22
21
|
retries: this.retries,
|
|
23
22
|
});
|
|
24
23
|
}
|
|
@@ -85,28 +84,43 @@ export class AuthenticatedGitOps {
|
|
|
85
84
|
return this.localOps.getDefaultBranchLocal();
|
|
86
85
|
}
|
|
87
86
|
// --- INetworkGitOps with auth wrapping ---
|
|
88
|
-
// Note: exec() usage here is safe — all user inputs are escaped via escapeShellArg()
|
|
89
87
|
async clone(gitUrl) {
|
|
90
88
|
if (!this.auth) {
|
|
91
|
-
|
|
92
|
-
await this.execWithRetry(command);
|
|
89
|
+
await this.execWithRetry("git", ["clone", "--", gitUrl, "."]);
|
|
93
90
|
return;
|
|
94
91
|
}
|
|
95
|
-
|
|
96
|
-
|
|
92
|
+
await this.execWithRetry("git", [
|
|
93
|
+
"clone",
|
|
94
|
+
"--",
|
|
95
|
+
this.getAuthenticatedUrl(),
|
|
96
|
+
".",
|
|
97
|
+
]);
|
|
97
98
|
}
|
|
98
99
|
async fetch(options) {
|
|
99
|
-
|
|
100
|
-
|
|
100
|
+
await this.execWithRetry("git", [
|
|
101
|
+
"fetch",
|
|
102
|
+
"origin",
|
|
103
|
+
...(options?.prune ? ["--prune"] : []),
|
|
104
|
+
]);
|
|
101
105
|
}
|
|
102
106
|
async push(branchName, options) {
|
|
103
|
-
const
|
|
104
|
-
|
|
105
|
-
|
|
107
|
+
const args = [
|
|
108
|
+
"push",
|
|
109
|
+
...(options?.force ? ["--force-with-lease"] : []),
|
|
110
|
+
"-u",
|
|
111
|
+
"origin",
|
|
112
|
+
"--",
|
|
113
|
+
branchName,
|
|
114
|
+
];
|
|
115
|
+
await this.execWithRetry("git", args);
|
|
106
116
|
}
|
|
107
117
|
async getDefaultBranch() {
|
|
108
118
|
try {
|
|
109
|
-
const remoteInfo = await this.execWithRetry(
|
|
119
|
+
const remoteInfo = await this.execWithRetry("git", [
|
|
120
|
+
"remote",
|
|
121
|
+
"show",
|
|
122
|
+
"origin",
|
|
123
|
+
]);
|
|
110
124
|
const match = remoteInfo.match(/HEAD branch: (\S+)/);
|
|
111
125
|
if (match && match[1] !== "(unknown)") {
|
|
112
126
|
return { branch: match[1], method: "remote HEAD" };
|
|
@@ -127,28 +141,36 @@ export class AuthenticatedGitOps {
|
|
|
127
141
|
* branch existence where failure is expected for new branches.
|
|
128
142
|
*/
|
|
129
143
|
lsRemote(branchName, options) {
|
|
130
|
-
const
|
|
131
|
-
const command = `git ls-remote --exit-code --heads origin ${safeBranch}`;
|
|
144
|
+
const args = ["ls-remote", "--exit-code", "--heads", "origin", branchName];
|
|
132
145
|
if (options?.skipRetry) {
|
|
133
|
-
return this.executor.exec(
|
|
146
|
+
return this.executor.exec("git", args, this.workDir);
|
|
134
147
|
}
|
|
135
|
-
return this.execWithRetry(
|
|
148
|
+
return this.execWithRetry("git", args);
|
|
136
149
|
}
|
|
137
150
|
/**
|
|
138
151
|
* Execute push with custom refspec (e.g., HEAD:branchName).
|
|
139
152
|
* Used by GraphQLCommitStrategy for creating/deleting remote branches.
|
|
140
153
|
*/
|
|
141
154
|
async pushRefspec(refspec, options) {
|
|
142
|
-
const
|
|
143
|
-
|
|
144
|
-
|
|
155
|
+
const args = [
|
|
156
|
+
"push",
|
|
157
|
+
...(options?.delete ? ["--delete"] : []),
|
|
158
|
+
"-u",
|
|
159
|
+
"origin",
|
|
160
|
+
"--",
|
|
161
|
+
refspec,
|
|
162
|
+
];
|
|
163
|
+
await this.execWithRetry("git", args);
|
|
145
164
|
}
|
|
146
165
|
/**
|
|
147
166
|
* Fetch a specific branch from remote.
|
|
148
167
|
* Used by GraphQLCommitStrategy to update local refs.
|
|
149
168
|
*/
|
|
150
169
|
async fetchBranch(branchName) {
|
|
151
|
-
|
|
152
|
-
|
|
170
|
+
await this.execWithRetry("git", [
|
|
171
|
+
"fetch",
|
|
172
|
+
"origin",
|
|
173
|
+
`+${branchName}:refs/remotes/origin/${branchName}`,
|
|
174
|
+
]);
|
|
153
175
|
}
|
|
154
176
|
}
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { withRetry } from "../shared/retry-utils.js";
|
|
2
|
-
import { escapeShellArg } from "../shared/shell-utils.js";
|
|
3
2
|
/**
|
|
4
3
|
* Git-based commit strategy using standard git commands (add, commit, push).
|
|
5
4
|
* Used with PAT authentication. Commits via this strategy are NOT verified
|
|
@@ -20,20 +19,25 @@ export class GitCommitStrategy {
|
|
|
20
19
|
const { branchName, message, workDir, retries = 3, force = true, gitOps, } = options;
|
|
21
20
|
// Commit with the message (--no-verify to skip pre-commit hooks)
|
|
22
21
|
// Staging is handled by CommitPushManager before calling commit()
|
|
23
|
-
await this.executor.exec(
|
|
22
|
+
await this.executor.exec("git", ["commit", "--no-verify", "-m", message], workDir);
|
|
24
23
|
// Push with authentication via gitOps if available
|
|
25
24
|
if (gitOps) {
|
|
26
25
|
await gitOps.push(branchName, { force });
|
|
27
26
|
}
|
|
28
27
|
else {
|
|
29
28
|
// Fallback for non-authenticated scenarios (shouldn't happen in practice)
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
|
|
29
|
+
const args = [
|
|
30
|
+
"push",
|
|
31
|
+
...(force ? ["--force-with-lease"] : []),
|
|
32
|
+
"-u",
|
|
33
|
+
"origin",
|
|
34
|
+
branchName,
|
|
35
|
+
];
|
|
36
|
+
await withRetry(() => this.executor.exec("git", args, workDir), {
|
|
33
37
|
retries,
|
|
34
38
|
});
|
|
35
39
|
}
|
|
36
|
-
const sha = await this.executor.exec("git rev-parse HEAD", workDir);
|
|
40
|
+
const sha = await this.executor.exec("git", ["rev-parse", "HEAD"], workDir);
|
|
37
41
|
return {
|
|
38
42
|
sha: sha.trim(),
|
|
39
43
|
verified: false, // Git-based commits are not verified
|
package/dist/vcs/git-ops.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { rmSync, existsSync, statSync, mkdirSync, writeFileSync, readFileSync, chmodSync, } from "node:fs";
|
|
2
2
|
import { join, resolve, relative, isAbsolute, dirname } from "node:path";
|
|
3
|
-
import { escapeShellArg } from "../shared/shell-utils.js";
|
|
4
3
|
import { toErrorMessage } from "../shared/type-guards.js";
|
|
5
4
|
import { ValidationError, SyncError } from "../shared/errors.js";
|
|
6
5
|
export class GitOps {
|
|
@@ -14,8 +13,8 @@ export class GitOps {
|
|
|
14
13
|
this.executor = options.executor;
|
|
15
14
|
this.log = options.log;
|
|
16
15
|
}
|
|
17
|
-
exec(
|
|
18
|
-
return this.executor.exec(
|
|
16
|
+
exec(executable, args, cwd) {
|
|
17
|
+
return this.executor.exec(executable, args, cwd ?? this.workDir);
|
|
19
18
|
}
|
|
20
19
|
/**
|
|
21
20
|
* Validates that a file path doesn't escape the workspace directory.
|
|
@@ -40,7 +39,7 @@ export class GitOps {
|
|
|
40
39
|
mkdirSync(this.workDir, { recursive: true });
|
|
41
40
|
}
|
|
42
41
|
catch (error) {
|
|
43
|
-
throw new SyncError(`Failed to clean workspace '${this.workDir}': ${toErrorMessage(error)}
|
|
42
|
+
throw new SyncError(`Failed to clean workspace '${this.workDir}': ${toErrorMessage(error)}`, { cause: error });
|
|
44
43
|
}
|
|
45
44
|
}
|
|
46
45
|
/**
|
|
@@ -50,11 +49,11 @@ export class GitOps {
|
|
|
50
49
|
*/
|
|
51
50
|
async createBranch(branchName) {
|
|
52
51
|
try {
|
|
53
|
-
await this.exec(
|
|
52
|
+
await this.exec("git", ["checkout", "-b", branchName]);
|
|
54
53
|
}
|
|
55
54
|
catch (error) {
|
|
56
55
|
const message = toErrorMessage(error);
|
|
57
|
-
throw new SyncError(`Failed to create branch '${branchName}': ${message}
|
|
56
|
+
throw new SyncError(`Failed to create branch '${branchName}': ${message}`, { cause: error });
|
|
58
57
|
}
|
|
59
58
|
}
|
|
60
59
|
writeFile(fileName, content) {
|
|
@@ -68,7 +67,7 @@ export class GitOps {
|
|
|
68
67
|
writeFileSync(filePath, normalized, "utf-8");
|
|
69
68
|
}
|
|
70
69
|
catch (error) {
|
|
71
|
-
throw new SyncError(`Failed to write file '${fileName}': ${toErrorMessage(error)}
|
|
70
|
+
throw new SyncError(`Failed to write file '${fileName}': ${toErrorMessage(error)}`, { cause: error });
|
|
72
71
|
}
|
|
73
72
|
}
|
|
74
73
|
/**
|
|
@@ -86,11 +85,16 @@ export class GitOps {
|
|
|
86
85
|
chmodSync(filePath, 0o755);
|
|
87
86
|
}
|
|
88
87
|
catch (error) {
|
|
89
|
-
throw new SyncError(`Failed to set executable permissions on '${fileName}': ${toErrorMessage(error)}
|
|
88
|
+
throw new SyncError(`Failed to set executable permissions on '${fileName}': ${toErrorMessage(error)}`, { cause: error });
|
|
90
89
|
}
|
|
91
|
-
// Also update git's index so the executable bit is committed
|
|
92
90
|
const relativePath = relative(this.workDir, filePath);
|
|
93
|
-
await this.exec(
|
|
91
|
+
await this.exec("git", [
|
|
92
|
+
"update-index",
|
|
93
|
+
"--add",
|
|
94
|
+
"--chmod=+x",
|
|
95
|
+
"--",
|
|
96
|
+
relativePath,
|
|
97
|
+
]);
|
|
94
98
|
}
|
|
95
99
|
/**
|
|
96
100
|
* Clears the executable bit on a file both on the filesystem and in git's index.
|
|
@@ -105,9 +109,9 @@ export class GitOps {
|
|
|
105
109
|
chmodSync(filePath, 0o644);
|
|
106
110
|
}
|
|
107
111
|
catch (error) {
|
|
108
|
-
throw new SyncError(`Failed to clear executable permissions on '${fileName}': ${toErrorMessage(error)}
|
|
112
|
+
throw new SyncError(`Failed to clear executable permissions on '${fileName}': ${toErrorMessage(error)}`, { cause: error });
|
|
109
113
|
}
|
|
110
|
-
await this.exec(
|
|
114
|
+
await this.exec("git", ["update-index", "--chmod=-x", "--", fileName]);
|
|
111
115
|
}
|
|
112
116
|
/**
|
|
113
117
|
* Returns the git index mode for a tracked file ("100755" or "100644"),
|
|
@@ -116,7 +120,7 @@ export class GitOps {
|
|
|
116
120
|
*/
|
|
117
121
|
async getFileMode(fileName) {
|
|
118
122
|
this.validatePath(fileName);
|
|
119
|
-
const output = await this.exec(
|
|
123
|
+
const output = await this.exec("git", ["ls-files", "-s", "--", fileName]);
|
|
120
124
|
const line = output.trim();
|
|
121
125
|
if (!line)
|
|
122
126
|
return null;
|
|
@@ -171,7 +175,7 @@ export class GitOps {
|
|
|
171
175
|
}
|
|
172
176
|
}
|
|
173
177
|
async hasChanges() {
|
|
174
|
-
const status = await this.exec("git status --porcelain"
|
|
178
|
+
const status = await this.exec("git", ["status", "--porcelain"]);
|
|
175
179
|
return status.length > 0;
|
|
176
180
|
}
|
|
177
181
|
/**
|
|
@@ -179,7 +183,7 @@ export class GitOps {
|
|
|
179
183
|
* Returns relative file paths for files that are modified, added, or untracked.
|
|
180
184
|
*/
|
|
181
185
|
async getChangedFiles() {
|
|
182
|
-
const status = await this.exec("git status --porcelain"
|
|
186
|
+
const status = await this.exec("git", ["status", "--porcelain"]);
|
|
183
187
|
if (!status)
|
|
184
188
|
return [];
|
|
185
189
|
return status
|
|
@@ -188,10 +192,10 @@ export class GitOps {
|
|
|
188
192
|
.map((line) => line.slice(3)); // Remove status prefix (e.g., " M ", "?? ", "A ")
|
|
189
193
|
}
|
|
190
194
|
async stageAll() {
|
|
191
|
-
await this.exec("git add -A"
|
|
195
|
+
await this.exec("git", ["add", "-A"]);
|
|
192
196
|
}
|
|
193
197
|
async hasStagedChanges() {
|
|
194
|
-
const diff = await this.exec("git diff --cached --name-only"
|
|
198
|
+
const diff = await this.exec("git", ["diff", "--cached", "--name-only"]);
|
|
195
199
|
return diff.length > 0;
|
|
196
200
|
}
|
|
197
201
|
/**
|
|
@@ -199,8 +203,11 @@ export class GitOps {
|
|
|
199
203
|
* Used for createOnly checks against the base branch (not the working directory).
|
|
200
204
|
*/
|
|
201
205
|
async fileExistsOnBranch(fileName, branch) {
|
|
206
|
+
if (branch.startsWith("-")) {
|
|
207
|
+
throw new ValidationError(`Branch name '${branch}' is not supported: branch names starting with '-' can be misinterpreted as git flags`);
|
|
208
|
+
}
|
|
202
209
|
try {
|
|
203
|
-
await this.exec(
|
|
210
|
+
await this.exec("git", ["show", `${branch}:${fileName}`]);
|
|
204
211
|
return true;
|
|
205
212
|
}
|
|
206
213
|
catch (error) {
|
|
@@ -236,7 +243,7 @@ export class GitOps {
|
|
|
236
243
|
rmSync(filePath);
|
|
237
244
|
}
|
|
238
245
|
catch (error) {
|
|
239
|
-
throw new SyncError(`Failed to delete file '${fileName}': ${toErrorMessage(error)}
|
|
246
|
+
throw new SyncError(`Failed to delete file '${fileName}': ${toErrorMessage(error)}`, { cause: error });
|
|
240
247
|
}
|
|
241
248
|
}
|
|
242
249
|
/**
|
|
@@ -248,13 +255,12 @@ export class GitOps {
|
|
|
248
255
|
if (this.dryRun) {
|
|
249
256
|
return true;
|
|
250
257
|
}
|
|
251
|
-
await this.exec("git add -A"
|
|
258
|
+
await this.exec("git", ["add", "-A"]);
|
|
252
259
|
// Check if there are actually staged changes after git add
|
|
253
260
|
if (!(await this.hasStagedChanges())) {
|
|
254
261
|
return false; // No changes to commit
|
|
255
262
|
}
|
|
256
|
-
|
|
257
|
-
await this.exec(`git commit --no-verify -m ${escapeShellArg(message)}`, this.workDir);
|
|
263
|
+
await this.exec("git", ["commit", "--no-verify", "-m", message]);
|
|
258
264
|
return true;
|
|
259
265
|
}
|
|
260
266
|
/**
|
|
@@ -263,7 +269,7 @@ export class GitOps {
|
|
|
263
269
|
*/
|
|
264
270
|
async getDefaultBranchLocal() {
|
|
265
271
|
try {
|
|
266
|
-
await this.exec("git rev-parse --verify origin/main"
|
|
272
|
+
await this.exec("git", ["rev-parse", "--verify", "origin/main"]);
|
|
267
273
|
return { branch: "main", method: "origin/main exists" };
|
|
268
274
|
}
|
|
269
275
|
catch (error) {
|
|
@@ -271,7 +277,7 @@ export class GitOps {
|
|
|
271
277
|
this.log?.debug(`origin/main check failed - ${msg}`);
|
|
272
278
|
}
|
|
273
279
|
try {
|
|
274
|
-
await this.exec("git rev-parse --verify origin/master"
|
|
280
|
+
await this.exec("git", ["rev-parse", "--verify", "origin/master"]);
|
|
275
281
|
return { branch: "master", method: "origin/master exists" };
|
|
276
282
|
}
|
|
277
283
|
catch (error) {
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import type { PRResult } from "./types.js";
|
|
2
2
|
import { BasePRStrategy } from "./pr-strategy.js";
|
|
3
|
-
import type { PRStrategyOptions, CloseExistingPROptions, MergeOptions, MergeResult } from "./types.js";
|
|
3
|
+
import type { PRStrategyOptions, CloseExistingPROptions, ClosePRResult, MergeOptions, MergeResult } from "./types.js";
|
|
4
4
|
export declare class GitHubPRStrategy extends BasePRStrategy {
|
|
5
|
+
private readonly bodyFilePath;
|
|
5
6
|
findExistingPRUrl(options: CloseExistingPROptions): Promise<string | null>;
|
|
6
|
-
closeExistingPR(options: CloseExistingPROptions): Promise<
|
|
7
|
+
closeExistingPR(options: CloseExistingPROptions): Promise<ClosePRResult>;
|
|
7
8
|
create(options: PRStrategyOptions): Promise<PRResult>;
|
|
8
9
|
/**
|
|
9
10
|
* Check if auto-merge is enabled on the repository.
|