@aspruyt/xfg 5.3.1 → 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 (93) 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.d.ts +4 -0
  13. package/dist/config/merge.js +47 -23
  14. package/dist/config/normalizer.js +31 -1
  15. package/dist/config/types.d.ts +12 -0
  16. package/dist/config/validator.js +104 -46
  17. package/dist/config/validators/ruleset-validator.js +46 -16
  18. package/dist/lifecycle/ado-migration-source.d.ts +1 -1
  19. package/dist/lifecycle/github-lifecycle-provider.d.ts +1 -1
  20. package/dist/lifecycle/index.d.ts +1 -1
  21. package/dist/lifecycle/lifecycle-helpers.d.ts +2 -1
  22. package/dist/lifecycle/lifecycle-helpers.js +1 -1
  23. package/dist/lifecycle/repo-lifecycle-factory.d.ts +1 -1
  24. package/dist/lifecycle/repo-lifecycle-manager.js +1 -1
  25. package/dist/output/types.d.ts +2 -1
  26. package/dist/settings/code-scanning/diff.d.ts +19 -0
  27. package/dist/settings/code-scanning/diff.js +75 -0
  28. package/dist/settings/code-scanning/formatter.d.ts +17 -0
  29. package/dist/settings/code-scanning/formatter.js +37 -0
  30. package/dist/settings/code-scanning/github-code-scanning-strategy.d.ts +19 -0
  31. package/dist/settings/code-scanning/github-code-scanning-strategy.js +20 -0
  32. package/dist/settings/code-scanning/index.d.ts +3 -0
  33. package/dist/settings/code-scanning/index.js +4 -0
  34. package/dist/settings/code-scanning/processor.d.ts +20 -0
  35. package/dist/settings/code-scanning/processor.js +81 -0
  36. package/dist/settings/code-scanning/types.d.ts +22 -0
  37. package/dist/settings/code-scanning/types.js +1 -0
  38. package/dist/settings/index.d.ts +1 -0
  39. package/dist/settings/index.js +2 -0
  40. package/dist/settings/labels/github-labels-strategy.d.ts +2 -2
  41. package/dist/settings/repo-settings/diff.d.ts +2 -1
  42. package/dist/settings/repo-settings/github-repo-settings-strategy.d.ts +1 -1
  43. package/dist/settings/repo-settings/processor.d.ts +4 -2
  44. package/dist/settings/repo-settings/processor.js +14 -16
  45. package/dist/settings/rulesets/github-ruleset-strategy.d.ts +2 -2
  46. package/dist/settings/rulesets/processor.d.ts +1 -1
  47. package/dist/shared/cleanup-utils.d.ts +8 -0
  48. package/dist/shared/cleanup-utils.js +15 -0
  49. package/dist/shared/gh-api-utils.d.ts +2 -2
  50. package/dist/shared/gh-api-utils.js +7 -4
  51. package/dist/shared/repo-detector.d.ts +1 -20
  52. package/dist/shared/repo-detector.js +3 -45
  53. package/dist/shared/repo-info-utils.d.ts +20 -0
  54. package/dist/shared/repo-info-utils.js +46 -0
  55. package/dist/shared/repo-metadata-provider.d.ts +27 -0
  56. package/dist/shared/repo-metadata-provider.js +20 -0
  57. package/dist/shared/retry-utils.d.ts +1 -1
  58. package/dist/shared/retry-utils.js +1 -1
  59. package/dist/shared/type-guards.d.ts +0 -8
  60. package/dist/shared/type-guards.js +0 -14
  61. package/dist/shared/xfg-template.js +2 -2
  62. package/dist/sync/auth-options-builder.d.ts +1 -1
  63. package/dist/sync/branch-manager.d.ts +3 -7
  64. package/dist/sync/commit-push-manager.d.ts +1 -1
  65. package/dist/sync/commit-push-manager.js +2 -2
  66. package/dist/sync/diff-utils.js +0 -7
  67. package/dist/sync/manifest.js +1 -8
  68. package/dist/sync/repository-processor.d.ts +1 -1
  69. package/dist/sync/repository-processor.js +1 -2
  70. package/dist/sync/repository-session.d.ts +2 -2
  71. package/dist/sync/repository-session.js +1 -1
  72. package/dist/sync/sync-workflow.d.ts +1 -1
  73. package/dist/sync/sync-workflow.js +1 -1
  74. package/dist/vcs/authenticated-git-ops.js +2 -2
  75. package/dist/vcs/azure-pr-strategy.d.ts +1 -1
  76. package/dist/vcs/azure-pr-strategy.js +2 -1
  77. package/dist/vcs/commit-strategy-selector.d.ts +2 -2
  78. package/dist/vcs/git-commit-strategy.d.ts +1 -1
  79. package/dist/vcs/git-ops.d.ts +1 -1
  80. package/dist/vcs/git-ops.js +1 -1
  81. package/dist/vcs/github-pr-strategy.js +3 -2
  82. package/dist/vcs/gitlab-pr-strategy.d.ts +1 -1
  83. package/dist/vcs/gitlab-pr-strategy.js +4 -3
  84. package/dist/vcs/graphql-commit-strategy.d.ts +1 -1
  85. package/dist/vcs/index.d.ts +2 -0
  86. package/dist/vcs/index.js +3 -0
  87. package/dist/vcs/pr-creator.d.ts +2 -2
  88. package/dist/vcs/pr-creator.js +1 -1
  89. package/dist/vcs/pr-strategy-factory.d.ts +2 -2
  90. package/dist/vcs/pr-strategy.d.ts +4 -2
  91. package/dist/vcs/pr-strategy.js +7 -2
  92. package/package.json +2 -1
  93. /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,5 @@
1
1
  import { ValidationError } from "../../shared/errors.js";
2
+ import { isPlainObject } from "../../shared/type-guards.js";
2
3
  /** Compile-time validates an array matches a type union, while keeping string[] runtime type for .includes() */
3
4
  function validValues(values) {
4
5
  return values;
@@ -62,6 +63,31 @@ const VALID_RULE_TYPES = validValues([
62
63
  "max_file_path_length",
63
64
  "max_file_size",
64
65
  ]);
66
+ // Intentionally duplicated from merge.ts — validator should not depend on merge internals
67
+ const VALID_MERGE_STRATEGIES = ["replace", "append", "prepend"];
68
+ /**
69
+ * Checks if a value is an $arrayMerge directive: { $arrayMerge: strategy, $values: [...] }
70
+ */
71
+ function isArrayMergeDirective(value) {
72
+ if (!isPlainObject(value))
73
+ return false;
74
+ const keys = Object.keys(value);
75
+ return (keys.length === 2 &&
76
+ keys.every((k) => k === "$arrayMerge" || k === "$values") &&
77
+ VALID_MERGE_STRATEGIES.includes(value.$arrayMerge) &&
78
+ Array.isArray(value.$values));
79
+ }
80
+ /**
81
+ * Extracts the $values array from a directive, or returns the value as-is if it's already an array.
82
+ * Returns null if value is neither an array nor a valid directive.
83
+ */
84
+ function extractArrayOrDirectiveValues(value) {
85
+ if (Array.isArray(value))
86
+ return value;
87
+ if (isArrayMergeDirective(value))
88
+ return value.$values;
89
+ return null;
90
+ }
65
91
  /**
66
92
  * Validates a single ruleset rule.
67
93
  */
@@ -158,11 +184,12 @@ export function validateRuleset(ruleset, name, context) {
158
184
  }
159
185
  // Validate bypassActors
160
186
  if (rs.bypassActors !== undefined) {
161
- if (!Array.isArray(rs.bypassActors)) {
162
- throw new ValidationError(`${context}: ruleset '${name}' bypassActors must be an array`);
187
+ const actors = extractArrayOrDirectiveValues(rs.bypassActors);
188
+ if (actors === null) {
189
+ throw new ValidationError(`${context}: ruleset '${name}' bypassActors must be an array or $arrayMerge directive`);
163
190
  }
164
- for (let i = 0; i < rs.bypassActors.length; i++) {
165
- const actor = rs.bypassActors[i];
191
+ for (let i = 0; i < actors.length; i++) {
192
+ const actor = actors[i];
166
193
  if (typeof actor !== "object" || actor === null) {
167
194
  throw new ValidationError(`${context}: ruleset '${name}' bypassActors[${i}] must be an object`);
168
195
  }
@@ -193,25 +220,28 @@ export function validateRuleset(ruleset, name, context) {
193
220
  Array.isArray(refName)) {
194
221
  throw new ValidationError(`${context}: ruleset '${name}' conditions.refName must be an object`);
195
222
  }
196
- if (refName.include !== undefined &&
197
- (!Array.isArray(refName.include) ||
198
- !refName.include.every((s) => typeof s === "string"))) {
199
- throw new ValidationError(`${context}: ruleset '${name}' conditions.refName.include must be an array of strings`);
223
+ if (refName.include !== undefined) {
224
+ const include = extractArrayOrDirectiveValues(refName.include);
225
+ if (include === null || !include.every((s) => typeof s === "string")) {
226
+ throw new ValidationError(`${context}: ruleset '${name}' conditions.refName.include must be an array of strings or $arrayMerge directive with string $values`);
227
+ }
200
228
  }
201
- if (refName.exclude !== undefined &&
202
- (!Array.isArray(refName.exclude) ||
203
- !refName.exclude.every((s) => typeof s === "string"))) {
204
- throw new ValidationError(`${context}: ruleset '${name}' conditions.refName.exclude must be an array of strings`);
229
+ if (refName.exclude !== undefined) {
230
+ const exclude = extractArrayOrDirectiveValues(refName.exclude);
231
+ if (exclude === null || !exclude.every((s) => typeof s === "string")) {
232
+ throw new ValidationError(`${context}: ruleset '${name}' conditions.refName.exclude must be an array of strings or $arrayMerge directive with string $values`);
233
+ }
205
234
  }
206
235
  }
207
236
  }
208
237
  // Validate rules array
209
238
  if (rs.rules !== undefined) {
210
- if (!Array.isArray(rs.rules)) {
211
- throw new ValidationError(`${context}: ruleset '${name}' rules must be an array`);
239
+ const rules = extractArrayOrDirectiveValues(rs.rules);
240
+ if (rules === null) {
241
+ throw new ValidationError(`${context}: ruleset '${name}' rules must be an array or $arrayMerge directive`);
212
242
  }
213
- for (let i = 0; i < rs.rules.length; i++) {
214
- validateRule(rs.rules[i], `${context}: ruleset '${name}' rules[${i}]`);
243
+ for (let i = 0; i < rules.length; i++) {
244
+ validateRule(rules[i], `${context}: ruleset '${name}' rules[${i}]`);
215
245
  }
216
246
  }
217
247
  }
@@ -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
+ }