@aspruyt/xfg 4.0.0 → 4.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -2
- package/dist/cli/index.d.ts +1 -2
- package/dist/cli/index.js +0 -1
- package/dist/cli/program.js +7 -2
- package/dist/cli/{settings/results-collector.d.ts → results-collector.d.ts} +1 -1
- package/dist/cli/{settings/results-collector.js → results-collector.js} +2 -1
- package/dist/cli/settings-report-builder.d.ts +1 -3
- package/dist/cli/sync-command.d.ts +2 -24
- package/dist/cli/sync-command.js +295 -301
- package/dist/cli/types.d.ts +60 -40
- package/dist/cli/types.js +1 -12
- package/dist/config/errors.d.ts +9 -0
- package/dist/config/errors.js +11 -0
- package/dist/config/file-reference-resolver.d.ts +2 -1
- package/dist/config/file-reference-resolver.js +10 -8
- package/dist/config/formatter.d.ts +3 -2
- package/dist/config/index.d.ts +4 -6
- package/dist/config/index.js +4 -8
- package/dist/config/loader.js +4 -2
- package/dist/config/merge.d.ts +0 -9
- package/dist/config/merge.js +2 -7
- package/dist/config/normalizer.d.ts +4 -0
- package/dist/config/normalizer.js +61 -110
- package/dist/config/types.d.ts +15 -19
- package/dist/config/types.js +1 -1
- package/dist/config/validator.d.ts +0 -4
- package/dist/config/validator.js +286 -363
- package/dist/config/validators/file-validator.d.ts +2 -8
- package/dist/config/validators/file-validator.js +6 -17
- package/dist/config/validators/index.d.ts +3 -3
- package/dist/config/validators/index.js +3 -3
- package/dist/config/validators/repo-settings-validator.d.ts +0 -6
- package/dist/config/validators/repo-settings-validator.js +9 -9
- package/dist/config/validators/ruleset-validator.d.ts +0 -14
- package/dist/config/validators/ruleset-validator.js +28 -28
- package/dist/lifecycle/ado-migration-source.js +2 -1
- package/dist/lifecycle/github-lifecycle-provider.d.ts +6 -5
- package/dist/lifecycle/github-lifecycle-provider.js +79 -90
- package/dist/lifecycle/index.d.ts +2 -6
- package/dist/lifecycle/index.js +0 -4
- package/dist/lifecycle/lifecycle-formatter.d.ts +2 -1
- package/dist/lifecycle/lifecycle-formatter.js +4 -0
- package/dist/lifecycle/lifecycle-helpers.d.ts +3 -2
- package/dist/lifecycle/repo-lifecycle-manager.js +4 -11
- package/dist/lifecycle/types.d.ts +0 -8
- package/dist/output/github-summary.d.ts +5 -0
- package/dist/output/github-summary.js +9 -2
- package/dist/output/index.d.ts +2 -2
- package/dist/output/index.js +1 -1
- package/dist/output/lifecycle-report.js +5 -23
- package/dist/output/settings-report.d.ts +14 -3
- package/dist/output/settings-report.js +137 -197
- package/dist/output/summary-utils.d.ts +1 -1
- package/dist/output/summary-utils.js +2 -1
- package/dist/output/sync-report.js +5 -8
- package/dist/output/unified-summary.d.ts +2 -1
- package/dist/output/unified-summary.js +71 -133
- package/dist/settings/base-processor.d.ts +67 -0
- package/dist/settings/base-processor.js +91 -0
- package/dist/settings/index.d.ts +4 -3
- package/dist/settings/index.js +3 -3
- package/dist/settings/labels/converter.d.ts +2 -1
- package/dist/settings/labels/github-labels-strategy.d.ts +9 -18
- package/dist/settings/labels/github-labels-strategy.js +17 -73
- package/dist/settings/labels/index.d.ts +2 -6
- package/dist/settings/labels/index.js +1 -9
- package/dist/settings/labels/processor.d.ts +6 -30
- package/dist/settings/labels/processor.js +62 -152
- package/dist/settings/labels/types.d.ts +5 -8
- package/dist/settings/repo-settings/formatter.d.ts +2 -2
- package/dist/settings/repo-settings/formatter.js +6 -6
- package/dist/settings/repo-settings/github-repo-settings-strategy.d.ts +11 -12
- package/dist/settings/repo-settings/github-repo-settings-strategy.js +32 -79
- package/dist/settings/repo-settings/index.d.ts +2 -5
- package/dist/settings/repo-settings/index.js +1 -9
- package/dist/settings/repo-settings/processor.d.ts +6 -27
- package/dist/settings/repo-settings/processor.js +51 -104
- package/dist/settings/repo-settings/types.d.ts +7 -9
- package/dist/settings/rulesets/diff-algorithm.d.ts +0 -4
- package/dist/settings/rulesets/diff-algorithm.js +1 -10
- package/dist/settings/rulesets/diff.d.ts +1 -1
- package/dist/settings/rulesets/diff.js +2 -21
- package/dist/settings/rulesets/formatter.d.ts +1 -3
- package/dist/settings/rulesets/formatter.js +1 -8
- package/dist/settings/rulesets/github-ruleset-strategy.d.ts +11 -51
- package/dist/settings/rulesets/github-ruleset-strategy.js +24 -85
- package/dist/settings/rulesets/index.d.ts +3 -6
- package/dist/settings/rulesets/index.js +5 -9
- package/dist/settings/rulesets/processor.d.ts +8 -33
- package/dist/settings/rulesets/processor.js +58 -151
- package/dist/settings/rulesets/types.d.ts +35 -6
- package/dist/shared/command-executor.d.ts +2 -22
- package/dist/shared/command-executor.js +8 -7
- package/dist/shared/env.d.ts +0 -8
- package/dist/shared/env.js +14 -70
- package/dist/shared/file-status.d.ts +2 -0
- package/dist/shared/file-status.js +13 -0
- package/dist/shared/gh-api-utils.d.ts +46 -0
- package/dist/shared/gh-api-utils.js +107 -0
- package/dist/shared/index.d.ts +5 -5
- package/dist/shared/index.js +3 -3
- package/dist/shared/interpolation-engine.d.ts +31 -0
- package/dist/shared/interpolation-engine.js +50 -0
- package/dist/shared/logger.d.ts +3 -7
- package/dist/shared/logger.js +4 -1
- package/dist/shared/repo-detector.d.ts +17 -2
- package/dist/shared/repo-detector.js +27 -0
- package/dist/shared/retry-utils.d.ts +9 -17
- package/dist/shared/retry-utils.js +22 -28
- package/dist/shared/sanitize-utils.d.ts +0 -7
- package/dist/shared/sanitize-utils.js +0 -7
- package/dist/shared/shell-utils.d.ts +1 -0
- package/dist/shared/shell-utils.js +3 -0
- package/dist/shared/string-utils.d.ts +4 -0
- package/dist/shared/string-utils.js +6 -0
- package/dist/shared/type-guards.d.ts +17 -0
- package/dist/shared/type-guards.js +26 -0
- package/dist/shared/workspace-utils.d.ts +0 -4
- package/dist/shared/workspace-utils.js +0 -4
- package/dist/{sync → shared}/xfg-template.d.ts +3 -2
- package/dist/{sync → shared}/xfg-template.js +13 -54
- package/dist/sync/auth-options-builder.d.ts +4 -5
- package/dist/sync/auth-options-builder.js +15 -26
- package/dist/sync/branch-manager.d.ts +5 -0
- package/dist/sync/branch-manager.js +12 -10
- package/dist/sync/commit-push-manager.d.ts +1 -1
- package/dist/sync/commit-push-manager.js +22 -18
- package/dist/sync/diff-utils.d.ts +4 -9
- package/dist/sync/diff-utils.js +2 -19
- package/dist/sync/file-sync-orchestrator.js +9 -8
- package/dist/sync/file-writer.d.ts +2 -1
- package/dist/sync/file-writer.js +3 -6
- package/dist/sync/index.d.ts +2 -15
- package/dist/sync/index.js +0 -19
- package/dist/sync/manifest-manager.d.ts +4 -0
- package/dist/sync/manifest-manager.js +5 -1
- package/dist/sync/manifest.d.ts +10 -41
- package/dist/sync/manifest.js +11 -56
- package/dist/sync/pr-merge-handler.d.ts +2 -6
- package/dist/sync/pr-merge-handler.js +6 -3
- package/dist/sync/repository-processor.d.ts +1 -2
- package/dist/sync/repository-processor.js +20 -12
- package/dist/sync/repository-session.js +5 -14
- package/dist/sync/sync-workflow.js +31 -38
- package/dist/sync/types.d.ts +43 -178
- package/dist/vcs/authenticated-git-ops.d.ts +27 -70
- package/dist/vcs/authenticated-git-ops.js +70 -96
- package/dist/vcs/azure-pr-strategy.d.ts +6 -4
- package/dist/vcs/azure-pr-strategy.js +34 -82
- package/dist/vcs/branch-utils.d.ts +6 -0
- package/dist/vcs/branch-utils.js +29 -0
- package/dist/vcs/commit-strategy-selector.d.ts +5 -0
- package/dist/vcs/commit-strategy-selector.js +10 -0
- package/dist/vcs/git-commit-strategy.js +1 -2
- package/dist/vcs/git-ops.d.ts +15 -59
- package/dist/vcs/git-ops.js +46 -110
- package/dist/vcs/github-app-token-manager.d.ts +0 -6
- package/dist/vcs/github-app-token-manager.js +5 -12
- package/dist/vcs/github-pr-strategy.d.ts +5 -5
- package/dist/vcs/github-pr-strategy.js +44 -122
- package/dist/vcs/gitlab-pr-strategy.d.ts +6 -4
- package/dist/vcs/gitlab-pr-strategy.js +39 -87
- package/dist/vcs/graphql-commit-strategy.d.ts +3 -4
- package/dist/vcs/graphql-commit-strategy.js +31 -63
- package/dist/vcs/index.d.ts +3 -16
- package/dist/vcs/index.js +2 -33
- package/dist/vcs/pr-creator.d.ts +9 -9
- package/dist/vcs/pr-creator.js +11 -10
- package/dist/vcs/pr-strategy-factory.d.ts +5 -0
- package/dist/vcs/pr-strategy-factory.js +17 -0
- package/dist/vcs/pr-strategy.d.ts +13 -26
- package/dist/vcs/pr-strategy.js +20 -25
- package/dist/vcs/types.d.ts +87 -21
- package/package.json +2 -1
|
@@ -1,75 +1,107 @@
|
|
|
1
1
|
import { escapeShellArg } from "../shared/shell-utils.js";
|
|
2
|
-
import { defaultExecutor, } from "../shared/command-executor.js";
|
|
3
2
|
import { withRetry } from "../shared/retry-utils.js";
|
|
3
|
+
import { toErrorMessage } from "../shared/type-guards.js";
|
|
4
4
|
/**
|
|
5
|
-
*
|
|
5
|
+
* Adds authentication to network git operations and delegates local ops.
|
|
6
6
|
*
|
|
7
|
-
* When auth options are provided,
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* Local operations (commit, writeFile, etc.) pass through unchanged.
|
|
7
|
+
* When auth options are provided, clone uses an embedded token URL which sets
|
|
8
|
+
* the remote origin. Subsequent operations (fetch, push, getDefaultBranch)
|
|
9
|
+
* reuse that authenticated remote URL — no extra auth setup per operation.
|
|
12
10
|
*/
|
|
13
11
|
export class AuthenticatedGitOps {
|
|
14
|
-
|
|
15
|
-
auth;
|
|
12
|
+
localOps;
|
|
16
13
|
executor;
|
|
17
14
|
workDir;
|
|
18
15
|
retries;
|
|
19
|
-
|
|
20
|
-
|
|
16
|
+
auth;
|
|
17
|
+
log;
|
|
18
|
+
constructor(localOps, executor, workDir, retries, auth, log) {
|
|
19
|
+
this.localOps = localOps;
|
|
20
|
+
this.executor = executor;
|
|
21
|
+
this.workDir = workDir;
|
|
22
|
+
this.retries = retries;
|
|
21
23
|
this.auth = auth;
|
|
22
|
-
|
|
23
|
-
const internal = gitOps;
|
|
24
|
-
this.executor = internal.executor ?? defaultExecutor;
|
|
25
|
-
this.workDir = internal.workDir ?? ".";
|
|
26
|
-
this.retries = internal.retries ?? 3;
|
|
24
|
+
this.log = log;
|
|
27
25
|
}
|
|
28
26
|
async execWithRetry(command) {
|
|
29
27
|
return withRetry(() => this.executor.exec(command, this.workDir), {
|
|
30
28
|
retries: this.retries,
|
|
31
29
|
});
|
|
32
30
|
}
|
|
33
|
-
// ============================================================
|
|
34
|
-
// Network operations - use authenticated command when token provided
|
|
35
|
-
// ============================================================
|
|
36
31
|
/**
|
|
37
32
|
* Build the authenticated remote URL.
|
|
38
33
|
*/
|
|
39
34
|
getAuthenticatedUrl() {
|
|
35
|
+
if (!this.auth) {
|
|
36
|
+
throw new Error("getAuthenticatedUrl() called without auth options");
|
|
37
|
+
}
|
|
40
38
|
const { token, host, owner, repo } = this.auth;
|
|
41
39
|
return `https://x-access-token:${token}@${host}/${owner}/${repo}`;
|
|
42
40
|
}
|
|
41
|
+
// --- ILocalGitOps delegation ---
|
|
42
|
+
cleanWorkspace() {
|
|
43
|
+
return this.localOps.cleanWorkspace();
|
|
44
|
+
}
|
|
45
|
+
createBranch(branchName) {
|
|
46
|
+
return this.localOps.createBranch(branchName);
|
|
47
|
+
}
|
|
48
|
+
writeFile(fileName, content) {
|
|
49
|
+
return this.localOps.writeFile(fileName, content);
|
|
50
|
+
}
|
|
51
|
+
setExecutable(fileName) {
|
|
52
|
+
return this.localOps.setExecutable(fileName);
|
|
53
|
+
}
|
|
54
|
+
getFileContent(fileName) {
|
|
55
|
+
return this.localOps.getFileContent(fileName);
|
|
56
|
+
}
|
|
57
|
+
wouldChange(fileName, content) {
|
|
58
|
+
return this.localOps.wouldChange(fileName, content);
|
|
59
|
+
}
|
|
60
|
+
hasChanges() {
|
|
61
|
+
return this.localOps.hasChanges();
|
|
62
|
+
}
|
|
63
|
+
getChangedFiles() {
|
|
64
|
+
return this.localOps.getChangedFiles();
|
|
65
|
+
}
|
|
66
|
+
hasStagedChanges() {
|
|
67
|
+
return this.localOps.hasStagedChanges();
|
|
68
|
+
}
|
|
69
|
+
fileExistsOnBranch(fileName, branch) {
|
|
70
|
+
return this.localOps.fileExistsOnBranch(fileName, branch);
|
|
71
|
+
}
|
|
72
|
+
fileExists(fileName) {
|
|
73
|
+
return this.localOps.fileExists(fileName);
|
|
74
|
+
}
|
|
75
|
+
deleteFile(fileName) {
|
|
76
|
+
return this.localOps.deleteFile(fileName);
|
|
77
|
+
}
|
|
78
|
+
commit(message) {
|
|
79
|
+
return this.localOps.commit(message);
|
|
80
|
+
}
|
|
81
|
+
getDefaultBranchLocal() {
|
|
82
|
+
return this.localOps.getDefaultBranchLocal();
|
|
83
|
+
}
|
|
84
|
+
// --- INetworkGitOps with auth wrapping ---
|
|
85
|
+
// Note: exec() usage here is safe — all user inputs are escaped via escapeShellArg()
|
|
43
86
|
async clone(gitUrl) {
|
|
44
87
|
if (!this.auth) {
|
|
45
|
-
|
|
88
|
+
const command = `git clone ${escapeShellArg(gitUrl)} .`;
|
|
89
|
+
await this.execWithRetry(command);
|
|
90
|
+
return;
|
|
46
91
|
}
|
|
47
|
-
// Clone using authenticated URL directly - no insteadOf needed
|
|
48
92
|
const authUrl = escapeShellArg(this.getAuthenticatedUrl());
|
|
49
93
|
await this.execWithRetry(`git clone ${authUrl} .`);
|
|
50
94
|
}
|
|
51
95
|
async fetch(options) {
|
|
52
|
-
if (!this.auth) {
|
|
53
|
-
return this.gitOps.fetch(options);
|
|
54
|
-
}
|
|
55
|
-
// Remote URL already has auth from clone, just fetch
|
|
56
96
|
const pruneFlag = options?.prune ? " --prune" : "";
|
|
57
97
|
await this.execWithRetry(`git fetch origin${pruneFlag}`);
|
|
58
98
|
}
|
|
59
99
|
async push(branchName, options) {
|
|
60
|
-
if (!this.auth) {
|
|
61
|
-
return this.gitOps.push(branchName, options);
|
|
62
|
-
}
|
|
63
|
-
// Remote URL already has auth from clone, just push
|
|
64
100
|
const forceFlag = options?.force ? "--force-with-lease " : "";
|
|
65
101
|
const safeBranch = escapeShellArg(branchName);
|
|
66
102
|
await this.execWithRetry(`git push ${forceFlag}-u origin ${safeBranch}`);
|
|
67
103
|
}
|
|
68
104
|
async getDefaultBranch() {
|
|
69
|
-
if (!this.auth) {
|
|
70
|
-
return this.gitOps.getDefaultBranch();
|
|
71
|
-
}
|
|
72
|
-
// Network operation - remote URL already has auth from clone
|
|
73
105
|
try {
|
|
74
106
|
const remoteInfo = await this.execWithRetry(`git remote show origin`);
|
|
75
107
|
const match = remoteInfo.match(/HEAD branch: (\S+)/);
|
|
@@ -77,25 +109,12 @@ export class AuthenticatedGitOps {
|
|
|
77
109
|
return { branch: match[1], method: "remote HEAD" };
|
|
78
110
|
}
|
|
79
111
|
}
|
|
80
|
-
catch {
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
// Local operations don't need auth
|
|
84
|
-
try {
|
|
85
|
-
await this.executor.exec("git rev-parse --verify origin/main", this.workDir);
|
|
86
|
-
return { branch: "main", method: "origin/main exists" };
|
|
87
|
-
}
|
|
88
|
-
catch {
|
|
89
|
-
// Continue
|
|
90
|
-
}
|
|
91
|
-
try {
|
|
92
|
-
await this.executor.exec("git rev-parse --verify origin/master", this.workDir);
|
|
93
|
-
return { branch: "master", method: "origin/master exists" };
|
|
94
|
-
}
|
|
95
|
-
catch {
|
|
96
|
-
// Continue
|
|
112
|
+
catch (error) {
|
|
113
|
+
const msg = toErrorMessage(error);
|
|
114
|
+
this.log?.debug(`git remote show origin failed - ${msg}`);
|
|
97
115
|
}
|
|
98
|
-
|
|
116
|
+
// Local fallback operations don't need auth — delegate to localOps
|
|
117
|
+
return this.localOps.getDefaultBranchLocal();
|
|
99
118
|
}
|
|
100
119
|
/**
|
|
101
120
|
* Execute ls-remote with authentication.
|
|
@@ -105,7 +124,6 @@ export class AuthenticatedGitOps {
|
|
|
105
124
|
* branch existence where failure is expected for new branches.
|
|
106
125
|
*/
|
|
107
126
|
async lsRemote(branchName, options) {
|
|
108
|
-
// Remote URL already has auth from clone
|
|
109
127
|
const safeBranch = escapeShellArg(branchName);
|
|
110
128
|
const command = `git ls-remote --exit-code --heads origin ${safeBranch}`;
|
|
111
129
|
if (options?.skipRetry) {
|
|
@@ -118,7 +136,6 @@ export class AuthenticatedGitOps {
|
|
|
118
136
|
* Used by GraphQLCommitStrategy for creating/deleting remote branches.
|
|
119
137
|
*/
|
|
120
138
|
async pushRefspec(refspec, options) {
|
|
121
|
-
// Remote URL already has auth from clone
|
|
122
139
|
const deleteFlag = options?.delete ? "--delete " : "";
|
|
123
140
|
const safeRefspec = escapeShellArg(refspec);
|
|
124
141
|
await this.execWithRetry(`git push ${deleteFlag}-u origin ${safeRefspec}`);
|
|
@@ -128,50 +145,7 @@ export class AuthenticatedGitOps {
|
|
|
128
145
|
* Used by GraphQLCommitStrategy to update local refs.
|
|
129
146
|
*/
|
|
130
147
|
async fetchBranch(branchName) {
|
|
131
|
-
// Remote URL already has auth from clone
|
|
132
148
|
const safeBranch = escapeShellArg(branchName);
|
|
133
149
|
await this.execWithRetry(`git fetch origin +${safeBranch}:refs/remotes/origin/${safeBranch}`);
|
|
134
150
|
}
|
|
135
|
-
// ============================================================
|
|
136
|
-
// Local operations - delegate directly to GitOps
|
|
137
|
-
// ============================================================
|
|
138
|
-
cleanWorkspace() {
|
|
139
|
-
return this.gitOps.cleanWorkspace();
|
|
140
|
-
}
|
|
141
|
-
async createBranch(branchName) {
|
|
142
|
-
return this.gitOps.createBranch(branchName);
|
|
143
|
-
}
|
|
144
|
-
writeFile(fileName, content) {
|
|
145
|
-
return this.gitOps.writeFile(fileName, content);
|
|
146
|
-
}
|
|
147
|
-
async setExecutable(fileName) {
|
|
148
|
-
return this.gitOps.setExecutable(fileName);
|
|
149
|
-
}
|
|
150
|
-
getFileContent(fileName) {
|
|
151
|
-
return this.gitOps.getFileContent(fileName);
|
|
152
|
-
}
|
|
153
|
-
wouldChange(fileName, content) {
|
|
154
|
-
return this.gitOps.wouldChange(fileName, content);
|
|
155
|
-
}
|
|
156
|
-
async hasChanges() {
|
|
157
|
-
return this.gitOps.hasChanges();
|
|
158
|
-
}
|
|
159
|
-
async getChangedFiles() {
|
|
160
|
-
return this.gitOps.getChangedFiles();
|
|
161
|
-
}
|
|
162
|
-
async hasStagedChanges() {
|
|
163
|
-
return this.gitOps.hasStagedChanges();
|
|
164
|
-
}
|
|
165
|
-
async fileExistsOnBranch(fileName, branch) {
|
|
166
|
-
return this.gitOps.fileExistsOnBranch(fileName, branch);
|
|
167
|
-
}
|
|
168
|
-
fileExists(fileName) {
|
|
169
|
-
return this.gitOps.fileExists(fileName);
|
|
170
|
-
}
|
|
171
|
-
deleteFile(fileName) {
|
|
172
|
-
return this.gitOps.deleteFile(fileName);
|
|
173
|
-
}
|
|
174
|
-
async commit(message) {
|
|
175
|
-
return this.gitOps.commit(message);
|
|
176
|
-
}
|
|
177
151
|
}
|
|
@@ -1,11 +1,13 @@
|
|
|
1
|
-
import { PRResult } from "./
|
|
2
|
-
import { BasePRStrategy
|
|
1
|
+
import type { PRResult } from "./types.js";
|
|
2
|
+
import { BasePRStrategy } from "./pr-strategy.js";
|
|
3
|
+
import type { IPRStrategyLogger } from "./pr-strategy.js";
|
|
4
|
+
import type { PRStrategyOptions, CloseExistingPROptions, MergeOptions, MergeResult } from "./types.js";
|
|
3
5
|
import { ICommandExecutor } from "../shared/command-executor.js";
|
|
4
6
|
export declare class AzurePRStrategy extends BasePRStrategy {
|
|
5
|
-
constructor(executor?: ICommandExecutor);
|
|
7
|
+
constructor(executor?: ICommandExecutor, log?: IPRStrategyLogger);
|
|
6
8
|
private getOrgUrl;
|
|
7
9
|
private buildPRUrl;
|
|
8
|
-
checkExistingPR(options:
|
|
10
|
+
checkExistingPR(options: CloseExistingPROptions): Promise<string | null>;
|
|
9
11
|
closeExistingPR(options: CloseExistingPROptions): Promise<boolean>;
|
|
10
12
|
create(options: PRStrategyOptions): Promise<PRResult>;
|
|
11
13
|
/**
|
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
import { existsSync, writeFileSync, unlinkSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { escapeShellArg } from "../shared/shell-utils.js";
|
|
4
|
-
import {
|
|
5
|
-
import { BasePRStrategy
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
4
|
+
import { assertAzureDevOpsRepo, } from "../shared/repo-detector.js";
|
|
5
|
+
import { BasePRStrategy } from "./pr-strategy.js";
|
|
6
|
+
import { withRetry } from "../shared/retry-utils.js";
|
|
7
|
+
import { toErrorMessage, safeCleanup } from "../shared/type-guards.js";
|
|
8
8
|
import { sanitizeCredentials } from "../shared/sanitize-utils.js";
|
|
9
|
+
import { getStderr } from "../shared/command-executor.js";
|
|
9
10
|
export class AzurePRStrategy extends BasePRStrategy {
|
|
10
|
-
constructor(executor) {
|
|
11
|
-
super(executor);
|
|
11
|
+
constructor(executor, log) {
|
|
12
|
+
super(executor, log);
|
|
12
13
|
this.bodyFilePath = ".pr-description.md";
|
|
13
14
|
}
|
|
14
15
|
getOrgUrl(repoInfo) {
|
|
@@ -19,9 +20,7 @@ export class AzurePRStrategy extends BasePRStrategy {
|
|
|
19
20
|
}
|
|
20
21
|
async checkExistingPR(options) {
|
|
21
22
|
const { repoInfo, branchName, baseBranch, workDir, retries = 3 } = options;
|
|
22
|
-
|
|
23
|
-
throw new Error("Expected Azure DevOps repository");
|
|
24
|
-
}
|
|
23
|
+
assertAzureDevOpsRepo(repoInfo, "Azure PR strategy");
|
|
25
24
|
const azureRepoInfo = repoInfo;
|
|
26
25
|
const orgUrl = this.getOrgUrl(azureRepoInfo);
|
|
27
26
|
const command = `az repos pr list --repository ${escapeShellArg(azureRepoInfo.repo)} --source-branch ${escapeShellArg(branchName)} --target-branch ${escapeShellArg(baseBranch)} --org ${escapeShellArg(orgUrl)} --project ${escapeShellArg(azureRepoInfo.project)} --query "[0].pullRequestId" -o tsv`;
|
|
@@ -30,23 +29,16 @@ export class AzurePRStrategy extends BasePRStrategy {
|
|
|
30
29
|
return existingPRId ? this.buildPRUrl(azureRepoInfo, existingPRId) : null;
|
|
31
30
|
}
|
|
32
31
|
catch (error) {
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
}
|
|
37
|
-
const stderr = error.stderr ?? "";
|
|
38
|
-
if (stderr && !stderr.includes("does not exist")) {
|
|
39
|
-
logger.info(`Debug: Azure PR check failed - ${sanitizeCredentials(stderr).trim()}`);
|
|
40
|
-
}
|
|
32
|
+
const stderr = getStderr(error);
|
|
33
|
+
if (stderr && !stderr.includes("does not exist")) {
|
|
34
|
+
this.log?.debug(`Azure PR check failed - ${sanitizeCredentials(stderr).trim()}`);
|
|
41
35
|
}
|
|
42
36
|
return null;
|
|
43
37
|
}
|
|
44
38
|
}
|
|
45
39
|
async closeExistingPR(options) {
|
|
46
40
|
const { repoInfo, branchName, baseBranch, workDir, retries = 3 } = options;
|
|
47
|
-
|
|
48
|
-
throw new Error("Expected Azure DevOps repository");
|
|
49
|
-
}
|
|
41
|
+
assertAzureDevOpsRepo(repoInfo, "Azure PR strategy");
|
|
50
42
|
const azureRepoInfo = repoInfo;
|
|
51
43
|
const orgUrl = this.getOrgUrl(azureRepoInfo);
|
|
52
44
|
// First check if there's an existing PR
|
|
@@ -56,8 +48,6 @@ export class AzurePRStrategy extends BasePRStrategy {
|
|
|
56
48
|
baseBranch,
|
|
57
49
|
workDir,
|
|
58
50
|
retries,
|
|
59
|
-
title: "", // Not used for check
|
|
60
|
-
body: "", // Not used for check
|
|
61
51
|
});
|
|
62
52
|
if (!existingUrl) {
|
|
63
53
|
return false;
|
|
@@ -65,7 +55,7 @@ export class AzurePRStrategy extends BasePRStrategy {
|
|
|
65
55
|
// Extract PR ID from URL
|
|
66
56
|
const prInfo = this.parsePRUrl(existingUrl);
|
|
67
57
|
if (!prInfo) {
|
|
68
|
-
|
|
58
|
+
this.log?.warn(`Could not parse PR URL: ${existingUrl}`);
|
|
69
59
|
return false;
|
|
70
60
|
}
|
|
71
61
|
// Abandon the PR (Azure DevOps equivalent of closing)
|
|
@@ -76,13 +66,11 @@ export class AzurePRStrategy extends BasePRStrategy {
|
|
|
76
66
|
});
|
|
77
67
|
}
|
|
78
68
|
catch (error) {
|
|
79
|
-
const message =
|
|
80
|
-
|
|
69
|
+
const message = toErrorMessage(error);
|
|
70
|
+
this.log?.warn(`Failed to abandon PR #${prInfo.prId}: ${message}`);
|
|
81
71
|
return false;
|
|
82
72
|
}
|
|
83
|
-
// Delete the source branch - need to get object_id first
|
|
84
73
|
try {
|
|
85
|
-
// Get the branch's object_id
|
|
86
74
|
const getRefCommand = `az repos ref list --repository ${escapeShellArg(azureRepoInfo.repo)} --org ${escapeShellArg(orgUrl)} --project ${escapeShellArg(azureRepoInfo.project)} --filter heads/${escapeShellArg(branchName)} --query "[0].objectId" -o tsv`;
|
|
87
75
|
const objectId = await withRetry(() => this.executor.exec(getRefCommand, workDir), { retries });
|
|
88
76
|
if (objectId) {
|
|
@@ -92,19 +80,16 @@ export class AzurePRStrategy extends BasePRStrategy {
|
|
|
92
80
|
}
|
|
93
81
|
catch (error) {
|
|
94
82
|
// Branch deletion failure is not critical - PR is already abandoned
|
|
95
|
-
const message =
|
|
96
|
-
|
|
83
|
+
const message = toErrorMessage(error);
|
|
84
|
+
this.log?.warn(`Failed to delete branch ${branchName}: ${message}`);
|
|
97
85
|
}
|
|
98
86
|
return true;
|
|
99
87
|
}
|
|
100
88
|
async create(options) {
|
|
101
89
|
const { repoInfo, title, body, branchName, baseBranch, workDir, retries = 3, } = options;
|
|
102
|
-
|
|
103
|
-
throw new Error("Expected Azure DevOps repository");
|
|
104
|
-
}
|
|
90
|
+
assertAzureDevOpsRepo(repoInfo, "Azure PR strategy");
|
|
105
91
|
const azureRepoInfo = repoInfo;
|
|
106
92
|
const orgUrl = this.getOrgUrl(azureRepoInfo);
|
|
107
|
-
// Write description to temp file to avoid shell escaping issues
|
|
108
93
|
const descFile = join(workDir, this.bodyFilePath);
|
|
109
94
|
writeFileSync(descFile, body, "utf-8");
|
|
110
95
|
// Azure CLI @file syntax: escape the full @path to handle special chars in workDir
|
|
@@ -120,15 +105,10 @@ export class AzurePRStrategy extends BasePRStrategy {
|
|
|
120
105
|
};
|
|
121
106
|
}
|
|
122
107
|
finally {
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
if (existsSync(descFile)) {
|
|
108
|
+
safeCleanup(() => {
|
|
109
|
+
if (existsSync(descFile))
|
|
126
110
|
unlinkSync(descFile);
|
|
127
|
-
|
|
128
|
-
}
|
|
129
|
-
catch (cleanupError) {
|
|
130
|
-
logger.info(`Warning: Failed to clean up temp file ${descFile}: ${cleanupError instanceof Error ? cleanupError.message : String(cleanupError)}`);
|
|
131
|
-
}
|
|
111
|
+
}, `failed to remove ${descFile}`, this.log ?? { debug() { } });
|
|
132
112
|
}
|
|
133
113
|
}
|
|
134
114
|
/**
|
|
@@ -171,50 +151,22 @@ export class AzurePRStrategy extends BasePRStrategy {
|
|
|
171
151
|
? "--delete-source-branch true"
|
|
172
152
|
: "";
|
|
173
153
|
if (config.mode === "auto") {
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
success: true,
|
|
182
|
-
message: "Auto-complete enabled. PR will merge when all policies pass.",
|
|
183
|
-
merged: false,
|
|
184
|
-
autoMergeEnabled: true,
|
|
185
|
-
};
|
|
186
|
-
}
|
|
187
|
-
catch (error) {
|
|
188
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
189
|
-
return {
|
|
190
|
-
success: false,
|
|
191
|
-
message: `Failed to enable auto-complete: ${message}`,
|
|
192
|
-
merged: false,
|
|
193
|
-
};
|
|
194
|
-
}
|
|
154
|
+
const autoCommand = `az repos pr update --id ${escapeShellArg(prInfo.prId)} --auto-complete true ${squashFlag} ${deleteBranchFlag} --org ${escapeShellArg(orgUrl)}`.trim();
|
|
155
|
+
return this.executeMergeCommand(() => this.executor.exec(autoCommand, workDir), retries, {
|
|
156
|
+
success: true,
|
|
157
|
+
message: "Auto-complete enabled. PR will merge when all policies pass.",
|
|
158
|
+
merged: false,
|
|
159
|
+
autoMergeEnabled: true,
|
|
160
|
+
}, "Failed to enable auto-complete");
|
|
195
161
|
}
|
|
196
162
|
if (config.mode === "force") {
|
|
197
|
-
// Bypass policies and complete the PR
|
|
198
163
|
const bypassReason = config.bypassReason ?? "Automated config sync via xfg";
|
|
199
|
-
const
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
success: true,
|
|
206
|
-
message: "PR completed by bypassing policies.",
|
|
207
|
-
merged: true,
|
|
208
|
-
};
|
|
209
|
-
}
|
|
210
|
-
catch (error) {
|
|
211
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
212
|
-
return {
|
|
213
|
-
success: false,
|
|
214
|
-
message: `Failed to bypass policies and complete PR: ${message}`,
|
|
215
|
-
merged: false,
|
|
216
|
-
};
|
|
217
|
-
}
|
|
164
|
+
const forceCommand = `az repos pr update --id ${escapeShellArg(prInfo.prId)} --bypass-policy true --bypass-policy-reason ${escapeShellArg(bypassReason)} --status completed ${squashFlag} ${deleteBranchFlag} --org ${escapeShellArg(orgUrl)}`.trim();
|
|
165
|
+
return this.executeMergeCommand(() => this.executor.exec(forceCommand, workDir), retries, {
|
|
166
|
+
success: true,
|
|
167
|
+
message: "PR completed by bypassing policies.",
|
|
168
|
+
merged: true,
|
|
169
|
+
}, "Failed to bypass policies and complete PR");
|
|
218
170
|
}
|
|
219
171
|
return {
|
|
220
172
|
success: false,
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export declare function sanitizeBranchName(fileName: string): string;
|
|
2
|
+
/**
|
|
3
|
+
* Validates a user-provided branch name against git's naming rules.
|
|
4
|
+
* @throws Error if the branch name is invalid
|
|
5
|
+
*/
|
|
6
|
+
export declare function validateBranchName(branchName: string): void;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export function sanitizeBranchName(fileName) {
|
|
2
|
+
return fileName
|
|
3
|
+
.toLowerCase()
|
|
4
|
+
.replace(/\.[^.]+$/, "") // Remove extension
|
|
5
|
+
.replace(/[^a-z0-9-]/g, "-") // Replace non-alphanumeric with dashes
|
|
6
|
+
.replace(/-+/g, "-") // Collapse multiple dashes
|
|
7
|
+
.replace(/^-|-$/g, ""); // Remove leading/trailing dashes
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Validates a user-provided branch name against git's naming rules.
|
|
11
|
+
* @throws Error if the branch name is invalid
|
|
12
|
+
*/
|
|
13
|
+
export function validateBranchName(branchName) {
|
|
14
|
+
if (!branchName || branchName.trim() === "") {
|
|
15
|
+
throw new Error("Branch name cannot be empty");
|
|
16
|
+
}
|
|
17
|
+
if (branchName.startsWith(".") || branchName.startsWith("-")) {
|
|
18
|
+
throw new Error('Branch name cannot start with "." or "-"');
|
|
19
|
+
}
|
|
20
|
+
// Git disallows: space, ~, ^, :, ?, *, [, \, and consecutive dots (..)
|
|
21
|
+
if (/[\s~^:?*[\\]/.test(branchName) || branchName.includes("..")) {
|
|
22
|
+
throw new Error("Branch name contains invalid characters");
|
|
23
|
+
}
|
|
24
|
+
if (branchName.endsWith("/") ||
|
|
25
|
+
branchName.endsWith(".lock") ||
|
|
26
|
+
branchName.endsWith(".")) {
|
|
27
|
+
throw new Error("Branch name has invalid ending");
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -1,11 +1,16 @@
|
|
|
1
1
|
import { RepoInfo } from "../shared/repo-detector.js";
|
|
2
2
|
import type { ICommitStrategy } from "./types.js";
|
|
3
|
+
import { GitHubAppTokenManager } from "./github-app-token-manager.js";
|
|
3
4
|
import { ICommandExecutor } from "../shared/command-executor.js";
|
|
4
5
|
/**
|
|
5
6
|
* Checks if GitHub App credentials are configured via environment variables.
|
|
6
7
|
* Both XFG_GITHUB_APP_ID and XFG_GITHUB_APP_PRIVATE_KEY must be set.
|
|
7
8
|
*/
|
|
8
9
|
export declare function hasGitHubAppCredentials(): boolean;
|
|
10
|
+
/**
|
|
11
|
+
* Creates a GitHubAppTokenManager if credentials are configured, otherwise null.
|
|
12
|
+
*/
|
|
13
|
+
export declare function createTokenManager(): GitHubAppTokenManager | null;
|
|
9
14
|
/**
|
|
10
15
|
* Factory function to get the appropriate commit strategy for a repository.
|
|
11
16
|
*
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { isGitHubRepo } from "../shared/repo-detector.js";
|
|
2
2
|
import { GitCommitStrategy } from "./git-commit-strategy.js";
|
|
3
3
|
import { GraphQLCommitStrategy } from "./graphql-commit-strategy.js";
|
|
4
|
+
import { GitHubAppTokenManager } from "./github-app-token-manager.js";
|
|
4
5
|
/**
|
|
5
6
|
* Checks if GitHub App credentials are configured via environment variables.
|
|
6
7
|
* Both XFG_GITHUB_APP_ID and XFG_GITHUB_APP_PRIVATE_KEY must be set.
|
|
@@ -8,6 +9,15 @@ import { GraphQLCommitStrategy } from "./graphql-commit-strategy.js";
|
|
|
8
9
|
export function hasGitHubAppCredentials() {
|
|
9
10
|
return !!(process.env.XFG_GITHUB_APP_ID && process.env.XFG_GITHUB_APP_PRIVATE_KEY);
|
|
10
11
|
}
|
|
12
|
+
/**
|
|
13
|
+
* Creates a GitHubAppTokenManager if credentials are configured, otherwise null.
|
|
14
|
+
*/
|
|
15
|
+
export function createTokenManager() {
|
|
16
|
+
if (!hasGitHubAppCredentials()) {
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
return new GitHubAppTokenManager(process.env.XFG_GITHUB_APP_ID, process.env.XFG_GITHUB_APP_PRIVATE_KEY);
|
|
20
|
+
}
|
|
11
21
|
/**
|
|
12
22
|
* Factory function to get the appropriate commit strategy for a repository.
|
|
13
23
|
*
|
|
@@ -19,9 +19,8 @@ export class GitCommitStrategy {
|
|
|
19
19
|
*/
|
|
20
20
|
async commit(options) {
|
|
21
21
|
const { branchName, message, workDir, retries = 3, force = true, gitOps, } = options;
|
|
22
|
-
// Stage all changes
|
|
23
|
-
await this.executor.exec("git add -A", workDir);
|
|
24
22
|
// Commit with the message (--no-verify to skip pre-commit hooks)
|
|
23
|
+
// Staging is handled by CommitPushManager before calling commit()
|
|
25
24
|
await this.executor.exec(`git commit --no-verify -m ${escapeShellArg(message)}`, workDir);
|
|
26
25
|
// Push with authentication via gitOps if available
|
|
27
26
|
if (gitOps) {
|
package/dist/vcs/git-ops.d.ts
CHANGED
|
@@ -1,49 +1,21 @@
|
|
|
1
1
|
import { ICommandExecutor } from "../shared/command-executor.js";
|
|
2
|
-
|
|
3
|
-
cleanWorkspace(): void;
|
|
4
|
-
clone(gitUrl: string): Promise<void>;
|
|
5
|
-
fetch(options?: {
|
|
6
|
-
prune?: boolean;
|
|
7
|
-
}): Promise<void>;
|
|
8
|
-
createBranch(branchName: string): Promise<void>;
|
|
9
|
-
commit(message: string): Promise<boolean>;
|
|
10
|
-
push(branchName: string, options?: {
|
|
11
|
-
force?: boolean;
|
|
12
|
-
}): Promise<void>;
|
|
13
|
-
getDefaultBranch(): Promise<{
|
|
14
|
-
branch: string;
|
|
15
|
-
method: string;
|
|
16
|
-
}>;
|
|
17
|
-
writeFile(fileName: string, content: string): void;
|
|
18
|
-
setExecutable(fileName: string): Promise<void>;
|
|
19
|
-
getFileContent(fileName: string): string | null;
|
|
20
|
-
deleteFile(fileName: string): void;
|
|
21
|
-
wouldChange(fileName: string, content: string): boolean;
|
|
22
|
-
hasChanges(): Promise<boolean>;
|
|
23
|
-
getChangedFiles(): Promise<string[]>;
|
|
24
|
-
hasStagedChanges(): Promise<boolean>;
|
|
25
|
-
fileExistsOnBranch(fileName: string, branch: string): Promise<boolean>;
|
|
26
|
-
fileExists(fileName: string): boolean;
|
|
27
|
-
}
|
|
2
|
+
import type { ILocalGitOps } from "./types.js";
|
|
28
3
|
export interface GitOpsOptions {
|
|
29
4
|
workDir: string;
|
|
30
5
|
dryRun?: boolean;
|
|
31
6
|
executor?: ICommandExecutor;
|
|
32
|
-
/**
|
|
33
|
-
|
|
7
|
+
/** Optional logger for debug messages */
|
|
8
|
+
log?: {
|
|
9
|
+
debug(msg: string): void;
|
|
10
|
+
};
|
|
34
11
|
}
|
|
35
|
-
export declare class GitOps implements
|
|
36
|
-
private
|
|
37
|
-
private dryRun;
|
|
38
|
-
private
|
|
39
|
-
private
|
|
12
|
+
export declare class GitOps implements ILocalGitOps {
|
|
13
|
+
private readonly _workDir;
|
|
14
|
+
private readonly dryRun;
|
|
15
|
+
private readonly _executor;
|
|
16
|
+
private readonly log?;
|
|
40
17
|
constructor(options: GitOpsOptions);
|
|
41
18
|
private exec;
|
|
42
|
-
/**
|
|
43
|
-
* Run a command with retry logic for transient failures.
|
|
44
|
-
* Used for network operations like clone, fetch, push.
|
|
45
|
-
*/
|
|
46
|
-
private execWithRetry;
|
|
47
19
|
/**
|
|
48
20
|
* Validates that a file path doesn't escape the workspace directory.
|
|
49
21
|
* @returns The resolved absolute file path
|
|
@@ -51,14 +23,6 @@ export declare class GitOps implements IGitOps {
|
|
|
51
23
|
*/
|
|
52
24
|
private validatePath;
|
|
53
25
|
cleanWorkspace(): void;
|
|
54
|
-
clone(gitUrl: string): Promise<void>;
|
|
55
|
-
/**
|
|
56
|
-
* Fetch from remote with optional pruning of stale refs.
|
|
57
|
-
* Used to update local tracking refs after remote branch deletion.
|
|
58
|
-
*/
|
|
59
|
-
fetch(options?: {
|
|
60
|
-
prune?: boolean;
|
|
61
|
-
}): Promise<void>;
|
|
62
26
|
/**
|
|
63
27
|
* Create a new branch from the current HEAD.
|
|
64
28
|
* Always creates fresh - existing branches should be cleaned up beforehand
|
|
@@ -100,9 +64,6 @@ export declare class GitOps implements IGitOps {
|
|
|
100
64
|
* Used for createOnly checks against the base branch (not the working directory).
|
|
101
65
|
*/
|
|
102
66
|
fileExistsOnBranch(fileName: string, branch: string): Promise<boolean>;
|
|
103
|
-
/**
|
|
104
|
-
* Check if a file exists in the working directory.
|
|
105
|
-
*/
|
|
106
67
|
fileExists(fileName: string): boolean;
|
|
107
68
|
/**
|
|
108
69
|
* Delete a file from the working directory.
|
|
@@ -117,17 +78,12 @@ export declare class GitOps implements IGitOps {
|
|
|
117
78
|
* @returns true if a commit was made, false if there were no staged changes
|
|
118
79
|
*/
|
|
119
80
|
commit(message: string): Promise<boolean>;
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
81
|
+
/**
|
|
82
|
+
* Fallback default branch detection using local refs only.
|
|
83
|
+
* Checks origin/main, then origin/master, then defaults to "main".
|
|
84
|
+
*/
|
|
85
|
+
getDefaultBranchLocal(): Promise<{
|
|
124
86
|
branch: string;
|
|
125
87
|
method: string;
|
|
126
88
|
}>;
|
|
127
89
|
}
|
|
128
|
-
export declare function sanitizeBranchName(fileName: string): string;
|
|
129
|
-
/**
|
|
130
|
-
* Validates a user-provided branch name against git's naming rules.
|
|
131
|
-
* @throws Error if the branch name is invalid
|
|
132
|
-
*/
|
|
133
|
-
export declare function validateBranchName(branchName: string): void;
|