@aspruyt/xfg 3.1.4 → 3.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/authenticated-git-ops.d.ts +9 -2
- package/dist/authenticated-git-ops.js +9 -2
- package/dist/command-executor.d.ts +3 -3
- package/dist/config-normalizer.js +52 -4
- package/dist/config-validator.d.ts +1 -1
- package/dist/config-validator.js +31 -2
- package/dist/config.d.ts +6 -2
- package/dist/git-ops.d.ts +29 -3
- package/dist/logger.d.ts +8 -1
- package/dist/pr-creator.d.ts +3 -3
- package/dist/repository-processor.d.ts +9 -3
- package/dist/ruleset-processor.d.ts +4 -1
- package/dist/strategies/azure-pr-strategy.d.ts +2 -2
- package/dist/strategies/commit-strategy-selector.d.ts +3 -3
- package/dist/strategies/commit-strategy.d.ts +1 -1
- package/dist/strategies/git-commit-strategy.d.ts +4 -4
- package/dist/strategies/github-ruleset-strategy.d.ts +4 -3
- package/dist/strategies/gitlab-pr-strategy.d.ts +2 -2
- package/dist/strategies/graphql-commit-strategy.d.ts +4 -4
- package/dist/strategies/graphql-commit-strategy.js +2 -1
- package/dist/strategies/index.d.ts +5 -5
- package/dist/strategies/pr-strategy.d.ts +6 -6
- package/dist/strategies/ruleset-strategy.d.ts +10 -0
- package/dist/strategies/ruleset-strategy.js +1 -0
- package/package.json +2 -2
|
@@ -28,7 +28,9 @@ export interface IAuthenticatedGitOps {
|
|
|
28
28
|
branch: string;
|
|
29
29
|
method: string;
|
|
30
30
|
}>;
|
|
31
|
-
lsRemote(branchName: string
|
|
31
|
+
lsRemote(branchName: string, options?: {
|
|
32
|
+
skipRetry?: boolean;
|
|
33
|
+
}): Promise<string>;
|
|
32
34
|
pushRefspec(refspec: string, options?: {
|
|
33
35
|
delete?: boolean;
|
|
34
36
|
}): Promise<void>;
|
|
@@ -82,8 +84,13 @@ export declare class AuthenticatedGitOps implements IAuthenticatedGitOps {
|
|
|
82
84
|
/**
|
|
83
85
|
* Execute ls-remote with authentication.
|
|
84
86
|
* Used by GraphQLCommitStrategy to check if branch exists on remote.
|
|
87
|
+
*
|
|
88
|
+
* @param options.skipRetry - If true, don't retry on failure. Use when checking
|
|
89
|
+
* branch existence where failure is expected for new branches.
|
|
85
90
|
*/
|
|
86
|
-
lsRemote(branchName: string
|
|
91
|
+
lsRemote(branchName: string, options?: {
|
|
92
|
+
skipRetry?: boolean;
|
|
93
|
+
}): Promise<string>;
|
|
87
94
|
/**
|
|
88
95
|
* Execute push with custom refspec (e.g., HEAD:branchName).
|
|
89
96
|
* Used by GraphQLCommitStrategy for creating/deleting remote branches.
|
|
@@ -100,11 +100,18 @@ export class AuthenticatedGitOps {
|
|
|
100
100
|
/**
|
|
101
101
|
* Execute ls-remote with authentication.
|
|
102
102
|
* Used by GraphQLCommitStrategy to check if branch exists on remote.
|
|
103
|
+
*
|
|
104
|
+
* @param options.skipRetry - If true, don't retry on failure. Use when checking
|
|
105
|
+
* branch existence where failure is expected for new branches.
|
|
103
106
|
*/
|
|
104
|
-
async lsRemote(branchName) {
|
|
107
|
+
async lsRemote(branchName, options) {
|
|
105
108
|
// Remote URL already has auth from clone
|
|
106
109
|
const safeBranch = escapeShellArg(branchName);
|
|
107
|
-
|
|
110
|
+
const command = `git ls-remote --exit-code --heads origin ${safeBranch}`;
|
|
111
|
+
if (options?.skipRetry) {
|
|
112
|
+
return this.executor.exec(command, this.workDir);
|
|
113
|
+
}
|
|
114
|
+
return this.execWithRetry(command);
|
|
108
115
|
}
|
|
109
116
|
/**
|
|
110
117
|
* Execute push with custom refspec (e.g., HEAD:branchName).
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Interface for executing shell commands.
|
|
3
3
|
* Enables dependency injection for testing and alternative implementations.
|
|
4
4
|
*/
|
|
5
|
-
export interface
|
|
5
|
+
export interface ICommandExecutor {
|
|
6
6
|
/**
|
|
7
7
|
* Execute a shell command and return the output.
|
|
8
8
|
* @param command The command to execute
|
|
@@ -16,10 +16,10 @@ export interface CommandExecutor {
|
|
|
16
16
|
* Default implementation that uses Node.js child_process.execSync.
|
|
17
17
|
* Note: Commands are escaped using escapeShellArg before being passed here.
|
|
18
18
|
*/
|
|
19
|
-
export declare class ShellCommandExecutor implements
|
|
19
|
+
export declare class ShellCommandExecutor implements ICommandExecutor {
|
|
20
20
|
exec(command: string, cwd: string): Promise<string>;
|
|
21
21
|
}
|
|
22
22
|
/**
|
|
23
23
|
* Default executor instance for production use.
|
|
24
24
|
*/
|
|
25
|
-
export declare const defaultExecutor:
|
|
25
|
+
export declare const defaultExecutor: ICommandExecutor;
|
|
@@ -60,14 +60,30 @@ export function mergeSettings(root, perRepo) {
|
|
|
60
60
|
// Merge rulesets by name - each ruleset is deep merged
|
|
61
61
|
const rootRulesets = root?.rulesets ?? {};
|
|
62
62
|
const repoRulesets = perRepo?.rulesets ?? {};
|
|
63
|
+
// Check if repo opts out of all inherited rulesets
|
|
64
|
+
const inheritRulesets = repoRulesets?.inherit !== false;
|
|
63
65
|
const allRulesetNames = new Set([
|
|
64
|
-
...Object.keys(rootRulesets),
|
|
65
|
-
...Object.keys(repoRulesets),
|
|
66
|
+
...Object.keys(rootRulesets).filter((name) => name !== "inherit"),
|
|
67
|
+
...Object.keys(repoRulesets).filter((name) => name !== "inherit"),
|
|
66
68
|
]);
|
|
67
69
|
if (allRulesetNames.size > 0) {
|
|
68
70
|
result.rulesets = {};
|
|
69
71
|
for (const name of allRulesetNames) {
|
|
70
|
-
|
|
72
|
+
const rootRuleset = rootRulesets[name];
|
|
73
|
+
const repoRuleset = repoRulesets[name];
|
|
74
|
+
// Skip if repo explicitly opts out of this ruleset
|
|
75
|
+
if (repoRuleset === false) {
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
// Skip root rulesets if inherit: false (unless repo has override)
|
|
79
|
+
if (!inheritRulesets && !repoRuleset && rootRuleset) {
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
result.rulesets[name] = mergeRuleset(rootRuleset, repoRuleset);
|
|
83
|
+
}
|
|
84
|
+
// Clean up empty rulesets object
|
|
85
|
+
if (Object.keys(result.rulesets).length === 0) {
|
|
86
|
+
delete result.rulesets;
|
|
71
87
|
}
|
|
72
88
|
}
|
|
73
89
|
// deleteOrphaned: per-repo overrides root
|
|
@@ -89,13 +105,23 @@ export function normalizeConfig(raw) {
|
|
|
89
105
|
const gitUrls = Array.isArray(rawRepo.git) ? rawRepo.git : [rawRepo.git];
|
|
90
106
|
for (const gitUrl of gitUrls) {
|
|
91
107
|
const files = [];
|
|
108
|
+
// Check if repo opts out of all inherited files
|
|
109
|
+
const inheritFiles = rawRepo.files?.inherit !==
|
|
110
|
+
false;
|
|
92
111
|
// Step 2: Process each file definition
|
|
93
112
|
for (const fileName of fileNames) {
|
|
113
|
+
// Skip reserved key
|
|
114
|
+
if (fileName === "inherit")
|
|
115
|
+
continue;
|
|
94
116
|
const repoOverride = rawRepo.files?.[fileName];
|
|
95
117
|
// Skip excluded files (set to false)
|
|
96
118
|
if (repoOverride === false) {
|
|
97
119
|
continue;
|
|
98
120
|
}
|
|
121
|
+
// Skip if inherit: false and no repo-specific override
|
|
122
|
+
if (!inheritFiles && !repoOverride) {
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
99
125
|
const fileConfig = raw.files[fileName];
|
|
100
126
|
const fileStrategy = fileConfig.mergeStrategy ?? "replace";
|
|
101
127
|
// Step 3: Compute merged content for this file
|
|
@@ -190,12 +216,34 @@ export function normalizeConfig(raw) {
|
|
|
190
216
|
});
|
|
191
217
|
}
|
|
192
218
|
}
|
|
219
|
+
// Normalize root settings (filter out inherit key if present)
|
|
220
|
+
let normalizedRootSettings;
|
|
221
|
+
if (raw.settings) {
|
|
222
|
+
normalizedRootSettings = {};
|
|
223
|
+
if (raw.settings.rulesets) {
|
|
224
|
+
const filteredRulesets = {};
|
|
225
|
+
for (const [name, ruleset] of Object.entries(raw.settings.rulesets)) {
|
|
226
|
+
if (name === "inherit" || ruleset === false)
|
|
227
|
+
continue;
|
|
228
|
+
filteredRulesets[name] = ruleset;
|
|
229
|
+
}
|
|
230
|
+
if (Object.keys(filteredRulesets).length > 0) {
|
|
231
|
+
normalizedRootSettings.rulesets = filteredRulesets;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
if (raw.settings.deleteOrphaned !== undefined) {
|
|
235
|
+
normalizedRootSettings.deleteOrphaned = raw.settings.deleteOrphaned;
|
|
236
|
+
}
|
|
237
|
+
if (Object.keys(normalizedRootSettings).length === 0) {
|
|
238
|
+
normalizedRootSettings = undefined;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
193
241
|
return {
|
|
194
242
|
id: raw.id,
|
|
195
243
|
repos: expandedRepos,
|
|
196
244
|
prTemplate: raw.prTemplate,
|
|
197
245
|
githubHosts: raw.githubHosts,
|
|
198
246
|
deleteOrphaned: raw.deleteOrphaned,
|
|
199
|
-
settings:
|
|
247
|
+
settings: normalizedRootSettings,
|
|
200
248
|
};
|
|
201
249
|
}
|
|
@@ -7,7 +7,7 @@ export declare function validateRawConfig(config: RawConfig): void;
|
|
|
7
7
|
/**
|
|
8
8
|
* Validates settings object containing rulesets.
|
|
9
9
|
*/
|
|
10
|
-
export declare function validateSettings(settings: unknown, context: string): void;
|
|
10
|
+
export declare function validateSettings(settings: unknown, context: string, rootRulesetNames?: string[]): void;
|
|
11
11
|
/**
|
|
12
12
|
* Validates that config is suitable for the sync command.
|
|
13
13
|
* @throws Error if files section is missing or empty
|
package/dist/config-validator.js
CHANGED
|
@@ -49,6 +49,10 @@ export function validateRawConfig(config) {
|
|
|
49
49
|
"Use 'files' to sync configuration files, or 'settings' to manage repository settings.");
|
|
50
50
|
}
|
|
51
51
|
const fileNames = hasFiles ? Object.keys(config.files) : [];
|
|
52
|
+
// Check for reserved key 'inherit' at root files level
|
|
53
|
+
if (hasFiles && "inherit" in config.files) {
|
|
54
|
+
throw new Error("'inherit' is a reserved key and cannot be used as a filename");
|
|
55
|
+
}
|
|
52
56
|
// Validate each file definition
|
|
53
57
|
for (const fileName of fileNames) {
|
|
54
58
|
validateFileName(fileName);
|
|
@@ -127,6 +131,10 @@ export function validateRawConfig(config) {
|
|
|
127
131
|
// Validate root settings
|
|
128
132
|
if (config.settings !== undefined) {
|
|
129
133
|
validateSettings(config.settings, "Root");
|
|
134
|
+
// Check for reserved key 'inherit' at root rulesets level
|
|
135
|
+
if (config.settings.rulesets && "inherit" in config.settings.rulesets) {
|
|
136
|
+
throw new Error("'inherit' is a reserved key and cannot be used as a ruleset name");
|
|
137
|
+
}
|
|
130
138
|
}
|
|
131
139
|
// Validate githubHosts if provided
|
|
132
140
|
if (config.githubHosts !== undefined) {
|
|
@@ -161,6 +169,14 @@ export function validateRawConfig(config) {
|
|
|
161
169
|
throw new Error(`Repo at index ${i}: files must be an object`);
|
|
162
170
|
}
|
|
163
171
|
for (const fileName of Object.keys(repo.files)) {
|
|
172
|
+
// Skip reserved key 'inherit'
|
|
173
|
+
if (fileName === "inherit") {
|
|
174
|
+
const inheritValue = repo.files.inherit;
|
|
175
|
+
if (typeof inheritValue !== "boolean") {
|
|
176
|
+
throw new Error(`Repo at index ${i}: files.inherit must be a boolean`);
|
|
177
|
+
}
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
164
180
|
// Ensure the file is defined at root level
|
|
165
181
|
if (!config.files || !config.files[fileName]) {
|
|
166
182
|
throw new Error(`Repo at index ${i} references undefined file '${fileName}'. File must be defined in root 'files' object.`);
|
|
@@ -233,7 +249,10 @@ export function validateRawConfig(config) {
|
|
|
233
249
|
}
|
|
234
250
|
// Validate per-repo settings
|
|
235
251
|
if (repo.settings !== undefined) {
|
|
236
|
-
|
|
252
|
+
const rootRulesetNames = config.settings?.rulesets
|
|
253
|
+
? Object.keys(config.settings.rulesets).filter((k) => k !== "inherit")
|
|
254
|
+
: [];
|
|
255
|
+
validateSettings(repo.settings, `Repo ${getGitDisplayName(repo.git)}`, rootRulesetNames);
|
|
237
256
|
}
|
|
238
257
|
}
|
|
239
258
|
}
|
|
@@ -465,7 +484,7 @@ function validateRuleset(ruleset, name, context) {
|
|
|
465
484
|
/**
|
|
466
485
|
* Validates settings object containing rulesets.
|
|
467
486
|
*/
|
|
468
|
-
export function validateSettings(settings, context) {
|
|
487
|
+
export function validateSettings(settings, context, rootRulesetNames) {
|
|
469
488
|
if (typeof settings !== "object" ||
|
|
470
489
|
settings === null ||
|
|
471
490
|
Array.isArray(settings)) {
|
|
@@ -480,6 +499,16 @@ export function validateSettings(settings, context) {
|
|
|
480
499
|
}
|
|
481
500
|
const rulesets = s.rulesets;
|
|
482
501
|
for (const [name, ruleset] of Object.entries(rulesets)) {
|
|
502
|
+
// Skip reserved key
|
|
503
|
+
if (name === "inherit")
|
|
504
|
+
continue;
|
|
505
|
+
// Check for opt-out of non-existent root ruleset
|
|
506
|
+
if (ruleset === false) {
|
|
507
|
+
if (rootRulesetNames && !rootRulesetNames.includes(name)) {
|
|
508
|
+
throw new Error(`${context}: Cannot opt out of '${name}' - not defined in root settings.rulesets`);
|
|
509
|
+
}
|
|
510
|
+
continue; // Skip further validation for false entries
|
|
511
|
+
}
|
|
483
512
|
validateRuleset(ruleset, name, context);
|
|
484
513
|
}
|
|
485
514
|
}
|
package/dist/config.d.ts
CHANGED
|
@@ -239,12 +239,16 @@ export interface RawRepoFileOverride {
|
|
|
239
239
|
deleteOrphaned?: boolean;
|
|
240
240
|
}
|
|
241
241
|
export interface RawRepoSettings {
|
|
242
|
-
rulesets?: Record<string, Ruleset
|
|
242
|
+
rulesets?: Record<string, Ruleset | false> & {
|
|
243
|
+
inherit?: boolean;
|
|
244
|
+
};
|
|
243
245
|
deleteOrphaned?: boolean;
|
|
244
246
|
}
|
|
245
247
|
export interface RawRepoConfig {
|
|
246
248
|
git: string | string[];
|
|
247
|
-
files?: Record<string, RawRepoFileOverride | false
|
|
249
|
+
files?: Record<string, RawRepoFileOverride | false> & {
|
|
250
|
+
inherit?: boolean;
|
|
251
|
+
};
|
|
248
252
|
prOptions?: PRMergeOptions;
|
|
249
253
|
settings?: RawRepoSettings;
|
|
250
254
|
}
|
package/dist/git-ops.d.ts
CHANGED
|
@@ -1,12 +1,38 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { ICommandExecutor } from "./command-executor.js";
|
|
2
|
+
export interface IGitOps {
|
|
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
28
|
export interface GitOpsOptions {
|
|
3
29
|
workDir: string;
|
|
4
30
|
dryRun?: boolean;
|
|
5
|
-
executor?:
|
|
31
|
+
executor?: ICommandExecutor;
|
|
6
32
|
/** Number of retries for network operations (default: 3) */
|
|
7
33
|
retries?: number;
|
|
8
34
|
}
|
|
9
|
-
export declare class GitOps {
|
|
35
|
+
export declare class GitOps implements IGitOps {
|
|
10
36
|
private workDir;
|
|
11
37
|
private dryRun;
|
|
12
38
|
private executor;
|
package/dist/logger.d.ts
CHANGED
|
@@ -3,6 +3,13 @@ export interface ILogger {
|
|
|
3
3
|
info(message: string): void;
|
|
4
4
|
fileDiff(fileName: string, status: FileStatus, diffLines: string[]): void;
|
|
5
5
|
diffSummary(newCount: number, modifiedCount: number, unchangedCount: number, deletedCount?: number): void;
|
|
6
|
+
setTotal(total: number): void;
|
|
7
|
+
progress(current: number, repoName: string, message: string): void;
|
|
8
|
+
success(current: number, repoName: string, message: string): void;
|
|
9
|
+
skip(current: number, repoName: string, reason: string): void;
|
|
10
|
+
error(current: number, repoName: string, error: string): void;
|
|
11
|
+
summary(): void;
|
|
12
|
+
hasFailures(): boolean;
|
|
6
13
|
}
|
|
7
14
|
export interface LoggerStats {
|
|
8
15
|
total: number;
|
|
@@ -10,7 +17,7 @@ export interface LoggerStats {
|
|
|
10
17
|
failed: number;
|
|
11
18
|
skipped: number;
|
|
12
19
|
}
|
|
13
|
-
export declare class Logger {
|
|
20
|
+
export declare class Logger implements ILogger {
|
|
14
21
|
private stats;
|
|
15
22
|
setTotal(total: number): void;
|
|
16
23
|
progress(current: number, repoName: string, message: string): void;
|
package/dist/pr-creator.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { RepoInfo } from "./repo-detector.js";
|
|
2
2
|
import { MergeResult, PRMergeConfig } from "./strategies/index.js";
|
|
3
|
-
import {
|
|
3
|
+
import { ICommandExecutor } from "./command-executor.js";
|
|
4
4
|
export { escapeShellArg } from "./shell-utils.js";
|
|
5
5
|
export interface FileAction {
|
|
6
6
|
fileName: string;
|
|
@@ -18,7 +18,7 @@ export interface PROptions {
|
|
|
18
18
|
/** Custom PR body template */
|
|
19
19
|
prTemplate?: string;
|
|
20
20
|
/** Optional command executor for shell commands (for testing) */
|
|
21
|
-
executor?:
|
|
21
|
+
executor?: ICommandExecutor;
|
|
22
22
|
/** GitHub App installation token for authentication */
|
|
23
23
|
token?: string;
|
|
24
24
|
}
|
|
@@ -51,7 +51,7 @@ export interface MergePROptions {
|
|
|
51
51
|
dryRun?: boolean;
|
|
52
52
|
retries?: number;
|
|
53
53
|
/** Optional command executor for shell commands (for testing) */
|
|
54
|
-
executor?:
|
|
54
|
+
executor?: ICommandExecutor;
|
|
55
55
|
/** GitHub App installation token for authentication */
|
|
56
56
|
token?: string;
|
|
57
57
|
}
|
|
@@ -3,8 +3,14 @@ import { RepoInfo } from "./repo-detector.js";
|
|
|
3
3
|
import { GitOpsOptions } from "./git-ops.js";
|
|
4
4
|
import { IAuthenticatedGitOps, GitAuthOptions } from "./authenticated-git-ops.js";
|
|
5
5
|
import { ILogger } from "./logger.js";
|
|
6
|
-
import {
|
|
6
|
+
import { ICommandExecutor } from "./command-executor.js";
|
|
7
7
|
import { DiffStats } from "./diff-utils.js";
|
|
8
|
+
export interface IRepositoryProcessor {
|
|
9
|
+
process(repoConfig: RepoConfig, repoInfo: RepoInfo, options: ProcessorOptions): Promise<ProcessorResult>;
|
|
10
|
+
updateManifestOnly(repoInfo: RepoInfo, repoConfig: RepoConfig, options: ProcessorOptions, manifestUpdate: {
|
|
11
|
+
rulesets: string[];
|
|
12
|
+
}): Promise<ProcessorResult>;
|
|
13
|
+
}
|
|
8
14
|
export interface ProcessorOptions {
|
|
9
15
|
branchName: string;
|
|
10
16
|
workDir: string;
|
|
@@ -14,7 +20,7 @@ export interface ProcessorOptions {
|
|
|
14
20
|
/** Number of retries for network operations (default: 3) */
|
|
15
21
|
retries?: number;
|
|
16
22
|
/** Command executor for shell commands (for testing) */
|
|
17
|
-
executor?:
|
|
23
|
+
executor?: ICommandExecutor;
|
|
18
24
|
/** Custom PR body template */
|
|
19
25
|
prTemplate?: string;
|
|
20
26
|
/** Skip deleting orphaned files even if deleteOrphaned is configured */
|
|
@@ -38,7 +44,7 @@ export interface ProcessorResult {
|
|
|
38
44
|
};
|
|
39
45
|
diffStats?: DiffStats;
|
|
40
46
|
}
|
|
41
|
-
export declare class RepositoryProcessor {
|
|
47
|
+
export declare class RepositoryProcessor implements IRepositoryProcessor {
|
|
42
48
|
private gitOps;
|
|
43
49
|
private readonly gitOpsFactory;
|
|
44
50
|
private readonly log;
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import type { RepoConfig } from "./config.js";
|
|
2
2
|
import type { RepoInfo } from "./repo-detector.js";
|
|
3
3
|
import { GitHubRulesetStrategy } from "./strategies/github-ruleset-strategy.js";
|
|
4
|
+
export interface IRulesetProcessor {
|
|
5
|
+
process(repoConfig: RepoConfig, repoInfo: RepoInfo, options: RulesetProcessorOptions): Promise<RulesetProcessorResult>;
|
|
6
|
+
}
|
|
4
7
|
export interface RulesetProcessorOptions {
|
|
5
8
|
configId: string;
|
|
6
9
|
dryRun?: boolean;
|
|
@@ -28,7 +31,7 @@ export interface RulesetProcessorResult {
|
|
|
28
31
|
* Processes ruleset configuration for a repository.
|
|
29
32
|
* Handles create/update/delete operations via GitHub Rulesets API.
|
|
30
33
|
*/
|
|
31
|
-
export declare class RulesetProcessor {
|
|
34
|
+
export declare class RulesetProcessor implements IRulesetProcessor {
|
|
32
35
|
private readonly strategy;
|
|
33
36
|
constructor(strategy?: GitHubRulesetStrategy);
|
|
34
37
|
/**
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { PRResult } from "../pr-creator.js";
|
|
2
2
|
import { BasePRStrategy, PRStrategyOptions, CloseExistingPROptions, MergeOptions, MergeResult } from "./pr-strategy.js";
|
|
3
|
-
import {
|
|
3
|
+
import { ICommandExecutor } from "../command-executor.js";
|
|
4
4
|
export declare class AzurePRStrategy extends BasePRStrategy {
|
|
5
|
-
constructor(executor?:
|
|
5
|
+
constructor(executor?: ICommandExecutor);
|
|
6
6
|
private getOrgUrl;
|
|
7
7
|
private buildPRUrl;
|
|
8
8
|
checkExistingPR(options: PRStrategyOptions): Promise<string | null>;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { RepoInfo } from "../repo-detector.js";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
2
|
+
import { ICommitStrategy } from "./commit-strategy.js";
|
|
3
|
+
import { ICommandExecutor } from "../command-executor.js";
|
|
4
4
|
/**
|
|
5
5
|
* Checks if GitHub App credentials are configured via environment variables.
|
|
6
6
|
* Both XFG_GITHUB_APP_ID and XFG_GITHUB_APP_PRIVATE_KEY must be set.
|
|
@@ -19,4 +19,4 @@ export declare function hasGitHubAppCredentials(): boolean;
|
|
|
19
19
|
* @param repoInfo - Repository information
|
|
20
20
|
* @param executor - Optional command executor for shell commands
|
|
21
21
|
*/
|
|
22
|
-
export declare function getCommitStrategy(repoInfo: RepoInfo, executor?:
|
|
22
|
+
export declare function getCommitStrategy(repoInfo: RepoInfo, executor?: ICommandExecutor): ICommitStrategy;
|
|
@@ -27,7 +27,7 @@ export interface CommitResult {
|
|
|
27
27
|
* Strategy interface for creating commits.
|
|
28
28
|
* Implementations handle platform-specific commit mechanisms.
|
|
29
29
|
*/
|
|
30
|
-
export interface
|
|
30
|
+
export interface ICommitStrategy {
|
|
31
31
|
/**
|
|
32
32
|
* Create a commit with the given file changes and push to remote.
|
|
33
33
|
* @returns Commit result with SHA and verification status
|
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { ICommitStrategy, CommitOptions, CommitResult } from "./commit-strategy.js";
|
|
2
|
+
import { ICommandExecutor } from "../command-executor.js";
|
|
3
3
|
/**
|
|
4
4
|
* Git-based commit strategy using standard git commands (add, commit, push).
|
|
5
5
|
* Used with PAT authentication. Commits via this strategy are NOT verified
|
|
6
6
|
* by GitHub (no signature).
|
|
7
7
|
*/
|
|
8
|
-
export declare class GitCommitStrategy implements
|
|
8
|
+
export declare class GitCommitStrategy implements ICommitStrategy {
|
|
9
9
|
private executor;
|
|
10
|
-
constructor(executor?:
|
|
10
|
+
constructor(executor?: ICommandExecutor);
|
|
11
11
|
/**
|
|
12
12
|
* Create a commit with the given file changes and push to remote.
|
|
13
13
|
* Runs: git add -A, git commit, git push (with optional --force-with-lease)
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { ICommandExecutor } from "../command-executor.js";
|
|
2
2
|
import { RepoInfo } from "../repo-detector.js";
|
|
3
3
|
import type { Ruleset } from "../config.js";
|
|
4
|
+
import type { IRulesetStrategy } from "./ruleset-strategy.js";
|
|
4
5
|
/**
|
|
5
6
|
* GitHub Ruleset response from API (snake_case).
|
|
6
7
|
*/
|
|
@@ -50,9 +51,9 @@ export interface RulesetStrategyOptions {
|
|
|
50
51
|
* GitHub Ruleset Strategy for managing repository rulesets via GitHub REST API.
|
|
51
52
|
* Uses `gh api` CLI for authentication and API calls.
|
|
52
53
|
*/
|
|
53
|
-
export declare class GitHubRulesetStrategy {
|
|
54
|
+
export declare class GitHubRulesetStrategy implements IRulesetStrategy {
|
|
54
55
|
private executor;
|
|
55
|
-
constructor(executor?:
|
|
56
|
+
constructor(executor?: ICommandExecutor);
|
|
56
57
|
/**
|
|
57
58
|
* Lists all rulesets for a repository.
|
|
58
59
|
*/
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { PRResult } from "../pr-creator.js";
|
|
2
2
|
import { BasePRStrategy, PRStrategyOptions, CloseExistingPROptions, MergeOptions, MergeResult } from "./pr-strategy.js";
|
|
3
|
-
import {
|
|
3
|
+
import { ICommandExecutor } from "../command-executor.js";
|
|
4
4
|
export declare class GitLabPRStrategy extends BasePRStrategy {
|
|
5
|
-
constructor(executor?:
|
|
5
|
+
constructor(executor?: ICommandExecutor);
|
|
6
6
|
/**
|
|
7
7
|
* Build the repo flag for glab commands.
|
|
8
8
|
* Format: namespace/repo (supports nested groups)
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { ICommitStrategy, CommitOptions, CommitResult } from "./commit-strategy.js";
|
|
2
|
+
import { ICommandExecutor } from "../command-executor.js";
|
|
3
3
|
/**
|
|
4
4
|
* Maximum payload size for GitHub GraphQL API (50MB).
|
|
5
5
|
* Base64 encoding adds ~33% overhead, so raw content should be checked.
|
|
@@ -29,9 +29,9 @@ export declare function validateBranchName(branchName: string): void;
|
|
|
29
29
|
*
|
|
30
30
|
* This strategy is GitHub-only and requires the `gh` CLI to be authenticated.
|
|
31
31
|
*/
|
|
32
|
-
export declare class GraphQLCommitStrategy implements
|
|
32
|
+
export declare class GraphQLCommitStrategy implements ICommitStrategy {
|
|
33
33
|
private executor;
|
|
34
|
-
constructor(executor?:
|
|
34
|
+
constructor(executor?: ICommandExecutor);
|
|
35
35
|
/**
|
|
36
36
|
* Create a commit with the given file changes using GitHub's GraphQL API.
|
|
37
37
|
* Uses the createCommitOnBranch mutation for verified commits.
|
|
@@ -191,8 +191,9 @@ export class GraphQLCommitStrategy {
|
|
|
191
191
|
// Branch name was validated in commit(), safe for shell use
|
|
192
192
|
try {
|
|
193
193
|
// Check if the branch exists on remote
|
|
194
|
+
// Use skipRetry because failure is expected for new branches
|
|
194
195
|
if (gitOps) {
|
|
195
|
-
await gitOps.lsRemote(branchName);
|
|
196
|
+
await gitOps.lsRemote(branchName, { skipRetry: true });
|
|
196
197
|
}
|
|
197
198
|
else {
|
|
198
199
|
await this.executor.exec(`git ls-remote --exit-code --heads origin ${escapeShellArg(branchName)}`, workDir);
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { RepoInfo } from "../repo-detector.js";
|
|
2
|
-
import type {
|
|
3
|
-
import {
|
|
4
|
-
export type {
|
|
2
|
+
import type { IPRStrategy } from "./pr-strategy.js";
|
|
3
|
+
import { ICommandExecutor } from "../command-executor.js";
|
|
4
|
+
export type { IPRStrategy, PRStrategyOptions, CloseExistingPROptions, PRMergeConfig, MergeOptions, MergeResult, } from "./pr-strategy.js";
|
|
5
5
|
export { BasePRStrategy, PRWorkflowExecutor } from "./pr-strategy.js";
|
|
6
6
|
export { GitHubPRStrategy } from "./github-pr-strategy.js";
|
|
7
7
|
export { AzurePRStrategy } from "./azure-pr-strategy.js";
|
|
8
8
|
export { GitLabPRStrategy } from "./gitlab-pr-strategy.js";
|
|
9
|
-
export type {
|
|
9
|
+
export type { ICommitStrategy, CommitOptions, CommitResult, FileChange, } from "./commit-strategy.js";
|
|
10
10
|
export { GitCommitStrategy } from "./git-commit-strategy.js";
|
|
11
11
|
export { GraphQLCommitStrategy, MAX_PAYLOAD_SIZE, } from "./graphql-commit-strategy.js";
|
|
12
12
|
export { getCommitStrategy, hasGitHubAppCredentials, } from "./commit-strategy-selector.js";
|
|
@@ -15,4 +15,4 @@ export { getCommitStrategy, hasGitHubAppCredentials, } from "./commit-strategy-s
|
|
|
15
15
|
* @param repoInfo - Repository information
|
|
16
16
|
* @param executor - Optional command executor for shell commands
|
|
17
17
|
*/
|
|
18
|
-
export declare function getPRStrategy(repoInfo: RepoInfo, executor?:
|
|
18
|
+
export declare function getPRStrategy(repoInfo: RepoInfo, executor?: ICommandExecutor): IPRStrategy;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { PRResult } from "../pr-creator.js";
|
|
2
2
|
import { RepoInfo } from "../repo-detector.js";
|
|
3
|
-
import {
|
|
3
|
+
import { ICommandExecutor } from "../command-executor.js";
|
|
4
4
|
import type { MergeMode, MergeStrategy } from "../config.js";
|
|
5
5
|
export interface PRMergeConfig {
|
|
6
6
|
mode: MergeMode;
|
|
@@ -51,7 +51,7 @@ export interface CloseExistingPROptions {
|
|
|
51
51
|
* Strategies focus on platform-specific logic (checkExistingPR, create, merge).
|
|
52
52
|
* Use PRWorkflowExecutor for full workflow orchestration with error handling.
|
|
53
53
|
*/
|
|
54
|
-
export interface
|
|
54
|
+
export interface IPRStrategy {
|
|
55
55
|
/**
|
|
56
56
|
* Check if a PR already exists for the given branch
|
|
57
57
|
* @returns PR URL if exists, null otherwise
|
|
@@ -79,10 +79,10 @@ export interface PRStrategy {
|
|
|
79
79
|
*/
|
|
80
80
|
execute(options: PRStrategyOptions): Promise<PRResult>;
|
|
81
81
|
}
|
|
82
|
-
export declare abstract class BasePRStrategy implements
|
|
82
|
+
export declare abstract class BasePRStrategy implements IPRStrategy {
|
|
83
83
|
protected bodyFilePath: string;
|
|
84
|
-
protected executor:
|
|
85
|
-
constructor(executor?:
|
|
84
|
+
protected executor: ICommandExecutor;
|
|
85
|
+
constructor(executor?: ICommandExecutor);
|
|
86
86
|
abstract checkExistingPR(options: PRStrategyOptions): Promise<string | null>;
|
|
87
87
|
abstract closeExistingPR(options: CloseExistingPROptions): Promise<boolean>;
|
|
88
88
|
abstract create(options: PRStrategyOptions): Promise<PRResult>;
|
|
@@ -110,7 +110,7 @@ export declare abstract class BasePRStrategy implements PRStrategy {
|
|
|
110
110
|
*/
|
|
111
111
|
export declare class PRWorkflowExecutor {
|
|
112
112
|
private readonly strategy;
|
|
113
|
-
constructor(strategy:
|
|
113
|
+
constructor(strategy: IPRStrategy);
|
|
114
114
|
/**
|
|
115
115
|
* Execute the full PR creation workflow with error handling.
|
|
116
116
|
*/
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { RepoInfo } from "../repo-detector.js";
|
|
2
|
+
import type { Ruleset } from "../config.js";
|
|
3
|
+
import type { GitHubRuleset, RulesetStrategyOptions } from "./github-ruleset-strategy.js";
|
|
4
|
+
export interface IRulesetStrategy {
|
|
5
|
+
list(repoInfo: RepoInfo, options?: RulesetStrategyOptions): Promise<GitHubRuleset[]>;
|
|
6
|
+
get(repoInfo: RepoInfo, rulesetId: number, options?: RulesetStrategyOptions): Promise<GitHubRuleset>;
|
|
7
|
+
create(repoInfo: RepoInfo, name: string, ruleset: Ruleset, options?: RulesetStrategyOptions): Promise<GitHubRuleset>;
|
|
8
|
+
update(repoInfo: RepoInfo, rulesetId: number, name: string, ruleset: Ruleset, options?: RulesetStrategyOptions): Promise<GitHubRuleset>;
|
|
9
|
+
delete(repoInfo: RepoInfo, rulesetId: number, options?: RulesetStrategyOptions): Promise<void>;
|
|
10
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aspruyt/xfg",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.2.0",
|
|
4
4
|
"description": "CLI tool for repository-as-code",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
"start": "node dist/cli.js",
|
|
28
28
|
"dev": "ts-node src/cli.ts",
|
|
29
29
|
"test": "node --import tsx scripts/run-tests.js",
|
|
30
|
-
"test:coverage": "c8 --check-coverage --lines 95 --reporter=text --reporter=lcov --all --src=src --exclude='
|
|
30
|
+
"test:coverage": "c8 --check-coverage --lines 95 --reporter=text --reporter=lcov --all --src=src --exclude='test/**/*.test.ts' --exclude='scripts/**' npm test",
|
|
31
31
|
"test:integration:github": "npm run build && node --import tsx --test test/integration/github.test.ts",
|
|
32
32
|
"test:integration:ado": "npm run build && node --import tsx --test test/integration/ado.test.ts",
|
|
33
33
|
"test:integration:gitlab": "npm run build && node --import tsx --test test/integration/gitlab.test.ts",
|