@aspruyt/xfg 3.13.1 → 4.0.1

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.
Files changed (190) hide show
  1. package/README.md +1 -4
  2. package/dist/cli/index.d.ts +1 -4
  3. package/dist/cli/index.js +0 -2
  4. package/dist/cli/program.js +7 -14
  5. package/dist/cli/{settings/results-collector.d.ts → results-collector.d.ts} +1 -1
  6. package/dist/cli/{settings/results-collector.js → results-collector.js} +2 -1
  7. package/dist/cli/settings-report-builder.d.ts +1 -3
  8. package/dist/cli/sync-command.d.ts +3 -26
  9. package/dist/cli/sync-command.js +312 -179
  10. package/dist/cli/types.d.ts +68 -41
  11. package/dist/cli/types.js +1 -12
  12. package/dist/config/errors.d.ts +9 -0
  13. package/dist/config/errors.js +11 -0
  14. package/dist/config/file-reference-resolver.d.ts +2 -1
  15. package/dist/config/file-reference-resolver.js +10 -8
  16. package/dist/config/formatter.d.ts +3 -2
  17. package/dist/config/index.d.ts +4 -6
  18. package/dist/config/index.js +4 -8
  19. package/dist/config/loader.js +4 -2
  20. package/dist/config/merge.d.ts +0 -9
  21. package/dist/config/merge.js +2 -7
  22. package/dist/config/normalizer.d.ts +4 -0
  23. package/dist/config/normalizer.js +61 -110
  24. package/dist/config/types.d.ts +15 -19
  25. package/dist/config/types.js +1 -1
  26. package/dist/config/validator.d.ts +0 -9
  27. package/dist/config/validator.js +297 -391
  28. package/dist/config/validators/file-validator.d.ts +2 -8
  29. package/dist/config/validators/file-validator.js +6 -17
  30. package/dist/config/validators/index.d.ts +3 -3
  31. package/dist/config/validators/index.js +3 -3
  32. package/dist/config/validators/repo-settings-validator.d.ts +0 -6
  33. package/dist/config/validators/repo-settings-validator.js +9 -9
  34. package/dist/config/validators/ruleset-validator.d.ts +0 -14
  35. package/dist/config/validators/ruleset-validator.js +28 -28
  36. package/dist/index.d.ts +2 -2
  37. package/dist/index.js +1 -1
  38. package/dist/lifecycle/ado-migration-source.js +2 -1
  39. package/dist/lifecycle/github-lifecycle-provider.d.ts +15 -5
  40. package/dist/lifecycle/github-lifecycle-provider.js +101 -81
  41. package/dist/lifecycle/index.d.ts +2 -6
  42. package/dist/lifecycle/index.js +0 -4
  43. package/dist/lifecycle/lifecycle-formatter.d.ts +2 -1
  44. package/dist/lifecycle/lifecycle-formatter.js +4 -0
  45. package/dist/lifecycle/lifecycle-helpers.d.ts +3 -2
  46. package/dist/lifecycle/repo-lifecycle-manager.js +4 -11
  47. package/dist/lifecycle/types.d.ts +0 -8
  48. package/dist/output/github-summary.d.ts +5 -0
  49. package/dist/output/github-summary.js +9 -2
  50. package/dist/output/index.d.ts +2 -2
  51. package/dist/output/index.js +1 -1
  52. package/dist/output/lifecycle-report.js +5 -23
  53. package/dist/output/settings-report.d.ts +14 -3
  54. package/dist/output/settings-report.js +137 -197
  55. package/dist/output/summary-utils.d.ts +1 -1
  56. package/dist/output/summary-utils.js +2 -1
  57. package/dist/output/sync-report.js +5 -8
  58. package/dist/output/unified-summary.d.ts +2 -1
  59. package/dist/output/unified-summary.js +71 -133
  60. package/dist/settings/base-processor.d.ts +67 -0
  61. package/dist/settings/base-processor.js +91 -0
  62. package/dist/settings/index.d.ts +4 -3
  63. package/dist/settings/index.js +3 -3
  64. package/dist/settings/labels/converter.d.ts +2 -1
  65. package/dist/settings/labels/diff.d.ts +2 -2
  66. package/dist/settings/labels/diff.js +15 -19
  67. package/dist/settings/labels/github-labels-strategy.d.ts +9 -18
  68. package/dist/settings/labels/github-labels-strategy.js +17 -73
  69. package/dist/settings/labels/index.d.ts +2 -6
  70. package/dist/settings/labels/index.js +1 -9
  71. package/dist/settings/labels/processor.d.ts +6 -40
  72. package/dist/settings/labels/processor.js +62 -165
  73. package/dist/settings/labels/types.d.ts +5 -8
  74. package/dist/settings/repo-settings/formatter.d.ts +2 -2
  75. package/dist/settings/repo-settings/formatter.js +6 -6
  76. package/dist/settings/repo-settings/github-repo-settings-strategy.d.ts +11 -12
  77. package/dist/settings/repo-settings/github-repo-settings-strategy.js +32 -79
  78. package/dist/settings/repo-settings/index.d.ts +2 -5
  79. package/dist/settings/repo-settings/index.js +1 -9
  80. package/dist/settings/repo-settings/processor.d.ts +6 -27
  81. package/dist/settings/repo-settings/processor.js +51 -104
  82. package/dist/settings/repo-settings/types.d.ts +7 -9
  83. package/dist/settings/rulesets/diff-algorithm.d.ts +0 -4
  84. package/dist/settings/rulesets/diff-algorithm.js +1 -10
  85. package/dist/settings/rulesets/diff.d.ts +3 -3
  86. package/dist/settings/rulesets/diff.js +8 -29
  87. package/dist/settings/rulesets/formatter.d.ts +1 -3
  88. package/dist/settings/rulesets/formatter.js +1 -8
  89. package/dist/settings/rulesets/github-ruleset-strategy.d.ts +11 -51
  90. package/dist/settings/rulesets/github-ruleset-strategy.js +24 -85
  91. package/dist/settings/rulesets/index.d.ts +3 -6
  92. package/dist/settings/rulesets/index.js +5 -9
  93. package/dist/settings/rulesets/processor.d.ts +8 -43
  94. package/dist/settings/rulesets/processor.js +58 -166
  95. package/dist/settings/rulesets/types.d.ts +35 -6
  96. package/dist/shared/command-executor.d.ts +2 -22
  97. package/dist/shared/command-executor.js +8 -7
  98. package/dist/shared/env.d.ts +0 -8
  99. package/dist/shared/env.js +14 -70
  100. package/dist/shared/file-status.d.ts +2 -0
  101. package/dist/shared/file-status.js +13 -0
  102. package/dist/shared/gh-api-utils.d.ts +46 -0
  103. package/dist/shared/gh-api-utils.js +107 -0
  104. package/dist/shared/index.d.ts +5 -5
  105. package/dist/shared/index.js +3 -3
  106. package/dist/shared/interpolation-engine.d.ts +31 -0
  107. package/dist/shared/interpolation-engine.js +50 -0
  108. package/dist/shared/logger.d.ts +3 -7
  109. package/dist/shared/logger.js +4 -1
  110. package/dist/shared/repo-detector.d.ts +17 -2
  111. package/dist/shared/repo-detector.js +27 -0
  112. package/dist/shared/retry-utils.d.ts +9 -17
  113. package/dist/shared/retry-utils.js +22 -28
  114. package/dist/shared/sanitize-utils.d.ts +0 -7
  115. package/dist/shared/sanitize-utils.js +0 -7
  116. package/dist/shared/shell-utils.d.ts +1 -0
  117. package/dist/shared/shell-utils.js +3 -0
  118. package/dist/shared/string-utils.d.ts +4 -0
  119. package/dist/shared/string-utils.js +6 -0
  120. package/dist/shared/type-guards.d.ts +17 -0
  121. package/dist/shared/type-guards.js +26 -0
  122. package/dist/shared/workspace-utils.d.ts +0 -4
  123. package/dist/shared/workspace-utils.js +0 -4
  124. package/dist/{sync → shared}/xfg-template.d.ts +3 -2
  125. package/dist/{sync → shared}/xfg-template.js +13 -54
  126. package/dist/sync/auth-options-builder.d.ts +4 -5
  127. package/dist/sync/auth-options-builder.js +15 -26
  128. package/dist/sync/branch-manager.d.ts +5 -0
  129. package/dist/sync/branch-manager.js +12 -10
  130. package/dist/sync/commit-push-manager.d.ts +1 -1
  131. package/dist/sync/commit-push-manager.js +22 -18
  132. package/dist/sync/diff-utils.d.ts +4 -9
  133. package/dist/sync/diff-utils.js +2 -19
  134. package/dist/sync/file-sync-orchestrator.js +9 -8
  135. package/dist/sync/file-writer.d.ts +2 -1
  136. package/dist/sync/file-writer.js +3 -6
  137. package/dist/sync/index.d.ts +2 -16
  138. package/dist/sync/index.js +0 -20
  139. package/dist/sync/manifest-manager.d.ts +4 -0
  140. package/dist/sync/manifest-manager.js +5 -1
  141. package/dist/sync/manifest.d.ts +11 -84
  142. package/dist/sync/manifest.js +50 -215
  143. package/dist/sync/pr-merge-handler.d.ts +2 -6
  144. package/dist/sync/pr-merge-handler.js +6 -3
  145. package/dist/sync/repository-processor.d.ts +2 -8
  146. package/dist/sync/repository-processor.js +21 -63
  147. package/dist/sync/repository-session.js +5 -14
  148. package/dist/sync/sync-workflow.js +31 -38
  149. package/dist/sync/types.d.ts +43 -182
  150. package/dist/vcs/authenticated-git-ops.d.ts +27 -70
  151. package/dist/vcs/authenticated-git-ops.js +70 -96
  152. package/dist/vcs/azure-pr-strategy.d.ts +6 -4
  153. package/dist/vcs/azure-pr-strategy.js +34 -82
  154. package/dist/vcs/branch-utils.d.ts +6 -0
  155. package/dist/vcs/branch-utils.js +29 -0
  156. package/dist/vcs/commit-strategy-selector.d.ts +5 -0
  157. package/dist/vcs/commit-strategy-selector.js +10 -0
  158. package/dist/vcs/git-commit-strategy.js +1 -2
  159. package/dist/vcs/git-ops.d.ts +15 -59
  160. package/dist/vcs/git-ops.js +46 -110
  161. package/dist/vcs/github-app-token-manager.d.ts +0 -6
  162. package/dist/vcs/github-app-token-manager.js +5 -12
  163. package/dist/vcs/github-pr-strategy.d.ts +5 -5
  164. package/dist/vcs/github-pr-strategy.js +44 -122
  165. package/dist/vcs/gitlab-pr-strategy.d.ts +6 -4
  166. package/dist/vcs/gitlab-pr-strategy.js +39 -87
  167. package/dist/vcs/graphql-commit-strategy.d.ts +3 -4
  168. package/dist/vcs/graphql-commit-strategy.js +45 -63
  169. package/dist/vcs/index.d.ts +3 -16
  170. package/dist/vcs/index.js +2 -33
  171. package/dist/vcs/pr-creator.d.ts +9 -9
  172. package/dist/vcs/pr-creator.js +11 -10
  173. package/dist/vcs/pr-strategy-factory.d.ts +5 -0
  174. package/dist/vcs/pr-strategy-factory.js +17 -0
  175. package/dist/vcs/pr-strategy.d.ts +13 -26
  176. package/dist/vcs/pr-strategy.js +20 -25
  177. package/dist/vcs/types.d.ts +87 -21
  178. package/package.json +2 -1
  179. package/dist/cli/settings/lifecycle-checks.d.ts +0 -11
  180. package/dist/cli/settings/lifecycle-checks.js +0 -64
  181. package/dist/cli/settings/process-labels.d.ts +0 -9
  182. package/dist/cli/settings/process-labels.js +0 -125
  183. package/dist/cli/settings/process-repo-settings.d.ts +0 -9
  184. package/dist/cli/settings/process-repo-settings.js +0 -80
  185. package/dist/cli/settings/process-rulesets.d.ts +0 -9
  186. package/dist/cli/settings/process-rulesets.js +0 -118
  187. package/dist/cli/settings-command.d.ts +0 -11
  188. package/dist/cli/settings-command.js +0 -90
  189. package/dist/sync/manifest-strategy.d.ts +0 -21
  190. package/dist/sync/manifest-strategy.js +0 -67
@@ -1,7 +1,6 @@
1
1
  import chalk from "chalk";
2
2
  import { projectToDesiredShape, normalizeRuleset, } from "./diff.js";
3
- import { computePropertyDiffs, isObject, } from "./index.js";
4
- export { computePropertyDiffs } from "./index.js";
3
+ import { computePropertyDiffs, isObject, } from "./diff-algorithm.js";
5
4
  /**
6
5
  * Build a tree structure from flat property diffs.
7
6
  */
@@ -187,9 +186,6 @@ export function formatPropertyTree(diffs) {
187
186
  const tree = buildTree(diffs);
188
187
  return renderTree(tree);
189
188
  }
190
- // =============================================================================
191
- // Ruleset Plan Formatter
192
- // =============================================================================
193
189
  /**
194
190
  * Format a full ruleset config as tree lines (for create action).
195
191
  */
@@ -253,7 +249,6 @@ export function formatRulesetPlan(changes) {
253
249
  updates = updateChanges.length;
254
250
  deletes = deleteChanges.length;
255
251
  unchanged = unchangedChanges.length;
256
- // Format creates
257
252
  if (createChanges.length > 0) {
258
253
  lines.push(chalk.bold(" Create:"));
259
254
  for (const change of createChanges) {
@@ -273,7 +268,6 @@ export function formatRulesetPlan(changes) {
273
268
  lines.push(""); // Blank line between rulesets
274
269
  }
275
270
  }
276
- // Format updates
277
271
  if (updateChanges.length > 0) {
278
272
  lines.push(chalk.bold(" Update:"));
279
273
  for (const change of updateChanges) {
@@ -303,7 +297,6 @@ export function formatRulesetPlan(changes) {
303
297
  lines.push(""); // Blank line between rulesets
304
298
  }
305
299
  }
306
- // Format deletes
307
300
  if (deleteChanges.length > 0) {
308
301
  lines.push(chalk.bold(" Delete:"));
309
302
  for (const change of deleteChanges) {
@@ -1,41 +1,13 @@
1
1
  import { ICommandExecutor } from "../../shared/command-executor.js";
2
2
  import { RepoInfo } from "../../shared/repo-detector.js";
3
+ import { type GhApiOptions } from "../../shared/gh-api-utils.js";
3
4
  import type { Ruleset } from "../../config/index.js";
4
- import type { IRulesetStrategy } from "./types.js";
5
- /**
6
- * GitHub Ruleset response from API (snake_case).
7
- */
8
- export interface GitHubRuleset {
9
- id: number;
10
- name: string;
11
- target: "branch" | "tag";
12
- enforcement: "active" | "disabled" | "evaluate";
13
- bypass_actors?: GitHubBypassActor[];
14
- conditions?: GitHubRulesetConditions;
15
- rules?: GitHubRule[];
16
- source_type?: string;
17
- source?: string;
18
- }
19
- export interface GitHubBypassActor {
20
- actor_id: number;
21
- actor_type: "Team" | "User" | "Integration";
22
- bypass_mode?: "always" | "pull_request";
23
- }
24
- export interface GitHubRulesetConditions {
25
- ref_name?: {
26
- include?: string[];
27
- exclude?: string[];
28
- };
29
- }
30
- export interface GitHubRule {
31
- type: string;
32
- parameters?: Record<string, unknown>;
33
- }
5
+ import type { IRulesetStrategy, GitHubRuleset, GitHubBypassActor, GitHubRulesetConditions, GitHubRule } from "./types.js";
34
6
  /**
35
7
  * Converts camelCase config ruleset to snake_case GitHub API format.
36
8
  */
37
9
  export declare function configToGitHub(name: string, ruleset: Ruleset): GitHubRulesetPayload;
38
- export interface GitHubRulesetPayload {
10
+ interface GitHubRulesetPayload {
39
11
  name: string;
40
12
  target: "branch" | "tag";
41
13
  enforcement: "active" | "disabled" | "evaluate";
@@ -43,11 +15,7 @@ export interface GitHubRulesetPayload {
43
15
  conditions?: GitHubRulesetConditions;
44
16
  rules?: GitHubRule[];
45
17
  }
46
- export interface RulesetStrategyOptions {
47
- token?: string;
48
- host?: string;
49
- }
50
- export interface GitHubRulesetStrategyOptions {
18
+ interface GitHubRulesetStrategyOptions {
51
19
  retries?: number;
52
20
  }
53
21
  /**
@@ -55,35 +23,27 @@ export interface GitHubRulesetStrategyOptions {
55
23
  * Uses `gh api` CLI for authentication and API calls.
56
24
  */
57
25
  export declare class GitHubRulesetStrategy implements IRulesetStrategy {
58
- private executor;
59
- private retries;
26
+ private api;
60
27
  constructor(executor?: ICommandExecutor, options?: GitHubRulesetStrategyOptions);
61
28
  /**
62
29
  * Lists all rulesets for a repository.
63
30
  */
64
- list(repoInfo: RepoInfo, options?: RulesetStrategyOptions): Promise<GitHubRuleset[]>;
31
+ list(repoInfo: RepoInfo, options?: GhApiOptions): Promise<GitHubRuleset[]>;
65
32
  /**
66
33
  * Gets a single ruleset by ID.
67
34
  */
68
- get(repoInfo: RepoInfo, rulesetId: number, options?: RulesetStrategyOptions): Promise<GitHubRuleset>;
35
+ get(repoInfo: RepoInfo, rulesetId: number, options?: GhApiOptions): Promise<GitHubRuleset>;
69
36
  /**
70
37
  * Creates a new ruleset.
71
38
  */
72
- create(repoInfo: RepoInfo, name: string, ruleset: Ruleset, options?: RulesetStrategyOptions): Promise<GitHubRuleset>;
39
+ create(repoInfo: RepoInfo, name: string, ruleset: Ruleset, options?: GhApiOptions): Promise<GitHubRuleset>;
73
40
  /**
74
41
  * Updates an existing ruleset.
75
42
  */
76
- update(repoInfo: RepoInfo, rulesetId: number, name: string, ruleset: Ruleset, options?: RulesetStrategyOptions): Promise<GitHubRuleset>;
43
+ update(repoInfo: RepoInfo, rulesetId: number, name: string, ruleset: Ruleset, options?: GhApiOptions): Promise<GitHubRuleset>;
77
44
  /**
78
45
  * Deletes a ruleset.
79
46
  */
80
- delete(repoInfo: RepoInfo, rulesetId: number, options?: RulesetStrategyOptions): Promise<void>;
81
- /**
82
- * Validates that the repo is a GitHub repository.
83
- */
84
- private validateGitHub;
85
- /**
86
- * Executes a GitHub API call using the gh CLI.
87
- */
88
- private ghApi;
47
+ delete(repoInfo: RepoInfo, rulesetId: number, options?: GhApiOptions): Promise<void>;
89
48
  }
49
+ export {};
@@ -1,10 +1,7 @@
1
1
  import { defaultExecutor, } from "../../shared/command-executor.js";
2
- import { isGitHubRepo, } from "../../shared/repo-detector.js";
3
- import { escapeShellArg } from "../../shared/shell-utils.js";
4
- import { withRetry } from "../../shared/retry-utils.js";
5
- // =============================================================================
6
- // Conversion Functions
7
- // =============================================================================
2
+ import { assertGitHubRepo } from "../../shared/repo-detector.js";
3
+ import { camelToSnake } from "../../shared/string-utils.js";
4
+ import { GhApiClient, parseApiJson, } from "../../shared/gh-api-utils.js";
8
5
  /**
9
6
  * Converts camelCase config ruleset to snake_case GitHub API format.
10
7
  */
@@ -101,117 +98,59 @@ function convertValue(value) {
101
98
  }
102
99
  return value;
103
100
  }
104
- /**
105
- * Converts camelCase to snake_case.
106
- */
107
- function camelToSnake(str) {
108
- return str.replace(/([A-Z])/g, "_$1").toLowerCase();
109
- }
110
101
  /**
111
102
  * GitHub Ruleset Strategy for managing repository rulesets via GitHub REST API.
112
103
  * Uses `gh api` CLI for authentication and API calls.
113
104
  */
114
105
  export class GitHubRulesetStrategy {
115
- executor;
116
- retries;
106
+ api;
117
107
  constructor(executor, options) {
118
- this.executor = executor ?? defaultExecutor;
119
- this.retries = options?.retries ?? 3;
108
+ this.api = new GhApiClient(executor ?? defaultExecutor, options?.retries ?? 3);
120
109
  }
121
110
  /**
122
111
  * Lists all rulesets for a repository.
123
112
  */
124
113
  async list(repoInfo, options) {
125
- this.validateGitHub(repoInfo);
126
- const github = repoInfo;
127
- const endpoint = `/repos/${github.owner}/${github.repo}/rulesets`;
128
- const result = await this.ghApi("GET", endpoint, undefined, options);
129
- return JSON.parse(result);
114
+ assertGitHubRepo(repoInfo, "GitHub Ruleset strategy");
115
+ const endpoint = `/repos/${repoInfo.owner}/${repoInfo.repo}/rulesets`;
116
+ const result = await this.api.call("GET", endpoint, undefined, options);
117
+ return parseApiJson(result, "rulesets response");
130
118
  }
131
119
  /**
132
120
  * Gets a single ruleset by ID.
133
121
  */
134
122
  async get(repoInfo, rulesetId, options) {
135
- this.validateGitHub(repoInfo);
136
- const github = repoInfo;
137
- const endpoint = `/repos/${github.owner}/${github.repo}/rulesets/${rulesetId}`;
138
- const result = await this.ghApi("GET", endpoint, undefined, options);
139
- return JSON.parse(result);
123
+ assertGitHubRepo(repoInfo, "GitHub Ruleset strategy");
124
+ const endpoint = `/repos/${repoInfo.owner}/${repoInfo.repo}/rulesets/${rulesetId}`;
125
+ const result = await this.api.call("GET", endpoint, undefined, options);
126
+ return parseApiJson(result, "ruleset response");
140
127
  }
141
128
  /**
142
129
  * Creates a new ruleset.
143
130
  */
144
131
  async create(repoInfo, name, ruleset, options) {
145
- this.validateGitHub(repoInfo);
146
- const github = repoInfo;
147
- const endpoint = `/repos/${github.owner}/${github.repo}/rulesets`;
132
+ assertGitHubRepo(repoInfo, "GitHub Ruleset strategy");
133
+ const endpoint = `/repos/${repoInfo.owner}/${repoInfo.repo}/rulesets`;
148
134
  const payload = configToGitHub(name, ruleset);
149
- const result = await this.ghApi("POST", endpoint, payload, options);
150
- return JSON.parse(result);
135
+ const result = await this.api.call("POST", endpoint, payload, options);
136
+ return parseApiJson(result, "ruleset response");
151
137
  }
152
138
  /**
153
139
  * Updates an existing ruleset.
154
140
  */
155
141
  async update(repoInfo, rulesetId, name, ruleset, options) {
156
- this.validateGitHub(repoInfo);
157
- const github = repoInfo;
158
- const endpoint = `/repos/${github.owner}/${github.repo}/rulesets/${rulesetId}`;
142
+ assertGitHubRepo(repoInfo, "GitHub Ruleset strategy");
143
+ const endpoint = `/repos/${repoInfo.owner}/${repoInfo.repo}/rulesets/${rulesetId}`;
159
144
  const payload = configToGitHub(name, ruleset);
160
- const result = await this.ghApi("PUT", endpoint, payload, options);
161
- return JSON.parse(result);
145
+ const result = await this.api.call("PUT", endpoint, payload, options);
146
+ return parseApiJson(result, "ruleset response");
162
147
  }
163
148
  /**
164
149
  * Deletes a ruleset.
165
150
  */
166
151
  async delete(repoInfo, rulesetId, options) {
167
- this.validateGitHub(repoInfo);
168
- const github = repoInfo;
169
- const endpoint = `/repos/${github.owner}/${github.repo}/rulesets/${rulesetId}`;
170
- await this.ghApi("DELETE", endpoint, undefined, options);
171
- }
172
- /**
173
- * Validates that the repo is a GitHub repository.
174
- */
175
- validateGitHub(repoInfo) {
176
- if (!isGitHubRepo(repoInfo)) {
177
- throw new Error(`GitHub Ruleset strategy requires GitHub repositories. Got: ${repoInfo.type}`);
178
- }
179
- }
180
- /**
181
- * Executes a GitHub API call using the gh CLI.
182
- */
183
- async ghApi(method, endpoint, payload, options) {
184
- const args = ["gh", "api"];
185
- // Add method flag
186
- if (method !== "GET") {
187
- args.push("-X", method);
188
- }
189
- // Add host flag for GitHub Enterprise
190
- if (options?.host && options.host !== "github.com") {
191
- args.push("--hostname", escapeShellArg(options.host));
192
- }
193
- // Add endpoint
194
- args.push(escapeShellArg(endpoint));
195
- // Build base command
196
- const baseCommand = args.join(" ");
197
- // Add GH_TOKEN environment variable prefix if token provided
198
- // Token is escaped to prevent command injection
199
- const tokenPrefix = options?.token
200
- ? `GH_TOKEN=${escapeShellArg(options.token)} `
201
- : "";
202
- // For POST/PUT with payload, use echo pipe pattern (same as graphql-commit-strategy)
203
- // This is safer than heredoc as escapeShellArg properly escapes the content
204
- if (payload && (method === "POST" || method === "PUT")) {
205
- const payloadJson = JSON.stringify(payload);
206
- const command = `echo ${escapeShellArg(payloadJson)} | ${tokenPrefix}${baseCommand} --input -`;
207
- return await withRetry(() => this.executor.exec(command, process.cwd()), {
208
- retries: this.retries,
209
- });
210
- }
211
- // For GET/DELETE, run command directly
212
- const command = `${tokenPrefix}${baseCommand}`;
213
- return await withRetry(() => this.executor.exec(command, process.cwd()), {
214
- retries: this.retries,
215
- });
152
+ assertGitHubRepo(repoInfo, "GitHub Ruleset strategy");
153
+ const endpoint = `/repos/${repoInfo.owner}/${repoInfo.repo}/rulesets/${rulesetId}`;
154
+ await this.api.call("DELETE", endpoint, undefined, options);
216
155
  }
217
156
  }
@@ -1,6 +1,3 @@
1
- export type { IRulesetStrategy } from "./types.js";
2
- export { computePropertyDiffs, diffObjectArrays, deepEqual, isObject, isArrayOfObjects, type DiffAction, type PropertyDiff, } from "./diff-algorithm.js";
3
- export { RulesetProcessor, type IRulesetProcessor, type RulesetProcessorOptions, type RulesetProcessorResult, } from "./processor.js";
4
- export { diffRulesets, normalizeRuleset, projectToDesiredShape, type RulesetAction, type RulesetChange, } from "./diff.js";
5
- export { formatRulesetPlan, type RulesetPlanResult, type RulesetPlanEntry, } from "./formatter.js";
6
- export { GitHubRulesetStrategy, configToGitHub, type GitHubRuleset, type GitHubBypassActor, type GitHubRulesetConditions, type GitHubRule, type RulesetStrategyOptions, } from "./github-ruleset-strategy.js";
1
+ export { computePropertyDiffs, deepEqual, isObject, isArrayOfObjects, type PropertyDiff, } from "./diff-algorithm.js";
2
+ export { formatPropertyTree, type RulesetPlanEntry } from "./formatter.js";
3
+ export { RulesetProcessor, type IRulesetProcessor } from "./processor.js";
@@ -1,10 +1,6 @@
1
1
  // Diff algorithm - property-level diffing for ruleset comparisons
2
- export { computePropertyDiffs, diffObjectArrays, deepEqual, isObject, isArrayOfObjects, } from "./diff-algorithm.js";
3
- // Ruleset processor
4
- export { RulesetProcessor, } from "./processor.js";
5
- // Ruleset diff
6
- export { diffRulesets, normalizeRuleset, projectToDesiredShape, } from "./diff.js";
7
- // Ruleset formatter
8
- export { formatRulesetPlan, } from "./formatter.js";
9
- // Ruleset strategies
10
- export { GitHubRulesetStrategy, configToGitHub, } from "./github-ruleset-strategy.js";
2
+ export { computePropertyDiffs, deepEqual, isObject, isArrayOfObjects, } from "./diff-algorithm.js";
3
+ // Formatter
4
+ export { formatPropertyTree } from "./formatter.js";
5
+ // Processor
6
+ export { RulesetProcessor } from "./processor.js";
@@ -1,32 +1,14 @@
1
1
  import type { RepoConfig } from "../../config/index.js";
2
2
  import type { RepoInfo } from "../../shared/repo-detector.js";
3
- import { GitHubRulesetStrategy } from "./github-ruleset-strategy.js";
3
+ import type { IRulesetStrategy } from "./types.js";
4
4
  import { RulesetPlanResult } from "./formatter.js";
5
- export interface IRulesetProcessor {
6
- process(repoConfig: RepoConfig, repoInfo: RepoInfo, options: RulesetProcessorOptions): Promise<RulesetProcessorResult>;
7
- }
8
- export interface RulesetProcessorOptions {
9
- configId: string;
10
- dryRun?: boolean;
11
- managedRulesets: string[];
5
+ import { type BaseProcessorOptions, type BaseProcessorResult, type ISettingsProcessor, type ChangeCounts } from "../base-processor.js";
6
+ export type IRulesetProcessor = ISettingsProcessor<RulesetProcessorOptions, RulesetProcessorResult>;
7
+ export interface RulesetProcessorOptions extends BaseProcessorOptions {
12
8
  noDelete?: boolean;
13
- token?: string;
14
9
  }
15
- export interface RulesetProcessorResult {
16
- success: boolean;
17
- repoName: string;
18
- message: string;
19
- skipped?: boolean;
20
- dryRun?: boolean;
21
- changes?: {
22
- create: number;
23
- update: number;
24
- delete: number;
25
- unchanged: number;
26
- };
27
- manifestUpdate?: {
28
- rulesets: string[];
29
- };
10
+ export interface RulesetProcessorResult extends BaseProcessorResult {
11
+ changes?: ChangeCounts;
30
12
  planOutput?: RulesetPlanResult;
31
13
  }
32
14
  /**
@@ -35,24 +17,7 @@ export interface RulesetProcessorResult {
35
17
  */
36
18
  export declare class RulesetProcessor implements IRulesetProcessor {
37
19
  private readonly strategy;
38
- private readonly tokenManager;
39
- constructor(strategy?: GitHubRulesetStrategy);
40
- /**
41
- * Process rulesets for a single repository.
42
- */
20
+ constructor(strategy?: IRulesetStrategy);
43
21
  process(repoConfig: RepoConfig, repoInfo: RepoInfo, options: RulesetProcessorOptions): Promise<RulesetProcessorResult>;
44
- /**
45
- * Format change counts into a summary string.
46
- */
47
- private formatChangeSummary;
48
- /**
49
- * Compute manifest update based on current config.
50
- * Only rulesets with deleteOrphaned enabled should be tracked.
51
- */
52
- private computeManifestUpdate;
53
- /**
54
- * Resolves a GitHub App installation token for the given repo.
55
- * Returns undefined if no token manager or token resolution fails.
56
- */
57
- private getInstallationToken;
22
+ private processSettings;
58
23
  }
@@ -1,188 +1,80 @@
1
- import { isGitHubRepo, getRepoDisplayName, } from "../../shared/repo-detector.js";
2
- import { GitHubRulesetStrategy, } from "./github-ruleset-strategy.js";
1
+ import { GitHubRulesetStrategy } from "./github-ruleset-strategy.js";
3
2
  import { diffRulesets } from "./diff.js";
4
3
  import { formatRulesetPlan } from "./formatter.js";
5
- import { hasGitHubAppCredentials } from "../../vcs/index.js";
6
- import { GitHubAppTokenManager } from "../../vcs/github-app-token-manager.js";
7
- // =============================================================================
8
- // Processor Implementation
9
- // =============================================================================
4
+ import { withGitHubGuards, countActions, buildDryRunResult, buildApplyResult, } from "../base-processor.js";
10
5
  /**
11
6
  * Processes ruleset configuration for a repository.
12
7
  * Handles create/update/delete operations via GitHub Rulesets API.
13
8
  */
14
9
  export class RulesetProcessor {
15
10
  strategy;
16
- tokenManager;
17
11
  constructor(strategy) {
18
12
  this.strategy = strategy ?? new GitHubRulesetStrategy();
19
- if (hasGitHubAppCredentials()) {
20
- this.tokenManager = new GitHubAppTokenManager(process.env.XFG_GITHUB_APP_ID, process.env.XFG_GITHUB_APP_PRIVATE_KEY);
21
- }
22
- else {
23
- this.tokenManager = null;
24
- }
25
13
  }
26
- /**
27
- * Process rulesets for a single repository.
28
- */
29
14
  async process(repoConfig, repoInfo, options) {
30
- const repoName = getRepoDisplayName(repoInfo);
31
- const { dryRun, managedRulesets, noDelete, token } = options;
32
- // Check if this is a GitHub repo
33
- if (!isGitHubRepo(repoInfo)) {
34
- return {
35
- success: true,
36
- repoName,
37
- message: `Skipped: ${repoName} is not a GitHub repository`,
38
- skipped: true,
39
- };
40
- }
41
- const githubRepo = repoInfo;
15
+ return withGitHubGuards(repoConfig, repoInfo, options, {
16
+ hasDesiredSettings: (rc) => Object.keys(rc.settings?.rulesets ?? {}).length > 0,
17
+ emptySettingsMessage: "No rulesets configured",
18
+ processSettings: (githubRepo, rc, opts, token, repoName) => this.processSettings(githubRepo, rc, opts, token, repoName),
19
+ });
20
+ }
21
+ async processSettings(githubRepo, repoConfig, options, effectiveToken, repoName) {
22
+ const { dryRun, noDelete } = options;
42
23
  const settings = repoConfig.settings;
43
24
  const desiredRulesets = settings?.rulesets ?? {};
44
25
  const deleteOrphaned = settings?.deleteOrphaned ?? false;
45
- // If no rulesets configured, skip
46
- if (Object.keys(desiredRulesets).length === 0 &&
47
- managedRulesets.length === 0) {
48
- return {
49
- success: true,
50
- repoName,
51
- message: "No rulesets configured",
52
- skipped: true,
53
- };
54
- }
55
- try {
56
- // Resolve App token if available, fall back to provided token
57
- const effectiveToken = token ?? (await this.getInstallationToken(githubRepo));
58
- const strategyOptions = { token: effectiveToken, host: githubRepo.host };
59
- const currentRulesets = await this.strategy.list(githubRepo, strategyOptions);
60
- // Convert desired rulesets to Map
61
- const desiredMap = new Map(Object.entries(desiredRulesets));
62
- // Hydrate rulesets that match desired names with full details from get()
63
- // The list endpoint only returns summary fields (id, name, target, enforcement)
64
- // but not rules, conditions, or bypass_actors needed for accurate diffing
65
- const fullRulesets = [];
66
- for (const summary of currentRulesets) {
67
- if (desiredMap.has(summary.name)) {
68
- const full = await this.strategy.get(githubRepo, summary.id, strategyOptions);
69
- fullRulesets.push(full);
70
- }
71
- else {
72
- fullRulesets.push(summary);
73
- }
74
- }
75
- // Compute diff
76
- const changes = diffRulesets(fullRulesets, desiredMap, managedRulesets);
77
- // Count changes by type
78
- const changeCounts = {
79
- create: changes.filter((c) => c.action === "create").length,
80
- update: changes.filter((c) => c.action === "update").length,
81
- delete: changes.filter((c) => c.action === "delete").length,
82
- unchanged: changes.filter((c) => c.action === "unchanged").length,
83
- };
84
- const planOutput = formatRulesetPlan(changes);
85
- // Dry run mode - report planned changes without applying
86
- if (dryRun) {
87
- const summary = this.formatChangeSummary(changeCounts);
88
- return {
89
- success: true,
90
- repoName,
91
- message: `[DRY RUN] ${summary}`,
92
- dryRun: true,
93
- changes: changeCounts,
94
- planOutput,
95
- manifestUpdate: this.computeManifestUpdate(desiredRulesets, deleteOrphaned),
96
- };
26
+ const strategyOptions = { token: effectiveToken, host: githubRepo.host };
27
+ const currentRulesets = await this.strategy.list(githubRepo, strategyOptions);
28
+ const desiredMap = new Map(Object.entries(desiredRulesets));
29
+ // Hydrate rulesets that match desired names with full details from get()
30
+ // The list endpoint only returns summary fields (id, name, target, enforcement)
31
+ // but not rules, conditions, or bypass_actors needed for accurate diffing
32
+ const fullRulesets = [];
33
+ for (const summary of currentRulesets) {
34
+ if (desiredMap.has(summary.name)) {
35
+ const full = await this.strategy.get(githubRepo, summary.id, strategyOptions);
36
+ fullRulesets.push(full);
97
37
  }
98
- // Apply changes
99
- let appliedCount = 0;
100
- for (const change of changes) {
101
- switch (change.action) {
102
- case "create":
103
- if (change.desired) {
104
- await this.strategy.create(githubRepo, change.name, change.desired, strategyOptions);
105
- appliedCount++;
106
- }
107
- break;
108
- case "update":
109
- if (change.rulesetId !== undefined && change.desired) {
110
- await this.strategy.update(githubRepo, change.rulesetId, change.name, change.desired, strategyOptions);
111
- appliedCount++;
112
- }
113
- break;
114
- case "delete":
115
- // Check if deletion is allowed
116
- if (!noDelete && deleteOrphaned && change.rulesetId !== undefined) {
117
- await this.strategy.delete(githubRepo, change.rulesetId, strategyOptions);
118
- appliedCount++;
119
- }
120
- break;
121
- case "unchanged":
122
- // No action needed
123
- break;
124
- }
38
+ else {
39
+ fullRulesets.push(summary);
125
40
  }
126
- const summary = this.formatChangeSummary(changeCounts);
127
- return {
128
- success: true,
129
- repoName,
130
- message: appliedCount > 0 ? `Applied: ${summary}` : "No changes needed",
131
- changes: changeCounts,
132
- planOutput,
133
- manifestUpdate: this.computeManifestUpdate(desiredRulesets, deleteOrphaned),
134
- };
135
41
  }
136
- catch (error) {
137
- const message = error instanceof Error ? error.message : String(error);
138
- return {
139
- success: false,
140
- repoName,
141
- message: `Failed: ${message}`,
142
- };
42
+ const changes = diffRulesets(fullRulesets, desiredMap, deleteOrphaned);
43
+ const changeCounts = countActions(changes);
44
+ const planOutput = formatRulesetPlan(changes);
45
+ if (dryRun) {
46
+ return buildDryRunResult(repoName, changeCounts, { planOutput });
143
47
  }
144
- }
145
- /**
146
- * Format change counts into a summary string.
147
- */
148
- formatChangeSummary(counts) {
149
- const parts = [];
150
- if (counts.create > 0)
151
- parts.push(`${counts.create} created`);
152
- if (counts.update > 0)
153
- parts.push(`${counts.update} updated`);
154
- if (counts.delete > 0)
155
- parts.push(`${counts.delete} deleted`);
156
- if (counts.unchanged > 0)
157
- parts.push(`${counts.unchanged} unchanged`);
158
- return parts.length > 0 ? parts.join(", ") : "no changes";
159
- }
160
- /**
161
- * Compute manifest update based on current config.
162
- * Only rulesets with deleteOrphaned enabled should be tracked.
163
- */
164
- computeManifestUpdate(rulesets, deleteOrphaned) {
165
- if (!deleteOrphaned) {
166
- return undefined;
167
- }
168
- // Track all ruleset names when deleteOrphaned is enabled
169
- const rulesetNames = Object.keys(rulesets).sort();
170
- return { rulesets: rulesetNames };
171
- }
172
- /**
173
- * Resolves a GitHub App installation token for the given repo.
174
- * Returns undefined if no token manager or token resolution fails.
175
- */
176
- async getInstallationToken(repoInfo) {
177
- if (!this.tokenManager) {
178
- return undefined;
179
- }
180
- try {
181
- const token = await this.tokenManager.getTokenForRepo(repoInfo);
182
- return token ?? undefined;
183
- }
184
- catch {
185
- return undefined;
48
+ // Apply changes
49
+ let appliedCount = 0;
50
+ for (const change of changes) {
51
+ switch (change.action) {
52
+ case "create":
53
+ if (change.desired) {
54
+ await this.strategy.create(githubRepo, change.name, change.desired, strategyOptions);
55
+ appliedCount++;
56
+ }
57
+ break;
58
+ case "update":
59
+ if (change.rulesetId !== undefined && change.desired) {
60
+ await this.strategy.update(githubRepo, change.rulesetId, change.name, change.desired, strategyOptions);
61
+ appliedCount++;
62
+ }
63
+ break;
64
+ case "delete":
65
+ // Check if deletion is allowed
66
+ if (!noDelete && deleteOrphaned && change.rulesetId !== undefined) {
67
+ await this.strategy.delete(githubRepo, change.rulesetId, strategyOptions);
68
+ appliedCount++;
69
+ }
70
+ break;
71
+ case "unchanged":
72
+ // No action needed
73
+ break;
74
+ }
186
75
  }
76
+ return buildApplyResult(repoName, changeCounts, appliedCount, {
77
+ planOutput,
78
+ });
187
79
  }
188
80
  }