@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.
@@ -28,7 +28,9 @@ export interface IAuthenticatedGitOps {
28
28
  branch: string;
29
29
  method: string;
30
30
  }>;
31
- lsRemote(branchName: string): Promise<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): Promise<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
- return this.execWithRetry(`git ls-remote --exit-code --heads origin ${safeBranch}`);
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 CommandExecutor {
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 CommandExecutor {
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: CommandExecutor;
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
- result.rulesets[name] = mergeRuleset(rootRulesets[name], repoRulesets[name]);
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: raw.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
@@ -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
- validateSettings(repo.settings, `Repo ${getGitDisplayName(repo.git)}`);
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 { CommandExecutor } from "./command-executor.js";
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?: CommandExecutor;
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;
@@ -1,6 +1,6 @@
1
1
  import { RepoInfo } from "./repo-detector.js";
2
2
  import { MergeResult, PRMergeConfig } from "./strategies/index.js";
3
- import { CommandExecutor } from "./command-executor.js";
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?: CommandExecutor;
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?: CommandExecutor;
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 { CommandExecutor } from "./command-executor.js";
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?: CommandExecutor;
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 { CommandExecutor } from "../command-executor.js";
3
+ import { ICommandExecutor } from "../command-executor.js";
4
4
  export declare class AzurePRStrategy extends BasePRStrategy {
5
- constructor(executor?: CommandExecutor);
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 { CommitStrategy } from "./commit-strategy.js";
3
- import { CommandExecutor } from "../command-executor.js";
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?: CommandExecutor): CommitStrategy;
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 CommitStrategy {
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 { CommitStrategy, CommitOptions, CommitResult } from "./commit-strategy.js";
2
- import { CommandExecutor } from "../command-executor.js";
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 CommitStrategy {
8
+ export declare class GitCommitStrategy implements ICommitStrategy {
9
9
  private executor;
10
- constructor(executor?: CommandExecutor);
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 { CommandExecutor } from "../command-executor.js";
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?: CommandExecutor);
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 { CommandExecutor } from "../command-executor.js";
3
+ import { ICommandExecutor } from "../command-executor.js";
4
4
  export declare class GitLabPRStrategy extends BasePRStrategy {
5
- constructor(executor?: CommandExecutor);
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 { CommitStrategy, CommitOptions, CommitResult } from "./commit-strategy.js";
2
- import { CommandExecutor } from "../command-executor.js";
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 CommitStrategy {
32
+ export declare class GraphQLCommitStrategy implements ICommitStrategy {
33
33
  private executor;
34
- constructor(executor?: CommandExecutor);
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 { PRStrategy } from "./pr-strategy.js";
3
- import { CommandExecutor } from "../command-executor.js";
4
- export type { PRStrategy, PRStrategyOptions, CloseExistingPROptions, PRMergeConfig, MergeOptions, MergeResult, } from "./pr-strategy.js";
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 { CommitStrategy, CommitOptions, CommitResult, FileChange, } from "./commit-strategy.js";
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?: CommandExecutor): PRStrategy;
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 { CommandExecutor } from "../command-executor.js";
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 PRStrategy {
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 PRStrategy {
82
+ export declare abstract class BasePRStrategy implements IPRStrategy {
83
83
  protected bodyFilePath: string;
84
- protected executor: CommandExecutor;
85
- constructor(executor?: CommandExecutor);
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: PRStrategy);
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.1.4",
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='src/**/*.test.ts' --exclude='scripts/**' npm test",
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",