@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.
- 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 +36 -5
- package/dist/config/types.d.ts +14 -0
- package/dist/config/validator.js +125 -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
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { SettingsReport } from "../output/settings-report.js";
|
|
2
|
-
import { type RepoSettingsPlanEntry, type RulesetPlanEntry, type LabelsPlanEntry } from "../settings/index.js";
|
|
2
|
+
import { type RepoSettingsPlanEntry, type RulesetPlanEntry, type LabelsPlanEntry, type CodeScanningPlanEntry } from "../settings/index.js";
|
|
3
3
|
/**
|
|
4
4
|
* Result from processing a repository's settings and rulesets.
|
|
5
5
|
* Used to collect results during settings command execution.
|
|
@@ -21,6 +21,11 @@ export interface ProcessorResults {
|
|
|
21
21
|
entries?: LabelsPlanEntry[];
|
|
22
22
|
};
|
|
23
23
|
};
|
|
24
|
+
codeScanningResult?: {
|
|
25
|
+
planOutput?: {
|
|
26
|
+
entries?: CodeScanningPlanEntry[];
|
|
27
|
+
};
|
|
28
|
+
};
|
|
24
29
|
error?: string;
|
|
25
30
|
}
|
|
26
31
|
export declare function buildSettingsReport(results: ProcessorResults[]): SettingsReport;
|
|
@@ -66,6 +66,25 @@ export function buildSettingsReport(results) {
|
|
|
66
66
|
totals.labels.update += counts.update;
|
|
67
67
|
totals.labels.delete += counts.delete;
|
|
68
68
|
}
|
|
69
|
+
// Convert code scanning processor output
|
|
70
|
+
if (result.codeScanningResult?.planOutput?.entries) {
|
|
71
|
+
let csCreates = 0;
|
|
72
|
+
let csUpdates = 0;
|
|
73
|
+
for (const entry of result.codeScanningResult.planOutput.entries) {
|
|
74
|
+
repoChanges.settings.push({
|
|
75
|
+
name: `codeScanning.${entry.property}`,
|
|
76
|
+
action: entry.action,
|
|
77
|
+
oldValue: entry.oldValue,
|
|
78
|
+
newValue: entry.newValue ?? null,
|
|
79
|
+
});
|
|
80
|
+
if (entry.action === "create")
|
|
81
|
+
csCreates++;
|
|
82
|
+
if (entry.action === "update")
|
|
83
|
+
csUpdates++;
|
|
84
|
+
}
|
|
85
|
+
totals.settings.create += csCreates;
|
|
86
|
+
totals.settings.update += csUpdates;
|
|
87
|
+
}
|
|
69
88
|
if (result.error) {
|
|
70
89
|
repoChanges.error = result.error;
|
|
71
90
|
}
|
package/dist/cli/sync-command.js
CHANGED
|
@@ -3,10 +3,11 @@ import { existsSync } from "node:fs";
|
|
|
3
3
|
import { loadRawConfig, normalizeConfig, validateForSync, } from "../config/index.js";
|
|
4
4
|
import { ValidationError, SyncError } from "../shared/errors.js";
|
|
5
5
|
import { parseGitUrl, getRepoDisplayName, isGitHubRepo, } from "../shared/repo-detector.js";
|
|
6
|
-
import { sanitizeBranchName, validateBranchName
|
|
6
|
+
import { sanitizeBranchName, validateBranchName } from "./branch-utils.js";
|
|
7
7
|
import { createTokenManager } from "../vcs/index.js";
|
|
8
8
|
import { RepositoryProcessor } from "../sync/index.js";
|
|
9
|
-
import { RulesetProcessor, RepoSettingsProcessor, LabelsProcessor, GitHubRulesetStrategy, GitHubRepoSettingsStrategy, GitHubLabelsStrategy, } from "../settings/index.js";
|
|
9
|
+
import { RulesetProcessor, RepoSettingsProcessor, LabelsProcessor, CodeScanningProcessor, GitHubRulesetStrategy, GitHubRepoSettingsStrategy, GitHubLabelsStrategy, GitHubCodeScanningStrategy, } from "../settings/index.js";
|
|
10
|
+
import { GitHubRepoMetadataProvider } from "../shared/repo-metadata-provider.js";
|
|
10
11
|
import { ShellCommandExecutor } from "../shared/command-executor.js";
|
|
11
12
|
import { Logger } from "../shared/logger.js";
|
|
12
13
|
import { generateWorkspaceName } from "../shared/workspace-utils.js";
|
|
@@ -24,12 +25,18 @@ function createDefaultRulesetProcessorFactory() {
|
|
|
24
25
|
}
|
|
25
26
|
function createDefaultRepoSettingsProcessorFactory() {
|
|
26
27
|
const cwd = process.cwd();
|
|
27
|
-
|
|
28
|
+
const executor = getDefaultExecutor();
|
|
29
|
+
return () => new RepoSettingsProcessor(new GitHubRepoSettingsStrategy(executor, { cwd }), new GitHubRepoMetadataProvider(executor, { cwd }));
|
|
28
30
|
}
|
|
29
31
|
function createDefaultLabelsProcessorFactory() {
|
|
30
32
|
const cwd = process.cwd();
|
|
31
33
|
return () => new LabelsProcessor(new GitHubLabelsStrategy(getDefaultExecutor(), { cwd }));
|
|
32
34
|
}
|
|
35
|
+
function createDefaultCodeScanningProcessorFactory() {
|
|
36
|
+
const cwd = process.cwd();
|
|
37
|
+
const executor = getDefaultExecutor();
|
|
38
|
+
return () => new CodeScanningProcessor(new GitHubCodeScanningStrategy(executor, { cwd }), new GitHubRepoMetadataProvider(executor, { cwd }));
|
|
39
|
+
}
|
|
33
40
|
import { ResultsCollector } from "./results-collector.js";
|
|
34
41
|
import { buildSettingsReport, } from "./settings-report-builder.js";
|
|
35
42
|
import { formatSettingsReportCLI } from "../output/settings-report.js";
|
|
@@ -40,7 +47,7 @@ import { formatLifecycleReportCLI, hasLifecycleChanges, } from "../output/lifecy
|
|
|
40
47
|
import { writeUnifiedSummary } from "../output/unified-summary.js";
|
|
41
48
|
import { toErrorMessage } from "../shared/type-guards.js";
|
|
42
49
|
import { resolveGitHubToken } from "../shared/gh-api-utils.js";
|
|
43
|
-
import { RepoLifecycleManager, runLifecycleCheck,
|
|
50
|
+
import { RepoLifecycleManager, runLifecycleCheck, } from "../lifecycle/index.js";
|
|
44
51
|
function getUniqueFileNames(config) {
|
|
45
52
|
const fileNames = new Set();
|
|
46
53
|
for (const repo of config.repos) {
|
|
@@ -97,46 +104,53 @@ function logSettingsResult(result, label, repoNumber, repoName, settingsCollecto
|
|
|
97
104
|
settingsCollector.appendError(repoName, result.message);
|
|
98
105
|
}
|
|
99
106
|
}
|
|
107
|
+
// Each processor returns a subtype of BaseProcessorResult whose planOutput
|
|
108
|
+
// contains both `lines` (for CLI display) and `entries` (for report building).
|
|
109
|
+
// ProcessorResults fields capture only the `entries` slice; the runtime object
|
|
110
|
+
// satisfies both views, so we assign with an explicit per-field cast.
|
|
111
|
+
async function runAndStoreResult(factory, repoConfig, repoInfo, opts, repoName, settingsCollector, assign) {
|
|
112
|
+
const result = await runSettingsProcessor(factory, repoConfig, repoInfo, opts);
|
|
113
|
+
if (!result.skipped) {
|
|
114
|
+
assign(settingsCollector.getOrCreate(repoName), result);
|
|
115
|
+
}
|
|
116
|
+
return result;
|
|
117
|
+
}
|
|
100
118
|
function buildSettingsDescriptors(ctx) {
|
|
101
|
-
const { repoConfig, repoInfo, options, token, repoName, settingsCollector, rulesetProcessorFactory, repoSettingsProcessorFactory, labelsProcessorFactory, } = ctx;
|
|
119
|
+
const { repoConfig, repoInfo, options, token, repoName, settingsCollector, rulesetProcessorFactory, repoSettingsProcessorFactory, labelsProcessorFactory, codeScanningProcessorFactory, } = ctx;
|
|
102
120
|
const sharedOpts = {
|
|
103
121
|
dryRun: options.dryRun,
|
|
104
122
|
noDelete: options.noDelete,
|
|
105
123
|
token,
|
|
106
124
|
};
|
|
107
|
-
// Each processor returns a subtype of BaseProcessorResult whose planOutput
|
|
108
|
-
// contains both `lines` (for CLI display) and `entries` (for report building).
|
|
109
|
-
// ProcessorResults fields capture only the `entries` slice; the runtime object
|
|
110
|
-
// satisfies both views, so we assign with an explicit per-field cast.
|
|
111
|
-
const runAndStore = async (factory, opts, assign) => {
|
|
112
|
-
const result = await runSettingsProcessor(factory, repoConfig, repoInfo, opts);
|
|
113
|
-
if (!result.skipped) {
|
|
114
|
-
assign(settingsCollector.getOrCreate(repoName), result);
|
|
115
|
-
}
|
|
116
|
-
return result;
|
|
117
|
-
};
|
|
118
125
|
return [
|
|
119
126
|
{
|
|
120
127
|
key: "rulesets",
|
|
121
128
|
label: "Rulesets",
|
|
122
|
-
run: () =>
|
|
129
|
+
run: () => runAndStoreResult(rulesetProcessorFactory, repoConfig, repoInfo, sharedOpts, repoName, settingsCollector, (e, r) => {
|
|
123
130
|
e.rulesetResult = r;
|
|
124
131
|
}),
|
|
125
132
|
},
|
|
126
133
|
{
|
|
127
134
|
key: "labels",
|
|
128
135
|
label: "Labels",
|
|
129
|
-
run: () =>
|
|
136
|
+
run: () => runAndStoreResult(labelsProcessorFactory, repoConfig, repoInfo, sharedOpts, repoName, settingsCollector, (e, r) => {
|
|
130
137
|
e.labelsResult = r;
|
|
131
138
|
}),
|
|
132
139
|
},
|
|
133
140
|
{
|
|
134
141
|
key: "repo",
|
|
135
142
|
label: "Repo Settings",
|
|
136
|
-
run: () =>
|
|
143
|
+
run: () => runAndStoreResult(repoSettingsProcessorFactory, repoConfig, repoInfo, { dryRun: options.dryRun, token }, repoName, settingsCollector, (e, r) => {
|
|
137
144
|
e.settingsResult = r;
|
|
138
145
|
}),
|
|
139
146
|
},
|
|
147
|
+
{
|
|
148
|
+
key: "codeScanning",
|
|
149
|
+
label: "Code Scanning",
|
|
150
|
+
run: () => runAndStoreResult(codeScanningProcessorFactory, repoConfig, repoInfo, sharedOpts, repoName, settingsCollector, (e, r) => {
|
|
151
|
+
e.codeScanningResult = r;
|
|
152
|
+
}),
|
|
153
|
+
},
|
|
140
154
|
];
|
|
141
155
|
}
|
|
142
156
|
function runSettingsProcessor(factory, repoConfig, repoInfo, processOptions) {
|
|
@@ -269,6 +283,7 @@ async function processSingleRepo(repoConfig, index, ctx) {
|
|
|
269
283
|
rulesetProcessorFactory: ctx.rulesetProcessorFactory,
|
|
270
284
|
repoSettingsProcessorFactory: ctx.repoSettingsProcessorFactory,
|
|
271
285
|
labelsProcessorFactory: ctx.labelsProcessorFactory,
|
|
286
|
+
codeScanningProcessorFactory: ctx.codeScanningProcessorFactory,
|
|
272
287
|
});
|
|
273
288
|
}
|
|
274
289
|
/**
|
|
@@ -278,7 +293,7 @@ async function processSingleRepo(repoConfig, index, ctx) {
|
|
|
278
293
|
async function runLifecyclePhase(repo, ctx) {
|
|
279
294
|
const repoNumber = repo.index + 1;
|
|
280
295
|
try {
|
|
281
|
-
const { outputLines, lifecycleResult } = await runLifecycleCheck(repo.repoConfig, repo.repoInfo, {
|
|
296
|
+
const { outputLines, lifecycleResult, createSettings } = await runLifecycleCheck(repo.repoConfig, repo.repoInfo, {
|
|
282
297
|
dryRun: ctx.options.dryRun ?? false,
|
|
283
298
|
resolvedWorkDir: repo.workDir,
|
|
284
299
|
githubHosts: ctx.config.githubHosts,
|
|
@@ -290,7 +305,6 @@ async function runLifecyclePhase(repo, ctx) {
|
|
|
290
305
|
for (const line of outputLines) {
|
|
291
306
|
getLogger().info(line);
|
|
292
307
|
}
|
|
293
|
-
const createSettings = toCreateRepoSettings(ctx.config.settings?.repo);
|
|
294
308
|
ctx.lifecycleReportInputs.push({
|
|
295
309
|
repoName: repo.repoName,
|
|
296
310
|
action: lifecycleResult.action,
|
|
@@ -381,10 +395,10 @@ export async function runSync(options, deps = {}) {
|
|
|
381
395
|
// Reset module-level singletons to ensure fresh state per invocation
|
|
382
396
|
_defaultExecutor = undefined;
|
|
383
397
|
_logger = undefined;
|
|
384
|
-
const { lifecycleManager, rulesetProcessorFactory = createDefaultRulesetProcessorFactory(), repoSettingsProcessorFactory = createDefaultRepoSettingsProcessorFactory(), labelsProcessorFactory = createDefaultLabelsProcessorFactory(), } = deps;
|
|
398
|
+
const { lifecycleManager, rulesetProcessorFactory = createDefaultRulesetProcessorFactory(), repoSettingsProcessorFactory = createDefaultRepoSettingsProcessorFactory(), labelsProcessorFactory = createDefaultLabelsProcessorFactory(), codeScanningProcessorFactory = createDefaultCodeScanningProcessorFactory(), } = deps;
|
|
385
399
|
const configPath = resolve(options.config);
|
|
386
400
|
if (!existsSync(configPath)) {
|
|
387
|
-
throw new ValidationError(`Config
|
|
401
|
+
throw new ValidationError(`Config path not found: ${configPath}`);
|
|
388
402
|
}
|
|
389
403
|
getLogger().log(`Loading config from: ${configPath}`);
|
|
390
404
|
if (options.dryRun) {
|
|
@@ -432,6 +446,7 @@ export async function runSync(options, deps = {}) {
|
|
|
432
446
|
rulesetProcessorFactory,
|
|
433
447
|
repoSettingsProcessorFactory,
|
|
434
448
|
labelsProcessorFactory,
|
|
449
|
+
codeScanningProcessorFactory,
|
|
435
450
|
};
|
|
436
451
|
for (let i = 0; i < config.repos.length; i++) {
|
|
437
452
|
await processSingleRepo(config.repos[i], i, ctx);
|
|
@@ -1,10 +1,11 @@
|
|
|
1
|
+
import type { MergeMode } from "../config/index.js";
|
|
1
2
|
import type { SyncReport, ReportFileChange } from "../output/sync-report.js";
|
|
2
3
|
interface SyncResultInput {
|
|
3
4
|
repoName: string;
|
|
4
5
|
success: boolean;
|
|
5
6
|
fileChanges: ReportFileChange[];
|
|
6
7
|
prUrl?: string;
|
|
7
|
-
mergeOutcome?:
|
|
8
|
+
mergeOutcome?: MergeMode;
|
|
8
9
|
error?: string;
|
|
9
10
|
}
|
|
10
11
|
export declare function buildSyncReport(results: SyncResultInput[]): SyncReport;
|
package/dist/cli/types.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { MergeMode, MergeStrategy, RepoConfig } from "../config/index.js";
|
|
2
2
|
import type { IRepoLifecycleManager } from "../lifecycle/index.js";
|
|
3
3
|
import type { IRepositoryProcessor } from "../sync/index.js";
|
|
4
|
-
import type { ISettingsProcessor, IRulesetProcessor, IRepoSettingsProcessor, ILabelsProcessor, BaseProcessorResult } from "../settings/index.js";
|
|
4
|
+
import type { ISettingsProcessor, IRulesetProcessor, IRepoSettingsProcessor, ILabelsProcessor, ICodeScanningProcessor, BaseProcessorResult } from "../settings/index.js";
|
|
5
5
|
import type { RepoInfo } from "../shared/repo-detector.js";
|
|
6
6
|
import type { ResultsCollector } from "./results-collector.js";
|
|
7
7
|
export type ProcessorFactory = () => IRepositoryProcessor;
|
|
@@ -9,6 +9,7 @@ export type SettingsProcessorFactory<T extends ISettingsProcessor> = () => T;
|
|
|
9
9
|
export type RulesetProcessorFactory = SettingsProcessorFactory<IRulesetProcessor>;
|
|
10
10
|
export type RepoSettingsProcessorFactory = SettingsProcessorFactory<IRepoSettingsProcessor>;
|
|
11
11
|
export type LabelsProcessorFactory = SettingsProcessorFactory<ILabelsProcessor>;
|
|
12
|
+
export type CodeScanningProcessorFactory = SettingsProcessorFactory<ICodeScanningProcessor>;
|
|
12
13
|
/**
|
|
13
14
|
* Dependencies for the sync command (dependency injection).
|
|
14
15
|
*/
|
|
@@ -18,6 +19,7 @@ export interface SyncDependencies {
|
|
|
18
19
|
rulesetProcessorFactory?: RulesetProcessorFactory;
|
|
19
20
|
repoSettingsProcessorFactory?: RepoSettingsProcessorFactory;
|
|
20
21
|
labelsProcessorFactory?: LabelsProcessorFactory;
|
|
22
|
+
codeScanningProcessorFactory?: CodeScanningProcessorFactory;
|
|
21
23
|
}
|
|
22
24
|
export interface SharedOptions {
|
|
23
25
|
config: string;
|
|
@@ -41,7 +43,7 @@ export interface SyncResultEntry {
|
|
|
41
43
|
diffLines?: string[];
|
|
42
44
|
}>;
|
|
43
45
|
prUrl?: string;
|
|
44
|
-
mergeOutcome?:
|
|
46
|
+
mergeOutcome?: MergeMode;
|
|
45
47
|
error?: string;
|
|
46
48
|
}
|
|
47
49
|
export interface SettingsResult extends BaseProcessorResult {
|
|
@@ -65,4 +67,5 @@ export interface ApplyRepoSettingsContext {
|
|
|
65
67
|
rulesetProcessorFactory: NonNullable<SyncDependencies["rulesetProcessorFactory"]>;
|
|
66
68
|
repoSettingsProcessorFactory: NonNullable<SyncDependencies["repoSettingsProcessorFactory"]>;
|
|
67
69
|
labelsProcessorFactory: NonNullable<SyncDependencies["labelsProcessorFactory"]>;
|
|
70
|
+
codeScanningProcessorFactory: NonNullable<SyncDependencies["codeScanningProcessorFactory"]>;
|
|
68
71
|
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { ValidationError } from "../shared/errors.js";
|
|
2
|
+
/** Keys that can only appear in one file across a config directory. */
|
|
3
|
+
const SINGLE_FILE_KEYS = [
|
|
4
|
+
"id",
|
|
5
|
+
"files",
|
|
6
|
+
"prOptions",
|
|
7
|
+
"prTemplate",
|
|
8
|
+
"settings",
|
|
9
|
+
"githubHosts",
|
|
10
|
+
"deleteOrphaned",
|
|
11
|
+
];
|
|
12
|
+
export function mergeConfigFragments(fragments) {
|
|
13
|
+
if (fragments.length === 0) {
|
|
14
|
+
throw new ValidationError("No config fragments to merge");
|
|
15
|
+
}
|
|
16
|
+
const merged = {};
|
|
17
|
+
const singleKeySource = {};
|
|
18
|
+
const allRepos = [];
|
|
19
|
+
const allGroups = {};
|
|
20
|
+
const groupSource = {};
|
|
21
|
+
const allConditionalGroups = [];
|
|
22
|
+
for (const { fileName, config } of fragments) {
|
|
23
|
+
// Enforce single-file keys
|
|
24
|
+
for (const key of SINGLE_FILE_KEYS) {
|
|
25
|
+
if (config[key] !== undefined) {
|
|
26
|
+
if (singleKeySource[key] !== undefined) {
|
|
27
|
+
throw new ValidationError(`'${key}' is defined in both ${singleKeySource[key]} and ${fileName} — this key can only appear in one file`);
|
|
28
|
+
}
|
|
29
|
+
singleKeySource[key] = fileName;
|
|
30
|
+
merged[key] = config[key];
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
// Concatenate repos
|
|
34
|
+
if (config.repos) {
|
|
35
|
+
allRepos.push(...config.repos);
|
|
36
|
+
}
|
|
37
|
+
// Merge groups (unique names only)
|
|
38
|
+
if (config.groups) {
|
|
39
|
+
for (const [groupName, groupConfig] of Object.entries(config.groups)) {
|
|
40
|
+
if (groupName in allGroups) {
|
|
41
|
+
throw new ValidationError(`group '${groupName}' is defined in both ${groupSource[groupName]} and ${fileName} — group names must be unique across files`);
|
|
42
|
+
}
|
|
43
|
+
allGroups[groupName] = groupConfig;
|
|
44
|
+
groupSource[groupName] = fileName;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
// Concatenate conditional groups
|
|
48
|
+
if (config.conditionalGroups) {
|
|
49
|
+
allConditionalGroups.push(...config.conditionalGroups);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
if (!merged.id) {
|
|
53
|
+
throw new ValidationError("No 'id' found in any config file — exactly one file must define 'id'");
|
|
54
|
+
}
|
|
55
|
+
if (allRepos.length === 0) {
|
|
56
|
+
throw new ValidationError("No 'repos' found in any config file — at least one file must define 'repos'");
|
|
57
|
+
}
|
|
58
|
+
return {
|
|
59
|
+
...merged,
|
|
60
|
+
repos: allRepos,
|
|
61
|
+
...(Object.keys(allGroups).length > 0 ? { groups: allGroups } : {}),
|
|
62
|
+
...(allConditionalGroups.length > 0
|
|
63
|
+
? { conditionalGroups: allConditionalGroups }
|
|
64
|
+
: {}),
|
|
65
|
+
};
|
|
66
|
+
}
|
package/dist/config/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export type { MergeMode, MergeStrategy, BypassActor, StatusCheckConfig, CodeScanningTool, PullRequestRuleParameters, RulesetRule, Ruleset, GitHubRepoSettings, RepoVisibility, SquashMergeCommitTitle, SquashMergeCommitMessage, MergeCommitTitle, MergeCommitMessage, Label, RepoSettings, RawFileConfig, RawRepoFileOverride, RawGroupConfig, RawRepoSettings, RawRepoConfig, RawConfig, RawConditionalGroupWhen, RawConditionalGroupConfig, RepoConfig, Config, FileContent, ContentValue, } from "./types.js";
|
|
1
|
+
export type { MergeMode, MergeStrategy, BypassActor, StatusCheckConfig, CodeScanningTool, PullRequestRuleParameters, RulesetRule, Ruleset, GitHubRepoSettings, RepoVisibility, SquashMergeCommitTitle, SquashMergeCommitMessage, MergeCommitTitle, MergeCommitMessage, Label, CodeScanningSettings, CodeScanningState, CodeScanningQuerySuite, CodeScanningLanguage, RepoSettings, RawFileConfig, RawRepoFileOverride, RawGroupConfig, RawRepoSettings, RawRepoConfig, RawConfig, RawConditionalGroupWhen, RawConditionalGroupConfig, RepoConfig, Config, FileContent, ContentValue, } from "./types.js";
|
|
2
2
|
export { RULESET_COMPARABLE_FIELDS } from "./types.js";
|
|
3
3
|
export { loadRawConfig, loadConfig, normalizeConfig } from "./loader.js";
|
|
4
4
|
export { convertContentToString } from "./formatter.js";
|
package/dist/config/loader.d.ts
CHANGED
|
@@ -5,5 +5,5 @@ export { normalizeConfigInternal as normalizeConfig };
|
|
|
5
5
|
* Load and validate raw config without normalization.
|
|
6
6
|
* Use this when you need to perform command-specific validation before normalizing.
|
|
7
7
|
*/
|
|
8
|
-
export declare function loadRawConfig(
|
|
9
|
-
export declare function loadConfig(
|
|
8
|
+
export declare function loadRawConfig(configPath: string): RawConfig;
|
|
9
|
+
export declare function loadConfig(configPath: string, env: Record<string, string | undefined>): Config;
|
package/dist/config/loader.js
CHANGED
|
@@ -1,17 +1,29 @@
|
|
|
1
|
-
import { readFileSync } from "node:fs";
|
|
2
|
-
import { dirname } from "node:path";
|
|
1
|
+
import { readFileSync, statSync, readdirSync } from "node:fs";
|
|
2
|
+
import { dirname, join, extname } from "node:path";
|
|
3
3
|
import { parse } from "yaml";
|
|
4
4
|
import { validateRawConfig } from "./validator.js";
|
|
5
5
|
import { normalizeConfig as normalizeConfigInternal } from "./normalizer.js";
|
|
6
6
|
import { resolveFileReferencesInConfig } from "./file-reference-resolver.js";
|
|
7
7
|
import { toErrorMessage } from "../shared/type-guards.js";
|
|
8
8
|
import { ValidationError } from "../shared/errors.js";
|
|
9
|
+
import { mergeConfigFragments } from "./config-merger.js";
|
|
9
10
|
export { normalizeConfigInternal as normalizeConfig };
|
|
10
11
|
/**
|
|
11
12
|
* Load and validate raw config without normalization.
|
|
12
13
|
* Use this when you need to perform command-specific validation before normalizing.
|
|
13
14
|
*/
|
|
14
|
-
export function loadRawConfig(
|
|
15
|
+
export function loadRawConfig(configPath) {
|
|
16
|
+
const stat = statSync(configPath);
|
|
17
|
+
if (stat.isDirectory()) {
|
|
18
|
+
return loadRawConfigFromDirectory(configPath);
|
|
19
|
+
}
|
|
20
|
+
return loadRawConfigFromFile(configPath);
|
|
21
|
+
}
|
|
22
|
+
export function loadConfig(configPath, env) {
|
|
23
|
+
const rawConfig = loadRawConfig(configPath);
|
|
24
|
+
return normalizeConfigInternal(rawConfig, env);
|
|
25
|
+
}
|
|
26
|
+
function loadRawConfigFromFile(filePath) {
|
|
15
27
|
const content = readFileSync(filePath, "utf-8");
|
|
16
28
|
const configDir = dirname(filePath);
|
|
17
29
|
let rawConfig;
|
|
@@ -27,7 +39,39 @@ export function loadRawConfig(filePath) {
|
|
|
27
39
|
validateRawConfig(rawConfig);
|
|
28
40
|
return rawConfig;
|
|
29
41
|
}
|
|
30
|
-
|
|
31
|
-
const
|
|
32
|
-
|
|
42
|
+
function loadRawConfigFromDirectory(dirPath) {
|
|
43
|
+
const entries = readdirSync(dirPath, { withFileTypes: true });
|
|
44
|
+
const yamlFiles = entries
|
|
45
|
+
.filter((entry) => entry.isFile() &&
|
|
46
|
+
[".yaml", ".yml"].includes(extname(entry.name).toLowerCase()))
|
|
47
|
+
.map((entry) => entry.name)
|
|
48
|
+
.sort();
|
|
49
|
+
if (yamlFiles.length === 0) {
|
|
50
|
+
throw new ValidationError(`No .yaml or .yml files found in directory: ${dirPath}`);
|
|
51
|
+
}
|
|
52
|
+
const fragments = yamlFiles.map((fileName) => {
|
|
53
|
+
const filePath = join(dirPath, fileName);
|
|
54
|
+
const content = readFileSync(filePath, "utf-8");
|
|
55
|
+
const configDir = dirname(filePath);
|
|
56
|
+
let config;
|
|
57
|
+
try {
|
|
58
|
+
config = parse(content);
|
|
59
|
+
}
|
|
60
|
+
catch (error) {
|
|
61
|
+
const message = toErrorMessage(error);
|
|
62
|
+
throw new ValidationError(`Failed to parse YAML config at ${filePath}: ${message}`);
|
|
63
|
+
}
|
|
64
|
+
if (!config || typeof config !== "object") {
|
|
65
|
+
throw new ValidationError(`Config file ${fileName} is empty or invalid — expected a YAML mapping`);
|
|
66
|
+
}
|
|
67
|
+
// Safe cast: resolveFileReferencesInConfig only accesses optional fields
|
|
68
|
+
// (files, groups, etc.), so fragments missing id/repos work correctly.
|
|
69
|
+
config = resolveFileReferencesInConfig(config, {
|
|
70
|
+
configDir,
|
|
71
|
+
});
|
|
72
|
+
return { fileName, config };
|
|
73
|
+
});
|
|
74
|
+
const merged = mergeConfigFragments(fragments);
|
|
75
|
+
validateRawConfig(merged);
|
|
76
|
+
return merged;
|
|
33
77
|
}
|
package/dist/config/merge.js
CHANGED
|
@@ -140,23 +140,18 @@ export function mergeTextContent(base, overlay, strategy = "replace") {
|
|
|
140
140
|
if (typeof overlay === "string") {
|
|
141
141
|
return overlay;
|
|
142
142
|
}
|
|
143
|
-
// If
|
|
144
|
-
if (Array.isArray(
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
default:
|
|
154
|
-
return overlay;
|
|
155
|
-
}
|
|
143
|
+
// If base is also an array, apply merge strategy
|
|
144
|
+
if (Array.isArray(base)) {
|
|
145
|
+
switch (strategy) {
|
|
146
|
+
case "append":
|
|
147
|
+
return [...base, ...overlay];
|
|
148
|
+
case "prepend":
|
|
149
|
+
return [...overlay, ...base];
|
|
150
|
+
case "replace":
|
|
151
|
+
default:
|
|
152
|
+
return overlay;
|
|
156
153
|
}
|
|
157
|
-
// Base is string, overlay is array - overlay replaces
|
|
158
|
-
return overlay;
|
|
159
154
|
}
|
|
160
|
-
//
|
|
155
|
+
// Base is string, overlay is array - overlay replaces
|
|
161
156
|
return overlay;
|
|
162
157
|
}
|
|
@@ -203,6 +203,26 @@ export function mergeSettings(root, perRepo) {
|
|
|
203
203
|
if (mergedLabels) {
|
|
204
204
|
result.labels = mergedLabels;
|
|
205
205
|
}
|
|
206
|
+
// Merge code scanning: per-repo fully replaces root (not shallow merge).
|
|
207
|
+
// Unlike `repo` settings (which shallow-merge via spread), code scanning
|
|
208
|
+
// uses full replacement because its 3 fields (state, querySuite, languages)
|
|
209
|
+
// are tightly coupled — partial inheritance (e.g., inheriting languages
|
|
210
|
+
// from root while changing querySuite) would be confusing.
|
|
211
|
+
// codeScanning: false means opt out of all root code scanning settings
|
|
212
|
+
if (perRepo?.codeScanning === false) {
|
|
213
|
+
// Opt-out: don't include any code scanning settings
|
|
214
|
+
}
|
|
215
|
+
else {
|
|
216
|
+
const mergedCodeScanning = perRepo?.codeScanning ?? root?.codeScanning;
|
|
217
|
+
if (mergedCodeScanning) {
|
|
218
|
+
// At this point mergedCodeScanning is CodeScanningSettings (not false),
|
|
219
|
+
// because root uses RawRootSettings where false is filtered by the
|
|
220
|
+
// outer check on perRepo?.codeScanning === false
|
|
221
|
+
if (typeof mergedCodeScanning === "object") {
|
|
222
|
+
result.codeScanning = mergedCodeScanning;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
206
226
|
return Object.keys(result).length > 0 ? result : undefined;
|
|
207
227
|
}
|
|
208
228
|
/**
|
|
@@ -344,6 +364,15 @@ function mergeRawSettings(base, overlay) {
|
|
|
344
364
|
}
|
|
345
365
|
}
|
|
346
366
|
}
|
|
367
|
+
// Merge code scanning: overlay fully replaces base (same semantics as mergeSettings)
|
|
368
|
+
if (overlay.codeScanning !== undefined) {
|
|
369
|
+
if (overlay.codeScanning === false) {
|
|
370
|
+
result.codeScanning = false;
|
|
371
|
+
}
|
|
372
|
+
else {
|
|
373
|
+
result.codeScanning = structuredClone(overlay.codeScanning);
|
|
374
|
+
}
|
|
375
|
+
}
|
|
347
376
|
// deleteOrphaned: overlay wins
|
|
348
377
|
if (overlay.deleteOrphaned !== undefined) {
|
|
349
378
|
result.deleteOrphaned = overlay.deleteOrphaned;
|
|
@@ -365,16 +394,18 @@ function mergeGroupSettings(rootSettings, groupNames, groupDefs) {
|
|
|
365
394
|
}
|
|
366
395
|
/**
|
|
367
396
|
* Evaluates a conditional group's `when` clause against a repo's effective groups.
|
|
368
|
-
*
|
|
369
|
-
*
|
|
397
|
+
* All specified operators must be satisfied: `allOf` (every listed group present),
|
|
398
|
+
* `anyOf` (at least one present), and `noneOf` (none of the listed groups present).
|
|
399
|
+
* Absent conditions are treated as satisfied.
|
|
370
400
|
*/
|
|
371
401
|
function evaluateWhenClause(when, effectiveGroups) {
|
|
372
|
-
// Defensive: if
|
|
373
|
-
if (!when.allOf && !when.anyOf)
|
|
402
|
+
// Defensive: if no condition is specified, don't match
|
|
403
|
+
if (!when.allOf && !when.anyOf && !when.noneOf)
|
|
374
404
|
return false;
|
|
375
405
|
const allOfSatisfied = !when.allOf || when.allOf.every((g) => effectiveGroups.has(g));
|
|
376
406
|
const anyOfSatisfied = !when.anyOf || when.anyOf.some((g) => effectiveGroups.has(g));
|
|
377
|
-
|
|
407
|
+
const noneOfSatisfied = !when.noneOf || when.noneOf.every((g) => !effectiveGroups.has(g));
|
|
408
|
+
return allOfSatisfied && anyOfSatisfied && noneOfSatisfied;
|
|
378
409
|
}
|
|
379
410
|
/**
|
|
380
411
|
* Merges matching conditional groups into the accumulated files/prOptions/settings.
|
package/dist/config/types.d.ts
CHANGED
|
@@ -259,6 +259,14 @@ export interface Label {
|
|
|
259
259
|
/** Rename target. Maps to GitHub API's new_name field. */
|
|
260
260
|
new_name?: string;
|
|
261
261
|
}
|
|
262
|
+
export type CodeScanningState = "configured" | "not-configured";
|
|
263
|
+
export type CodeScanningQuerySuite = "default" | "extended";
|
|
264
|
+
export type CodeScanningLanguage = "actions" | "c-cpp" | "csharp" | "go" | "java-kotlin" | "javascript-typescript" | "python" | "ruby" | "swift";
|
|
265
|
+
export interface CodeScanningSettings {
|
|
266
|
+
state: CodeScanningState;
|
|
267
|
+
querySuite?: CodeScanningQuerySuite;
|
|
268
|
+
languages?: CodeScanningLanguage[];
|
|
269
|
+
}
|
|
262
270
|
export interface RepoSettings {
|
|
263
271
|
/** GitHub rulesets keyed by name */
|
|
264
272
|
rulesets?: Record<string, Ruleset>;
|
|
@@ -266,6 +274,8 @@ export interface RepoSettings {
|
|
|
266
274
|
repo?: GitHubRepoSettings;
|
|
267
275
|
/** GitHub labels keyed by name */
|
|
268
276
|
labels?: Record<string, Label>;
|
|
277
|
+
/** GitHub code scanning default setup */
|
|
278
|
+
codeScanning?: CodeScanningSettings;
|
|
269
279
|
deleteOrphaned?: boolean;
|
|
270
280
|
}
|
|
271
281
|
export type ContentValue = Record<string, unknown> | string | string[];
|
|
@@ -305,6 +315,8 @@ export interface RawConditionalGroupWhen {
|
|
|
305
315
|
allOf?: string[];
|
|
306
316
|
/** At least one listed group must be present */
|
|
307
317
|
anyOf?: string[];
|
|
318
|
+
/** None of the listed groups may be present */
|
|
319
|
+
noneOf?: string[];
|
|
308
320
|
}
|
|
309
321
|
/** Conditional group: activates based on which groups a repo has */
|
|
310
322
|
export interface RawConditionalGroupConfig {
|
|
@@ -323,6 +335,7 @@ export interface RawRootSettings {
|
|
|
323
335
|
rulesets?: Record<string, Ruleset | false>;
|
|
324
336
|
repo?: GitHubRepoSettings | false;
|
|
325
337
|
labels?: Record<string, Label | false>;
|
|
338
|
+
codeScanning?: CodeScanningSettings | false;
|
|
326
339
|
deleteOrphaned?: boolean;
|
|
327
340
|
}
|
|
328
341
|
export interface RawRepoSettings {
|
|
@@ -333,6 +346,7 @@ export interface RawRepoSettings {
|
|
|
333
346
|
labels?: Record<string, Label | false> & {
|
|
334
347
|
inherit?: boolean;
|
|
335
348
|
};
|
|
349
|
+
codeScanning?: CodeScanningSettings | false;
|
|
336
350
|
deleteOrphaned?: boolean;
|
|
337
351
|
}
|
|
338
352
|
export interface RawRepoConfig {
|