@aspruyt/xfg 4.0.0 → 4.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (174) hide show
  1. package/README.md +1 -2
  2. package/dist/cli/index.d.ts +1 -2
  3. package/dist/cli/index.js +0 -1
  4. package/dist/cli/program.js +7 -2
  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 +2 -24
  9. package/dist/cli/sync-command.js +295 -301
  10. package/dist/cli/types.d.ts +60 -40
  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 -4
  27. package/dist/config/validator.js +286 -363
  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/lifecycle/ado-migration-source.js +2 -1
  37. package/dist/lifecycle/github-lifecycle-provider.d.ts +6 -5
  38. package/dist/lifecycle/github-lifecycle-provider.js +79 -90
  39. package/dist/lifecycle/index.d.ts +2 -6
  40. package/dist/lifecycle/index.js +0 -4
  41. package/dist/lifecycle/lifecycle-formatter.d.ts +2 -1
  42. package/dist/lifecycle/lifecycle-formatter.js +4 -0
  43. package/dist/lifecycle/lifecycle-helpers.d.ts +3 -2
  44. package/dist/lifecycle/repo-lifecycle-manager.js +4 -11
  45. package/dist/lifecycle/types.d.ts +0 -8
  46. package/dist/output/github-summary.d.ts +5 -0
  47. package/dist/output/github-summary.js +9 -2
  48. package/dist/output/index.d.ts +2 -2
  49. package/dist/output/index.js +1 -1
  50. package/dist/output/lifecycle-report.js +5 -23
  51. package/dist/output/settings-report.d.ts +14 -3
  52. package/dist/output/settings-report.js +137 -197
  53. package/dist/output/summary-utils.d.ts +1 -1
  54. package/dist/output/summary-utils.js +2 -1
  55. package/dist/output/sync-report.js +5 -8
  56. package/dist/output/unified-summary.d.ts +2 -1
  57. package/dist/output/unified-summary.js +71 -133
  58. package/dist/settings/base-processor.d.ts +67 -0
  59. package/dist/settings/base-processor.js +91 -0
  60. package/dist/settings/index.d.ts +4 -3
  61. package/dist/settings/index.js +3 -3
  62. package/dist/settings/labels/converter.d.ts +2 -1
  63. package/dist/settings/labels/github-labels-strategy.d.ts +9 -18
  64. package/dist/settings/labels/github-labels-strategy.js +17 -73
  65. package/dist/settings/labels/index.d.ts +2 -6
  66. package/dist/settings/labels/index.js +1 -9
  67. package/dist/settings/labels/processor.d.ts +6 -30
  68. package/dist/settings/labels/processor.js +62 -152
  69. package/dist/settings/labels/types.d.ts +5 -8
  70. package/dist/settings/repo-settings/formatter.d.ts +2 -2
  71. package/dist/settings/repo-settings/formatter.js +6 -6
  72. package/dist/settings/repo-settings/github-repo-settings-strategy.d.ts +11 -12
  73. package/dist/settings/repo-settings/github-repo-settings-strategy.js +32 -79
  74. package/dist/settings/repo-settings/index.d.ts +2 -5
  75. package/dist/settings/repo-settings/index.js +1 -9
  76. package/dist/settings/repo-settings/processor.d.ts +6 -27
  77. package/dist/settings/repo-settings/processor.js +51 -104
  78. package/dist/settings/repo-settings/types.d.ts +7 -9
  79. package/dist/settings/rulesets/diff-algorithm.d.ts +0 -4
  80. package/dist/settings/rulesets/diff-algorithm.js +1 -10
  81. package/dist/settings/rulesets/diff.d.ts +1 -1
  82. package/dist/settings/rulesets/diff.js +2 -21
  83. package/dist/settings/rulesets/formatter.d.ts +1 -3
  84. package/dist/settings/rulesets/formatter.js +1 -8
  85. package/dist/settings/rulesets/github-ruleset-strategy.d.ts +11 -51
  86. package/dist/settings/rulesets/github-ruleset-strategy.js +24 -85
  87. package/dist/settings/rulesets/index.d.ts +3 -6
  88. package/dist/settings/rulesets/index.js +5 -9
  89. package/dist/settings/rulesets/processor.d.ts +8 -33
  90. package/dist/settings/rulesets/processor.js +58 -151
  91. package/dist/settings/rulesets/types.d.ts +35 -6
  92. package/dist/shared/command-executor.d.ts +2 -22
  93. package/dist/shared/command-executor.js +8 -7
  94. package/dist/shared/env.d.ts +0 -8
  95. package/dist/shared/env.js +14 -70
  96. package/dist/shared/file-status.d.ts +2 -0
  97. package/dist/shared/file-status.js +13 -0
  98. package/dist/shared/gh-api-utils.d.ts +46 -0
  99. package/dist/shared/gh-api-utils.js +107 -0
  100. package/dist/shared/index.d.ts +5 -5
  101. package/dist/shared/index.js +3 -3
  102. package/dist/shared/interpolation-engine.d.ts +31 -0
  103. package/dist/shared/interpolation-engine.js +50 -0
  104. package/dist/shared/logger.d.ts +3 -7
  105. package/dist/shared/logger.js +4 -1
  106. package/dist/shared/repo-detector.d.ts +17 -2
  107. package/dist/shared/repo-detector.js +27 -0
  108. package/dist/shared/retry-utils.d.ts +9 -17
  109. package/dist/shared/retry-utils.js +22 -28
  110. package/dist/shared/sanitize-utils.d.ts +0 -7
  111. package/dist/shared/sanitize-utils.js +0 -7
  112. package/dist/shared/shell-utils.d.ts +1 -0
  113. package/dist/shared/shell-utils.js +3 -0
  114. package/dist/shared/string-utils.d.ts +4 -0
  115. package/dist/shared/string-utils.js +6 -0
  116. package/dist/shared/type-guards.d.ts +17 -0
  117. package/dist/shared/type-guards.js +26 -0
  118. package/dist/shared/workspace-utils.d.ts +0 -4
  119. package/dist/shared/workspace-utils.js +0 -4
  120. package/dist/{sync → shared}/xfg-template.d.ts +3 -2
  121. package/dist/{sync → shared}/xfg-template.js +13 -54
  122. package/dist/sync/auth-options-builder.d.ts +4 -5
  123. package/dist/sync/auth-options-builder.js +15 -26
  124. package/dist/sync/branch-manager.d.ts +5 -0
  125. package/dist/sync/branch-manager.js +12 -10
  126. package/dist/sync/commit-push-manager.d.ts +1 -1
  127. package/dist/sync/commit-push-manager.js +22 -18
  128. package/dist/sync/diff-utils.d.ts +4 -9
  129. package/dist/sync/diff-utils.js +2 -19
  130. package/dist/sync/file-sync-orchestrator.js +9 -8
  131. package/dist/sync/file-writer.d.ts +2 -1
  132. package/dist/sync/file-writer.js +3 -6
  133. package/dist/sync/index.d.ts +2 -15
  134. package/dist/sync/index.js +0 -19
  135. package/dist/sync/manifest-manager.d.ts +4 -0
  136. package/dist/sync/manifest-manager.js +5 -1
  137. package/dist/sync/manifest.d.ts +10 -41
  138. package/dist/sync/manifest.js +11 -56
  139. package/dist/sync/pr-merge-handler.d.ts +2 -6
  140. package/dist/sync/pr-merge-handler.js +6 -3
  141. package/dist/sync/repository-processor.d.ts +1 -2
  142. package/dist/sync/repository-processor.js +20 -12
  143. package/dist/sync/repository-session.js +5 -14
  144. package/dist/sync/sync-workflow.js +31 -38
  145. package/dist/sync/types.d.ts +43 -178
  146. package/dist/vcs/authenticated-git-ops.d.ts +27 -70
  147. package/dist/vcs/authenticated-git-ops.js +70 -96
  148. package/dist/vcs/azure-pr-strategy.d.ts +6 -4
  149. package/dist/vcs/azure-pr-strategy.js +34 -82
  150. package/dist/vcs/branch-utils.d.ts +6 -0
  151. package/dist/vcs/branch-utils.js +29 -0
  152. package/dist/vcs/commit-strategy-selector.d.ts +5 -0
  153. package/dist/vcs/commit-strategy-selector.js +10 -0
  154. package/dist/vcs/git-commit-strategy.js +1 -2
  155. package/dist/vcs/git-ops.d.ts +15 -59
  156. package/dist/vcs/git-ops.js +46 -110
  157. package/dist/vcs/github-app-token-manager.d.ts +0 -6
  158. package/dist/vcs/github-app-token-manager.js +5 -12
  159. package/dist/vcs/github-pr-strategy.d.ts +5 -5
  160. package/dist/vcs/github-pr-strategy.js +44 -122
  161. package/dist/vcs/gitlab-pr-strategy.d.ts +6 -4
  162. package/dist/vcs/gitlab-pr-strategy.js +39 -87
  163. package/dist/vcs/graphql-commit-strategy.d.ts +3 -4
  164. package/dist/vcs/graphql-commit-strategy.js +31 -63
  165. package/dist/vcs/index.d.ts +3 -16
  166. package/dist/vcs/index.js +2 -33
  167. package/dist/vcs/pr-creator.d.ts +9 -9
  168. package/dist/vcs/pr-creator.js +11 -10
  169. package/dist/vcs/pr-strategy-factory.d.ts +5 -0
  170. package/dist/vcs/pr-strategy-factory.js +17 -0
  171. package/dist/vcs/pr-strategy.d.ts +13 -26
  172. package/dist/vcs/pr-strategy.js +20 -25
  173. package/dist/vcs/types.d.ts +87 -21
  174. package/package.json +2 -1
@@ -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,27 +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
- dryRun?: boolean;
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 {
10
8
  noDelete?: boolean;
11
- token?: string;
12
9
  }
13
- export interface RulesetProcessorResult {
14
- success: boolean;
15
- repoName: string;
16
- message: string;
17
- skipped?: boolean;
18
- dryRun?: boolean;
19
- changes?: {
20
- create: number;
21
- update: number;
22
- delete: number;
23
- unchanged: number;
24
- };
10
+ export interface RulesetProcessorResult extends BaseProcessorResult {
11
+ changes?: ChangeCounts;
25
12
  planOutput?: RulesetPlanResult;
26
13
  }
27
14
  /**
@@ -30,19 +17,7 @@ export interface RulesetProcessorResult {
30
17
  */
31
18
  export declare class RulesetProcessor implements IRulesetProcessor {
32
19
  private readonly strategy;
33
- private readonly tokenManager;
34
- constructor(strategy?: GitHubRulesetStrategy);
35
- /**
36
- * Process rulesets for a single repository.
37
- */
20
+ constructor(strategy?: IRulesetStrategy);
38
21
  process(repoConfig: RepoConfig, repoInfo: RepoInfo, options: RulesetProcessorOptions): Promise<RulesetProcessorResult>;
39
- /**
40
- * Format change counts into a summary string.
41
- */
42
- private formatChangeSummary;
43
- /**
44
- * Resolves a GitHub App installation token for the given repo.
45
- * Returns undefined if no token manager or token resolution fails.
46
- */
47
- private getInstallationToken;
22
+ private processSettings;
48
23
  }
@@ -1,173 +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, 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
- return {
48
- success: true,
49
- repoName,
50
- message: "No rulesets configured",
51
- skipped: true,
52
- };
53
- }
54
- try {
55
- // Resolve App token if available, fall back to provided token
56
- const effectiveToken = token ?? (await this.getInstallationToken(githubRepo));
57
- const strategyOptions = { token: effectiveToken, host: githubRepo.host };
58
- const currentRulesets = await this.strategy.list(githubRepo, strategyOptions);
59
- // Convert desired rulesets to Map
60
- const desiredMap = new Map(Object.entries(desiredRulesets));
61
- // Hydrate rulesets that match desired names with full details from get()
62
- // The list endpoint only returns summary fields (id, name, target, enforcement)
63
- // but not rules, conditions, or bypass_actors needed for accurate diffing
64
- const fullRulesets = [];
65
- for (const summary of currentRulesets) {
66
- if (desiredMap.has(summary.name)) {
67
- const full = await this.strategy.get(githubRepo, summary.id, strategyOptions);
68
- fullRulesets.push(full);
69
- }
70
- else {
71
- fullRulesets.push(summary);
72
- }
73
- }
74
- // Compute diff
75
- const changes = diffRulesets(fullRulesets, desiredMap, deleteOrphaned);
76
- // Count changes by type
77
- const changeCounts = {
78
- create: changes.filter((c) => c.action === "create").length,
79
- update: changes.filter((c) => c.action === "update").length,
80
- delete: changes.filter((c) => c.action === "delete").length,
81
- unchanged: changes.filter((c) => c.action === "unchanged").length,
82
- };
83
- const planOutput = formatRulesetPlan(changes);
84
- // Dry run mode - report planned changes without applying
85
- if (dryRun) {
86
- const summary = this.formatChangeSummary(changeCounts);
87
- return {
88
- success: true,
89
- repoName,
90
- message: `[DRY RUN] ${summary}`,
91
- dryRun: true,
92
- changes: changeCounts,
93
- planOutput,
94
- };
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);
95
37
  }
96
- // Apply changes
97
- let appliedCount = 0;
98
- for (const change of changes) {
99
- switch (change.action) {
100
- case "create":
101
- if (change.desired) {
102
- await this.strategy.create(githubRepo, change.name, change.desired, strategyOptions);
103
- appliedCount++;
104
- }
105
- break;
106
- case "update":
107
- if (change.rulesetId !== undefined && change.desired) {
108
- await this.strategy.update(githubRepo, change.rulesetId, change.name, change.desired, strategyOptions);
109
- appliedCount++;
110
- }
111
- break;
112
- case "delete":
113
- // Check if deletion is allowed
114
- if (!noDelete && deleteOrphaned && change.rulesetId !== undefined) {
115
- await this.strategy.delete(githubRepo, change.rulesetId, strategyOptions);
116
- appliedCount++;
117
- }
118
- break;
119
- case "unchanged":
120
- // No action needed
121
- break;
122
- }
38
+ else {
39
+ fullRulesets.push(summary);
123
40
  }
124
- const summary = this.formatChangeSummary(changeCounts);
125
- return {
126
- success: true,
127
- repoName,
128
- message: appliedCount > 0 ? `Applied: ${summary}` : "No changes needed",
129
- changes: changeCounts,
130
- planOutput,
131
- };
132
41
  }
133
- catch (error) {
134
- const message = error instanceof Error ? error.message : String(error);
135
- return {
136
- success: false,
137
- repoName,
138
- message: `Failed: ${message}`,
139
- };
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 });
140
47
  }
141
- }
142
- /**
143
- * Format change counts into a summary string.
144
- */
145
- formatChangeSummary(counts) {
146
- const parts = [];
147
- if (counts.create > 0)
148
- parts.push(`${counts.create} created`);
149
- if (counts.update > 0)
150
- parts.push(`${counts.update} updated`);
151
- if (counts.delete > 0)
152
- parts.push(`${counts.delete} deleted`);
153
- if (counts.unchanged > 0)
154
- parts.push(`${counts.unchanged} unchanged`);
155
- return parts.length > 0 ? parts.join(", ") : "no changes";
156
- }
157
- /**
158
- * Resolves a GitHub App installation token for the given repo.
159
- * Returns undefined if no token manager or token resolution fails.
160
- */
161
- async getInstallationToken(repoInfo) {
162
- if (!this.tokenManager) {
163
- return undefined;
164
- }
165
- try {
166
- const token = await this.tokenManager.getTokenForRepo(repoInfo);
167
- return token ?? undefined;
168
- }
169
- catch {
170
- 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
+ }
171
75
  }
76
+ return buildApplyResult(repoName, changeCounts, appliedCount, {
77
+ planOutput,
78
+ });
172
79
  }
173
80
  }
@@ -1,10 +1,39 @@
1
1
  import type { RepoInfo } from "../../shared/repo-detector.js";
2
2
  import type { Ruleset } from "../../config/index.js";
3
- import type { GitHubRuleset, RulesetStrategyOptions } from "./github-ruleset-strategy.js";
3
+ import type { GhApiOptions } from "../../shared/gh-api-utils.js";
4
+ /**
5
+ * GitHub Ruleset response from API (snake_case).
6
+ */
7
+ export interface GitHubRuleset {
8
+ id: number;
9
+ name: string;
10
+ target: "branch" | "tag";
11
+ enforcement: "active" | "disabled" | "evaluate";
12
+ bypass_actors?: GitHubBypassActor[];
13
+ conditions?: GitHubRulesetConditions;
14
+ rules?: GitHubRule[];
15
+ source_type?: string;
16
+ source?: string;
17
+ }
18
+ export interface GitHubBypassActor {
19
+ actor_id: number;
20
+ actor_type: "Team" | "User" | "Integration";
21
+ bypass_mode?: "always" | "pull_request";
22
+ }
23
+ export interface GitHubRulesetConditions {
24
+ ref_name?: {
25
+ include?: string[];
26
+ exclude?: string[];
27
+ };
28
+ }
29
+ export interface GitHubRule {
30
+ type: string;
31
+ parameters?: Record<string, unknown>;
32
+ }
4
33
  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>;
34
+ list(repoInfo: RepoInfo, options?: GhApiOptions): Promise<GitHubRuleset[]>;
35
+ get(repoInfo: RepoInfo, rulesetId: number, options?: GhApiOptions): Promise<GitHubRuleset>;
36
+ create(repoInfo: RepoInfo, name: string, ruleset: Ruleset, options?: GhApiOptions): Promise<GitHubRuleset>;
37
+ update(repoInfo: RepoInfo, rulesetId: number, name: string, ruleset: Ruleset, options?: GhApiOptions): Promise<GitHubRuleset>;
38
+ delete(repoInfo: RepoInfo, rulesetId: number, options?: GhApiOptions): Promise<void>;
10
39
  }
@@ -1,33 +1,13 @@
1
- /**
2
- * Options for command execution.
3
- */
4
1
  export interface ExecOptions {
5
2
  /** Additional environment variables to set for the command */
6
3
  env?: Record<string, string>;
7
4
  }
8
- /**
9
- * Interface for executing shell commands.
10
- * Enables dependency injection for testing and alternative implementations.
11
- */
12
5
  export interface ICommandExecutor {
13
- /**
14
- * Execute a shell command and return the output.
15
- * @param command The command to execute
16
- * @param cwd The working directory for the command
17
- * @param options Optional execution options (env vars, etc.)
18
- * @returns Promise resolving to the trimmed stdout output
19
- * @throws Error if the command fails
20
- */
21
6
  exec(command: string, cwd: string, options?: ExecOptions): Promise<string>;
22
7
  }
23
- /**
24
- * Default implementation that uses Node.js child_process.execSync.
25
- * Note: Commands are escaped using escapeShellArg before being passed here.
26
- */
27
8
  export declare class ShellCommandExecutor implements ICommandExecutor {
28
9
  exec(command: string, cwd: string, options?: ExecOptions): Promise<string>;
29
10
  }
30
- /**
31
- * Default executor instance for production use.
32
- */
33
11
  export declare const defaultExecutor: ICommandExecutor;
12
+ /** Extract stderr string from an exec error (child_process errors attach stderr). */
13
+ export declare function getStderr(error: unknown): string;
@@ -1,9 +1,5 @@
1
1
  import { execSync } from "node:child_process";
2
2
  import { sanitizeCredentials } from "./sanitize-utils.js";
3
- /**
4
- * Default implementation that uses Node.js child_process.execSync.
5
- * Note: Commands are escaped using escapeShellArg before being passed here.
6
- */
7
3
  export class ShellCommandExecutor {
8
4
  async exec(command, cwd, options) {
9
5
  try {
@@ -36,7 +32,12 @@ export class ShellCommandExecutor {
36
32
  }
37
33
  }
38
34
  }
39
- /**
40
- * Default executor instance for production use.
41
- */
42
35
  export const defaultExecutor = new ShellCommandExecutor();
36
+ /** Extract stderr string from an exec error (child_process errors attach stderr). */
37
+ export function getStderr(error) {
38
+ if (error != null && typeof error === "object" && "stderr" in error) {
39
+ const { stderr } = error;
40
+ return typeof stderr === "string" ? stderr : "";
41
+ }
42
+ return "";
43
+ }
@@ -24,14 +24,6 @@ export interface EnvInterpolationOptions {
24
24
  * @returns A new object with interpolated values
25
25
  */
26
26
  export declare function interpolateEnvVars(json: Record<string, unknown>, options?: EnvInterpolationOptions): Record<string, unknown>;
27
- /**
28
- * Interpolate environment variables in a string.
29
- */
30
- export declare function interpolateEnvVarsInString(value: string, options?: EnvInterpolationOptions): string;
31
- /**
32
- * Interpolate environment variables in an array of strings.
33
- */
34
- export declare function interpolateEnvVarsInLines(lines: string[], options?: EnvInterpolationOptions): string[];
35
27
  /**
36
28
  * Interpolate environment variables in content of any supported type.
37
29
  * Handles objects, strings, and string arrays.