@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.
- 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.d.ts +4 -0
- package/dist/config/merge.js +47 -23
- package/dist/config/normalizer.js +31 -1
- package/dist/config/types.d.ts +12 -0
- package/dist/config/validator.js +104 -46
- package/dist/config/validators/ruleset-validator.js +46 -16
- 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.d.ts
CHANGED
|
@@ -18,6 +18,10 @@ export declare function deepMerge(base: Record<string, unknown>, overlay: Record
|
|
|
18
18
|
* Strip xfg merge directive keys ($arrayMerge, $values) from an object.
|
|
19
19
|
* Works recursively on nested objects and arrays.
|
|
20
20
|
* Standard $-prefixed keys ($schema, $id, $ref, etc.) are preserved.
|
|
21
|
+
*
|
|
22
|
+
* When an unresolved directive object is found (only contains $arrayMerge + $values),
|
|
23
|
+
* it is replaced with the $values array. This handles the case where a directive
|
|
24
|
+
* had no base array to merge with.
|
|
21
25
|
*/
|
|
22
26
|
export declare function stripMergeDirectives(obj: Record<string, unknown>): Record<string, unknown>;
|
|
23
27
|
export declare function createMergeContext(defaultStrategy?: ArrayMergeStrategy): MergeContext;
|
package/dist/config/merge.js
CHANGED
|
@@ -14,6 +14,20 @@ const arrayMergeStrategies = new Map([
|
|
|
14
14
|
["append", (base, overlay) => [...base, ...overlay]],
|
|
15
15
|
["prepend", (base, overlay) => [...overlay, ...base]],
|
|
16
16
|
]);
|
|
17
|
+
/**
|
|
18
|
+
* Checks if a value is an unresolved $arrayMerge directive object
|
|
19
|
+
* (only contains $arrayMerge + $values keys, with a valid strategy and array values).
|
|
20
|
+
*/
|
|
21
|
+
function isUnresolvedDirective(value) {
|
|
22
|
+
if (!isPlainObject(value))
|
|
23
|
+
return false;
|
|
24
|
+
const keys = Object.keys(value);
|
|
25
|
+
return (keys.length === 2 &&
|
|
26
|
+
keys.every((k) => XFG_DIRECTIVES.has(k)) &&
|
|
27
|
+
typeof value.$arrayMerge === "string" &&
|
|
28
|
+
arrayMergeStrategies.has(value.$arrayMerge) &&
|
|
29
|
+
Array.isArray(value.$values));
|
|
30
|
+
}
|
|
17
31
|
function mergeArrays(base, overlay, strategy) {
|
|
18
32
|
const handler = arrayMergeStrategies.get(strategy);
|
|
19
33
|
if (handler) {
|
|
@@ -36,6 +50,11 @@ export function deepMerge(base, overlay, ctx) {
|
|
|
36
50
|
if (XFG_DIRECTIVES.has(key))
|
|
37
51
|
continue;
|
|
38
52
|
const baseValue = base[key];
|
|
53
|
+
// If base is an unresolved directive (from a previous layer with no base array),
|
|
54
|
+
// resolve it to its $values array before proceeding with merge logic.
|
|
55
|
+
const resolvedBase = isUnresolvedDirective(baseValue)
|
|
56
|
+
? baseValue.$values
|
|
57
|
+
: baseValue;
|
|
39
58
|
// Per-field $arrayMerge + $values directive
|
|
40
59
|
if (isPlainObject(overlayValue) && "$arrayMerge" in overlayValue) {
|
|
41
60
|
const strategy = overlayValue.$arrayMerge;
|
|
@@ -44,19 +63,19 @@ export function deepMerge(base, overlay, ctx) {
|
|
|
44
63
|
strategy === "append" ||
|
|
45
64
|
strategy === "prepend") &&
|
|
46
65
|
Array.isArray(values) &&
|
|
47
|
-
Array.isArray(
|
|
48
|
-
result[key] = mergeArrays(
|
|
66
|
+
Array.isArray(resolvedBase)) {
|
|
67
|
+
result[key] = mergeArrays(resolvedBase, values, strategy);
|
|
49
68
|
continue;
|
|
50
69
|
}
|
|
51
70
|
}
|
|
52
71
|
// Both are arrays — use default strategy
|
|
53
|
-
if (Array.isArray(
|
|
54
|
-
result[key] = mergeArrays(
|
|
72
|
+
if (Array.isArray(resolvedBase) && Array.isArray(overlayValue)) {
|
|
73
|
+
result[key] = mergeArrays(resolvedBase, overlayValue, ctx.defaultArrayStrategy);
|
|
55
74
|
continue;
|
|
56
75
|
}
|
|
57
76
|
// Both are plain objects — recurse
|
|
58
|
-
if (isPlainObject(
|
|
59
|
-
result[key] = deepMerge(
|
|
77
|
+
if (isPlainObject(resolvedBase) && isPlainObject(overlayValue)) {
|
|
78
|
+
result[key] = deepMerge(resolvedBase, overlayValue, ctx);
|
|
60
79
|
continue;
|
|
61
80
|
}
|
|
62
81
|
// Otherwise, overlay wins (including null values)
|
|
@@ -68,6 +87,10 @@ export function deepMerge(base, overlay, ctx) {
|
|
|
68
87
|
* Strip xfg merge directive keys ($arrayMerge, $values) from an object.
|
|
69
88
|
* Works recursively on nested objects and arrays.
|
|
70
89
|
* Standard $-prefixed keys ($schema, $id, $ref, etc.) are preserved.
|
|
90
|
+
*
|
|
91
|
+
* When an unresolved directive object is found (only contains $arrayMerge + $values),
|
|
92
|
+
* it is replaced with the $values array. This handles the case where a directive
|
|
93
|
+
* had no base array to merge with.
|
|
71
94
|
*/
|
|
72
95
|
export function stripMergeDirectives(obj) {
|
|
73
96
|
const result = {};
|
|
@@ -76,7 +99,13 @@ export function stripMergeDirectives(obj) {
|
|
|
76
99
|
if (XFG_DIRECTIVES.has(key))
|
|
77
100
|
continue;
|
|
78
101
|
if (isPlainObject(value)) {
|
|
79
|
-
|
|
102
|
+
if (isUnresolvedDirective(value)) {
|
|
103
|
+
// Resolve to the $values array, stripping directives from items
|
|
104
|
+
result[key] = value.$values.map((item) => isPlainObject(item) ? stripMergeDirectives(item) : item);
|
|
105
|
+
}
|
|
106
|
+
else {
|
|
107
|
+
result[key] = stripMergeDirectives(value);
|
|
108
|
+
}
|
|
80
109
|
}
|
|
81
110
|
else if (Array.isArray(value)) {
|
|
82
111
|
result[key] = value.map((item) => isPlainObject(item) ? stripMergeDirectives(item) : item);
|
|
@@ -111,23 +140,18 @@ export function mergeTextContent(base, overlay, strategy = "replace") {
|
|
|
111
140
|
if (typeof overlay === "string") {
|
|
112
141
|
return overlay;
|
|
113
142
|
}
|
|
114
|
-
// If
|
|
115
|
-
if (Array.isArray(
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
default:
|
|
125
|
-
return overlay;
|
|
126
|
-
}
|
|
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;
|
|
127
153
|
}
|
|
128
|
-
// Base is string, overlay is array - overlay replaces
|
|
129
|
-
return overlay;
|
|
130
154
|
}
|
|
131
|
-
//
|
|
155
|
+
// Base is string, overlay is array - overlay replaces
|
|
132
156
|
return overlay;
|
|
133
157
|
}
|
|
@@ -170,7 +170,8 @@ export function mergeSettings(root, perRepo) {
|
|
|
170
170
|
if (!inheritRulesets && !repoRuleset && rootRuleset) {
|
|
171
171
|
continue;
|
|
172
172
|
}
|
|
173
|
-
|
|
173
|
+
const merged = mergeRuleset(rootRuleset, repoRuleset);
|
|
174
|
+
result.rulesets[name] = stripMergeDirectives(merged);
|
|
174
175
|
}
|
|
175
176
|
// Clean up empty rulesets object
|
|
176
177
|
if (Object.keys(result.rulesets).length === 0) {
|
|
@@ -202,6 +203,26 @@ export function mergeSettings(root, perRepo) {
|
|
|
202
203
|
if (mergedLabels) {
|
|
203
204
|
result.labels = mergedLabels;
|
|
204
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
|
+
}
|
|
205
226
|
return Object.keys(result).length > 0 ? result : undefined;
|
|
206
227
|
}
|
|
207
228
|
/**
|
|
@@ -343,6 +364,15 @@ function mergeRawSettings(base, overlay) {
|
|
|
343
364
|
}
|
|
344
365
|
}
|
|
345
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
|
+
}
|
|
346
376
|
// deleteOrphaned: overlay wins
|
|
347
377
|
if (overlay.deleteOrphaned !== undefined) {
|
|
348
378
|
result.deleteOrphaned = overlay.deleteOrphaned;
|
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[];
|
|
@@ -323,6 +333,7 @@ export interface RawRootSettings {
|
|
|
323
333
|
rulesets?: Record<string, Ruleset | false>;
|
|
324
334
|
repo?: GitHubRepoSettings | false;
|
|
325
335
|
labels?: Record<string, Label | false>;
|
|
336
|
+
codeScanning?: CodeScanningSettings | false;
|
|
326
337
|
deleteOrphaned?: boolean;
|
|
327
338
|
}
|
|
328
339
|
export interface RawRepoSettings {
|
|
@@ -333,6 +344,7 @@ export interface RawRepoSettings {
|
|
|
333
344
|
labels?: Record<string, Label | false> & {
|
|
334
345
|
inherit?: boolean;
|
|
335
346
|
};
|
|
347
|
+
codeScanning?: CodeScanningSettings | false;
|
|
336
348
|
deleteOrphaned?: boolean;
|
|
337
349
|
}
|
|
338
350
|
export interface RawRepoConfig {
|