@aspruyt/xfg 6.1.0 → 6.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (143) 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 +2 -3
  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/settings-factories.d.ts +7 -0
  10. package/dist/cli/settings-factories.js +27 -0
  11. package/dist/cli/settings-report-builder.d.ts +1 -1
  12. package/dist/cli/settings-report-builder.js +12 -23
  13. package/dist/cli/settings-runner.d.ts +2 -0
  14. package/dist/cli/settings-runner.js +87 -0
  15. package/dist/cli/sync-command.d.ts +1 -1
  16. package/dist/cli/sync-command.js +31 -372
  17. package/dist/cli/sync-report-builder.d.ts +1 -1
  18. package/dist/cli/sync-utils.d.ts +8 -0
  19. package/dist/cli/sync-utils.js +36 -0
  20. package/dist/cli/types.d.ts +5 -7
  21. package/dist/cli/unified-summary.d.ts +1 -3
  22. package/dist/cli/unified-summary.js +7 -5
  23. package/dist/cli.js +2 -1
  24. package/dist/{shared → config}/env.js +2 -2
  25. package/dist/config/extends-resolver.js +4 -3
  26. package/dist/config/file-reference-resolver.js +4 -2
  27. package/dist/config/formatter.js +1 -0
  28. package/dist/config/index.d.ts +1 -1
  29. package/dist/config/loader.js +30 -6
  30. package/dist/config/merge.d.ts +11 -1
  31. package/dist/config/merge.js +78 -6
  32. package/dist/config/normalizer.js +43 -48
  33. package/dist/config/validator.d.ts +1 -4
  34. package/dist/config/validator.js +13 -609
  35. package/dist/config/validators/file-validator.d.ts +2 -1
  36. package/dist/config/validators/file-validator.js +9 -1
  37. package/dist/config/validators/group-validator.d.ts +3 -0
  38. package/dist/config/validators/group-validator.js +167 -0
  39. package/dist/config/validators/repo-entry-validator.d.ts +2 -0
  40. package/dist/config/validators/repo-entry-validator.js +165 -0
  41. package/dist/config/validators/repo-settings-validator.js +18 -7
  42. package/dist/config/validators/ruleset-validator.js +2 -5
  43. package/dist/config/validators/shared.d.ts +11 -0
  44. package/dist/config/validators/shared.js +242 -0
  45. package/dist/lifecycle/ado-migration-source.js +2 -4
  46. package/dist/lifecycle/github-lifecycle-provider.d.ts +7 -11
  47. package/dist/lifecycle/github-lifecycle-provider.js +125 -136
  48. package/dist/lifecycle/{lifecycle-helpers.d.ts → helpers.d.ts} +5 -1
  49. package/dist/lifecycle/{lifecycle-helpers.js → helpers.js} +9 -8
  50. package/dist/lifecycle/index.d.ts +2 -2
  51. package/dist/lifecycle/index.js +1 -1
  52. package/dist/lifecycle/repo-lifecycle-factory.d.ts +2 -2
  53. package/dist/output/github-summary.js +2 -3
  54. package/dist/output/index.d.ts +4 -0
  55. package/dist/output/index.js +4 -0
  56. package/dist/output/lifecycle-report.d.ts +1 -1
  57. package/dist/output/lifecycle-report.js +5 -0
  58. package/dist/output/sync-report.d.ts +25 -3
  59. package/dist/output/sync-report.js +11 -11
  60. package/dist/settings/base-processor.d.ts +18 -7
  61. package/dist/settings/base-processor.js +26 -5
  62. package/dist/settings/code-scanning/diff.js +2 -2
  63. package/dist/settings/code-scanning/formatter.d.ts +2 -6
  64. package/dist/settings/code-scanning/formatter.js +2 -25
  65. package/dist/settings/code-scanning/github-code-scanning-strategy.d.ts +3 -7
  66. package/dist/settings/code-scanning/github-code-scanning-strategy.js +2 -2
  67. package/dist/settings/code-scanning/processor.js +6 -4
  68. package/dist/settings/code-scanning/types.d.ts +10 -8
  69. package/dist/settings/labels/github-labels-strategy.d.ts +3 -11
  70. package/dist/settings/labels/types.d.ts +12 -10
  71. package/dist/settings/repo-settings/diff.d.ts +1 -1
  72. package/dist/settings/repo-settings/diff.js +1 -1
  73. package/dist/settings/repo-settings/formatter.d.ts +2 -6
  74. package/dist/settings/repo-settings/formatter.js +4 -23
  75. package/dist/settings/repo-settings/github-repo-settings-strategy.d.ts +2 -2
  76. package/dist/settings/repo-settings/github-repo-settings-strategy.js +8 -7
  77. package/dist/settings/repo-settings/processor.js +11 -11
  78. package/dist/settings/repo-settings/types.d.ts +2 -2
  79. package/dist/settings/rulesets/diff-algorithm.js +4 -2
  80. package/dist/settings/rulesets/diff.js +2 -51
  81. package/dist/settings/rulesets/formatter.js +4 -0
  82. package/dist/settings/rulesets/github-ruleset-strategy.d.ts +3 -3
  83. package/dist/settings/rulesets/github-ruleset-strategy.js +4 -6
  84. package/dist/settings/rulesets/index.d.ts +1 -1
  85. package/dist/settings/rulesets/index.js +0 -2
  86. package/dist/settings/rulesets/processor.js +1 -1
  87. package/dist/settings/rulesets/types.d.ts +6 -2
  88. package/dist/shared/command-executor.d.ts +4 -4
  89. package/dist/shared/command-executor.js +9 -7
  90. package/dist/shared/diff-format.d.ts +1 -0
  91. package/dist/shared/diff-format.js +10 -0
  92. package/dist/shared/errors.d.ts +7 -4
  93. package/dist/shared/errors.js +8 -8
  94. package/dist/shared/gh-api-utils.d.ts +3 -34
  95. package/dist/shared/gh-api-utils.js +23 -53
  96. package/dist/shared/gh-token-utils.d.ts +26 -0
  97. package/dist/shared/gh-token-utils.js +32 -0
  98. package/dist/shared/json-utils.js +1 -1
  99. package/dist/shared/regex-utils.d.ts +1 -0
  100. package/dist/shared/regex-utils.js +3 -0
  101. package/dist/shared/retry-utils.d.ts +1 -0
  102. package/dist/shared/retry-utils.js +13 -7
  103. package/dist/sync/auth-options-builder.js +1 -1
  104. package/dist/sync/branch-manager.js +5 -3
  105. package/dist/sync/commit-push-manager.js +2 -3
  106. package/dist/sync/diff-utils.d.ts +0 -1
  107. package/dist/sync/diff-utils.js +5 -10
  108. package/dist/sync/file-sync-orchestrator.js +0 -2
  109. package/dist/sync/file-writer.d.ts +3 -0
  110. package/dist/sync/file-writer.js +84 -81
  111. package/dist/sync/index.d.ts +0 -1
  112. package/dist/sync/index.js +0 -1
  113. package/dist/sync/manifest.js +1 -1
  114. package/dist/sync/pr-merge-handler.js +6 -6
  115. package/dist/sync/sync-workflow.js +1 -1
  116. package/dist/sync/types.d.ts +2 -2
  117. package/dist/vcs/ado-pr-strategy.d.ts +3 -5
  118. package/dist/vcs/ado-pr-strategy.js +131 -33
  119. package/dist/vcs/authenticated-git-ops.js +45 -23
  120. package/dist/vcs/git-commit-strategy.js +10 -6
  121. package/dist/vcs/git-ops.js +30 -24
  122. package/dist/vcs/github-pr-strategy.d.ts +3 -2
  123. package/dist/vcs/github-pr-strategy.js +80 -30
  124. package/dist/vcs/gitlab-pr-strategy.d.ts +2 -5
  125. package/dist/vcs/gitlab-pr-strategy.js +88 -87
  126. package/dist/vcs/graphql-commit-strategy.d.ts +1 -5
  127. package/dist/vcs/graphql-commit-strategy.js +21 -37
  128. package/dist/vcs/pr-creator.js +9 -2
  129. package/dist/vcs/pr-strategy.d.ts +2 -3
  130. package/dist/vcs/pr-strategy.js +0 -1
  131. package/dist/vcs/types.d.ts +9 -5
  132. package/package.json +5 -5
  133. package/dist/config/validators/index.d.ts +0 -3
  134. package/dist/config/validators/index.js +0 -6
  135. package/dist/output/types.d.ts +0 -20
  136. package/dist/output/types.js +0 -1
  137. package/dist/shared/shell-utils.d.ts +0 -6
  138. package/dist/shared/shell-utils.js +0 -17
  139. /package/dist/{shared → config}/env.d.ts +0 -0
  140. /package/dist/lifecycle/{lifecycle-formatter.d.ts → formatter.d.ts} +0 -0
  141. /package/dist/lifecycle/{lifecycle-formatter.js → formatter.js} +0 -0
  142. /package/dist/{vcs → shared}/sanitize-utils.d.ts +0 -0
  143. /package/dist/{vcs → shared}/sanitize-utils.js +0 -0
@@ -1,6 +1,28 @@
1
- import type { SyncReport, RepoFileChanges, ReportFileChange } from "./types.js";
2
- export type { SyncReport, RepoFileChanges, ReportFileChange };
1
+ import type { MergeMode } from "../config/index.js";
2
+ import type { ActiveAction } from "../settings/index.js";
3
+ export interface ReportFileChange {
4
+ path: string;
5
+ action: ActiveAction;
6
+ diffLines?: string[];
7
+ }
8
+ export interface SyncReport {
9
+ repos: RepoFileChanges[];
10
+ totals: {
11
+ files: {
12
+ create: number;
13
+ update: number;
14
+ delete: number;
15
+ };
16
+ };
17
+ }
18
+ export interface RepoFileChanges {
19
+ repoName: string;
20
+ files: ReportFileChange[];
21
+ prUrl?: string;
22
+ mergeOutcome?: MergeMode;
23
+ error?: string;
24
+ }
3
25
  export declare function formatSyncReportCLI(report: SyncReport): string[];
4
26
  export declare function formatSyncReportMarkdown(report: SyncReport, dryRun: boolean): string;
5
- export declare function renderSyncLines(syncRepo: RepoFileChanges, diffLines: string[]): void;
27
+ export declare function renderSyncLines(syncRepo: RepoFileChanges): string[];
6
28
  export declare function writeSyncReportSummary(report: SyncReport, dryRun: boolean, summaryPath: string | undefined): void;
@@ -1,7 +1,7 @@
1
1
  import chalk from "chalk";
2
2
  import { writeGitHubStepSummary } from "./github-summary.js";
3
3
  import { formatCountEntry } from "./settings-report.js";
4
- import { formatDiffLine } from "../sync/index.js";
4
+ import { formatDiffLine } from "../shared/diff-format.js";
5
5
  function formatSyncSummary(totals) {
6
6
  const entry = formatCountEntry("file", "files", [
7
7
  { label: "to create", value: totals.files.create },
@@ -65,8 +65,7 @@ export function formatSyncReportMarkdown(report, dryRun) {
65
65
  }
66
66
  lines.push(`### ${repo.repoName}`);
67
67
  lines.push("");
68
- const diffLines = [];
69
- renderSyncLines(repo, diffLines);
68
+ const diffLines = renderSyncLines(repo);
70
69
  if (diffLines.length > 0) {
71
70
  lines.push("```diff");
72
71
  lines.push(...diffLines);
@@ -78,28 +77,29 @@ export function formatSyncReportMarkdown(report, dryRun) {
78
77
  lines.push(`**${formatSyncSummary(report.totals)}**`);
79
78
  return lines.join("\n");
80
79
  }
81
- export function renderSyncLines(syncRepo, diffLines) {
80
+ export function renderSyncLines(syncRepo) {
81
+ const lines = [];
82
82
  for (let i = 0; i < syncRepo.files.length; i++) {
83
83
  const file = syncRepo.files[i];
84
- // Blank line between files for readability
85
84
  if (i > 0)
86
- diffLines.push("");
85
+ lines.push("");
87
86
  if (file.action === "create") {
88
- diffLines.push(`+ ${file.path}`);
87
+ lines.push(`+ ${file.path}`);
89
88
  }
90
89
  else if (file.action === "update") {
91
- diffLines.push(`! ${file.path}`);
90
+ lines.push(`! ${file.path}`);
92
91
  }
93
92
  else if (file.action === "delete") {
94
- diffLines.push(`- ${file.path}`);
93
+ lines.push(`- ${file.path}`);
95
94
  }
96
95
  if (file.diffLines) {
97
- diffLines.push(...file.diffLines);
96
+ lines.push(...file.diffLines);
98
97
  }
99
98
  }
100
99
  if (syncRepo.error) {
101
- diffLines.push(`- Error: ${syncRepo.error}`);
100
+ lines.push(`- Error: ${syncRepo.error}`);
102
101
  }
102
+ return lines;
103
103
  }
104
104
  export function writeSyncReportSummary(report, dryRun, summaryPath) {
105
105
  const markdown = formatSyncReportMarkdown(report, dryRun);
@@ -33,11 +33,8 @@ interface SettingsGuards<TOptions extends BaseProcessorOptions, TResult extends
33
33
  * empty settings check, token resolution, and error wrapping.
34
34
  */
35
35
  export declare function withGitHubGuards<TOptions extends BaseProcessorOptions, TResult extends BaseProcessorResult>(repoConfig: RepoConfig, repoInfo: RepoInfo, options: TOptions, guards: SettingsGuards<TOptions, TResult>): Promise<TResult>;
36
- /** Common action literals shared by all settings processors. */
37
36
  export type SettingsAction = "create" | "update" | "delete" | "unchanged";
38
- /** Actions that represent an actual change (excludes "unchanged"). */
39
37
  export type ActiveAction = Exclude<SettingsAction, "unchanged">;
40
- /** Type predicate that narrows entries with an active (non-"unchanged") action. */
41
38
  export declare function isActiveAction<T extends {
42
39
  action: SettingsAction;
43
40
  }>(entry: T): entry is T & {
@@ -49,14 +46,28 @@ export interface ChangeCounts {
49
46
  delete: number;
50
47
  unchanged: number;
51
48
  }
52
- /**
53
- * Count actions from a diff result array.
54
- * Works with any change type that has an `action` field.
55
- */
56
49
  export declare function countActions(changes: ReadonlyArray<{
57
50
  action: SettingsAction;
58
51
  }>): ChangeCounts;
59
52
  export declare function formatChangeSummary(counts: ChangeCounts): string;
53
+ export interface PlanEntry {
54
+ property: string;
55
+ action: "create" | "update";
56
+ oldValue?: unknown;
57
+ newValue?: unknown;
58
+ }
59
+ export interface FormatChangeResult {
60
+ lines: string[];
61
+ entries: PlanEntry[];
62
+ creates: number;
63
+ updates: number;
64
+ }
65
+ export declare function formatChangeLines(changes: ReadonlyArray<{
66
+ property: string;
67
+ action: SettingsAction;
68
+ oldValue?: unknown;
69
+ newValue?: unknown;
70
+ }>, formatValue: (val: unknown) => string): FormatChangeResult;
60
71
  /**
61
72
  * Build a standardized dry-run result for settings processors.
62
73
  * Returns an intersection of BaseProcessorResult with the extra fields,
@@ -1,3 +1,4 @@
1
+ import chalk from "chalk";
1
2
  import { isGitHubRepo, getRepoDisplayName } from "../repo/index.js";
2
3
  import { toErrorMessage } from "../shared/type-guards.js";
3
4
  /**
@@ -43,14 +44,9 @@ export async function withGitHubGuards(repoConfig, repoInfo, options, guards) {
43
44
  });
44
45
  }
45
46
  }
46
- /** Type predicate that narrows entries with an active (non-"unchanged") action. */
47
47
  export function isActiveAction(entry) {
48
48
  return entry.action !== "unchanged";
49
49
  }
50
- /**
51
- * Count actions from a diff result array.
52
- * Works with any change type that has an `action` field.
53
- */
54
50
  export function countActions(changes) {
55
51
  const counts = {
56
52
  create: 0,
@@ -75,6 +71,31 @@ export function formatChangeSummary(counts) {
75
71
  parts.push(`${counts.unchanged} unchanged`);
76
72
  return parts.length > 0 ? parts.join(", ") : "no changes";
77
73
  }
74
+ export function formatChangeLines(changes, formatValue) {
75
+ const lines = [];
76
+ const entries = [];
77
+ const { create: creates, update: updates } = countActions(changes);
78
+ for (const change of changes) {
79
+ if (change.action === "create") {
80
+ lines.push(chalk.green(` + ${change.property}: ${formatValue(change.newValue)}`));
81
+ entries.push({
82
+ property: change.property,
83
+ action: "create",
84
+ newValue: change.newValue,
85
+ });
86
+ }
87
+ else if (change.action === "update") {
88
+ lines.push(chalk.yellow(` ~ ${change.property}: ${formatValue(change.oldValue)} → ${formatValue(change.newValue)}`));
89
+ entries.push({
90
+ property: change.property,
91
+ action: "update",
92
+ oldValue: change.oldValue,
93
+ newValue: change.newValue,
94
+ });
95
+ }
96
+ }
97
+ return { lines, entries, creates, updates };
98
+ }
78
99
  /**
79
100
  * Build a standardized dry-run result for settings processors.
80
101
  * Returns an intersection of BaseProcessorResult with the extra fields,
@@ -44,8 +44,8 @@ export function diffCodeScanning(current, desired) {
44
44
  }
45
45
  // languages: only diff if specified in desired (sorted comparison)
46
46
  if (desired.languages !== undefined) {
47
- const currentLangs = [...(current.languages ?? [])].sort();
48
- const desiredLangs = [...desired.languages].sort();
47
+ const currentLangs = [...(current.languages ?? [])].sort((a, b) => a.localeCompare(b));
48
+ const desiredLangs = [...desired.languages].sort((a, b) => a.localeCompare(b));
49
49
  const langsMatch = currentLangs.length === desiredLangs.length &&
50
50
  currentLangs.every((lang, i) => lang === desiredLangs[i]);
51
51
  if (!langsMatch) {
@@ -1,10 +1,6 @@
1
1
  import type { CodeScanningChange } from "./diff.js";
2
- export interface CodeScanningPlanEntry {
3
- property: string;
4
- action: "create" | "update";
5
- oldValue?: unknown;
6
- newValue?: unknown;
7
- }
2
+ import { type PlanEntry } from "../base-processor.js";
3
+ export type CodeScanningPlanEntry = PlanEntry;
8
4
  export interface CodeScanningPlanResult {
9
5
  lines: string[];
10
6
  creates: number;
@@ -1,6 +1,5 @@
1
- import chalk from "chalk";
2
1
  import { formatScalarValue } from "../../shared/string-utils.js";
3
- import { countActions } from "../base-processor.js";
2
+ import { formatChangeLines } from "../base-processor.js";
4
3
  function formatValue(val) {
5
4
  if (Array.isArray(val)) {
6
5
  return `[${val.join(", ")}]`;
@@ -11,27 +10,5 @@ function formatValue(val) {
11
10
  * Formats code scanning changes as Terraform-style plan output.
12
11
  */
13
12
  export function formatCodeScanningPlan(changes) {
14
- const lines = [];
15
- const entries = [];
16
- const { create: creates, update: updates } = countActions(changes);
17
- for (const change of changes) {
18
- if (change.action === "create") {
19
- lines.push(chalk.green(` + ${change.property}: ${formatValue(change.newValue)}`));
20
- entries.push({
21
- property: change.property,
22
- action: "create",
23
- newValue: change.newValue,
24
- });
25
- }
26
- else if (change.action === "update") {
27
- lines.push(chalk.yellow(` ~ ${change.property}: ${formatValue(change.oldValue)} → ${formatValue(change.newValue)}`));
28
- entries.push({
29
- property: change.property,
30
- action: "update",
31
- oldValue: change.oldValue,
32
- newValue: change.newValue,
33
- });
34
- }
35
- }
36
- return { lines, creates, updates, entries };
13
+ return formatChangeLines(changes, formatValue);
37
14
  }
@@ -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 { ICodeScanningStrategy, CurrentCodeScanningSettings } from "./types.js";
4
+ import type { ICodeScanningStrategy, CurrentCodeScanningSettings, CodeScanningUpdateParams } from "./types.js";
5
5
  interface GitHubCodeScanningStrategyOptions {
6
6
  retries?: number;
7
7
  cwd: string;
@@ -9,11 +9,7 @@ interface GitHubCodeScanningStrategyOptions {
9
9
  export declare class GitHubCodeScanningStrategy implements ICodeScanningStrategy {
10
10
  private api;
11
11
  constructor(executor: ICommandExecutor, options: GitHubCodeScanningStrategyOptions);
12
- getDefaultSetup(repoInfo: RepoInfo, options?: GhApiOptions): Promise<CurrentCodeScanningSettings>;
13
- updateDefaultSetup(repoInfo: RepoInfo, settings: {
14
- state: string;
15
- query_suite?: string;
16
- languages?: string[];
17
- }, options?: GhApiOptions): Promise<void>;
12
+ get(repoInfo: RepoInfo, options?: GhApiOptions): Promise<CurrentCodeScanningSettings>;
13
+ update(repoInfo: RepoInfo, settings: CodeScanningUpdateParams, options?: GhApiOptions): Promise<void>;
18
14
  }
19
15
  export {};
@@ -6,13 +6,13 @@ export class GitHubCodeScanningStrategy {
6
6
  constructor(executor, options) {
7
7
  this.api = new GhApiClient(executor, options.retries ?? 3, options.cwd);
8
8
  }
9
- async getDefaultSetup(repoInfo, options) {
9
+ async get(repoInfo, options) {
10
10
  assertGitHubRepo(repoInfo, "GitHub Code Scanning strategy");
11
11
  const endpoint = `/repos/${repoInfo.owner}/${repoInfo.repo}/code-scanning/default-setup`;
12
12
  const result = await this.api.call("GET", endpoint, { options });
13
13
  return parseApiJson(result, "code scanning default setup response");
14
14
  }
15
- async updateDefaultSetup(repoInfo, settings, options) {
15
+ async update(repoInfo, settings, options) {
16
16
  assertGitHubRepo(repoInfo, "GitHub Code Scanning strategy");
17
17
  const endpoint = `/repos/${repoInfo.owner}/${repoInfo.repo}/code-scanning/default-setup`;
18
18
  await this.api.call("PATCH", endpoint, { payload: settings, options });
@@ -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
  }
@@ -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
  */