@aspruyt/xfg 5.4.0 → 5.5.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 (91) hide show
  1. package/dist/{shared → cli}/branch-utils.js +1 -1
  2. package/dist/cli/settings-report-builder.d.ts +6 -1
  3. package/dist/cli/settings-report-builder.js +19 -0
  4. package/dist/cli/sync-command.js +38 -23
  5. package/dist/cli/sync-report-builder.d.ts +2 -1
  6. package/dist/cli/types.d.ts +5 -2
  7. package/dist/config/config-merger.d.ts +6 -0
  8. package/dist/config/config-merger.js +66 -0
  9. package/dist/config/index.d.ts +1 -1
  10. package/dist/config/loader.d.ts +2 -2
  11. package/dist/config/loader.js +50 -6
  12. package/dist/config/merge.js +11 -16
  13. package/dist/config/normalizer.js +29 -0
  14. package/dist/config/types.d.ts +12 -0
  15. package/dist/config/validator.js +104 -46
  16. package/dist/lifecycle/ado-migration-source.d.ts +1 -1
  17. package/dist/lifecycle/github-lifecycle-provider.d.ts +1 -1
  18. package/dist/lifecycle/index.d.ts +1 -1
  19. package/dist/lifecycle/lifecycle-helpers.d.ts +2 -1
  20. package/dist/lifecycle/lifecycle-helpers.js +1 -1
  21. package/dist/lifecycle/repo-lifecycle-factory.d.ts +1 -1
  22. package/dist/lifecycle/repo-lifecycle-manager.js +1 -1
  23. package/dist/output/types.d.ts +2 -1
  24. package/dist/settings/code-scanning/diff.d.ts +19 -0
  25. package/dist/settings/code-scanning/diff.js +75 -0
  26. package/dist/settings/code-scanning/formatter.d.ts +17 -0
  27. package/dist/settings/code-scanning/formatter.js +37 -0
  28. package/dist/settings/code-scanning/github-code-scanning-strategy.d.ts +19 -0
  29. package/dist/settings/code-scanning/github-code-scanning-strategy.js +20 -0
  30. package/dist/settings/code-scanning/index.d.ts +3 -0
  31. package/dist/settings/code-scanning/index.js +4 -0
  32. package/dist/settings/code-scanning/processor.d.ts +20 -0
  33. package/dist/settings/code-scanning/processor.js +81 -0
  34. package/dist/settings/code-scanning/types.d.ts +22 -0
  35. package/dist/settings/code-scanning/types.js +1 -0
  36. package/dist/settings/index.d.ts +1 -0
  37. package/dist/settings/index.js +2 -0
  38. package/dist/settings/labels/github-labels-strategy.d.ts +2 -2
  39. package/dist/settings/repo-settings/diff.d.ts +2 -1
  40. package/dist/settings/repo-settings/github-repo-settings-strategy.d.ts +1 -1
  41. package/dist/settings/repo-settings/processor.d.ts +4 -2
  42. package/dist/settings/repo-settings/processor.js +14 -16
  43. package/dist/settings/rulesets/github-ruleset-strategy.d.ts +2 -2
  44. package/dist/settings/rulesets/processor.d.ts +1 -1
  45. package/dist/shared/cleanup-utils.d.ts +8 -0
  46. package/dist/shared/cleanup-utils.js +15 -0
  47. package/dist/shared/gh-api-utils.d.ts +2 -2
  48. package/dist/shared/gh-api-utils.js +7 -4
  49. package/dist/shared/repo-detector.d.ts +1 -20
  50. package/dist/shared/repo-detector.js +3 -45
  51. package/dist/shared/repo-info-utils.d.ts +20 -0
  52. package/dist/shared/repo-info-utils.js +46 -0
  53. package/dist/shared/repo-metadata-provider.d.ts +27 -0
  54. package/dist/shared/repo-metadata-provider.js +20 -0
  55. package/dist/shared/retry-utils.d.ts +1 -1
  56. package/dist/shared/retry-utils.js +1 -1
  57. package/dist/shared/type-guards.d.ts +0 -8
  58. package/dist/shared/type-guards.js +0 -14
  59. package/dist/shared/xfg-template.js +2 -2
  60. package/dist/sync/auth-options-builder.d.ts +1 -1
  61. package/dist/sync/branch-manager.d.ts +3 -7
  62. package/dist/sync/commit-push-manager.d.ts +1 -1
  63. package/dist/sync/commit-push-manager.js +2 -2
  64. package/dist/sync/diff-utils.js +0 -7
  65. package/dist/sync/manifest.js +1 -8
  66. package/dist/sync/repository-processor.d.ts +1 -1
  67. package/dist/sync/repository-processor.js +1 -2
  68. package/dist/sync/repository-session.d.ts +2 -2
  69. package/dist/sync/repository-session.js +1 -1
  70. package/dist/sync/sync-workflow.d.ts +1 -1
  71. package/dist/sync/sync-workflow.js +1 -1
  72. package/dist/vcs/authenticated-git-ops.js +2 -2
  73. package/dist/vcs/azure-pr-strategy.d.ts +1 -1
  74. package/dist/vcs/azure-pr-strategy.js +2 -1
  75. package/dist/vcs/commit-strategy-selector.d.ts +2 -2
  76. package/dist/vcs/git-commit-strategy.d.ts +1 -1
  77. package/dist/vcs/git-ops.d.ts +1 -1
  78. package/dist/vcs/git-ops.js +1 -1
  79. package/dist/vcs/github-pr-strategy.js +3 -2
  80. package/dist/vcs/gitlab-pr-strategy.d.ts +1 -1
  81. package/dist/vcs/gitlab-pr-strategy.js +4 -3
  82. package/dist/vcs/graphql-commit-strategy.d.ts +1 -1
  83. package/dist/vcs/index.d.ts +2 -0
  84. package/dist/vcs/index.js +3 -0
  85. package/dist/vcs/pr-creator.d.ts +2 -2
  86. package/dist/vcs/pr-creator.js +1 -1
  87. package/dist/vcs/pr-strategy-factory.d.ts +2 -2
  88. package/dist/vcs/pr-strategy.d.ts +4 -2
  89. package/dist/vcs/pr-strategy.js +7 -2
  90. package/package.json +2 -1
  91. /package/dist/{shared → cli}/branch-utils.d.ts +0 -0
@@ -124,6 +124,8 @@ function buildRootSettingsContext(config) {
124
124
  ? Object.keys(config.settings.rulesets).filter((k) => k !== "inherit")
125
125
  : [],
126
126
  hasRepoSettings: config.settings?.repo !== undefined && config.settings.repo !== false,
127
+ hasCodeScanningSettings: config.settings?.codeScanning !== undefined &&
128
+ config.settings.codeScanning !== false,
127
129
  labelNames: config.settings?.labels
128
130
  ? Object.keys(config.settings.labels).filter((k) => k !== "inherit")
129
131
  : [],
@@ -191,6 +193,58 @@ function validateSettings(settings, context, rootCtx) {
191
193
  validateRepoSettings(settings.repo, context);
192
194
  }
193
195
  }
196
+ if (settings.codeScanning !== undefined) {
197
+ if (settings.codeScanning === false) {
198
+ if (!rootCtx) {
199
+ throw new ValidationError(`${context}: codeScanning: false is not valid at root level. Define codeScanning settings or remove the field.`);
200
+ }
201
+ // Per-repo level — check root has codeScanning settings to opt out of
202
+ if (!rootCtx.hasCodeScanningSettings) {
203
+ throw new ValidationError(`${context}: Cannot opt out of code scanning settings — not defined in root settings.codeScanning`);
204
+ }
205
+ }
206
+ else {
207
+ validateCodeScanningSettings(settings.codeScanning, `${context} codeScanning`);
208
+ }
209
+ }
210
+ }
211
+ const VALID_CODE_SCANNING_STATES = ["configured", "not-configured"];
212
+ const VALID_CODE_SCANNING_QUERY_SUITES = ["default", "extended"];
213
+ const VALID_CODE_SCANNING_LANGUAGES = [
214
+ "actions",
215
+ "c-cpp",
216
+ "csharp",
217
+ "go",
218
+ "java-kotlin",
219
+ "javascript-typescript",
220
+ "python",
221
+ "ruby",
222
+ "swift",
223
+ ];
224
+ function validateCodeScanningSettings(settings, context) {
225
+ if (!isPlainObject(settings)) {
226
+ throw new ValidationError(`${context}: must be an object`);
227
+ }
228
+ if (settings.state === undefined) {
229
+ throw new ValidationError(`${context}: state is required`);
230
+ }
231
+ if (!VALID_CODE_SCANNING_STATES.includes(settings.state)) {
232
+ throw new ValidationError(`${context}: state must be one of: ${VALID_CODE_SCANNING_STATES.join(", ")}`);
233
+ }
234
+ if (settings.querySuite !== undefined &&
235
+ !VALID_CODE_SCANNING_QUERY_SUITES.includes(settings.querySuite)) {
236
+ throw new ValidationError(`${context}: querySuite must be one of: ${VALID_CODE_SCANNING_QUERY_SUITES.join(", ")}`);
237
+ }
238
+ if (settings.languages !== undefined) {
239
+ if (!Array.isArray(settings.languages)) {
240
+ throw new ValidationError(`${context}: languages must be an array`);
241
+ }
242
+ for (const lang of settings.languages) {
243
+ if (!VALID_CODE_SCANNING_LANGUAGES.includes(lang)) {
244
+ throw new ValidationError(`${context}: invalid language "${lang}". Valid languages: ${VALID_CODE_SCANNING_LANGUAGES.join(", ")}`);
245
+ }
246
+ }
247
+ }
194
248
  }
195
249
  function validateConfigId(config) {
196
250
  if (!config.id || typeof config.id !== "string") {
@@ -362,6 +416,24 @@ function validateGroups(config) {
362
416
  // Validate no circular extends after individual validation
363
417
  validateNoCircularExtends(config.groups);
364
418
  }
419
+ function validateGroupRefArray(arr, fieldName, ctx, groupNames) {
420
+ if (!Array.isArray(arr) || arr.length === 0) {
421
+ throw new ValidationError(`${ctx}: '${fieldName}' must be a non-empty array of strings`);
422
+ }
423
+ const seen = new Set();
424
+ for (const name of arr) {
425
+ if (typeof name !== "string") {
426
+ throw new ValidationError(`${ctx}: '${fieldName}' entries must be strings`);
427
+ }
428
+ if (!groupNames.includes(name)) {
429
+ throw new ValidationError(`${ctx}: group '${name}' in ${fieldName} is not defined in root 'groups'`);
430
+ }
431
+ if (seen.has(name)) {
432
+ throw new ValidationError(`${ctx}: duplicate group '${name}' in ${fieldName}`);
433
+ }
434
+ seen.add(name);
435
+ }
436
+ }
365
437
  function validateConditionalGroups(config) {
366
438
  if (config.conditionalGroups === undefined)
367
439
  return;
@@ -382,40 +454,10 @@ function validateConditionalGroups(config) {
382
454
  throw new ValidationError(`${ctx}: 'when' must have at least one of 'allOf' or 'anyOf'`);
383
455
  }
384
456
  if (allOf !== undefined) {
385
- if (!Array.isArray(allOf) || allOf.length === 0) {
386
- throw new ValidationError(`${ctx}: 'allOf' must be a non-empty array of strings`);
387
- }
388
- const seen = new Set();
389
- for (const name of allOf) {
390
- if (typeof name !== "string") {
391
- throw new ValidationError(`${ctx}: 'allOf' entries must be strings`);
392
- }
393
- if (!groupNames.includes(name)) {
394
- throw new ValidationError(`${ctx}: group '${name}' in allOf is not defined in root 'groups'`);
395
- }
396
- if (seen.has(name)) {
397
- throw new ValidationError(`${ctx}: duplicate group '${name}' in allOf`);
398
- }
399
- seen.add(name);
400
- }
457
+ validateGroupRefArray(allOf, "allOf", ctx, groupNames);
401
458
  }
402
459
  if (anyOf !== undefined) {
403
- if (!Array.isArray(anyOf) || anyOf.length === 0) {
404
- throw new ValidationError(`${ctx}: 'anyOf' must be a non-empty array of strings`);
405
- }
406
- const seen = new Set();
407
- for (const name of anyOf) {
408
- if (typeof name !== "string") {
409
- throw new ValidationError(`${ctx}: 'anyOf' entries must be strings`);
410
- }
411
- if (!groupNames.includes(name)) {
412
- throw new ValidationError(`${ctx}: group '${name}' in anyOf is not defined in root 'groups'`);
413
- }
414
- if (seen.has(name)) {
415
- throw new ValidationError(`${ctx}: duplicate group '${name}' in anyOf`);
416
- }
417
- seen.add(name);
418
- }
460
+ validateGroupRefArray(anyOf, "anyOf", ctx, groupNames);
419
461
  }
420
462
  // Validate files
421
463
  if (entry.files) {
@@ -565,6 +607,10 @@ function validateRepoSettingsEntry(config, repo, repoLabel) {
565
607
  group.settings.repo !== false) {
566
608
  rootCtx.hasRepoSettings = true;
567
609
  }
610
+ if (group?.settings?.codeScanning !== undefined &&
611
+ group.settings.codeScanning !== false) {
612
+ rootCtx.hasCodeScanningSettings = true;
613
+ }
568
614
  }
569
615
  }
570
616
  if (config.conditionalGroups) {
@@ -584,6 +630,10 @@ function validateRepoSettingsEntry(config, repo, repoLabel) {
584
630
  if (cg.settings?.repo !== undefined && cg.settings.repo !== false) {
585
631
  rootCtx.hasRepoSettings = true;
586
632
  }
633
+ if (cg.settings?.codeScanning !== undefined &&
634
+ cg.settings.codeScanning !== false) {
635
+ rootCtx.hasCodeScanningSettings = true;
636
+ }
587
637
  }
588
638
  }
589
639
  validateSettings(repo.settings, `Repo ${repoLabel}`, rootCtx);
@@ -600,6 +650,19 @@ function hasGroupFiles(config) {
600
650
  Object.values(config.groups).some((g) => g.files &&
601
651
  Object.keys(g.files).filter((k) => k !== "inherit" && g.files[k] !== false).length > 0));
602
652
  }
653
+ function hasConditionalGroupFiles(config) {
654
+ return (Array.isArray(config.conditionalGroups) &&
655
+ config.conditionalGroups.some((cg) => cg.files &&
656
+ Object.keys(cg.files).filter((k) => k !== "inherit" && cg.files[k] !== false).length > 0));
657
+ }
658
+ function hasConditionalGroupSettings(config, predicate) {
659
+ return (Array.isArray(config.conditionalGroups) &&
660
+ config.conditionalGroups.some((cg) => cg.settings && predicate(cg.settings)));
661
+ }
662
+ function hasConditionalGroupPR(config) {
663
+ return (Array.isArray(config.conditionalGroups) &&
664
+ config.conditionalGroups.some((cg) => cg.prOptions && isPlainObject(cg.prOptions)));
665
+ }
603
666
  /**
604
667
  * Validates raw config structure before normalization.
605
668
  * @throws ValidationError if validation fails
@@ -611,13 +674,9 @@ export function validateRawConfig(config) {
611
674
  const hasGrpFiles = hasGroupFiles(config);
612
675
  const hasGrpSettings = isPlainObject(config.groups) &&
613
676
  Object.values(config.groups).some((g) => g.settings && isPlainObject(g.settings));
614
- const hasCondGrpFiles = Array.isArray(config.conditionalGroups) &&
615
- config.conditionalGroups.some((cg) => cg.files &&
616
- Object.keys(cg.files).filter((k) => k !== "inherit" && cg.files[k] !== false).length > 0);
617
- const hasCondGrpSettings = Array.isArray(config.conditionalGroups) &&
618
- config.conditionalGroups.some((cg) => cg.settings && isPlainObject(cg.settings));
619
- const hasCondGrpPR = Array.isArray(config.conditionalGroups) &&
620
- config.conditionalGroups.some((cg) => cg.prOptions && isPlainObject(cg.prOptions));
677
+ const hasCondGrpFiles = hasConditionalGroupFiles(config);
678
+ const hasCondGrpSettings = hasConditionalGroupSettings(config, isPlainObject);
679
+ const hasCondGrpPR = hasConditionalGroupPR(config);
621
680
  if (!hasFiles &&
622
681
  !hasSettings &&
623
682
  !hasGrpFiles &&
@@ -659,13 +718,9 @@ export function validateForSync(config) {
659
718
  const hasRepoSettings = config.repos.some((repo) => hasActionableSettings(repo.settings));
660
719
  const hasGroupSettings = isPlainObject(config.groups) &&
661
720
  Object.values(config.groups).some((g) => g.settings && hasActionableSettings(g.settings));
662
- const hasCondGrpFiles = Array.isArray(config.conditionalGroups) &&
663
- config.conditionalGroups.some((cg) => cg.files &&
664
- Object.keys(cg.files).filter((k) => k !== "inherit" && cg.files[k] !== false).length > 0);
665
- const hasCondGrpSettings = Array.isArray(config.conditionalGroups) &&
666
- config.conditionalGroups.some((cg) => cg.settings && hasActionableSettings(cg.settings));
667
- const hasCondGrpPR = Array.isArray(config.conditionalGroups) &&
668
- config.conditionalGroups.some((cg) => cg.prOptions && isPlainObject(cg.prOptions));
721
+ const hasCondGrpFiles = hasConditionalGroupFiles(config);
722
+ const hasCondGrpSettings = hasConditionalGroupSettings(config, hasActionableSettings);
723
+ const hasCondGrpPR = hasConditionalGroupPR(config);
669
724
  if (!hasRootFiles &&
670
725
  !hasGrpFiles &&
671
726
  !hasSettings &&
@@ -695,5 +750,8 @@ export function hasActionableSettings(settings) {
695
750
  Object.keys(settings.labels).filter((k) => k !== "inherit").length > 0) {
696
751
  return true;
697
752
  }
753
+ if (settings.codeScanning && typeof settings.codeScanning === "object") {
754
+ return true;
755
+ }
698
756
  return false;
699
757
  }
@@ -1,4 +1,4 @@
1
- import { ICommandExecutor } from "../shared/command-executor.js";
1
+ import type { ICommandExecutor } from "../shared/command-executor.js";
2
2
  import { type RepoInfo } from "../shared/repo-detector.js";
3
3
  import type { IMigrationSource, LifecyclePlatform } from "./types.js";
4
4
  /**
@@ -1,4 +1,4 @@
1
- import { ICommandExecutor } from "../shared/command-executor.js";
1
+ import type { ICommandExecutor } from "../shared/command-executor.js";
2
2
  import { type RepoInfo } from "../shared/repo-detector.js";
3
3
  import type { DebugWarnLog } from "../shared/logger.js";
4
4
  import type { IRepoLifecycleProvider, LifecyclePlatform, CreateRepoSettings } from "./types.js";
@@ -1,3 +1,3 @@
1
1
  export type { IRepoLifecycleManager } from "./types.js";
2
2
  export { RepoLifecycleManager } from "./repo-lifecycle-manager.js";
3
- export { runLifecycleCheck, toCreateRepoSettings, } from "./lifecycle-helpers.js";
3
+ export { runLifecycleCheck, toCreateRepoSettings, type LifecycleCheckResult, } from "./lifecycle-helpers.js";
@@ -20,9 +20,10 @@ interface LifecycleCheckOptions {
20
20
  * Extracts only the fields relevant for repo creation.
21
21
  */
22
22
  export declare function toCreateRepoSettings(repo: GitHubRepoSettings | undefined): CreateRepoSettings | undefined;
23
- interface LifecycleCheckResult {
23
+ export interface LifecycleCheckResult {
24
24
  lifecycleResult: LifecycleResult;
25
25
  outputLines: string[];
26
+ createSettings: CreateRepoSettings | undefined;
26
27
  }
27
28
  /**
28
29
  * Run lifecycle check for a single repo.
@@ -45,5 +45,5 @@ export async function runLifecycleCheck(repoConfig, repoInfo, options) {
45
45
  }
46
46
  : undefined,
47
47
  });
48
- return { lifecycleResult, outputLines };
48
+ return { lifecycleResult, outputLines, createSettings };
49
49
  }
@@ -1,4 +1,4 @@
1
- import { ICommandExecutor } from "../shared/command-executor.js";
1
+ import type { ICommandExecutor } from "../shared/command-executor.js";
2
2
  import type { DebugWarnLog } from "../shared/logger.js";
3
3
  import type { IRepoLifecycleFactory, IRepoLifecycleProvider, IMigrationSource, LifecyclePlatform } from "./types.js";
4
4
  export declare class RepoLifecycleFactory implements IRepoLifecycleFactory {
@@ -1,7 +1,7 @@
1
1
  import { join } from "node:path";
2
2
  import { rm } from "node:fs/promises";
3
3
  import { parseGitUrl } from "../shared/repo-detector.js";
4
- import { safeCleanup } from "../shared/type-guards.js";
4
+ import { safeCleanup } from "../shared/cleanup-utils.js";
5
5
  import { LifecycleError } from "../shared/errors.js";
6
6
  import { NO_OP_DEBUG_LOG } from "../shared/logger.js";
7
7
  import { RepoLifecycleFactory } from "./repo-lifecycle-factory.js";
@@ -1,3 +1,4 @@
1
+ import type { MergeMode } from "../config/index.js";
1
2
  import type { FileChangeDetail } from "../sync/index.js";
2
3
  export type ReportFileChange = FileChangeDetail;
3
4
  export interface SyncReport {
@@ -14,6 +15,6 @@ export interface RepoFileChanges {
14
15
  repoName: string;
15
16
  files: ReportFileChange[];
16
17
  prUrl?: string;
17
- mergeOutcome?: "manual" | "auto" | "force" | "direct";
18
+ mergeOutcome?: MergeMode;
18
19
  error?: string;
19
20
  }
@@ -0,0 +1,19 @@
1
+ import type { CodeScanningSettings } from "../../config/index.js";
2
+ import type { CurrentCodeScanningSettings } from "./types.js";
3
+ import type { SettingsAction } from "../base-processor.js";
4
+ export interface CodeScanningChange {
5
+ property: "state" | "querySuite" | "languages";
6
+ action: SettingsAction;
7
+ oldValue?: unknown;
8
+ newValue?: unknown;
9
+ }
10
+ /**
11
+ * Compares current code scanning default setup with desired settings.
12
+ * Only compares properties that are explicitly set in desired.
13
+ * Languages are compared as sorted arrays (order doesn't matter).
14
+ */
15
+ export declare function diffCodeScanning(current: CurrentCodeScanningSettings, desired: CodeScanningSettings): CodeScanningChange[];
16
+ /**
17
+ * Checks if there are any actual changes to apply.
18
+ */
19
+ export declare function hasCodeScanningChanges(changes: CodeScanningChange[]): boolean;
@@ -0,0 +1,75 @@
1
+ /**
2
+ * Compares current code scanning default setup with desired settings.
3
+ * Only compares properties that are explicitly set in desired.
4
+ * Languages are compared as sorted arrays (order doesn't matter).
5
+ */
6
+ export function diffCodeScanning(current, desired) {
7
+ const changes = [];
8
+ // state is always compared (required field)
9
+ if (current.state !== desired.state) {
10
+ changes.push({
11
+ property: "state",
12
+ action: current.state === undefined ? "create" : "update",
13
+ oldValue: current.state,
14
+ newValue: desired.state,
15
+ });
16
+ }
17
+ else {
18
+ changes.push({
19
+ property: "state",
20
+ action: "unchanged",
21
+ oldValue: current.state,
22
+ newValue: desired.state,
23
+ });
24
+ }
25
+ // querySuite: only diff if specified in desired
26
+ if (desired.querySuite !== undefined) {
27
+ const currentQS = current.query_suite;
28
+ if (currentQS !== desired.querySuite) {
29
+ changes.push({
30
+ property: "querySuite",
31
+ action: currentQS === undefined ? "create" : "update",
32
+ oldValue: currentQS,
33
+ newValue: desired.querySuite,
34
+ });
35
+ }
36
+ else {
37
+ changes.push({
38
+ property: "querySuite",
39
+ action: "unchanged",
40
+ oldValue: currentQS,
41
+ newValue: desired.querySuite,
42
+ });
43
+ }
44
+ }
45
+ // languages: only diff if specified in desired (sorted comparison)
46
+ if (desired.languages !== undefined) {
47
+ const currentLangs = [...(current.languages ?? [])].sort();
48
+ const desiredLangs = [...desired.languages].sort();
49
+ const langsMatch = currentLangs.length === desiredLangs.length &&
50
+ currentLangs.every((lang, i) => lang === desiredLangs[i]);
51
+ if (!langsMatch) {
52
+ changes.push({
53
+ property: "languages",
54
+ action: current.languages === undefined ? "create" : "update",
55
+ oldValue: current.languages,
56
+ newValue: desired.languages,
57
+ });
58
+ }
59
+ else {
60
+ changes.push({
61
+ property: "languages",
62
+ action: "unchanged",
63
+ oldValue: current.languages,
64
+ newValue: desired.languages,
65
+ });
66
+ }
67
+ }
68
+ return changes;
69
+ }
70
+ /**
71
+ * Checks if there are any actual changes to apply.
72
+ */
73
+ export function hasCodeScanningChanges(changes) {
74
+ return changes.some((c) => c.action !== "unchanged");
75
+ }
@@ -0,0 +1,17 @@
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
+ }
8
+ export interface CodeScanningPlanResult {
9
+ lines: string[];
10
+ creates: number;
11
+ updates: number;
12
+ entries: CodeScanningPlanEntry[];
13
+ }
14
+ /**
15
+ * Formats code scanning changes as Terraform-style plan output.
16
+ */
17
+ export declare function formatCodeScanningPlan(changes: CodeScanningChange[]): CodeScanningPlanResult;
@@ -0,0 +1,37 @@
1
+ import chalk from "chalk";
2
+ import { formatScalarValue } from "../../shared/string-utils.js";
3
+ import { countActions } from "../base-processor.js";
4
+ function formatValue(val) {
5
+ if (Array.isArray(val)) {
6
+ return `[${val.join(", ")}]`;
7
+ }
8
+ return formatScalarValue(val) ?? String(val);
9
+ }
10
+ /**
11
+ * Formats code scanning changes as Terraform-style plan output.
12
+ */
13
+ 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 };
37
+ }
@@ -0,0 +1,19 @@
1
+ import type { ICommandExecutor } from "../../shared/command-executor.js";
2
+ import { type RepoInfo } from "../../shared/repo-detector.js";
3
+ import { type GhApiOptions } from "../../shared/gh-api-utils.js";
4
+ import type { ICodeScanningStrategy, CurrentCodeScanningSettings } from "./types.js";
5
+ interface GitHubCodeScanningStrategyOptions {
6
+ retries?: number;
7
+ cwd: string;
8
+ }
9
+ export declare class GitHubCodeScanningStrategy implements ICodeScanningStrategy {
10
+ private api;
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>;
18
+ }
19
+ export {};
@@ -0,0 +1,20 @@
1
+ import { assertGitHubRepo } from "../../shared/repo-detector.js";
2
+ import { GhApiClient } from "../../shared/gh-api-utils.js";
3
+ import { parseApiJson } from "../../shared/json-utils.js";
4
+ export class GitHubCodeScanningStrategy {
5
+ api;
6
+ constructor(executor, options) {
7
+ this.api = new GhApiClient(executor, options.retries ?? 3, options.cwd);
8
+ }
9
+ async getDefaultSetup(repoInfo, options) {
10
+ assertGitHubRepo(repoInfo, "GitHub Code Scanning strategy");
11
+ const endpoint = `/repos/${repoInfo.owner}/${repoInfo.repo}/code-scanning/default-setup`;
12
+ const result = await this.api.call("GET", endpoint, { options });
13
+ return parseApiJson(result, "code scanning default setup response");
14
+ }
15
+ async updateDefaultSetup(repoInfo, settings, options) {
16
+ assertGitHubRepo(repoInfo, "GitHub Code Scanning strategy");
17
+ const endpoint = `/repos/${repoInfo.owner}/${repoInfo.repo}/code-scanning/default-setup`;
18
+ await this.api.call("PATCH", endpoint, { payload: settings, options });
19
+ }
20
+ }
@@ -0,0 +1,3 @@
1
+ export { type CodeScanningPlanEntry } from "./formatter.js";
2
+ export { CodeScanningProcessor, type ICodeScanningProcessor, } from "./processor.js";
3
+ export { GitHubCodeScanningStrategy } from "./github-code-scanning-strategy.js";
@@ -0,0 +1,4 @@
1
+ // Processor
2
+ export { CodeScanningProcessor, } from "./processor.js";
3
+ // Strategy
4
+ export { GitHubCodeScanningStrategy } from "./github-code-scanning-strategy.js";
@@ -0,0 +1,20 @@
1
+ import type { RepoConfig } from "../../config/index.js";
2
+ import type { RepoInfo } from "../../shared/repo-detector.js";
3
+ import type { ICodeScanningStrategy } from "./types.js";
4
+ import type { IRepoMetadataProvider } from "../../shared/repo-metadata-provider.js";
5
+ import { type CodeScanningPlanResult } from "./formatter.js";
6
+ import { type BaseProcessorOptions, type BaseProcessorResult, type ISettingsProcessor, type ChangeCounts } from "../base-processor.js";
7
+ export type ICodeScanningProcessor = ISettingsProcessor<CodeScanningProcessorOptions, CodeScanningProcessorResult>;
8
+ export type CodeScanningProcessorOptions = BaseProcessorOptions;
9
+ export interface CodeScanningProcessorResult extends BaseProcessorResult {
10
+ changes?: ChangeCounts;
11
+ planOutput?: CodeScanningPlanResult;
12
+ }
13
+ export declare class CodeScanningProcessor implements ICodeScanningProcessor {
14
+ private readonly strategy;
15
+ private readonly metadataProvider;
16
+ constructor(strategy: ICodeScanningStrategy, metadataProvider: IRepoMetadataProvider);
17
+ process(repoConfig: RepoConfig, repoInfo: RepoInfo, options: CodeScanningProcessorOptions): Promise<CodeScanningProcessorResult>;
18
+ private applySettings;
19
+ private validateGHAS;
20
+ }
@@ -0,0 +1,81 @@
1
+ import { diffCodeScanning, hasCodeScanningChanges } from "./diff.js";
2
+ import { formatCodeScanningPlan, } from "./formatter.js";
3
+ import { withGitHubGuards, countActions, buildDryRunResult, buildApplyResult, } from "../base-processor.js";
4
+ export class CodeScanningProcessor {
5
+ strategy;
6
+ metadataProvider;
7
+ constructor(strategy, metadataProvider) {
8
+ this.strategy = strategy;
9
+ this.metadataProvider = metadataProvider;
10
+ }
11
+ async process(repoConfig, repoInfo, options) {
12
+ return withGitHubGuards(repoConfig, repoInfo, options, {
13
+ hasDesiredSettings: (rc) => {
14
+ const cs = rc.settings?.codeScanning;
15
+ return !!cs && typeof cs === "object";
16
+ },
17
+ emptySettingsMessage: "No code scanning settings configured",
18
+ applySettings: (githubRepo, rc, opts, token, repoName) => this.applySettings(githubRepo, rc, opts, token, repoName),
19
+ });
20
+ }
21
+ async applySettings(githubRepo, repoConfig, options, effectiveToken, repoName) {
22
+ const { dryRun } = options;
23
+ const desiredSettings = repoConfig.settings
24
+ .codeScanning;
25
+ const strategyOptions = { token: effectiveToken, host: githubRepo.host };
26
+ // Validate GHAS availability for private repos
27
+ const metadata = await this.metadataProvider.getMetadata(githubRepo, strategyOptions);
28
+ const validationError = this.validateGHAS(desiredSettings, metadata);
29
+ if (validationError) {
30
+ return {
31
+ success: false,
32
+ repoName,
33
+ message: `Failed: ${validationError}`,
34
+ };
35
+ }
36
+ // Fetch current settings
37
+ const currentSettings = await this.strategy.getDefaultSetup(githubRepo, strategyOptions);
38
+ // Compute diff
39
+ const changes = diffCodeScanning(currentSettings, desiredSettings);
40
+ const changeCounts = countActions(changes);
41
+ if (!hasCodeScanningChanges(changes)) {
42
+ return {
43
+ success: true,
44
+ repoName,
45
+ message: "No changes needed",
46
+ changes: changeCounts,
47
+ };
48
+ }
49
+ // Format plan output
50
+ const planOutput = formatCodeScanningPlan(changes);
51
+ if (dryRun) {
52
+ return buildDryRunResult(repoName, changeCounts, { planOutput });
53
+ }
54
+ // Build API payload from desired settings
55
+ const payload = {
56
+ state: desiredSettings.state,
57
+ };
58
+ if (desiredSettings.querySuite !== undefined) {
59
+ payload.query_suite = desiredSettings.querySuite;
60
+ }
61
+ if (desiredSettings.languages !== undefined) {
62
+ payload.languages = desiredSettings.languages;
63
+ }
64
+ await this.strategy.updateDefaultSetup(githubRepo, payload, strategyOptions);
65
+ const appliedCount = changes.filter((c) => c.action !== "unchanged").length;
66
+ return buildApplyResult(repoName, changeCounts, appliedCount, {
67
+ planOutput,
68
+ });
69
+ }
70
+ validateGHAS(desired, metadata) {
71
+ if (desired.state !== "configured")
72
+ return undefined;
73
+ const isPublic = metadata.visibility === "public";
74
+ if (isPublic)
75
+ return undefined;
76
+ if (!metadata.hasGHAS) {
77
+ return "Code scanning default setup requires GitHub Advanced Security (not available for this repository)";
78
+ }
79
+ return undefined;
80
+ }
81
+ }
@@ -0,0 +1,22 @@
1
+ import type { RepoInfo } from "../../shared/repo-detector.js";
2
+ import type { GhApiOptions } from "../../shared/gh-api-utils.js";
3
+ /**
4
+ * Current code scanning default setup state from GitHub API.
5
+ */
6
+ export interface CurrentCodeScanningSettings {
7
+ state: "configured" | "not-configured";
8
+ query_suite?: "default" | "extended";
9
+ languages?: string[];
10
+ }
11
+ /**
12
+ * Strategy interface for code scanning default setup operations.
13
+ * Abstracts the GitHub API calls for testability.
14
+ */
15
+ 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
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -2,3 +2,4 @@ export { type BaseProcessorResult, type ISettingsProcessor, countActions, isActi
2
2
  export { type PropertyDiff, type RulesetPlanEntry, RulesetProcessor, type IRulesetProcessor, GitHubRulesetStrategy, } from "./rulesets/index.js";
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
+ export { type CodeScanningPlanEntry, CodeScanningProcessor, type ICodeScanningProcessor, GitHubCodeScanningStrategy, } from "./code-scanning/index.js";
@@ -6,3 +6,5 @@ export { RulesetProcessor, GitHubRulesetStrategy, } from "./rulesets/index.js";
6
6
  export { RepoSettingsProcessor, GitHubRepoSettingsStrategy, } from "./repo-settings/index.js";
7
7
  // Labels
8
8
  export { LabelsProcessor, GitHubLabelsStrategy, } from "./labels/index.js";
9
+ // Code scanning
10
+ export { CodeScanningProcessor, GitHubCodeScanningStrategy, } from "./code-scanning/index.js";
@@ -1,5 +1,5 @@
1
- import { ICommandExecutor } from "../../shared/command-executor.js";
2
- import { RepoInfo } from "../../shared/repo-detector.js";
1
+ import type { ICommandExecutor } from "../../shared/command-executor.js";
2
+ import { type RepoInfo } from "../../shared/repo-detector.js";
3
3
  import { type GhApiOptions } from "../../shared/gh-api-utils.js";
4
4
  import type { ILabelsStrategy, GitHubLabel } from "./types.js";
5
5
  interface GitHubLabelsStrategyOptions {