@aspruyt/json-config-sync 3.10.0 → 3.10.3
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 -1
- package/dist/command-executor.js +4 -0
- package/dist/git-ops.d.ts +21 -1
- package/dist/git-ops.js +45 -35
- package/dist/repository-processor.d.ts +3 -0
- package/dist/repository-processor.js +46 -11
- package/dist/strategies/azure-pr-strategy.d.ts +2 -1
- package/dist/strategies/azure-pr-strategy.js +52 -0
- package/dist/strategies/github-pr-strategy.d.ts +2 -1
- package/dist/strategies/github-pr-strategy.js +36 -0
- package/dist/strategies/index.d.ts +5 -4
- package/dist/strategies/index.js +5 -5
- package/dist/strategies/pr-strategy.d.ts +17 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -540,7 +540,7 @@ repos:
|
|
|
540
540
|
|
|
541
541
|
### Executable Files
|
|
542
542
|
|
|
543
|
-
Shell scripts (`.sh` files) are automatically marked as executable using `git update-index --chmod=+x`. You can control this behavior:
|
|
543
|
+
Shell scripts (`.sh` files) are automatically marked as executable using `git update-index --add --chmod=+x`. You can control this behavior:
|
|
544
544
|
|
|
545
545
|
```yaml
|
|
546
546
|
files:
|
package/dist/command-executor.js
CHANGED
|
@@ -18,6 +18,10 @@ export class ShellCommandExecutor {
|
|
|
18
18
|
if (execError.stderr && typeof execError.stderr !== "string") {
|
|
19
19
|
execError.stderr = execError.stderr.toString();
|
|
20
20
|
}
|
|
21
|
+
// Include stderr in error message for better debugging
|
|
22
|
+
if (execError.stderr && execError.message) {
|
|
23
|
+
execError.message = `${execError.message}\n${execError.stderr}`;
|
|
24
|
+
}
|
|
21
25
|
throw error;
|
|
22
26
|
}
|
|
23
27
|
}
|
package/dist/git-ops.d.ts
CHANGED
|
@@ -26,6 +26,11 @@ export declare class GitOps {
|
|
|
26
26
|
private validatePath;
|
|
27
27
|
cleanWorkspace(): void;
|
|
28
28
|
clone(gitUrl: string): Promise<void>;
|
|
29
|
+
/**
|
|
30
|
+
* Create a new branch from the current HEAD.
|
|
31
|
+
* Always creates fresh - existing branches should be cleaned up beforehand
|
|
32
|
+
* by closing any existing PRs (which deletes the remote branch).
|
|
33
|
+
*/
|
|
29
34
|
createBranch(branchName: string): Promise<void>;
|
|
30
35
|
writeFile(fileName: string, content: string): void;
|
|
31
36
|
/**
|
|
@@ -40,7 +45,22 @@ export declare class GitOps {
|
|
|
40
45
|
*/
|
|
41
46
|
wouldChange(fileName: string, content: string): boolean;
|
|
42
47
|
hasChanges(): Promise<boolean>;
|
|
43
|
-
|
|
48
|
+
/**
|
|
49
|
+
* Check if there are staged changes ready to commit.
|
|
50
|
+
* Uses `git diff --cached --quiet` which exits with 1 if there are staged changes.
|
|
51
|
+
*/
|
|
52
|
+
hasStagedChanges(): Promise<boolean>;
|
|
53
|
+
/**
|
|
54
|
+
* Check if a file exists on a specific branch.
|
|
55
|
+
* Used for createOnly checks against the base branch (not the working directory).
|
|
56
|
+
*/
|
|
57
|
+
fileExistsOnBranch(fileName: string, branch: string): Promise<boolean>;
|
|
58
|
+
/**
|
|
59
|
+
* Stage all changes and commit with the given message.
|
|
60
|
+
* Uses --no-verify to skip pre-commit hooks (config sync should always succeed).
|
|
61
|
+
* @returns true if a commit was made, false if there were no staged changes
|
|
62
|
+
*/
|
|
63
|
+
commit(message: string): Promise<boolean>;
|
|
44
64
|
push(branchName: string): Promise<void>;
|
|
45
65
|
getDefaultBranch(): Promise<{
|
|
46
66
|
branch: string;
|
package/dist/git-ops.js
CHANGED
|
@@ -4,21 +4,6 @@ import { escapeShellArg } from "./shell-utils.js";
|
|
|
4
4
|
import { defaultExecutor } from "./command-executor.js";
|
|
5
5
|
import { withRetry } from "./retry-utils.js";
|
|
6
6
|
import { logger } from "./logger.js";
|
|
7
|
-
/**
|
|
8
|
-
* Patterns indicating a git branch does not exist.
|
|
9
|
-
* Used to distinguish "branch not found" from other errors.
|
|
10
|
-
*/
|
|
11
|
-
const BRANCH_NOT_FOUND_PATTERNS = [
|
|
12
|
-
"couldn't find remote ref",
|
|
13
|
-
"pathspec",
|
|
14
|
-
"did not match any",
|
|
15
|
-
];
|
|
16
|
-
/**
|
|
17
|
-
* Checks if an error message indicates a branch was not found.
|
|
18
|
-
*/
|
|
19
|
-
function isBranchNotFoundError(message) {
|
|
20
|
-
return BRANCH_NOT_FOUND_PATTERNS.some((pattern) => message.includes(pattern));
|
|
21
|
-
}
|
|
22
7
|
export class GitOps {
|
|
23
8
|
workDir;
|
|
24
9
|
dryRun;
|
|
@@ -66,24 +51,12 @@ export class GitOps {
|
|
|
66
51
|
async clone(gitUrl) {
|
|
67
52
|
await this.execWithRetry(`git clone ${escapeShellArg(gitUrl)} .`, this.workDir);
|
|
68
53
|
}
|
|
54
|
+
/**
|
|
55
|
+
* Create a new branch from the current HEAD.
|
|
56
|
+
* Always creates fresh - existing branches should be cleaned up beforehand
|
|
57
|
+
* by closing any existing PRs (which deletes the remote branch).
|
|
58
|
+
*/
|
|
69
59
|
async createBranch(branchName) {
|
|
70
|
-
try {
|
|
71
|
-
// Check if branch exists on remote (network operation with retry)
|
|
72
|
-
await this.execWithRetry(`git fetch origin ${escapeShellArg(branchName)}`, this.workDir);
|
|
73
|
-
// Ensure clean workspace before checkout (defensive - handles edge cases)
|
|
74
|
-
await this.exec("git reset --hard HEAD", this.workDir);
|
|
75
|
-
await this.exec("git clean -fd", this.workDir);
|
|
76
|
-
await this.execWithRetry(`git checkout ${escapeShellArg(branchName)}`, this.workDir);
|
|
77
|
-
return;
|
|
78
|
-
}
|
|
79
|
-
catch (error) {
|
|
80
|
-
// Only proceed to create branch if error indicates branch doesn't exist
|
|
81
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
82
|
-
if (!isBranchNotFoundError(message)) {
|
|
83
|
-
throw new Error(`Failed to fetch/checkout branch '${branchName}': ${message}`);
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
// Branch doesn't exist on remote, create it locally
|
|
87
60
|
try {
|
|
88
61
|
await this.exec(`git checkout -b ${escapeShellArg(branchName)}`, this.workDir);
|
|
89
62
|
}
|
|
@@ -115,7 +88,7 @@ export class GitOps {
|
|
|
115
88
|
const filePath = this.validatePath(fileName);
|
|
116
89
|
// Use relative path from workDir for git command
|
|
117
90
|
const relativePath = relative(this.workDir, filePath);
|
|
118
|
-
await this.exec(`git update-index --chmod=+x ${escapeShellArg(relativePath)}`, this.workDir);
|
|
91
|
+
await this.exec(`git update-index --add --chmod=+x ${escapeShellArg(relativePath)}`, this.workDir);
|
|
119
92
|
}
|
|
120
93
|
/**
|
|
121
94
|
* Checks if writing the given content would result in changes.
|
|
@@ -142,12 +115,49 @@ export class GitOps {
|
|
|
142
115
|
const status = await this.exec("git status --porcelain", this.workDir);
|
|
143
116
|
return status.length > 0;
|
|
144
117
|
}
|
|
118
|
+
/**
|
|
119
|
+
* Check if there are staged changes ready to commit.
|
|
120
|
+
* Uses `git diff --cached --quiet` which exits with 1 if there are staged changes.
|
|
121
|
+
*/
|
|
122
|
+
async hasStagedChanges() {
|
|
123
|
+
try {
|
|
124
|
+
await this.exec("git diff --cached --quiet", this.workDir);
|
|
125
|
+
return false; // Exit code 0 = no staged changes
|
|
126
|
+
}
|
|
127
|
+
catch {
|
|
128
|
+
return true; // Exit code 1 = there are staged changes
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Check if a file exists on a specific branch.
|
|
133
|
+
* Used for createOnly checks against the base branch (not the working directory).
|
|
134
|
+
*/
|
|
135
|
+
async fileExistsOnBranch(fileName, branch) {
|
|
136
|
+
try {
|
|
137
|
+
await this.exec(`git show ${escapeShellArg(branch)}:${escapeShellArg(fileName)}`, this.workDir);
|
|
138
|
+
return true;
|
|
139
|
+
}
|
|
140
|
+
catch {
|
|
141
|
+
return false;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Stage all changes and commit with the given message.
|
|
146
|
+
* Uses --no-verify to skip pre-commit hooks (config sync should always succeed).
|
|
147
|
+
* @returns true if a commit was made, false if there were no staged changes
|
|
148
|
+
*/
|
|
145
149
|
async commit(message) {
|
|
146
150
|
if (this.dryRun) {
|
|
147
|
-
return;
|
|
151
|
+
return true;
|
|
148
152
|
}
|
|
149
153
|
await this.exec("git add -A", this.workDir);
|
|
150
|
-
|
|
154
|
+
// Check if there are actually staged changes after git add
|
|
155
|
+
if (!(await this.hasStagedChanges())) {
|
|
156
|
+
return false; // No changes to commit
|
|
157
|
+
}
|
|
158
|
+
// Use --no-verify to skip pre-commit hooks
|
|
159
|
+
await this.exec(`git commit --no-verify -m ${escapeShellArg(message)}`, this.workDir);
|
|
160
|
+
return true;
|
|
151
161
|
}
|
|
152
162
|
async push(branchName) {
|
|
153
163
|
if (this.dryRun) {
|
|
@@ -2,12 +2,15 @@ import { RepoConfig } from "./config.js";
|
|
|
2
2
|
import { RepoInfo } from "./repo-detector.js";
|
|
3
3
|
import { GitOps, GitOpsOptions } from "./git-ops.js";
|
|
4
4
|
import { ILogger } from "./logger.js";
|
|
5
|
+
import { CommandExecutor } from "./command-executor.js";
|
|
5
6
|
export interface ProcessorOptions {
|
|
6
7
|
branchName: string;
|
|
7
8
|
workDir: string;
|
|
8
9
|
dryRun?: boolean;
|
|
9
10
|
/** Number of retries for network operations (default: 3) */
|
|
10
11
|
retries?: number;
|
|
12
|
+
/** Command executor for shell commands (for testing) */
|
|
13
|
+
executor?: CommandExecutor;
|
|
11
14
|
}
|
|
12
15
|
/**
|
|
13
16
|
* Factory function type for creating GitOps instances.
|
|
@@ -5,6 +5,8 @@ import { getRepoDisplayName } from "./repo-detector.js";
|
|
|
5
5
|
import { GitOps } from "./git-ops.js";
|
|
6
6
|
import { createPR, mergePR } from "./pr-creator.js";
|
|
7
7
|
import { logger } from "./logger.js";
|
|
8
|
+
import { getPRStrategy } from "./strategies/index.js";
|
|
9
|
+
import { defaultExecutor } from "./command-executor.js";
|
|
8
10
|
/**
|
|
9
11
|
* Determines if a file should be marked as executable.
|
|
10
12
|
* .sh files are auto-executable unless explicit executable: false is set.
|
|
@@ -35,6 +37,7 @@ export class RepositoryProcessor {
|
|
|
35
37
|
async process(repoConfig, repoInfo, options) {
|
|
36
38
|
const repoName = getRepoDisplayName(repoInfo);
|
|
37
39
|
const { branchName, workDir, dryRun, retries } = options;
|
|
40
|
+
const executor = options.executor ?? defaultExecutor;
|
|
38
41
|
this.gitOps = this.gitOpsFactory({ workDir, dryRun, retries });
|
|
39
42
|
try {
|
|
40
43
|
// Step 1: Clean workspace
|
|
@@ -46,19 +49,39 @@ export class RepositoryProcessor {
|
|
|
46
49
|
// Step 3: Get default branch for PR base
|
|
47
50
|
const { branch: baseBranch, method: detectionMethod } = await this.gitOps.getDefaultBranch();
|
|
48
51
|
this.log.info(`Default branch: ${baseBranch} (detected via ${detectionMethod})`);
|
|
49
|
-
// Step
|
|
50
|
-
|
|
52
|
+
// Step 3.5: Close existing PR if exists (fresh start approach)
|
|
53
|
+
// This ensures isolated sync attempts - each run starts from clean state
|
|
54
|
+
if (!dryRun) {
|
|
55
|
+
this.log.info("Checking for existing PR...");
|
|
56
|
+
const strategy = getPRStrategy(repoInfo, executor);
|
|
57
|
+
const closed = await strategy.closeExistingPR({
|
|
58
|
+
repoInfo,
|
|
59
|
+
branchName,
|
|
60
|
+
baseBranch,
|
|
61
|
+
workDir,
|
|
62
|
+
retries,
|
|
63
|
+
});
|
|
64
|
+
if (closed) {
|
|
65
|
+
this.log.info("Closed existing PR and deleted branch for fresh sync");
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
// Step 4: Create branch (always fresh from base branch)
|
|
69
|
+
this.log.info(`Creating branch: ${branchName}`);
|
|
51
70
|
await this.gitOps.createBranch(branchName);
|
|
52
71
|
// Step 5: Write all config files and track changes
|
|
53
72
|
const changedFiles = [];
|
|
54
73
|
for (const file of repoConfig.files) {
|
|
55
74
|
const filePath = join(workDir, file.fileName);
|
|
56
|
-
const
|
|
57
|
-
// Handle createOnly -
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
75
|
+
const fileExistsLocal = existsSync(filePath);
|
|
76
|
+
// Handle createOnly - check against BASE branch, not current working directory
|
|
77
|
+
// This ensures consistent behavior: createOnly means "only create if doesn't exist on main"
|
|
78
|
+
if (file.createOnly) {
|
|
79
|
+
const existsOnBase = await this.gitOps.fileExistsOnBranch(file.fileName, baseBranch);
|
|
80
|
+
if (existsOnBase) {
|
|
81
|
+
this.log.info(`Skipping ${file.fileName} (createOnly: exists on ${baseBranch})`);
|
|
82
|
+
changedFiles.push({ fileName: file.fileName, action: "skip" });
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
62
85
|
}
|
|
63
86
|
this.log.info(`Writing ${file.fileName}...`);
|
|
64
87
|
const fileContent = convertContentToString(file.content, file.fileName, {
|
|
@@ -66,7 +89,9 @@ export class RepositoryProcessor {
|
|
|
66
89
|
schemaUrl: file.schemaUrl,
|
|
67
90
|
});
|
|
68
91
|
// Determine action type (create vs update)
|
|
69
|
-
const action =
|
|
92
|
+
const action = fileExistsLocal
|
|
93
|
+
? "update"
|
|
94
|
+
: "create";
|
|
70
95
|
if (dryRun) {
|
|
71
96
|
// In dry-run, check if file would change without writing
|
|
72
97
|
if (this.gitOps.wouldChange(file.fileName, fileContent)) {
|
|
@@ -125,9 +150,19 @@ export class RepositoryProcessor {
|
|
|
125
150
|
};
|
|
126
151
|
}
|
|
127
152
|
// Step 7: Commit
|
|
128
|
-
this.log.info("
|
|
153
|
+
this.log.info("Staging changes...");
|
|
129
154
|
const commitMessage = this.formatCommitMessage(changedFiles);
|
|
130
|
-
await this.gitOps.commit(commitMessage);
|
|
155
|
+
const committed = await this.gitOps.commit(commitMessage);
|
|
156
|
+
if (!committed) {
|
|
157
|
+
this.log.info("No staged changes after git add -A, skipping commit");
|
|
158
|
+
return {
|
|
159
|
+
success: true,
|
|
160
|
+
repoName,
|
|
161
|
+
message: "No changes detected after staging",
|
|
162
|
+
skipped: true,
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
this.log.info(`Committed: ${commitMessage}`);
|
|
131
166
|
// Step 8: Push
|
|
132
167
|
this.log.info("Pushing to remote...");
|
|
133
168
|
await this.gitOps.push(branchName);
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { PRResult } from "../pr-creator.js";
|
|
2
|
-
import { BasePRStrategy, PRStrategyOptions, MergeOptions, MergeResult } from "./pr-strategy.js";
|
|
2
|
+
import { BasePRStrategy, PRStrategyOptions, CloseExistingPROptions, MergeOptions, MergeResult } from "./pr-strategy.js";
|
|
3
3
|
import { CommandExecutor } from "../command-executor.js";
|
|
4
4
|
export declare class AzurePRStrategy extends BasePRStrategy {
|
|
5
5
|
constructor(executor?: CommandExecutor);
|
|
6
6
|
private getOrgUrl;
|
|
7
7
|
private buildPRUrl;
|
|
8
8
|
checkExistingPR(options: PRStrategyOptions): Promise<string | null>;
|
|
9
|
+
closeExistingPR(options: CloseExistingPROptions): Promise<boolean>;
|
|
9
10
|
create(options: PRStrategyOptions): Promise<PRResult>;
|
|
10
11
|
/**
|
|
11
12
|
* Extract PR ID and repo info from Azure DevOps PR URL.
|
|
@@ -41,6 +41,58 @@ export class AzurePRStrategy extends BasePRStrategy {
|
|
|
41
41
|
return null;
|
|
42
42
|
}
|
|
43
43
|
}
|
|
44
|
+
async closeExistingPR(options) {
|
|
45
|
+
const { repoInfo, branchName, baseBranch, workDir, retries = 3 } = options;
|
|
46
|
+
if (!isAzureDevOpsRepo(repoInfo)) {
|
|
47
|
+
throw new Error("Expected Azure DevOps repository");
|
|
48
|
+
}
|
|
49
|
+
const azureRepoInfo = repoInfo;
|
|
50
|
+
const orgUrl = this.getOrgUrl(azureRepoInfo);
|
|
51
|
+
// First check if there's an existing PR
|
|
52
|
+
const existingUrl = await this.checkExistingPR({
|
|
53
|
+
repoInfo,
|
|
54
|
+
branchName,
|
|
55
|
+
baseBranch,
|
|
56
|
+
workDir,
|
|
57
|
+
retries,
|
|
58
|
+
title: "", // Not used for check
|
|
59
|
+
body: "", // Not used for check
|
|
60
|
+
});
|
|
61
|
+
if (!existingUrl) {
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
// Extract PR ID from URL
|
|
65
|
+
const prInfo = this.parsePRUrl(existingUrl);
|
|
66
|
+
if (!prInfo) {
|
|
67
|
+
logger.info(`Warning: Could not parse PR URL: ${existingUrl}`);
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
// Abandon the PR (Azure DevOps equivalent of closing)
|
|
71
|
+
const abandonCommand = `az repos pr update --id ${escapeShellArg(prInfo.prId)} --status abandoned --org ${escapeShellArg(orgUrl)} --project ${escapeShellArg(prInfo.project)}`;
|
|
72
|
+
try {
|
|
73
|
+
await withRetry(() => this.executor.exec(abandonCommand, workDir), {
|
|
74
|
+
retries,
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
catch (error) {
|
|
78
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
79
|
+
logger.info(`Warning: Failed to abandon PR #${prInfo.prId}: ${message}`);
|
|
80
|
+
return false;
|
|
81
|
+
}
|
|
82
|
+
// Delete the source branch
|
|
83
|
+
const deleteBranchCommand = `az repos ref delete --name refs/heads/${escapeShellArg(branchName)} --repository ${escapeShellArg(azureRepoInfo.repo)} --org ${escapeShellArg(orgUrl)} --project ${escapeShellArg(azureRepoInfo.project)}`;
|
|
84
|
+
try {
|
|
85
|
+
await withRetry(() => this.executor.exec(deleteBranchCommand, workDir), {
|
|
86
|
+
retries,
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
catch (error) {
|
|
90
|
+
// Branch deletion failure is not critical - PR is already abandoned
|
|
91
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
92
|
+
logger.info(`Warning: Failed to delete branch ${branchName}: ${message}`);
|
|
93
|
+
}
|
|
94
|
+
return true;
|
|
95
|
+
}
|
|
44
96
|
async create(options) {
|
|
45
97
|
const { repoInfo, title, body, branchName, baseBranch, workDir, retries = 3, } = options;
|
|
46
98
|
if (!isAzureDevOpsRepo(repoInfo)) {
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { GitHubRepoInfo } from "../repo-detector.js";
|
|
2
2
|
import { PRResult } from "../pr-creator.js";
|
|
3
|
-
import { BasePRStrategy, PRStrategyOptions, MergeOptions, MergeResult } from "./pr-strategy.js";
|
|
3
|
+
import { BasePRStrategy, PRStrategyOptions, CloseExistingPROptions, MergeOptions, MergeResult } from "./pr-strategy.js";
|
|
4
4
|
export declare class GitHubPRStrategy extends BasePRStrategy {
|
|
5
5
|
checkExistingPR(options: PRStrategyOptions): Promise<string | null>;
|
|
6
|
+
closeExistingPR(options: CloseExistingPROptions): Promise<boolean>;
|
|
6
7
|
create(options: PRStrategyOptions): Promise<PRResult>;
|
|
7
8
|
/**
|
|
8
9
|
* Check if auto-merge is enabled on the repository.
|
|
@@ -31,6 +31,42 @@ export class GitHubPRStrategy extends BasePRStrategy {
|
|
|
31
31
|
return null;
|
|
32
32
|
}
|
|
33
33
|
}
|
|
34
|
+
async closeExistingPR(options) {
|
|
35
|
+
const { repoInfo, branchName, baseBranch, workDir, retries = 3 } = options;
|
|
36
|
+
if (!isGitHubRepo(repoInfo)) {
|
|
37
|
+
throw new Error("Expected GitHub repository");
|
|
38
|
+
}
|
|
39
|
+
// First check if there's an existing PR
|
|
40
|
+
const existingUrl = await this.checkExistingPR({
|
|
41
|
+
repoInfo,
|
|
42
|
+
branchName,
|
|
43
|
+
baseBranch,
|
|
44
|
+
workDir,
|
|
45
|
+
retries,
|
|
46
|
+
title: "", // Not used for check
|
|
47
|
+
body: "", // Not used for check
|
|
48
|
+
});
|
|
49
|
+
if (!existingUrl) {
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
// Extract PR number from URL
|
|
53
|
+
const prNumber = existingUrl.match(/\/pull\/(\d+)/)?.[1];
|
|
54
|
+
if (!prNumber) {
|
|
55
|
+
logger.info(`Warning: Could not extract PR number from URL: ${existingUrl}`);
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
// Close the PR and delete the branch
|
|
59
|
+
const command = `gh pr close ${escapeShellArg(prNumber)} --repo ${escapeShellArg(repoInfo.owner)}/${escapeShellArg(repoInfo.repo)} --delete-branch`;
|
|
60
|
+
try {
|
|
61
|
+
await withRetry(() => this.executor.exec(command, workDir), { retries });
|
|
62
|
+
return true;
|
|
63
|
+
}
|
|
64
|
+
catch (error) {
|
|
65
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
66
|
+
logger.info(`Warning: Failed to close existing PR #${prNumber}: ${message}`);
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
34
70
|
async create(options) {
|
|
35
71
|
const { repoInfo, title, body, branchName, baseBranch, workDir, retries = 3, } = options;
|
|
36
72
|
if (!isGitHubRepo(repoInfo)) {
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { RepoInfo } from "../repo-detector.js";
|
|
2
2
|
import type { PRStrategy } from "./pr-strategy.js";
|
|
3
|
-
|
|
3
|
+
import { CommandExecutor } from "../command-executor.js";
|
|
4
|
+
export type { PRStrategy, PRStrategyOptions, CloseExistingPROptions, PRMergeConfig, MergeOptions, MergeResult, } from "./pr-strategy.js";
|
|
4
5
|
export { BasePRStrategy, PRWorkflowExecutor } from "./pr-strategy.js";
|
|
5
6
|
export { GitHubPRStrategy } from "./github-pr-strategy.js";
|
|
6
7
|
export { AzurePRStrategy } from "./azure-pr-strategy.js";
|
|
7
8
|
/**
|
|
8
9
|
* Factory function to get the appropriate PR strategy for a repository.
|
|
9
|
-
*
|
|
10
|
-
*
|
|
10
|
+
* @param repoInfo - Repository information
|
|
11
|
+
* @param executor - Optional command executor for shell commands
|
|
11
12
|
*/
|
|
12
|
-
export declare function getPRStrategy(repoInfo: RepoInfo): PRStrategy;
|
|
13
|
+
export declare function getPRStrategy(repoInfo: RepoInfo, executor?: CommandExecutor): PRStrategy;
|
package/dist/strategies/index.js
CHANGED
|
@@ -6,15 +6,15 @@ export { GitHubPRStrategy } from "./github-pr-strategy.js";
|
|
|
6
6
|
export { AzurePRStrategy } from "./azure-pr-strategy.js";
|
|
7
7
|
/**
|
|
8
8
|
* Factory function to get the appropriate PR strategy for a repository.
|
|
9
|
-
*
|
|
10
|
-
*
|
|
9
|
+
* @param repoInfo - Repository information
|
|
10
|
+
* @param executor - Optional command executor for shell commands
|
|
11
11
|
*/
|
|
12
|
-
export function getPRStrategy(repoInfo) {
|
|
12
|
+
export function getPRStrategy(repoInfo, executor) {
|
|
13
13
|
if (isGitHubRepo(repoInfo)) {
|
|
14
|
-
return new GitHubPRStrategy();
|
|
14
|
+
return new GitHubPRStrategy(executor);
|
|
15
15
|
}
|
|
16
16
|
if (isAzureDevOpsRepo(repoInfo)) {
|
|
17
|
-
return new AzurePRStrategy();
|
|
17
|
+
return new AzurePRStrategy(executor);
|
|
18
18
|
}
|
|
19
19
|
// Type exhaustiveness check - should never reach here
|
|
20
20
|
const _exhaustive = repoInfo;
|
|
@@ -30,6 +30,16 @@ export interface MergeOptions {
|
|
|
30
30
|
workDir: string;
|
|
31
31
|
retries?: number;
|
|
32
32
|
}
|
|
33
|
+
/**
|
|
34
|
+
* Options for closing an existing PR.
|
|
35
|
+
*/
|
|
36
|
+
export interface CloseExistingPROptions {
|
|
37
|
+
repoInfo: RepoInfo;
|
|
38
|
+
branchName: string;
|
|
39
|
+
baseBranch: string;
|
|
40
|
+
workDir: string;
|
|
41
|
+
retries?: number;
|
|
42
|
+
}
|
|
33
43
|
/**
|
|
34
44
|
* Interface for PR creation strategies (platform-specific implementations).
|
|
35
45
|
* Strategies focus on platform-specific logic (checkExistingPR, create, merge).
|
|
@@ -41,6 +51,12 @@ export interface PRStrategy {
|
|
|
41
51
|
* @returns PR URL if exists, null otherwise
|
|
42
52
|
*/
|
|
43
53
|
checkExistingPR(options: PRStrategyOptions): Promise<string | null>;
|
|
54
|
+
/**
|
|
55
|
+
* Close an existing PR and delete its branch.
|
|
56
|
+
* Used for fresh start approach - always create new PR from clean state.
|
|
57
|
+
* @returns true if PR was closed, false if no PR existed
|
|
58
|
+
*/
|
|
59
|
+
closeExistingPR(options: CloseExistingPROptions): Promise<boolean>;
|
|
44
60
|
/**
|
|
45
61
|
* Create a new PR
|
|
46
62
|
* @returns Result with URL and status
|
|
@@ -62,6 +78,7 @@ export declare abstract class BasePRStrategy implements PRStrategy {
|
|
|
62
78
|
protected executor: CommandExecutor;
|
|
63
79
|
constructor(executor?: CommandExecutor);
|
|
64
80
|
abstract checkExistingPR(options: PRStrategyOptions): Promise<string | null>;
|
|
81
|
+
abstract closeExistingPR(options: CloseExistingPROptions): Promise<boolean>;
|
|
65
82
|
abstract create(options: PRStrategyOptions): Promise<PRResult>;
|
|
66
83
|
abstract merge(options: MergeOptions): Promise<MergeResult>;
|
|
67
84
|
/**
|
package/package.json
CHANGED