@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.
- package/dist/{shared → cli}/branch-utils.js +1 -1
- package/dist/cli/settings-report-builder.d.ts +6 -1
- package/dist/cli/settings-report-builder.js +19 -0
- package/dist/cli/sync-command.js +38 -23
- package/dist/cli/sync-report-builder.d.ts +2 -1
- package/dist/cli/types.d.ts +5 -2
- package/dist/config/config-merger.d.ts +6 -0
- package/dist/config/config-merger.js +66 -0
- package/dist/config/index.d.ts +1 -1
- package/dist/config/loader.d.ts +2 -2
- package/dist/config/loader.js +50 -6
- package/dist/config/merge.js +11 -16
- package/dist/config/normalizer.js +29 -0
- package/dist/config/types.d.ts +12 -0
- package/dist/config/validator.js +104 -46
- package/dist/lifecycle/ado-migration-source.d.ts +1 -1
- package/dist/lifecycle/github-lifecycle-provider.d.ts +1 -1
- package/dist/lifecycle/index.d.ts +1 -1
- package/dist/lifecycle/lifecycle-helpers.d.ts +2 -1
- package/dist/lifecycle/lifecycle-helpers.js +1 -1
- package/dist/lifecycle/repo-lifecycle-factory.d.ts +1 -1
- package/dist/lifecycle/repo-lifecycle-manager.js +1 -1
- package/dist/output/types.d.ts +2 -1
- package/dist/settings/code-scanning/diff.d.ts +19 -0
- package/dist/settings/code-scanning/diff.js +75 -0
- package/dist/settings/code-scanning/formatter.d.ts +17 -0
- package/dist/settings/code-scanning/formatter.js +37 -0
- package/dist/settings/code-scanning/github-code-scanning-strategy.d.ts +19 -0
- package/dist/settings/code-scanning/github-code-scanning-strategy.js +20 -0
- package/dist/settings/code-scanning/index.d.ts +3 -0
- package/dist/settings/code-scanning/index.js +4 -0
- package/dist/settings/code-scanning/processor.d.ts +20 -0
- package/dist/settings/code-scanning/processor.js +81 -0
- package/dist/settings/code-scanning/types.d.ts +22 -0
- package/dist/settings/code-scanning/types.js +1 -0
- package/dist/settings/index.d.ts +1 -0
- package/dist/settings/index.js +2 -0
- package/dist/settings/labels/github-labels-strategy.d.ts +2 -2
- package/dist/settings/repo-settings/diff.d.ts +2 -1
- package/dist/settings/repo-settings/github-repo-settings-strategy.d.ts +1 -1
- package/dist/settings/repo-settings/processor.d.ts +4 -2
- package/dist/settings/repo-settings/processor.js +14 -16
- package/dist/settings/rulesets/github-ruleset-strategy.d.ts +2 -2
- package/dist/settings/rulesets/processor.d.ts +1 -1
- package/dist/shared/cleanup-utils.d.ts +8 -0
- package/dist/shared/cleanup-utils.js +15 -0
- package/dist/shared/gh-api-utils.d.ts +2 -2
- package/dist/shared/gh-api-utils.js +7 -4
- package/dist/shared/repo-detector.d.ts +1 -20
- package/dist/shared/repo-detector.js +3 -45
- package/dist/shared/repo-info-utils.d.ts +20 -0
- package/dist/shared/repo-info-utils.js +46 -0
- package/dist/shared/repo-metadata-provider.d.ts +27 -0
- package/dist/shared/repo-metadata-provider.js +20 -0
- package/dist/shared/retry-utils.d.ts +1 -1
- package/dist/shared/retry-utils.js +1 -1
- package/dist/shared/type-guards.d.ts +0 -8
- package/dist/shared/type-guards.js +0 -14
- package/dist/shared/xfg-template.js +2 -2
- package/dist/sync/auth-options-builder.d.ts +1 -1
- package/dist/sync/branch-manager.d.ts +3 -7
- package/dist/sync/commit-push-manager.d.ts +1 -1
- package/dist/sync/commit-push-manager.js +2 -2
- package/dist/sync/diff-utils.js +0 -7
- package/dist/sync/manifest.js +1 -8
- package/dist/sync/repository-processor.d.ts +1 -1
- package/dist/sync/repository-processor.js +1 -2
- package/dist/sync/repository-session.d.ts +2 -2
- package/dist/sync/repository-session.js +1 -1
- package/dist/sync/sync-workflow.d.ts +1 -1
- package/dist/sync/sync-workflow.js +1 -1
- package/dist/vcs/authenticated-git-ops.js +2 -2
- package/dist/vcs/azure-pr-strategy.d.ts +1 -1
- package/dist/vcs/azure-pr-strategy.js +2 -1
- package/dist/vcs/commit-strategy-selector.d.ts +2 -2
- package/dist/vcs/git-commit-strategy.d.ts +1 -1
- package/dist/vcs/git-ops.d.ts +1 -1
- package/dist/vcs/git-ops.js +1 -1
- package/dist/vcs/github-pr-strategy.js +3 -2
- package/dist/vcs/gitlab-pr-strategy.d.ts +1 -1
- package/dist/vcs/gitlab-pr-strategy.js +4 -3
- package/dist/vcs/graphql-commit-strategy.d.ts +1 -1
- package/dist/vcs/index.d.ts +2 -0
- package/dist/vcs/index.js +3 -0
- package/dist/vcs/pr-creator.d.ts +2 -2
- package/dist/vcs/pr-creator.js +1 -1
- package/dist/vcs/pr-strategy-factory.d.ts +2 -2
- package/dist/vcs/pr-strategy.d.ts +4 -2
- package/dist/vcs/pr-strategy.js +7 -2
- package/package.json +2 -1
- /package/dist/{shared → cli}/branch-utils.d.ts +0 -0
package/dist/config/validator.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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 =
|
|
615
|
-
|
|
616
|
-
|
|
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 =
|
|
663
|
-
|
|
664
|
-
|
|
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.
|
|
@@ -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/
|
|
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";
|
package/dist/output/types.d.ts
CHANGED
|
@@ -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?:
|
|
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,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 {};
|
package/dist/settings/index.d.ts
CHANGED
|
@@ -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";
|
package/dist/settings/index.js
CHANGED
|
@@ -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 {
|