@aspruyt/xfg 6.1.0 → 6.3.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.
Files changed (174) hide show
  1. package/dist/cli/lifecycle-report-builder.d.ts +2 -2
  2. package/dist/cli/lifecycle-report-builder.js +3 -11
  3. package/dist/cli/program.d.ts +2 -1
  4. package/dist/cli/program.js +43 -6
  5. package/dist/cli/repo-sync-runner.d.ts +24 -0
  6. package/dist/cli/repo-sync-runner.js +156 -0
  7. package/dist/cli/results-collector.d.ts +1 -1
  8. package/dist/cli/results-collector.js +2 -2
  9. package/dist/cli/secrets-command.d.ts +25 -0
  10. package/dist/cli/secrets-command.js +75 -0
  11. package/dist/cli/settings-factories.d.ts +8 -0
  12. package/dist/cli/settings-factories.js +32 -0
  13. package/dist/cli/settings-report-builder.d.ts +7 -2
  14. package/dist/cli/settings-report-builder.js +28 -20
  15. package/dist/cli/settings-runner.d.ts +2 -0
  16. package/dist/cli/settings-runner.js +94 -0
  17. package/dist/cli/sync-command.d.ts +1 -1
  18. package/dist/cli/sync-command.js +31 -372
  19. package/dist/cli/sync-report-builder.d.ts +1 -1
  20. package/dist/cli/sync-utils.d.ts +8 -0
  21. package/dist/cli/sync-utils.js +36 -0
  22. package/dist/cli/types.d.ts +8 -8
  23. package/dist/cli/unified-summary.d.ts +1 -3
  24. package/dist/cli/unified-summary.js +7 -5
  25. package/dist/cli.js +2 -1
  26. package/dist/{shared → config}/env.js +2 -2
  27. package/dist/config/extends-resolver.js +4 -3
  28. package/dist/config/file-reference-resolver.js +4 -2
  29. package/dist/config/formatter.js +1 -0
  30. package/dist/config/index.d.ts +2 -2
  31. package/dist/config/index.js +1 -1
  32. package/dist/config/loader.js +30 -6
  33. package/dist/config/merge.d.ts +11 -1
  34. package/dist/config/merge.js +78 -6
  35. package/dist/config/normalizer.js +129 -49
  36. package/dist/config/types.d.ts +20 -0
  37. package/dist/config/validator.d.ts +5 -4
  38. package/dist/config/validator.js +187 -614
  39. package/dist/config/validators/file-validator.d.ts +2 -1
  40. package/dist/config/validators/file-validator.js +9 -1
  41. package/dist/config/validators/group-validator.d.ts +3 -0
  42. package/dist/config/validators/group-validator.js +167 -0
  43. package/dist/config/validators/repo-entry-validator.d.ts +2 -0
  44. package/dist/config/validators/repo-entry-validator.js +165 -0
  45. package/dist/config/validators/repo-settings-validator.js +18 -7
  46. package/dist/config/validators/ruleset-validator.js +2 -5
  47. package/dist/config/validators/shared.d.ts +11 -0
  48. package/dist/config/validators/shared.js +242 -0
  49. package/dist/lifecycle/ado-migration-source.js +2 -4
  50. package/dist/lifecycle/github-lifecycle-provider.d.ts +7 -11
  51. package/dist/lifecycle/github-lifecycle-provider.js +125 -136
  52. package/dist/lifecycle/{lifecycle-helpers.d.ts → helpers.d.ts} +5 -1
  53. package/dist/lifecycle/{lifecycle-helpers.js → helpers.js} +9 -8
  54. package/dist/lifecycle/index.d.ts +2 -2
  55. package/dist/lifecycle/index.js +1 -1
  56. package/dist/lifecycle/repo-lifecycle-factory.d.ts +2 -2
  57. package/dist/output/github-summary.js +2 -3
  58. package/dist/output/index.d.ts +4 -0
  59. package/dist/output/index.js +4 -0
  60. package/dist/output/lifecycle-report.d.ts +1 -1
  61. package/dist/output/lifecycle-report.js +5 -0
  62. package/dist/output/settings-report.d.ts +11 -0
  63. package/dist/output/settings-report.js +24 -0
  64. package/dist/output/sync-report.d.ts +25 -3
  65. package/dist/output/sync-report.js +11 -11
  66. package/dist/secrets/encryption.d.ts +9 -0
  67. package/dist/secrets/encryption.js +29 -0
  68. package/dist/secrets/github-secrets-strategy.d.ts +17 -0
  69. package/dist/secrets/github-secrets-strategy.js +38 -0
  70. package/dist/secrets/index.d.ts +5 -0
  71. package/dist/secrets/index.js +3 -0
  72. package/dist/secrets/processor.d.ts +31 -0
  73. package/dist/secrets/processor.js +115 -0
  74. package/dist/secrets/types.d.ts +21 -0
  75. package/dist/settings/base-processor.d.ts +18 -7
  76. package/dist/settings/base-processor.js +26 -5
  77. package/dist/settings/code-scanning/diff.js +2 -2
  78. package/dist/settings/code-scanning/formatter.d.ts +2 -6
  79. package/dist/settings/code-scanning/formatter.js +2 -25
  80. package/dist/settings/code-scanning/github-code-scanning-strategy.d.ts +3 -7
  81. package/dist/settings/code-scanning/github-code-scanning-strategy.js +2 -2
  82. package/dist/settings/code-scanning/processor.js +6 -4
  83. package/dist/settings/code-scanning/types.d.ts +10 -8
  84. package/dist/settings/index.d.ts +1 -0
  85. package/dist/settings/index.js +2 -0
  86. package/dist/settings/labels/github-labels-strategy.d.ts +3 -11
  87. package/dist/settings/labels/types.d.ts +12 -10
  88. package/dist/settings/repo-settings/diff.d.ts +1 -1
  89. package/dist/settings/repo-settings/diff.js +1 -1
  90. package/dist/settings/repo-settings/formatter.d.ts +2 -6
  91. package/dist/settings/repo-settings/formatter.js +4 -23
  92. package/dist/settings/repo-settings/github-repo-settings-strategy.d.ts +2 -2
  93. package/dist/settings/repo-settings/github-repo-settings-strategy.js +8 -7
  94. package/dist/settings/repo-settings/processor.js +11 -11
  95. package/dist/settings/repo-settings/types.d.ts +2 -2
  96. package/dist/settings/rulesets/diff-algorithm.js +4 -2
  97. package/dist/settings/rulesets/diff.js +2 -51
  98. package/dist/settings/rulesets/formatter.js +4 -0
  99. package/dist/settings/rulesets/github-ruleset-strategy.d.ts +3 -3
  100. package/dist/settings/rulesets/github-ruleset-strategy.js +4 -6
  101. package/dist/settings/rulesets/index.d.ts +1 -1
  102. package/dist/settings/rulesets/index.js +0 -2
  103. package/dist/settings/rulesets/processor.js +1 -1
  104. package/dist/settings/rulesets/types.d.ts +6 -2
  105. package/dist/settings/variables/diff.d.ts +10 -0
  106. package/dist/settings/variables/diff.js +39 -0
  107. package/dist/settings/variables/formatter.d.ts +16 -0
  108. package/dist/settings/variables/formatter.js +70 -0
  109. package/dist/settings/variables/github-variables-strategy.d.ts +17 -0
  110. package/dist/settings/variables/github-variables-strategy.js +40 -0
  111. package/dist/settings/variables/index.d.ts +4 -0
  112. package/dist/settings/variables/index.js +2 -0
  113. package/dist/settings/variables/processor.d.ts +19 -0
  114. package/dist/settings/variables/processor.js +60 -0
  115. package/dist/settings/variables/types.d.ts +18 -0
  116. package/dist/settings/variables/types.js +1 -0
  117. package/dist/shared/command-executor.d.ts +4 -4
  118. package/dist/shared/command-executor.js +9 -7
  119. package/dist/shared/diff-format.d.ts +1 -0
  120. package/dist/shared/diff-format.js +10 -0
  121. package/dist/shared/env-resolver.d.ts +16 -0
  122. package/dist/shared/env-resolver.js +33 -0
  123. package/dist/shared/errors.d.ts +7 -4
  124. package/dist/shared/errors.js +8 -8
  125. package/dist/shared/gh-api-utils.d.ts +3 -34
  126. package/dist/shared/gh-api-utils.js +23 -53
  127. package/dist/shared/gh-token-utils.d.ts +26 -0
  128. package/dist/shared/gh-token-utils.js +32 -0
  129. package/dist/shared/json-utils.js +1 -1
  130. package/dist/shared/regex-utils.d.ts +1 -0
  131. package/dist/shared/regex-utils.js +3 -0
  132. package/dist/shared/retry-utils.d.ts +1 -0
  133. package/dist/shared/retry-utils.js +13 -7
  134. package/dist/sync/auth-options-builder.js +1 -1
  135. package/dist/sync/branch-manager.js +5 -3
  136. package/dist/sync/commit-push-manager.js +2 -3
  137. package/dist/sync/diff-utils.d.ts +0 -1
  138. package/dist/sync/diff-utils.js +5 -10
  139. package/dist/sync/file-sync-orchestrator.js +0 -2
  140. package/dist/sync/file-writer.d.ts +3 -0
  141. package/dist/sync/file-writer.js +84 -81
  142. package/dist/sync/index.d.ts +0 -1
  143. package/dist/sync/index.js +0 -1
  144. package/dist/sync/manifest.js +1 -1
  145. package/dist/sync/pr-merge-handler.js +6 -6
  146. package/dist/sync/sync-workflow.js +1 -1
  147. package/dist/sync/types.d.ts +2 -2
  148. package/dist/vcs/ado-pr-strategy.d.ts +3 -5
  149. package/dist/vcs/ado-pr-strategy.js +131 -33
  150. package/dist/vcs/authenticated-git-ops.js +45 -23
  151. package/dist/vcs/git-commit-strategy.js +10 -6
  152. package/dist/vcs/git-ops.js +30 -24
  153. package/dist/vcs/github-pr-strategy.d.ts +3 -2
  154. package/dist/vcs/github-pr-strategy.js +80 -30
  155. package/dist/vcs/gitlab-pr-strategy.d.ts +2 -5
  156. package/dist/vcs/gitlab-pr-strategy.js +88 -87
  157. package/dist/vcs/graphql-commit-strategy.d.ts +1 -5
  158. package/dist/vcs/graphql-commit-strategy.js +21 -37
  159. package/dist/vcs/pr-creator.js +9 -2
  160. package/dist/vcs/pr-strategy.d.ts +2 -3
  161. package/dist/vcs/pr-strategy.js +0 -1
  162. package/dist/vcs/types.d.ts +9 -5
  163. package/package.json +7 -5
  164. package/dist/config/validators/index.d.ts +0 -3
  165. package/dist/config/validators/index.js +0 -6
  166. package/dist/output/types.d.ts +0 -20
  167. package/dist/shared/shell-utils.d.ts +0 -6
  168. package/dist/shared/shell-utils.js +0 -17
  169. /package/dist/{shared → config}/env.d.ts +0 -0
  170. /package/dist/lifecycle/{lifecycle-formatter.d.ts → formatter.d.ts} +0 -0
  171. /package/dist/lifecycle/{lifecycle-formatter.js → formatter.js} +0 -0
  172. /package/dist/{output → secrets}/types.js +0 -0
  173. /package/dist/{vcs → shared}/sanitize-utils.d.ts +0 -0
  174. /package/dist/{vcs → shared}/sanitize-utils.js +0 -0
@@ -20,8 +20,10 @@ export class CodeScanningProcessor {
20
20
  }
21
21
  async applySettings(githubRepo, repoConfig, options, effectiveToken, repoName) {
22
22
  const { dryRun } = options;
23
- const desiredSettings = repoConfig.settings
24
- .codeScanning;
23
+ const desiredSettings = repoConfig.settings?.codeScanning;
24
+ if (!desiredSettings || typeof desiredSettings !== "object") {
25
+ throw new Error("applySettings called without codeScanning settings");
26
+ }
25
27
  const strategyOptions = { token: effectiveToken, host: githubRepo.host };
26
28
  // Validate GHAS availability for private repos
27
29
  const metadata = await this.metadataProvider.getMetadata(githubRepo, strategyOptions);
@@ -34,7 +36,7 @@ export class CodeScanningProcessor {
34
36
  };
35
37
  }
36
38
  // Fetch current settings
37
- const currentSettings = await this.strategy.getDefaultSetup(githubRepo, strategyOptions);
39
+ const currentSettings = await this.strategy.get(githubRepo, strategyOptions);
38
40
  // Compute diff
39
41
  const changes = diffCodeScanning(currentSettings, desiredSettings);
40
42
  const changeCounts = countActions(changes);
@@ -61,7 +63,7 @@ export class CodeScanningProcessor {
61
63
  if (desiredSettings.languages !== undefined) {
62
64
  payload.languages = desiredSettings.languages;
63
65
  }
64
- await this.strategy.updateDefaultSetup(githubRepo, payload, strategyOptions);
66
+ await this.strategy.update(githubRepo, payload, strategyOptions);
65
67
  const appliedCount = changes.filter((c) => c.action !== "unchanged").length;
66
68
  return buildApplyResult(repoName, changeCounts, appliedCount, {
67
69
  planOutput,
@@ -1,11 +1,17 @@
1
1
  import type { RepoInfo } from "../../repo/index.js";
2
2
  import type { GhApiOptions } from "../../shared/gh-api-utils.js";
3
+ import type { CodeScanningState, CodeScanningQuerySuite } from "../../config/types.js";
3
4
  /**
4
5
  * Current code scanning default setup state from GitHub API.
5
6
  */
6
7
  export interface CurrentCodeScanningSettings {
7
- state: "configured" | "not-configured";
8
- query_suite?: "default" | "extended";
8
+ state: CodeScanningState;
9
+ query_suite?: CodeScanningQuerySuite;
10
+ languages?: string[];
11
+ }
12
+ export interface CodeScanningUpdateParams {
13
+ state: string;
14
+ query_suite?: string;
9
15
  languages?: string[];
10
16
  }
11
17
  /**
@@ -13,10 +19,6 @@ export interface CurrentCodeScanningSettings {
13
19
  * Abstracts the GitHub API calls for testability.
14
20
  */
15
21
  export interface ICodeScanningStrategy {
16
- getDefaultSetup(repoInfo: RepoInfo, options?: GhApiOptions): Promise<CurrentCodeScanningSettings>;
17
- updateDefaultSetup(repoInfo: RepoInfo, settings: {
18
- state: string;
19
- query_suite?: string;
20
- languages?: string[];
21
- }, options?: GhApiOptions): Promise<void>;
22
+ get(repoInfo: RepoInfo, options?: GhApiOptions): Promise<CurrentCodeScanningSettings>;
23
+ update(repoInfo: RepoInfo, settings: CodeScanningUpdateParams, options?: GhApiOptions): Promise<void>;
22
24
  }
@@ -3,3 +3,4 @@ export { type PropertyDiff, type RulesetPlanEntry, RulesetProcessor, type IRules
3
3
  export { RepoSettingsProcessor, type IRepoSettingsProcessor, type RepoSettingsPlanEntry, GitHubRepoSettingsStrategy, } from "./repo-settings/index.js";
4
4
  export { type LabelsPlanEntry, LabelsProcessor, type ILabelsProcessor, GitHubLabelsStrategy, } from "./labels/index.js";
5
5
  export { type CodeScanningPlanEntry, CodeScanningProcessor, type ICodeScanningProcessor, GitHubCodeScanningStrategy, } from "./code-scanning/index.js";
6
+ export { type VariablesPlanEntry, VariablesProcessor, type IVariablesProcessor, GitHubVariablesStrategy, } from "./variables/index.js";
@@ -8,3 +8,5 @@ export { RepoSettingsProcessor, GitHubRepoSettingsStrategy, } from "./repo-setti
8
8
  export { LabelsProcessor, GitHubLabelsStrategy, } from "./labels/index.js";
9
9
  // Code scanning
10
10
  export { CodeScanningProcessor, GitHubCodeScanningStrategy, } from "./code-scanning/index.js";
11
+ // Variables
12
+ export { VariablesProcessor, GitHubVariablesStrategy, } from "./variables/index.js";
@@ -1,7 +1,7 @@
1
1
  import type { ICommandExecutor } from "../../shared/command-executor.js";
2
2
  import { type RepoInfo } from "../../repo/index.js";
3
3
  import { type GhApiOptions } from "../../shared/gh-api-utils.js";
4
- import type { ILabelsStrategy, GitHubLabel } from "./types.js";
4
+ import type { ILabelsStrategy, GitHubLabel, LabelCreateParams, LabelUpdateParams } from "./types.js";
5
5
  interface GitHubLabelsStrategyOptions {
6
6
  retries?: number;
7
7
  cwd: string;
@@ -10,16 +10,8 @@ export declare class GitHubLabelsStrategy implements ILabelsStrategy {
10
10
  private api;
11
11
  constructor(executor: ICommandExecutor, options: GitHubLabelsStrategyOptions);
12
12
  list(repoInfo: RepoInfo, options?: GhApiOptions): Promise<GitHubLabel[]>;
13
- create(repoInfo: RepoInfo, label: {
14
- name: string;
15
- color: string;
16
- description?: string;
17
- }, options?: GhApiOptions): Promise<void>;
18
- update(repoInfo: RepoInfo, currentName: string, label: {
19
- new_name?: string;
20
- color?: string;
21
- description?: string;
22
- }, options?: GhApiOptions): Promise<void>;
13
+ create(repoInfo: RepoInfo, label: LabelCreateParams, options?: GhApiOptions): Promise<void>;
14
+ update(repoInfo: RepoInfo, currentName: string, label: LabelUpdateParams, options?: GhApiOptions): Promise<void>;
23
15
  delete(repoInfo: RepoInfo, name: string, options?: GhApiOptions): Promise<void>;
24
16
  }
25
17
  export {};
@@ -11,17 +11,19 @@ export interface GitHubLabel {
11
11
  * Strategy interface for label operations.
12
12
  * Abstracts platform-specific API calls.
13
13
  */
14
+ export interface LabelCreateParams {
15
+ name: string;
16
+ color: string;
17
+ description?: string;
18
+ }
19
+ export interface LabelUpdateParams {
20
+ new_name?: string;
21
+ color?: string;
22
+ description?: string;
23
+ }
14
24
  export interface ILabelsStrategy {
15
25
  list(repoInfo: RepoInfo, options?: GhApiOptions): Promise<GitHubLabel[]>;
16
- create(repoInfo: RepoInfo, label: {
17
- name: string;
18
- color: string;
19
- description?: string;
20
- }, options?: GhApiOptions): Promise<void>;
21
- update(repoInfo: RepoInfo, currentName: string, label: {
22
- new_name?: string;
23
- color?: string;
24
- description?: string;
25
- }, options?: GhApiOptions): Promise<void>;
26
+ create(repoInfo: RepoInfo, label: LabelCreateParams, options?: GhApiOptions): Promise<void>;
27
+ update(repoInfo: RepoInfo, currentName: string, label: LabelUpdateParams, options?: GhApiOptions): Promise<void>;
26
28
  delete(repoInfo: RepoInfo, name: string, options?: GhApiOptions): Promise<void>;
27
29
  }
@@ -1,7 +1,7 @@
1
1
  import type { GitHubRepoSettings } from "../../config/index.js";
2
2
  import type { SettingsAction } from "../base-processor.js";
3
3
  import type { CurrentRepoSettings } from "./types.js";
4
- export type RepoSettingsAction = Exclude<SettingsAction, "delete">;
4
+ export type RepoSettingsAction = Exclude<SettingsAction, "delete" | "unchanged">;
5
5
  export interface RepoSettingsChange {
6
6
  property: keyof GitHubRepoSettings;
7
7
  action: RepoSettingsAction;
@@ -83,5 +83,5 @@ export function diffRepoSettings(current, desired) {
83
83
  * Checks if there are any changes to apply.
84
84
  */
85
85
  export function hasRepoSettingsChanges(changes) {
86
- return changes.some((c) => c.action !== "unchanged");
86
+ return changes.length > 0;
87
87
  }
@@ -1,10 +1,6 @@
1
+ import { type PlanEntry } from "../base-processor.js";
1
2
  import type { RepoSettingsChange } from "./diff.js";
2
- export interface RepoSettingsPlanEntry {
3
- property: string;
4
- action: "create" | "update";
5
- oldValue?: unknown;
6
- newValue?: unknown;
7
- }
3
+ export type RepoSettingsPlanEntry = PlanEntry;
8
4
  export interface RepoSettingsPlanResult {
9
5
  lines: string[];
10
6
  creates: number;
@@ -1,6 +1,6 @@
1
1
  import chalk from "chalk";
2
2
  import { formatScalarValue } from "../../shared/string-utils.js";
3
- import { countActions } from "../base-processor.js";
3
+ import { formatChangeLines } from "../base-processor.js";
4
4
  /**
5
5
  * Format a value for display.
6
6
  */
@@ -32,37 +32,18 @@ function getWarning(change) {
32
32
  * Formats repo settings changes as Terraform-style plan output.
33
33
  */
34
34
  export function formatRepoSettingsPlan(changes) {
35
- const lines = [];
36
35
  const warnings = [];
37
- const { create: creates, update: updates } = countActions(changes);
38
- const entries = [];
39
36
  if (changes.length === 0) {
40
- return { lines, creates, updates, warnings, entries };
37
+ return { lines: [], creates: 0, updates: 0, warnings, entries: [] };
41
38
  }
42
39
  for (const change of changes) {
43
40
  const warning = getWarning(change);
44
41
  if (warning) {
45
42
  warnings.push(warning);
46
43
  }
47
- if (change.action === "create") {
48
- lines.push(chalk.green(` + ${change.property}: ${formatValue(change.newValue)}`));
49
- entries.push({
50
- property: change.property,
51
- action: "create",
52
- newValue: change.newValue,
53
- });
54
- }
55
- else if (change.action === "update") {
56
- lines.push(chalk.yellow(` ~ ${change.property}: ${formatValue(change.oldValue)} → ${formatValue(change.newValue)}`));
57
- entries.push({
58
- property: change.property,
59
- action: "update",
60
- oldValue: change.oldValue,
61
- newValue: change.newValue,
62
- });
63
- }
64
44
  }
65
- return { lines, creates, updates, warnings, entries };
45
+ const result = formatChangeLines(changes, formatValue);
46
+ return { ...result, warnings };
66
47
  }
67
48
  /**
68
49
  * Formats warnings for display.
@@ -13,8 +13,8 @@ export declare class GitHubRepoSettingsStrategy implements IRepoSettingsStrategy
13
13
  get(repoInfo: RepoInfo, options?: GhApiOptions): Promise<CurrentRepoSettings>;
14
14
  update(repoInfo: RepoInfo, settings: GitHubRepoSettings, options?: GhApiOptions): Promise<void>;
15
15
  updateVulnerabilityAlerts(repoInfo: RepoInfo, enable: boolean, options?: GhApiOptions): Promise<void>;
16
- setAutomatedSecurityFixes(repoInfo: RepoInfo, enable: boolean, options?: GhApiOptions): Promise<void>;
17
- setPrivateVulnerabilityReporting(repoInfo: RepoInfo, enable: boolean, options?: GhApiOptions): Promise<void>;
16
+ updateAutomatedSecurityFixes(repoInfo: RepoInfo, enable: boolean, options?: GhApiOptions): Promise<void>;
17
+ updatePrivateVulnerabilityReporting(repoInfo: RepoInfo, enable: boolean, options?: GhApiOptions): Promise<void>;
18
18
  branchExists(repoInfo: RepoInfo, branch: string, options?: GhApiOptions): Promise<boolean>;
19
19
  private getVulnerabilityAlerts;
20
20
  private getAutomatedSecurityFixes;
@@ -1,5 +1,6 @@
1
1
  import { assertGitHubRepo, } from "../../repo/index.js";
2
- import { GhApiClient, isHttp404Error, } from "../../shared/gh-api-utils.js";
2
+ import { GhApiClient } from "../../shared/gh-api-utils.js";
3
+ import { isHttp404Error } from "../../shared/gh-token-utils.js";
3
4
  import { parseApiJson } from "../../shared/json-utils.js";
4
5
  import { camelToSnake } from "../../shared/string-utils.js";
5
6
  /**
@@ -71,7 +72,7 @@ export class GitHubRepoSettingsStrategy {
71
72
  settings.owner_type = parsed.owner?.type;
72
73
  settings.vulnerability_alerts = await this.getVulnerabilityAlerts(repoInfo, options);
73
74
  // Pass vulnerability_alerts state - automated security fixes requires it enabled
74
- settings.automated_security_fixes = await this.getAutomatedSecurityFixes(repoInfo, options, settings.vulnerability_alerts);
75
+ settings.automated_security_fixes = await this.getAutomatedSecurityFixes(repoInfo, options);
75
76
  settings.private_vulnerability_reporting =
76
77
  await this.getPrivateVulnerabilityReporting(repoInfo, options);
77
78
  return settings;
@@ -92,13 +93,13 @@ export class GitHubRepoSettingsStrategy {
92
93
  const method = enable ? "PUT" : "DELETE";
93
94
  await this.api.call(method, endpoint, { options });
94
95
  }
95
- async setAutomatedSecurityFixes(repoInfo, enable, options) {
96
+ async updateAutomatedSecurityFixes(repoInfo, enable, options) {
96
97
  assertGitHubRepo(repoInfo, "GitHub Repo Settings strategy");
97
98
  const endpoint = `/repos/${repoInfo.owner}/${repoInfo.repo}/automated-security-fixes`;
98
99
  const method = enable ? "PUT" : "DELETE";
99
100
  await this.api.call(method, endpoint, { options });
100
101
  }
101
- async setPrivateVulnerabilityReporting(repoInfo, enable, options) {
102
+ async updatePrivateVulnerabilityReporting(repoInfo, enable, options) {
102
103
  assertGitHubRepo(repoInfo, "GitHub Repo Settings strategy");
103
104
  const endpoint = `/repos/${repoInfo.owner}/${repoInfo.repo}/private-vulnerability-reporting`;
104
105
  const method = enable ? "PUT" : "DELETE";
@@ -128,10 +129,10 @@ export class GitHubRepoSettingsStrategy {
128
129
  if (isHttp404Error(error)) {
129
130
  return false; // 404 = disabled
130
131
  }
131
- throw error; // Re-throw other errors
132
+ throw error;
132
133
  }
133
134
  }
134
- async getAutomatedSecurityFixes(github, options, _vulnerabilityAlertsEnabled) {
135
+ async getAutomatedSecurityFixes(github, options) {
135
136
  // Note: GitHub returns JSON with {enabled: boolean} for this endpoint
136
137
  const endpoint = `/repos/${github.owner}/${github.repo}/automated-security-fixes`;
137
138
  try {
@@ -162,7 +163,7 @@ export class GitHubRepoSettingsStrategy {
162
163
  if (isHttp404Error(error)) {
163
164
  return false; // 404 = not available (e.g. private repos)
164
165
  }
165
- throw error; // Re-throw other errors
166
+ throw error;
166
167
  }
167
168
  }
168
169
  }
@@ -20,7 +20,10 @@ export class RepoSettingsProcessor {
20
20
  }
21
21
  async applySettings(githubRepo, repoConfig, options, effectiveToken, repoName) {
22
22
  const { dryRun } = options;
23
- const desiredSettings = repoConfig.settings.repo;
23
+ const desiredSettings = repoConfig.settings?.repo;
24
+ if (!desiredSettings || typeof desiredSettings !== "object") {
25
+ throw new Error("applySettings called without repo settings");
26
+ }
24
27
  const strategyOptions = { token: effectiveToken, host: githubRepo.host };
25
28
  // Fetch current settings and metadata in parallel
26
29
  const [currentSettings, metadata] = await Promise.all([
@@ -39,16 +42,15 @@ export class RepoSettingsProcessor {
39
42
  // Compute diff
40
43
  const changes = diffRepoSettings(currentSettings, desiredSettings);
41
44
  if (!hasRepoSettingsChanges(changes)) {
42
- const unchangedCount = changes.filter((c) => c.action === "unchanged").length;
43
45
  return {
44
46
  success: true,
45
47
  repoName,
46
48
  message: "No changes needed",
47
- changes: { create: 0, update: 0, delete: 0, unchanged: unchangedCount },
49
+ changes: { create: 0, update: 0, delete: 0, unchanged: 0 },
48
50
  };
49
51
  }
50
52
  // Validate defaultBranch target exists before attempting to apply
51
- const defaultBranchChange = changes.find((c) => c.property === "defaultBranch" && c.action !== "unchanged");
53
+ const defaultBranchChange = changes.find((c) => c.property === "defaultBranch");
52
54
  if (defaultBranchChange) {
53
55
  const targetBranch = String(defaultBranchChange.newValue);
54
56
  const exists = await this.strategy.branchExists(githubRepo, targetBranch, strategyOptions);
@@ -69,7 +71,7 @@ export class RepoSettingsProcessor {
69
71
  create: planOutput.creates,
70
72
  update: planOutput.updates,
71
73
  delete: 0,
72
- unchanged: changes.filter((c) => c.action === "unchanged").length,
74
+ unchanged: 0,
73
75
  };
74
76
  if (dryRun) {
75
77
  return buildDryRunResult(repoName, changeCounts, {
@@ -80,10 +82,8 @@ export class RepoSettingsProcessor {
80
82
  // Apply changes - only send settings that actually changed
81
83
  const changedSettings = {};
82
84
  for (const change of changes) {
83
- if (change.action !== "unchanged") {
84
- changedSettings[change.property] =
85
- change.newValue;
86
- }
85
+ changedSettings[change.property] =
86
+ change.newValue;
87
87
  }
88
88
  await this.applyChanges(githubRepo, changedSettings, strategyOptions);
89
89
  const appliedCount = Object.keys(changedSettings).length;
@@ -106,12 +106,12 @@ export class RepoSettingsProcessor {
106
106
  }
107
107
  // Handle private vulnerability reporting (separate endpoint)
108
108
  if (privateVulnerabilityReporting !== undefined) {
109
- await this.strategy.setPrivateVulnerabilityReporting(repoInfo, privateVulnerabilityReporting, options);
109
+ await this.strategy.updatePrivateVulnerabilityReporting(repoInfo, privateVulnerabilityReporting, options);
110
110
  }
111
111
  // Handle automated security fixes (separate endpoint)
112
112
  // Done last to ensure vulnerability alerts have been fully processed
113
113
  if (automatedSecurityFixes !== undefined) {
114
- await this.strategy.setAutomatedSecurityFixes(repoInfo, automatedSecurityFixes, options);
114
+ await this.strategy.updateAutomatedSecurityFixes(repoInfo, automatedSecurityFixes, options);
115
115
  }
116
116
  }
117
117
  validateSecuritySettings(desiredSettings, metadata) {
@@ -58,11 +58,11 @@ export interface IRepoSettingsStrategy {
58
58
  /**
59
59
  * Enables or disables automated security fixes.
60
60
  */
61
- setAutomatedSecurityFixes(repoInfo: RepoInfo, enable: boolean, options?: GhApiOptions): Promise<void>;
61
+ updateAutomatedSecurityFixes(repoInfo: RepoInfo, enable: boolean, options?: GhApiOptions): Promise<void>;
62
62
  /**
63
63
  * Enables or disables private vulnerability reporting.
64
64
  */
65
- setPrivateVulnerabilityReporting(repoInfo: RepoInfo, enable: boolean, options?: GhApiOptions): Promise<void>;
65
+ updatePrivateVulnerabilityReporting(repoInfo: RepoInfo, enable: boolean, options?: GhApiOptions): Promise<void>;
66
66
  /**
67
67
  * Checks whether a branch exists in the repository.
68
68
  */
@@ -78,14 +78,16 @@ function diffObjectArrays(currentArr, desiredArr, parentPath) {
78
78
  const currentByType = new Map();
79
79
  for (let i = 0; i < currentArr.length; i++) {
80
80
  const item = currentArr[i];
81
- const type = item.type;
81
+ const type = typeof item.type === "string" ? item.type : String(item.type ?? "");
82
82
  if (type)
83
83
  currentByType.set(type, { item, index: i });
84
84
  }
85
85
  const matchedTypes = new Set();
86
86
  for (let i = 0; i < desiredArr.length; i++) {
87
87
  const desiredItem = desiredArr[i];
88
- const type = desiredItem.type;
88
+ const type = typeof desiredItem.type === "string"
89
+ ? desiredItem.type
90
+ : String(desiredItem.type ?? "");
89
91
  const label = `[${i}] (${type})`;
90
92
  const currentEntry = currentByType.get(type);
91
93
  if (currentEntry) {
@@ -1,7 +1,9 @@
1
1
  import { RULESET_COMPARABLE_FIELDS } from "../../config/index.js";
2
+ import { findMatchKey } from "../../config/merge.js";
2
3
  import { isPlainObject } from "../../shared/type-guards.js";
3
4
  import { camelToSnake } from "../../shared/string-utils.js";
4
5
  import { countActions } from "../base-processor.js";
6
+ import { deepEqual } from "./diff-algorithm.js";
5
7
  /**
6
8
  * Normalizes a value recursively, converting keys to a consistent format (snake_case).
7
9
  * This allows comparing GitHub API responses (snake_case) with config (camelCase).
@@ -54,37 +56,6 @@ function normalizeConfigRuleset(ruleset) {
54
56
  };
55
57
  return normalizeRuleset(withDefaults);
56
58
  }
57
- /**
58
- * Performs deep equality comparison of two normalized values.
59
- */
60
- function deepEqual(a, b) {
61
- if (a === b) {
62
- return true;
63
- }
64
- if (a === null || b === null || a === undefined || b === undefined) {
65
- return a === b;
66
- }
67
- if (typeof a !== typeof b) {
68
- return false;
69
- }
70
- if (Array.isArray(a) && Array.isArray(b)) {
71
- if (a.length !== b.length) {
72
- return false;
73
- }
74
- return a.every((val, i) => deepEqual(val, b[i]));
75
- }
76
- if (typeof a === "object" && typeof b === "object") {
77
- const objA = a;
78
- const objB = b;
79
- const keysA = Object.keys(objA);
80
- const keysB = Object.keys(objB);
81
- if (keysA.length !== keysB.length) {
82
- return false;
83
- }
84
- return keysA.every((key) => deepEqual(objA[key], objB[key]));
85
- }
86
- return false;
87
- }
88
59
  /**
89
60
  * Projects `current` onto the shape of `desired`.
90
61
  * Only keeps keys/structure present in `desired`, filtering out API noise.
@@ -129,26 +100,6 @@ function projectObjects(current, desired) {
129
100
  }
130
101
  return result;
131
102
  }
132
- /**
133
- * Candidate keys for matching array items by identity rather than index.
134
- * Order matters — first key found across all items wins.
135
- */
136
- const MATCH_KEY_CANDIDATES = ["type", "actor_id"];
137
- /**
138
- * Finds a key that uniquely identifies items in both arrays.
139
- * Returns the first candidate key present in every item of both arrays, or undefined.
140
- */
141
- function findMatchKey(current, desired) {
142
- const allItems = [...current, ...desired];
143
- if (allItems.length === 0)
144
- return undefined;
145
- for (const candidate of MATCH_KEY_CANDIDATES) {
146
- const everyItemHasKey = allItems.every((item) => isPlainObject(item) && candidate in item);
147
- if (everyItemHasKey)
148
- return candidate;
149
- }
150
- return undefined;
151
- }
152
103
  function projectArrays(current, desired) {
153
104
  // Primitive arrays — return current as-is
154
105
  if (desired.length === 0 || !isPlainObject(desired[0])) {
@@ -114,6 +114,10 @@ function getActionStyle(action) {
114
114
  return { symbol: "-", color: chalk.red };
115
115
  case "change":
116
116
  return { symbol: "~", color: chalk.yellow };
117
+ default: {
118
+ const _ = action;
119
+ throw new Error(`Unknown DiffAction: ${String(_)}`);
120
+ }
117
121
  }
118
122
  }
119
123
  function hasComplexValue(value) {
@@ -2,7 +2,7 @@ import type { ICommandExecutor } from "../../shared/command-executor.js";
2
2
  import { type RepoInfo } from "../../repo/index.js";
3
3
  import { type GhApiOptions } from "../../shared/gh-api-utils.js";
4
4
  import type { Ruleset } from "../../config/index.js";
5
- import type { IRulesetStrategy, GitHubRuleset, GitHubBypassActor, GitHubRulesetConditions, GitHubRule, RulesetUpdateParams } from "./types.js";
5
+ import type { IRulesetStrategy, GitHubRuleset, GitHubBypassActor, GitHubRulesetConditions, GitHubRule, RulesetCreateParams, RulesetUpdateParams } from "./types.js";
6
6
  /**
7
7
  * Converts camelCase config ruleset to snake_case GitHub API format.
8
8
  */
@@ -24,8 +24,8 @@ export declare class GitHubRulesetStrategy implements IRulesetStrategy {
24
24
  constructor(executor: ICommandExecutor, options: GitHubRulesetStrategyOptions);
25
25
  list(repoInfo: RepoInfo, options?: GhApiOptions): Promise<GitHubRuleset[]>;
26
26
  get(repoInfo: RepoInfo, rulesetId: number, options?: GhApiOptions): Promise<GitHubRuleset>;
27
- create(repoInfo: RepoInfo, name: string, ruleset: Ruleset, options?: GhApiOptions): Promise<GitHubRuleset>;
28
- update(repoInfo: RepoInfo, params: RulesetUpdateParams, options?: GhApiOptions): Promise<GitHubRuleset>;
27
+ create(repoInfo: RepoInfo, params: RulesetCreateParams, options?: GhApiOptions): Promise<void>;
28
+ update(repoInfo: RepoInfo, params: RulesetUpdateParams, options?: GhApiOptions): Promise<void>;
29
29
  delete(repoInfo: RepoInfo, rulesetId: number, options?: GhApiOptions): Promise<void>;
30
30
  }
31
31
  export {};
@@ -111,22 +111,20 @@ export class GitHubRulesetStrategy {
111
111
  const result = await this.api.call("GET", endpoint, { options });
112
112
  return parseApiJson(result, "ruleset response");
113
113
  }
114
- async create(repoInfo, name, ruleset, options) {
114
+ async create(repoInfo, params, options) {
115
115
  assertGitHubRepo(repoInfo, "GitHub Ruleset strategy");
116
116
  const endpoint = `/repos/${repoInfo.owner}/${repoInfo.repo}/rulesets`;
117
- const payload = configToGitHub(name, ruleset);
118
- const result = await this.api.call("POST", endpoint, { payload, options });
119
- return parseApiJson(result, "ruleset response");
117
+ const payload = configToGitHub(params.name, params.ruleset);
118
+ await this.api.call("POST", endpoint, { payload, options });
120
119
  }
121
120
  async update(repoInfo, params, options) {
122
121
  assertGitHubRepo(repoInfo, "GitHub Ruleset strategy");
123
122
  const endpoint = `/repos/${repoInfo.owner}/${repoInfo.repo}/rulesets/${params.rulesetId}`;
124
123
  const payload = configToGitHub(params.name, params.ruleset);
125
- const result = await this.api.call("PUT", endpoint, {
124
+ await this.api.call("PUT", endpoint, {
126
125
  payload,
127
126
  options,
128
127
  });
129
- return parseApiJson(result, "ruleset response");
130
128
  }
131
129
  async delete(repoInfo, rulesetId, options) {
132
130
  assertGitHubRepo(repoInfo, "GitHub Ruleset strategy");
@@ -1,4 +1,4 @@
1
- export { computePropertyDiffs, deepEqual, isArrayOfObjects, type PropertyDiff, } from "./diff-algorithm.js";
1
+ export { type PropertyDiff } from "./diff-algorithm.js";
2
2
  export { type RulesetPlanEntry } from "./formatter.js";
3
3
  export { RulesetProcessor, type IRulesetProcessor } from "./processor.js";
4
4
  export { GitHubRulesetStrategy } from "./github-ruleset-strategy.js";
@@ -1,5 +1,3 @@
1
- // Diff algorithm - property-level diffing for ruleset comparisons
2
- export { computePropertyDiffs, deepEqual, isArrayOfObjects, } from "./diff-algorithm.js";
3
1
  // Processor
4
2
  export { RulesetProcessor } from "./processor.js";
5
3
  // Strategy
@@ -50,7 +50,7 @@ export class RulesetProcessor {
50
50
  switch (change.action) {
51
51
  case "create":
52
52
  if (change.desired) {
53
- await this.strategy.create(githubRepo, change.name, change.desired, strategyOptions);
53
+ await this.strategy.create(githubRepo, { name: change.name, ruleset: change.desired }, strategyOptions);
54
54
  appliedCount++;
55
55
  }
56
56
  break;
@@ -30,6 +30,10 @@ export interface GitHubRule {
30
30
  type: string;
31
31
  parameters?: Record<string, unknown>;
32
32
  }
33
+ export interface RulesetCreateParams {
34
+ name: string;
35
+ ruleset: Ruleset;
36
+ }
33
37
  export interface RulesetUpdateParams {
34
38
  rulesetId: number;
35
39
  name: string;
@@ -38,7 +42,7 @@ export interface RulesetUpdateParams {
38
42
  export interface IRulesetStrategy {
39
43
  list(repoInfo: RepoInfo, options?: GhApiOptions): Promise<GitHubRuleset[]>;
40
44
  get(repoInfo: RepoInfo, rulesetId: number, options?: GhApiOptions): Promise<GitHubRuleset>;
41
- create(repoInfo: RepoInfo, name: string, ruleset: Ruleset, options?: GhApiOptions): Promise<GitHubRuleset>;
42
- update(repoInfo: RepoInfo, params: RulesetUpdateParams, options?: GhApiOptions): Promise<GitHubRuleset>;
45
+ create(repoInfo: RepoInfo, params: RulesetCreateParams, options?: GhApiOptions): Promise<void>;
46
+ update(repoInfo: RepoInfo, params: RulesetUpdateParams, options?: GhApiOptions): Promise<void>;
43
47
  delete(repoInfo: RepoInfo, rulesetId: number, options?: GhApiOptions): Promise<void>;
44
48
  }
@@ -0,0 +1,10 @@
1
+ import type { GitHubVariable } from "./types.js";
2
+ import type { SettingsAction } from "../base-processor.js";
3
+ export type VariableAction = SettingsAction;
4
+ export interface VariableChange {
5
+ action: VariableAction;
6
+ name: string;
7
+ oldValue?: string;
8
+ newValue?: string;
9
+ }
10
+ export declare function diffVariables(current: GitHubVariable[], desired: Record<string, string>, deleteOrphaned: boolean): VariableChange[];
@@ -0,0 +1,39 @@
1
+ export function diffVariables(current, desired, deleteOrphaned) {
2
+ const changes = [];
3
+ const currentByName = new Map();
4
+ for (const v of current) {
5
+ currentByName.set(v.name.toUpperCase(), v);
6
+ }
7
+ const desiredUpper = new Set(Object.keys(desired).map((n) => n.toUpperCase()));
8
+ for (const [name, desiredValue] of Object.entries(desired)) {
9
+ const currentVar = currentByName.get(name.toUpperCase());
10
+ if (!currentVar) {
11
+ changes.push({ action: "create", name, newValue: desiredValue });
12
+ }
13
+ else if (currentVar.value !== desiredValue) {
14
+ changes.push({
15
+ action: "update",
16
+ name: currentVar.name,
17
+ oldValue: currentVar.value,
18
+ newValue: desiredValue,
19
+ });
20
+ }
21
+ else {
22
+ changes.push({ action: "unchanged", name: currentVar.name });
23
+ }
24
+ }
25
+ if (deleteOrphaned) {
26
+ for (const [nameUpper, currentVar] of currentByName) {
27
+ if (!desiredUpper.has(nameUpper)) {
28
+ changes.push({ action: "delete", name: currentVar.name });
29
+ }
30
+ }
31
+ }
32
+ const actionOrder = {
33
+ delete: 0,
34
+ update: 1,
35
+ create: 2,
36
+ unchanged: 3,
37
+ };
38
+ return changes.sort((a, b) => actionOrder[a.action] - actionOrder[b.action]);
39
+ }
@@ -0,0 +1,16 @@
1
+ import type { VariableChange, VariableAction } from "./diff.js";
2
+ export interface VariablesPlanEntry {
3
+ name: string;
4
+ action: VariableAction;
5
+ oldValue?: string;
6
+ newValue?: string;
7
+ }
8
+ export interface VariablesPlanResult {
9
+ lines: string[];
10
+ creates: number;
11
+ updates: number;
12
+ deletes: number;
13
+ unchanged: number;
14
+ entries: VariablesPlanEntry[];
15
+ }
16
+ export declare function formatVariablesPlan(changes: VariableChange[]): VariablesPlanResult;