@aspruyt/xfg 5.4.0 → 5.6.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 +36 -5
  14. package/dist/config/types.d.ts +14 -0
  15. package/dist/config/validator.js +125 -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;
@@ -377,44 +449,35 @@ function validateConditionalGroups(config) {
377
449
  if (!entry.when || !isPlainObject(entry.when)) {
378
450
  throw new ValidationError(`${ctx}: 'when' is required and must be an object`);
379
451
  }
380
- const { allOf, anyOf } = entry.when;
381
- if (!allOf && !anyOf) {
382
- throw new ValidationError(`${ctx}: 'when' must have at least one of 'allOf' or 'anyOf'`);
452
+ const { allOf, anyOf, noneOf } = entry.when;
453
+ if (!allOf && !anyOf && !noneOf) {
454
+ throw new ValidationError(`${ctx}: 'when' must have at least one of 'allOf', 'anyOf', or 'noneOf'`);
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`);
460
+ validateGroupRefArray(anyOf, "anyOf", ctx, groupNames);
461
+ }
462
+ if (noneOf !== undefined) {
463
+ validateGroupRefArray(noneOf, "noneOf", ctx, groupNames);
464
+ }
465
+ // Cross-operator overlap: noneOf must not share groups with allOf or anyOf
466
+ if (noneOf) {
467
+ const noneOfSet = new Set(noneOf);
468
+ if (allOf) {
469
+ for (const g of allOf) {
470
+ if (noneOfSet.has(g)) {
471
+ throw new ValidationError(`${ctx}: noneOf group '${g}' overlaps with allOf (contradictory condition)`);
472
+ }
410
473
  }
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`);
474
+ }
475
+ if (anyOf) {
476
+ for (const g of anyOf) {
477
+ if (noneOfSet.has(g)) {
478
+ throw new ValidationError(`${ctx}: noneOf group '${g}' overlaps with anyOf (contradictory condition)`);
479
+ }
416
480
  }
417
- seen.add(name);
418
481
  }
419
482
  }
420
483
  // Validate files
@@ -565,6 +628,10 @@ function validateRepoSettingsEntry(config, repo, repoLabel) {
565
628
  group.settings.repo !== false) {
566
629
  rootCtx.hasRepoSettings = true;
567
630
  }
631
+ if (group?.settings?.codeScanning !== undefined &&
632
+ group.settings.codeScanning !== false) {
633
+ rootCtx.hasCodeScanningSettings = true;
634
+ }
568
635
  }
569
636
  }
570
637
  if (config.conditionalGroups) {
@@ -584,6 +651,10 @@ function validateRepoSettingsEntry(config, repo, repoLabel) {
584
651
  if (cg.settings?.repo !== undefined && cg.settings.repo !== false) {
585
652
  rootCtx.hasRepoSettings = true;
586
653
  }
654
+ if (cg.settings?.codeScanning !== undefined &&
655
+ cg.settings.codeScanning !== false) {
656
+ rootCtx.hasCodeScanningSettings = true;
657
+ }
587
658
  }
588
659
  }
589
660
  validateSettings(repo.settings, `Repo ${repoLabel}`, rootCtx);
@@ -600,6 +671,19 @@ function hasGroupFiles(config) {
600
671
  Object.values(config.groups).some((g) => g.files &&
601
672
  Object.keys(g.files).filter((k) => k !== "inherit" && g.files[k] !== false).length > 0));
602
673
  }
674
+ function hasConditionalGroupFiles(config) {
675
+ return (Array.isArray(config.conditionalGroups) &&
676
+ config.conditionalGroups.some((cg) => cg.files &&
677
+ Object.keys(cg.files).filter((k) => k !== "inherit" && cg.files[k] !== false).length > 0));
678
+ }
679
+ function hasConditionalGroupSettings(config, predicate) {
680
+ return (Array.isArray(config.conditionalGroups) &&
681
+ config.conditionalGroups.some((cg) => cg.settings && predicate(cg.settings)));
682
+ }
683
+ function hasConditionalGroupPR(config) {
684
+ return (Array.isArray(config.conditionalGroups) &&
685
+ config.conditionalGroups.some((cg) => cg.prOptions && isPlainObject(cg.prOptions)));
686
+ }
603
687
  /**
604
688
  * Validates raw config structure before normalization.
605
689
  * @throws ValidationError if validation fails
@@ -611,13 +695,9 @@ export function validateRawConfig(config) {
611
695
  const hasGrpFiles = hasGroupFiles(config);
612
696
  const hasGrpSettings = isPlainObject(config.groups) &&
613
697
  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));
698
+ const hasCondGrpFiles = hasConditionalGroupFiles(config);
699
+ const hasCondGrpSettings = hasConditionalGroupSettings(config, isPlainObject);
700
+ const hasCondGrpPR = hasConditionalGroupPR(config);
621
701
  if (!hasFiles &&
622
702
  !hasSettings &&
623
703
  !hasGrpFiles &&
@@ -659,13 +739,9 @@ export function validateForSync(config) {
659
739
  const hasRepoSettings = config.repos.some((repo) => hasActionableSettings(repo.settings));
660
740
  const hasGroupSettings = isPlainObject(config.groups) &&
661
741
  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));
742
+ const hasCondGrpFiles = hasConditionalGroupFiles(config);
743
+ const hasCondGrpSettings = hasConditionalGroupSettings(config, hasActionableSettings);
744
+ const hasCondGrpPR = hasConditionalGroupPR(config);
669
745
  if (!hasRootFiles &&
670
746
  !hasGrpFiles &&
671
747
  !hasSettings &&
@@ -695,5 +771,8 @@ export function hasActionableSettings(settings) {
695
771
  Object.keys(settings.labels).filter((k) => k !== "inherit").length > 0) {
696
772
  return true;
697
773
  }
774
+ if (settings.codeScanning && typeof settings.codeScanning === "object") {
775
+ return true;
776
+ }
698
777
  return false;
699
778
  }
@@ -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 {};