@aspruyt/xfg 4.0.1 → 4.0.4
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/README.md +1 -2
- package/dist/cli/index.d.ts +1 -1
- package/dist/cli/index.js +0 -6
- package/dist/cli/program.js +3 -2
- package/dist/cli/settings-report-builder.js +4 -4
- package/dist/cli/sync-command.js +72 -36
- package/dist/cli/sync-report-builder.d.ts +2 -6
- package/dist/cli/types.d.ts +2 -14
- package/dist/cli/types.js +1 -9
- package/dist/config/file-reference-resolver.js +13 -23
- package/dist/config/formatter.d.ts +0 -6
- package/dist/config/formatter.js +0 -9
- package/dist/config/index.d.ts +1 -2
- package/dist/config/index.js +0 -2
- package/dist/config/loader.d.ts +1 -1
- package/dist/config/loader.js +3 -3
- package/dist/config/normalizer.d.ts +1 -1
- package/dist/config/normalizer.js +44 -57
- package/dist/config/validator.d.ts +1 -1
- package/dist/config/validator.js +120 -121
- package/dist/config/validators/file-validator.d.ts +2 -4
- package/dist/config/validators/file-validator.js +3 -7
- package/dist/config/validators/repo-settings-validator.js +1 -1
- package/dist/config/validators/ruleset-validator.js +28 -12
- package/dist/index.d.ts +3 -1
- package/dist/index.js +0 -1
- package/dist/lifecycle/ado-migration-source.d.ts +2 -1
- package/dist/lifecycle/ado-migration-source.js +7 -5
- package/dist/lifecycle/github-lifecycle-provider.d.ts +6 -3
- package/dist/lifecycle/github-lifecycle-provider.js +29 -19
- package/dist/lifecycle/lifecycle-formatter.js +2 -1
- package/dist/lifecycle/lifecycle-helpers.d.ts +5 -1
- package/dist/lifecycle/lifecycle-helpers.js +4 -4
- package/dist/lifecycle/repo-lifecycle-factory.d.ts +4 -4
- package/dist/lifecycle/repo-lifecycle-factory.js +12 -9
- package/dist/lifecycle/repo-lifecycle-manager.d.ts +4 -1
- package/dist/lifecycle/repo-lifecycle-manager.js +11 -7
- package/dist/lifecycle/types.d.ts +0 -15
- package/dist/output/github-summary.d.ts +6 -5
- package/dist/output/github-summary.js +36 -52
- package/dist/output/index.d.ts +2 -2
- package/dist/output/index.js +1 -1
- package/dist/output/lifecycle-report.d.ts +2 -12
- package/dist/output/lifecycle-report.js +18 -35
- package/dist/output/settings-report.d.ts +4 -4
- package/dist/output/settings-report.js +6 -6
- package/dist/output/sync-report.d.ts +4 -6
- package/dist/output/sync-report.js +2 -2
- package/dist/output/unified-summary.d.ts +1 -0
- package/dist/output/unified-summary.js +8 -8
- package/dist/settings/base-processor.d.ts +1 -1
- package/dist/settings/base-processor.js +1 -1
- package/dist/settings/index.d.ts +3 -3
- package/dist/settings/index.js +3 -3
- package/dist/settings/labels/diff.js +3 -2
- package/dist/settings/labels/formatter.js +3 -3
- package/dist/settings/labels/github-labels-strategy.d.ts +2 -23
- package/dist/settings/labels/github-labels-strategy.js +8 -28
- package/dist/settings/labels/index.d.ts +1 -0
- package/dist/settings/labels/index.js +2 -0
- package/dist/settings/labels/processor.d.ts +2 -2
- package/dist/settings/labels/processor.js +3 -4
- package/dist/settings/labels/types.d.ts +0 -3
- package/dist/settings/repo-settings/diff.d.ts +1 -1
- package/dist/settings/repo-settings/diff.js +2 -2
- package/dist/settings/repo-settings/formatter.d.ts +1 -1
- package/dist/settings/repo-settings/formatter.js +4 -4
- package/dist/settings/repo-settings/github-repo-settings-strategy.d.ts +2 -7
- package/dist/settings/repo-settings/github-repo-settings-strategy.js +9 -17
- package/dist/settings/repo-settings/index.d.ts +1 -0
- package/dist/settings/repo-settings/index.js +2 -0
- package/dist/settings/repo-settings/processor.d.ts +2 -2
- package/dist/settings/repo-settings/processor.js +5 -6
- package/dist/settings/repo-settings/types.d.ts +9 -13
- package/dist/settings/repo-settings/types.js +1 -14
- package/dist/settings/rulesets/diff-algorithm.d.ts +0 -1
- package/dist/settings/rulesets/diff-algorithm.js +6 -8
- package/dist/settings/rulesets/formatter.js +15 -51
- package/dist/settings/rulesets/github-ruleset-strategy.d.ts +2 -20
- package/dist/settings/rulesets/github-ruleset-strategy.js +6 -30
- package/dist/settings/rulesets/index.d.ts +2 -1
- package/dist/settings/rulesets/index.js +3 -1
- package/dist/settings/rulesets/processor.d.ts +2 -2
- package/dist/settings/rulesets/processor.js +3 -4
- package/dist/{vcs → shared}/branch-utils.js +5 -4
- package/dist/shared/command-executor.d.ts +2 -1
- package/dist/shared/command-executor.js +9 -5
- package/dist/shared/env.d.ts +6 -6
- package/dist/shared/env.js +10 -17
- package/dist/shared/errors.d.ts +26 -0
- package/dist/shared/errors.js +34 -0
- package/dist/shared/gh-api-utils.d.ts +21 -14
- package/dist/shared/gh-api-utils.js +33 -22
- package/dist/shared/index.d.ts +9 -2
- package/dist/shared/index.js +16 -2
- package/dist/shared/logger.d.ts +24 -1
- package/dist/shared/logger.js +8 -3
- package/dist/shared/repo-detector.js +9 -11
- package/dist/shared/retry-utils.d.ts +5 -7
- package/dist/shared/retry-utils.js +3 -10
- package/dist/shared/shell-utils.d.ts +0 -3
- package/dist/shared/shell-utils.js +2 -4
- package/dist/shared/type-guards.d.ts +2 -9
- package/dist/shared/type-guards.js +0 -6
- package/dist/shared/xfg-template.d.ts +2 -2
- package/dist/shared/xfg-template.js +2 -1
- package/dist/sync/auth-options-builder.d.ts +3 -2
- package/dist/sync/auth-options-builder.js +14 -10
- package/dist/sync/branch-manager.d.ts +12 -7
- package/dist/sync/branch-manager.js +4 -7
- package/dist/sync/commit-message.d.ts +1 -1
- package/dist/sync/commit-push-manager.d.ts +8 -2
- package/dist/sync/commit-push-manager.js +6 -5
- package/dist/sync/file-sync-orchestrator.js +17 -21
- package/dist/sync/file-writer.js +3 -5
- package/dist/sync/index.d.ts +1 -1
- package/dist/sync/manifest-manager.d.ts +1 -0
- package/dist/sync/manifest.d.ts +4 -7
- package/dist/sync/manifest.js +42 -45
- package/dist/sync/repository-processor.d.ts +5 -2
- package/dist/sync/repository-processor.js +11 -17
- package/dist/sync/repository-session.js +2 -1
- package/dist/sync/sync-workflow.d.ts +2 -2
- package/dist/sync/sync-workflow.js +16 -23
- package/dist/sync/types.d.ts +20 -25
- package/dist/vcs/authenticated-git-ops.d.ts +3 -4
- package/dist/vcs/authenticated-git-ops.js +5 -1
- package/dist/vcs/azure-pr-strategy.d.ts +6 -1
- package/dist/vcs/azure-pr-strategy.js +38 -31
- package/dist/vcs/commit-strategy-selector.d.ts +10 -19
- package/dist/vcs/commit-strategy-selector.js +8 -24
- package/dist/vcs/git-commit-strategy.d.ts +1 -1
- package/dist/vcs/git-commit-strategy.js +1 -3
- package/dist/vcs/git-ops.d.ts +4 -8
- package/dist/vcs/git-ops.js +18 -22
- package/dist/vcs/github-app-token-manager.js +9 -8
- package/dist/vcs/github-pr-strategy.js +18 -11
- package/dist/vcs/gitlab-pr-strategy.d.ts +1 -1
- package/dist/vcs/gitlab-pr-strategy.js +14 -7
- package/dist/vcs/graphql-commit-strategy.d.ts +1 -7
- package/dist/vcs/graphql-commit-strategy.js +24 -32
- package/dist/vcs/index.d.ts +2 -1
- package/dist/vcs/pr-creator.d.ts +6 -9
- package/dist/vcs/pr-strategy-factory.d.ts +1 -1
- package/dist/vcs/pr-strategy-factory.js +2 -1
- package/dist/vcs/pr-strategy.d.ts +1 -1
- package/dist/vcs/pr-strategy.js +2 -3
- package/dist/vcs/types.d.ts +6 -10
- package/package.json +2 -2
- package/dist/config/errors.d.ts +0 -9
- package/dist/config/errors.js +0 -11
- /package/dist/{vcs → shared}/branch-utils.d.ts +0 -0
|
@@ -104,17 +104,12 @@ function mergeRuleset(root, perRepo) {
|
|
|
104
104
|
return structuredClone(perRepo ?? {});
|
|
105
105
|
if (!perRepo)
|
|
106
106
|
return structuredClone(root);
|
|
107
|
-
// Deep merge using the existing merge utility with replace strategy
|
|
107
|
+
// Deep merge using the existing merge utility with replace strategy.
|
|
108
|
+
// deepMerge operates on Record<string, unknown> — the cast is safe because
|
|
109
|
+
// merging two Ruleset-shaped objects preserves the Ruleset structure.
|
|
108
110
|
const ctx = createMergeContext("replace");
|
|
109
|
-
|
|
110
|
-
return merged;
|
|
111
|
+
return deepMerge(structuredClone(root), perRepo, ctx);
|
|
111
112
|
}
|
|
112
|
-
/**
|
|
113
|
-
* Merges root and per-repo label configs.
|
|
114
|
-
* Per-repo labels override root labels by name.
|
|
115
|
-
* inherit: false skips all root labels.
|
|
116
|
-
* label: false opts out of a specific root label.
|
|
117
|
-
*/
|
|
118
113
|
function mergeLabels(rootLabels, repoLabels) {
|
|
119
114
|
if (!rootLabels && !repoLabels)
|
|
120
115
|
return undefined;
|
|
@@ -135,12 +130,12 @@ function mergeLabels(rootLabels, repoLabels) {
|
|
|
135
130
|
continue;
|
|
136
131
|
if (!inheritLabels && !repoLabel && rootLabel)
|
|
137
132
|
continue;
|
|
138
|
-
const
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
merged
|
|
133
|
+
const base = rootLabel && typeof rootLabel === "object" ? rootLabel : {};
|
|
134
|
+
const overlay = repoLabel && typeof repoLabel === "object" ? repoLabel : {};
|
|
135
|
+
const color = (overlay.color ?? base.color ?? "")
|
|
136
|
+
.replace(/^#/, "")
|
|
137
|
+
.toLowerCase();
|
|
138
|
+
const merged = { ...base, ...overlay, color };
|
|
144
139
|
result[name] = merged;
|
|
145
140
|
}
|
|
146
141
|
return Object.keys(result).length > 0 ? result : undefined;
|
|
@@ -363,11 +358,41 @@ function mergeGroupSettings(rootSettings, groupNames, groupDefs) {
|
|
|
363
358
|
}
|
|
364
359
|
return accumulated;
|
|
365
360
|
}
|
|
361
|
+
/**
|
|
362
|
+
* Resolves a single file entry by merging root config with repo overrides.
|
|
363
|
+
* Returns null if the file should be skipped.
|
|
364
|
+
*/
|
|
365
|
+
function resolveFileEntry(fileName, fileConfig, repoOverride, inheritFiles, globalDeleteOrphaned, env) {
|
|
366
|
+
if (repoOverride === false)
|
|
367
|
+
return null;
|
|
368
|
+
if (!inheritFiles && !repoOverride)
|
|
369
|
+
return null;
|
|
370
|
+
const fileStrategy = fileConfig.mergeStrategy ?? "replace";
|
|
371
|
+
let mergedContent = resolveFileContent(fileConfig.content, repoOverride, fileStrategy);
|
|
372
|
+
if (mergedContent !== null) {
|
|
373
|
+
mergedContent = interpolateContent(mergedContent, { strict: true, env });
|
|
374
|
+
}
|
|
375
|
+
return {
|
|
376
|
+
fileName,
|
|
377
|
+
content: mergedContent,
|
|
378
|
+
createOnly: repoOverride?.createOnly ?? fileConfig.createOnly,
|
|
379
|
+
executable: repoOverride?.executable ?? fileConfig.executable,
|
|
380
|
+
header: normalizeHeader(repoOverride?.header ?? fileConfig.header),
|
|
381
|
+
schemaUrl: repoOverride?.schemaUrl ?? fileConfig.schemaUrl,
|
|
382
|
+
template: repoOverride?.template ?? fileConfig.template,
|
|
383
|
+
vars: fileConfig.vars || repoOverride?.vars
|
|
384
|
+
? { ...fileConfig.vars, ...repoOverride?.vars }
|
|
385
|
+
: undefined,
|
|
386
|
+
deleteOrphaned: repoOverride?.deleteOrphaned ??
|
|
387
|
+
fileConfig.deleteOrphaned ??
|
|
388
|
+
globalDeleteOrphaned,
|
|
389
|
+
};
|
|
390
|
+
}
|
|
366
391
|
/**
|
|
367
392
|
* Normalizes raw config into expanded, merged config.
|
|
368
393
|
* Pipeline: expand git arrays -> merge content -> interpolate env vars
|
|
369
394
|
*/
|
|
370
|
-
export function normalizeConfig(raw) {
|
|
395
|
+
export function normalizeConfig(raw, env) {
|
|
371
396
|
const expandedRepos = [];
|
|
372
397
|
for (const rawRepo of raw.repos) {
|
|
373
398
|
const gitUrls = Array.isArray(rawRepo.git) ? rawRepo.git : [rawRepo.git];
|
|
@@ -389,47 +414,9 @@ export function normalizeConfig(raw) {
|
|
|
389
414
|
// Skip reserved key
|
|
390
415
|
if (fileName === "inherit")
|
|
391
416
|
continue;
|
|
392
|
-
const
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
continue;
|
|
396
|
-
}
|
|
397
|
-
// Skip if inherit: false and no repo-specific override
|
|
398
|
-
if (!inheritFiles && !repoOverride) {
|
|
399
|
-
continue;
|
|
400
|
-
}
|
|
401
|
-
const fileConfig = effectiveRootFiles[fileName];
|
|
402
|
-
const fileStrategy = fileConfig.mergeStrategy ?? "replace";
|
|
403
|
-
let mergedContent = resolveFileContent(fileConfig.content, repoOverride, fileStrategy);
|
|
404
|
-
if (mergedContent !== null) {
|
|
405
|
-
mergedContent = interpolateContent(mergedContent, { strict: true });
|
|
406
|
-
}
|
|
407
|
-
// Resolve fields: per-repo overrides root level
|
|
408
|
-
const createOnly = repoOverride?.createOnly ?? fileConfig.createOnly;
|
|
409
|
-
const executable = repoOverride?.executable ?? fileConfig.executable;
|
|
410
|
-
const header = normalizeHeader(repoOverride?.header ?? fileConfig.header);
|
|
411
|
-
const schemaUrl = repoOverride?.schemaUrl ?? fileConfig.schemaUrl;
|
|
412
|
-
// Template: per-repo overrides root level
|
|
413
|
-
const template = repoOverride?.template ?? fileConfig.template;
|
|
414
|
-
// Vars: merge root + per-repo (per-repo takes precedence)
|
|
415
|
-
const vars = fileConfig.vars || repoOverride?.vars
|
|
416
|
-
? { ...fileConfig.vars, ...repoOverride?.vars }
|
|
417
|
-
: undefined;
|
|
418
|
-
// deleteOrphaned: per-repo overrides per-file overrides global
|
|
419
|
-
const deleteOrphaned = repoOverride?.deleteOrphaned ??
|
|
420
|
-
fileConfig.deleteOrphaned ??
|
|
421
|
-
raw.deleteOrphaned;
|
|
422
|
-
files.push({
|
|
423
|
-
fileName,
|
|
424
|
-
content: mergedContent,
|
|
425
|
-
createOnly,
|
|
426
|
-
executable,
|
|
427
|
-
header,
|
|
428
|
-
schemaUrl,
|
|
429
|
-
template,
|
|
430
|
-
vars,
|
|
431
|
-
deleteOrphaned,
|
|
432
|
-
});
|
|
417
|
+
const entry = resolveFileEntry(fileName, effectiveRootFiles[fileName], rawRepo.files?.[fileName], inheritFiles, raw.deleteOrphaned, env);
|
|
418
|
+
if (entry)
|
|
419
|
+
files.push(entry);
|
|
433
420
|
}
|
|
434
421
|
// Merge PR options: per-repo overrides effective (root + groups)
|
|
435
422
|
const prOptions = mergePROptions(effectivePROptions, rawRepo.prOptions);
|
|
@@ -6,7 +6,7 @@ import type { RawConfig, RawRepoSettings, RawRootSettings } from "./types.js";
|
|
|
6
6
|
export declare function validateRawConfig(config: RawConfig): void;
|
|
7
7
|
/**
|
|
8
8
|
* Validates that config is suitable for the sync command.
|
|
9
|
-
* @throws
|
|
9
|
+
* @throws ValidationError if neither files nor settings are present
|
|
10
10
|
*/
|
|
11
11
|
export declare function validateForSync(config: RawConfig): void;
|
|
12
12
|
/**
|
package/dist/config/validator.js
CHANGED
|
@@ -2,7 +2,8 @@ import { isTextContent, isObjectContent, isStructuredFileExtension, validateFile
|
|
|
2
2
|
import { validateRepoSettings } from "./validators/repo-settings-validator.js";
|
|
3
3
|
import { validateRuleset } from "./validators/ruleset-validator.js";
|
|
4
4
|
import { escapeRegExp } from "../shared/shell-utils.js";
|
|
5
|
-
import {
|
|
5
|
+
import { isPlainObject } from "../shared/type-guards.js";
|
|
6
|
+
import { ValidationError } from "../shared/errors.js";
|
|
6
7
|
// Pattern for valid config ID: alphanumeric, hyphens, underscores
|
|
7
8
|
const CONFIG_ID_PATTERN = /^[a-zA-Z0-9_-]+$/;
|
|
8
9
|
const CONFIG_ID_MAX_LENGTH = 64;
|
|
@@ -90,9 +91,7 @@ function validateFileConfigFields(fileConfig, fileName, context) {
|
|
|
90
91
|
}
|
|
91
92
|
}
|
|
92
93
|
if (fileConfig.vars !== undefined) {
|
|
93
|
-
if (
|
|
94
|
-
fileConfig.vars === null ||
|
|
95
|
-
Array.isArray(fileConfig.vars)) {
|
|
94
|
+
if (!isPlainObject(fileConfig.vars)) {
|
|
96
95
|
throw new ValidationError(`${context} file '${fileName}' vars must be an object with string values`);
|
|
97
96
|
}
|
|
98
97
|
for (const [key, value] of Object.entries(fileConfig.vars)) {
|
|
@@ -106,7 +105,7 @@ function validateFileConfigFields(fileConfig, fileName, context) {
|
|
|
106
105
|
* Validates a single label configuration.
|
|
107
106
|
*/
|
|
108
107
|
function validateLabel(label, name, context) {
|
|
109
|
-
if (
|
|
108
|
+
if (!isPlainObject(label)) {
|
|
110
109
|
throw new ValidationError(`${context}: label '${name}' must be an object`);
|
|
111
110
|
}
|
|
112
111
|
const l = label;
|
|
@@ -139,26 +138,21 @@ function buildRootSettingsContext(config) {
|
|
|
139
138
|
/**
|
|
140
139
|
* Validates settings object containing rulesets, labels, and repo settings.
|
|
141
140
|
*/
|
|
142
|
-
function validateSettings(settings, context,
|
|
143
|
-
if (
|
|
144
|
-
settings === null ||
|
|
145
|
-
Array.isArray(settings)) {
|
|
141
|
+
function validateSettings(settings, context, rootCtx) {
|
|
142
|
+
if (!isPlainObject(settings)) {
|
|
146
143
|
throw new ValidationError(`${context}: settings must be an object`);
|
|
147
144
|
}
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
if (typeof s.rulesets !== "object" ||
|
|
151
|
-
s.rulesets === null ||
|
|
152
|
-
Array.isArray(s.rulesets)) {
|
|
145
|
+
if (settings.rulesets !== undefined) {
|
|
146
|
+
if (!isPlainObject(settings.rulesets)) {
|
|
153
147
|
throw new ValidationError(`${context}: rulesets must be an object`);
|
|
154
148
|
}
|
|
155
|
-
const rulesets =
|
|
149
|
+
const rulesets = settings.rulesets;
|
|
156
150
|
for (const [name, ruleset] of Object.entries(rulesets)) {
|
|
157
151
|
// Skip reserved key
|
|
158
152
|
if (name === "inherit")
|
|
159
153
|
continue;
|
|
160
154
|
if (ruleset === false) {
|
|
161
|
-
if (
|
|
155
|
+
if (rootCtx && !rootCtx.rulesetNames.includes(name)) {
|
|
162
156
|
throw new ValidationError(`${context}: Cannot opt out of '${name}' - not defined in root settings.rulesets`);
|
|
163
157
|
}
|
|
164
158
|
continue; // Skip further validation for false entries
|
|
@@ -166,18 +160,16 @@ function validateSettings(settings, context, rootRulesetNames, hasRootRepoSettin
|
|
|
166
160
|
validateRuleset(ruleset, name, context);
|
|
167
161
|
}
|
|
168
162
|
}
|
|
169
|
-
if (
|
|
170
|
-
if (
|
|
171
|
-
s.labels === null ||
|
|
172
|
-
Array.isArray(s.labels)) {
|
|
163
|
+
if (settings.labels !== undefined) {
|
|
164
|
+
if (!isPlainObject(settings.labels)) {
|
|
173
165
|
throw new ValidationError(`${context}: labels must be an object`);
|
|
174
166
|
}
|
|
175
|
-
const labels =
|
|
167
|
+
const labels = settings.labels;
|
|
176
168
|
for (const [name, label] of Object.entries(labels)) {
|
|
177
169
|
if (name === "inherit")
|
|
178
170
|
continue;
|
|
179
171
|
if (label === false) {
|
|
180
|
-
if (
|
|
172
|
+
if (rootCtx && !rootCtx.labelNames.includes(name)) {
|
|
181
173
|
throw new ValidationError(`${context}: Cannot opt out of label '${name}' - not defined in root settings.labels`);
|
|
182
174
|
}
|
|
183
175
|
continue;
|
|
@@ -185,23 +177,24 @@ function validateSettings(settings, context, rootRulesetNames, hasRootRepoSettin
|
|
|
185
177
|
validateLabel(label, name, context);
|
|
186
178
|
}
|
|
187
179
|
}
|
|
188
|
-
if (
|
|
180
|
+
if (settings.deleteOrphaned !== undefined &&
|
|
181
|
+
typeof settings.deleteOrphaned !== "boolean") {
|
|
189
182
|
throw new ValidationError(`${context}: settings.deleteOrphaned must be a boolean`);
|
|
190
183
|
}
|
|
191
|
-
if (
|
|
192
|
-
if (
|
|
193
|
-
if (!
|
|
184
|
+
if (settings.repo !== undefined) {
|
|
185
|
+
if (settings.repo === false) {
|
|
186
|
+
if (!rootCtx) {
|
|
194
187
|
// Root level — repo: false not valid here
|
|
195
188
|
throw new ValidationError(`${context}: repo: false is not valid at root level. Define repo settings or remove the field.`);
|
|
196
189
|
}
|
|
197
190
|
// Per-repo level — check root has repo settings to opt out of
|
|
198
|
-
if (!
|
|
191
|
+
if (!rootCtx.hasRepoSettings) {
|
|
199
192
|
throw new ValidationError(`${context}: Cannot opt out of repo settings — not defined in root settings.repo`);
|
|
200
193
|
}
|
|
201
194
|
// Valid opt-out, skip further repo validation
|
|
202
195
|
}
|
|
203
196
|
else {
|
|
204
|
-
validateRepoSettings(
|
|
197
|
+
validateRepoSettings(settings.repo, context);
|
|
205
198
|
}
|
|
206
199
|
}
|
|
207
200
|
}
|
|
@@ -225,7 +218,7 @@ function validateRootFiles(config) {
|
|
|
225
218
|
for (const fileName of Object.keys(config.files)) {
|
|
226
219
|
validateFileName(fileName);
|
|
227
220
|
const fileConfig = config.files[fileName];
|
|
228
|
-
if (!fileConfig
|
|
221
|
+
if (!isPlainObject(fileConfig)) {
|
|
229
222
|
throw new ValidationError(`File '${fileName}' must have a configuration object`);
|
|
230
223
|
}
|
|
231
224
|
validateFileConfigFields(fileConfig, fileName, `File '${fileName}':`);
|
|
@@ -276,9 +269,7 @@ function validatePrOptions(config) {
|
|
|
276
269
|
function validateGroups(config) {
|
|
277
270
|
if (config.groups === undefined)
|
|
278
271
|
return;
|
|
279
|
-
if (
|
|
280
|
-
config.groups === null ||
|
|
281
|
-
Array.isArray(config.groups)) {
|
|
272
|
+
if (!isPlainObject(config.groups)) {
|
|
282
273
|
throw new ValidationError("groups must be an object");
|
|
283
274
|
}
|
|
284
275
|
const rootCtx = buildRootSettingsContext(config);
|
|
@@ -298,18 +289,20 @@ function validateGroups(config) {
|
|
|
298
289
|
}
|
|
299
290
|
}
|
|
300
291
|
if (group.settings !== undefined) {
|
|
301
|
-
validateSettings(group.settings, `groups.${groupName}`, rootCtx
|
|
292
|
+
validateSettings(group.settings, `groups.${groupName}`, rootCtx);
|
|
302
293
|
}
|
|
303
294
|
}
|
|
304
295
|
}
|
|
305
|
-
function
|
|
296
|
+
function validateRepoGitField(repo, index) {
|
|
306
297
|
if (!repo.git) {
|
|
307
298
|
throw new ValidationError(`Repo at index ${index} missing required field: git`);
|
|
308
299
|
}
|
|
309
300
|
if (Array.isArray(repo.git) && repo.git.length === 0) {
|
|
310
301
|
throw new ValidationError(`Repo at index ${index} has empty git array`);
|
|
311
302
|
}
|
|
312
|
-
|
|
303
|
+
return getGitDisplayName(repo.git);
|
|
304
|
+
}
|
|
305
|
+
function validateRepoOrigins(config, repo, repoLabel) {
|
|
313
306
|
if (repo.upstream !== undefined && repo.source !== undefined) {
|
|
314
307
|
throw new ValidationError(`Repo ${repoLabel}: 'upstream' and 'source' are mutually exclusive. ` +
|
|
315
308
|
`Use 'upstream' to fork, or 'source' to migrate, not both.`);
|
|
@@ -336,81 +329,99 @@ function validateRepoEntry(config, repo, index) {
|
|
|
336
329
|
`Migration from GitHub is not supported. Currently supported sources: Azure DevOps`);
|
|
337
330
|
}
|
|
338
331
|
}
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
332
|
+
}
|
|
333
|
+
function validateRepoGroups(config, repo, index) {
|
|
334
|
+
if (repo.groups === undefined)
|
|
335
|
+
return;
|
|
336
|
+
if (!Array.isArray(repo.groups) ||
|
|
337
|
+
!repo.groups.every((g) => typeof g === "string")) {
|
|
338
|
+
throw new ValidationError(`Repo at index ${index}: groups must be an array of strings`);
|
|
339
|
+
}
|
|
340
|
+
const seen = new Set();
|
|
341
|
+
for (const groupName of repo.groups) {
|
|
342
|
+
if (!config.groups || !config.groups[groupName]) {
|
|
343
|
+
throw new ValidationError(`Repo at index ${index}: group '${groupName}' is not defined in root 'groups'`);
|
|
343
344
|
}
|
|
344
|
-
|
|
345
|
+
if (seen.has(groupName)) {
|
|
346
|
+
throw new ValidationError(`Repo at index ${index}: duplicate group '${groupName}'`);
|
|
347
|
+
}
|
|
348
|
+
seen.add(groupName);
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
function validateRepoFiles(config, repo, index, repoLabel) {
|
|
352
|
+
if (!repo.files)
|
|
353
|
+
return;
|
|
354
|
+
if (!isPlainObject(repo.files)) {
|
|
355
|
+
throw new ValidationError(`Repo at index ${index}: files must be an object`);
|
|
356
|
+
}
|
|
357
|
+
const knownFiles = new Set(config.files ? Object.keys(config.files) : []);
|
|
358
|
+
if (repo.groups && config.groups) {
|
|
345
359
|
for (const groupName of repo.groups) {
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
}
|
|
352
|
-
seen.add(groupName);
|
|
353
|
-
}
|
|
354
|
-
}
|
|
355
|
-
if (repo.files) {
|
|
356
|
-
if (typeof repo.files !== "object" || Array.isArray(repo.files)) {
|
|
357
|
-
throw new ValidationError(`Repo at index ${index}: files must be an object`);
|
|
358
|
-
}
|
|
359
|
-
const knownFiles = new Set(config.files ? Object.keys(config.files) : []);
|
|
360
|
-
if (repo.groups && config.groups) {
|
|
361
|
-
for (const groupName of repo.groups) {
|
|
362
|
-
const group = config.groups[groupName];
|
|
363
|
-
if (group?.files) {
|
|
364
|
-
for (const fn of Object.keys(group.files)) {
|
|
365
|
-
if (fn !== "inherit")
|
|
366
|
-
knownFiles.add(fn);
|
|
367
|
-
}
|
|
360
|
+
const group = config.groups[groupName];
|
|
361
|
+
if (group?.files) {
|
|
362
|
+
for (const fn of Object.keys(group.files)) {
|
|
363
|
+
if (fn !== "inherit")
|
|
364
|
+
knownFiles.add(fn);
|
|
368
365
|
}
|
|
369
366
|
}
|
|
370
367
|
}
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
}
|
|
377
|
-
continue;
|
|
378
|
-
}
|
|
379
|
-
if (!knownFiles.has(fileName)) {
|
|
380
|
-
throw new ValidationError(`Repo at index ${index} references undefined file '${fileName}'. File must be defined in root 'files' object or in a referenced group.`);
|
|
381
|
-
}
|
|
382
|
-
const fileOverride = repo.files[fileName];
|
|
383
|
-
if (fileOverride === false) {
|
|
384
|
-
continue;
|
|
385
|
-
}
|
|
386
|
-
if (fileOverride.override && !fileOverride.content) {
|
|
387
|
-
throw new ValidationError(`Repo ${repoLabel} has override: true for file '${fileName}' but no content defined. ` +
|
|
388
|
-
`Use content: "" for an empty text file override, or content: {} for an empty JSON/YAML override.`);
|
|
368
|
+
}
|
|
369
|
+
for (const fileName of Object.keys(repo.files)) {
|
|
370
|
+
if (fileName === "inherit") {
|
|
371
|
+
const inheritValue = repo.files.inherit;
|
|
372
|
+
if (typeof inheritValue !== "boolean") {
|
|
373
|
+
throw new ValidationError(`Repo at index ${index}: files.inherit must be a boolean`);
|
|
389
374
|
}
|
|
390
|
-
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
375
|
+
continue;
|
|
376
|
+
}
|
|
377
|
+
if (!knownFiles.has(fileName)) {
|
|
378
|
+
throw new ValidationError(`Repo at index ${index} references undefined file '${fileName}'. File must be defined in root 'files' object or in a referenced group.`);
|
|
379
|
+
}
|
|
380
|
+
const fileOverride = repo.files[fileName];
|
|
381
|
+
if (fileOverride === false) {
|
|
382
|
+
continue;
|
|
383
|
+
}
|
|
384
|
+
if (fileOverride.override && !fileOverride.content) {
|
|
385
|
+
throw new ValidationError(`Repo ${repoLabel} has override: true for file '${fileName}' but no content defined. ` +
|
|
386
|
+
`Use content: "" for an empty text file override, or content: {} for an empty JSON/YAML override.`);
|
|
387
|
+
}
|
|
388
|
+
validateFileConfigFields(fileOverride, fileName, `Repo ${repoLabel}:`);
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
function validateRepoSettingsEntry(config, repo, repoLabel) {
|
|
392
|
+
if (repo.settings === undefined)
|
|
393
|
+
return;
|
|
394
|
+
const rootCtx = buildRootSettingsContext(config);
|
|
395
|
+
if (repo.groups && config.groups) {
|
|
396
|
+
for (const groupName of repo.groups) {
|
|
397
|
+
const group = config.groups[groupName];
|
|
398
|
+
if (group?.settings?.rulesets) {
|
|
399
|
+
for (const name of Object.keys(group.settings.rulesets)) {
|
|
400
|
+
if (name !== "inherit")
|
|
401
|
+
rootCtx.rulesetNames.push(name);
|
|
403
402
|
}
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
403
|
+
}
|
|
404
|
+
if (group?.settings?.labels) {
|
|
405
|
+
for (const name of Object.keys(group.settings.labels)) {
|
|
406
|
+
if (name !== "inherit")
|
|
407
|
+
rootCtx.labelNames.push(name);
|
|
409
408
|
}
|
|
410
409
|
}
|
|
411
410
|
}
|
|
412
|
-
validateSettings(repo.settings, `Repo ${repoLabel}`, rootCtx.rulesetNames, rootCtx.hasRepoSettings, rootCtx.labelNames);
|
|
413
411
|
}
|
|
412
|
+
validateSettings(repo.settings, `Repo ${repoLabel}`, rootCtx);
|
|
413
|
+
}
|
|
414
|
+
function validateRepoEntry(config, repo, index) {
|
|
415
|
+
const repoLabel = validateRepoGitField(repo, index);
|
|
416
|
+
validateRepoOrigins(config, repo, repoLabel);
|
|
417
|
+
validateRepoGroups(config, repo, index);
|
|
418
|
+
validateRepoFiles(config, repo, index, repoLabel);
|
|
419
|
+
validateRepoSettingsEntry(config, repo, repoLabel);
|
|
420
|
+
}
|
|
421
|
+
function hasGroupFiles(config) {
|
|
422
|
+
return (isPlainObject(config.groups) &&
|
|
423
|
+
Object.values(config.groups).some((g) => g.files &&
|
|
424
|
+
Object.keys(g.files).filter((k) => k !== "inherit" && g.files[k] !== false).length > 0));
|
|
414
425
|
}
|
|
415
426
|
/**
|
|
416
427
|
* Validates raw config structure before normalization.
|
|
@@ -418,20 +429,12 @@ function validateRepoEntry(config, repo, index) {
|
|
|
418
429
|
*/
|
|
419
430
|
export function validateRawConfig(config) {
|
|
420
431
|
validateConfigId(config);
|
|
421
|
-
const hasFiles = config.files &&
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
const
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
!Array.isArray(config.groups) &&
|
|
428
|
-
Object.values(config.groups).some((g) => g.files &&
|
|
429
|
-
Object.keys(g.files).filter((k) => k !== "inherit" && g.files[k] !== false).length > 0);
|
|
430
|
-
const hasGroupSettings = config.groups &&
|
|
431
|
-
typeof config.groups === "object" &&
|
|
432
|
-
!Array.isArray(config.groups) &&
|
|
433
|
-
Object.values(config.groups).some((g) => g.settings && typeof g.settings === "object");
|
|
434
|
-
if (!hasFiles && !hasSettings && !hasGroupFiles && !hasGroupSettings) {
|
|
432
|
+
const hasFiles = isPlainObject(config.files) && Object.keys(config.files).length > 0;
|
|
433
|
+
const hasSettings = isPlainObject(config.settings);
|
|
434
|
+
const hasGrpFiles = hasGroupFiles(config);
|
|
435
|
+
const hasGrpSettings = isPlainObject(config.groups) &&
|
|
436
|
+
Object.values(config.groups).some((g) => g.settings && isPlainObject(g.settings));
|
|
437
|
+
if (!hasFiles && !hasSettings && !hasGrpFiles && !hasGrpSettings) {
|
|
435
438
|
throw new ValidationError("Config requires at least one of: 'files' or 'settings'. " +
|
|
436
439
|
"Use 'files' to sync configuration files, or 'settings' to manage repository settings.");
|
|
437
440
|
}
|
|
@@ -456,21 +459,17 @@ export function validateRawConfig(config) {
|
|
|
456
459
|
// =============================================================================
|
|
457
460
|
/**
|
|
458
461
|
* Validates that config is suitable for the sync command.
|
|
459
|
-
* @throws
|
|
462
|
+
* @throws ValidationError if neither files nor settings are present
|
|
460
463
|
*/
|
|
461
464
|
export function validateForSync(config) {
|
|
462
465
|
const hasRootFiles = config.files && Object.keys(config.files).length > 0;
|
|
463
|
-
const
|
|
464
|
-
Object.values(config.groups).some((g) => g.files &&
|
|
465
|
-
Object.keys(g.files).filter((k) => k !== "inherit" && g.files[k] !== false).length > 0);
|
|
466
|
+
const hasGrpFiles = hasGroupFiles(config);
|
|
466
467
|
const hasSettings = hasActionableSettings(config.settings);
|
|
467
468
|
const hasRepoSettings = config.repos.some((repo) => hasActionableSettings(repo.settings));
|
|
468
|
-
const hasGroupSettings = config.groups &&
|
|
469
|
-
typeof config.groups === "object" &&
|
|
470
|
-
!Array.isArray(config.groups) &&
|
|
469
|
+
const hasGroupSettings = isPlainObject(config.groups) &&
|
|
471
470
|
Object.values(config.groups).some((g) => g.settings && hasActionableSettings(g.settings));
|
|
472
471
|
if (!hasRootFiles &&
|
|
473
|
-
!
|
|
472
|
+
!hasGrpFiles &&
|
|
474
473
|
!hasSettings &&
|
|
475
474
|
!hasRepoSettings &&
|
|
476
475
|
!hasGroupSettings) {
|
|
@@ -1,10 +1,8 @@
|
|
|
1
1
|
import { isTextContent } from "../merge.js";
|
|
2
|
+
import { isPlainObject } from "../../shared/type-guards.js";
|
|
2
3
|
export { isTextContent };
|
|
4
|
+
export { isPlainObject as isObjectContent };
|
|
3
5
|
declare const VALID_STRATEGIES: string[];
|
|
4
|
-
/**
|
|
5
|
-
* Check if content is object type (for JSON/YAML output).
|
|
6
|
-
*/
|
|
7
|
-
export declare function isObjectContent(content: unknown): boolean;
|
|
8
6
|
/**
|
|
9
7
|
* Check if file extension is for structured output (JSON/YAML).
|
|
10
8
|
*/
|
|
@@ -1,14 +1,10 @@
|
|
|
1
1
|
import { extname, isAbsolute } from "node:path";
|
|
2
2
|
import { isTextContent } from "../merge.js";
|
|
3
|
-
import { ValidationError } from "
|
|
3
|
+
import { ValidationError } from "../../shared/errors.js";
|
|
4
|
+
import { isPlainObject } from "../../shared/type-guards.js";
|
|
4
5
|
export { isTextContent };
|
|
6
|
+
export { isPlainObject as isObjectContent };
|
|
5
7
|
const VALID_STRATEGIES = ["replace", "append", "prepend"];
|
|
6
|
-
/**
|
|
7
|
-
* Check if content is object type (for JSON/YAML output).
|
|
8
|
-
*/
|
|
9
|
-
export function isObjectContent(content) {
|
|
10
|
-
return (typeof content === "object" && content !== null && !Array.isArray(content));
|
|
11
|
-
}
|
|
12
8
|
/**
|
|
13
9
|
* Check if file extension is for structured output (JSON/YAML).
|
|
14
10
|
*/
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ValidationError } from "
|
|
1
|
+
import { ValidationError } from "../../shared/errors.js";
|
|
2
2
|
const VALID_VISIBILITY = ["public", "private", "internal"];
|
|
3
3
|
const VALID_SQUASH_MERGE_COMMIT_TITLE = ["PR_TITLE", "COMMIT_OR_PR_TITLE"];
|
|
4
4
|
const VALID_SQUASH_MERGE_COMMIT_MESSAGE = [
|
|
@@ -1,29 +1,45 @@
|
|
|
1
|
-
import { ValidationError } from "
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
1
|
+
import { ValidationError } from "../../shared/errors.js";
|
|
2
|
+
/** Compile-time validates an array matches a type union, while keeping string[] runtime type for .includes() */
|
|
3
|
+
function validValues(values) {
|
|
4
|
+
return values;
|
|
5
|
+
}
|
|
6
|
+
const VALID_RULESET_TARGETS = validValues(["branch", "tag"]);
|
|
7
|
+
const VALID_ENFORCEMENT_LEVELS = validValues([
|
|
8
|
+
"active",
|
|
9
|
+
"disabled",
|
|
10
|
+
"evaluate",
|
|
11
|
+
]);
|
|
12
|
+
const VALID_ACTOR_TYPES = validValues([
|
|
13
|
+
"Team",
|
|
14
|
+
"User",
|
|
15
|
+
"Integration",
|
|
16
|
+
]);
|
|
17
|
+
const VALID_BYPASS_MODES = validValues(["always", "pull_request"]);
|
|
6
18
|
const VALID_PATTERN_OPERATORS = [
|
|
7
19
|
"starts_with",
|
|
8
20
|
"ends_with",
|
|
9
21
|
"contains",
|
|
10
22
|
"regex",
|
|
11
23
|
];
|
|
12
|
-
const VALID_MERGE_METHODS = [
|
|
13
|
-
|
|
24
|
+
const VALID_MERGE_METHODS = validValues([
|
|
25
|
+
"merge",
|
|
26
|
+
"squash",
|
|
27
|
+
"rebase",
|
|
28
|
+
]);
|
|
29
|
+
const VALID_ALERTS_THRESHOLDS = validValues([
|
|
14
30
|
"none",
|
|
15
31
|
"errors",
|
|
16
32
|
"errors_and_warnings",
|
|
17
33
|
"all",
|
|
18
|
-
];
|
|
19
|
-
const VALID_SECURITY_THRESHOLDS = [
|
|
34
|
+
]);
|
|
35
|
+
const VALID_SECURITY_THRESHOLDS = validValues([
|
|
20
36
|
"none",
|
|
21
37
|
"critical",
|
|
22
38
|
"high_or_higher",
|
|
23
39
|
"medium_or_higher",
|
|
24
40
|
"all",
|
|
25
|
-
];
|
|
26
|
-
const VALID_RULE_TYPES = [
|
|
41
|
+
]);
|
|
42
|
+
const VALID_RULE_TYPES = validValues([
|
|
27
43
|
"pull_request",
|
|
28
44
|
"required_status_checks",
|
|
29
45
|
"required_signatures",
|
|
@@ -45,7 +61,7 @@ const VALID_RULE_TYPES = [
|
|
|
45
61
|
"file_extension_restriction",
|
|
46
62
|
"max_file_path_length",
|
|
47
63
|
"max_file_size",
|
|
48
|
-
];
|
|
64
|
+
]);
|
|
49
65
|
/**
|
|
50
66
|
* Validates a single ruleset rule.
|
|
51
67
|
*/
|
package/dist/index.d.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
1
|
export { runSync } from "./cli/index.js";
|
|
2
2
|
export type { SyncOptions, SharedOptions } from "./cli/index.js";
|
|
3
|
-
export { type
|
|
3
|
+
export { type ProcessorFactory, type RulesetProcessorFactory, type RepoSettingsProcessorFactory, type LabelsProcessorFactory, } from "./cli/index.js";
|
|
4
|
+
export type { IRepositoryProcessor } from "./sync/index.js";
|
|
5
|
+
export type { IRulesetProcessor, ILabelsProcessor } from "./settings/index.js";
|
package/dist/index.js
CHANGED
|
@@ -8,8 +8,9 @@ import type { IMigrationSource, LifecyclePlatform } from "./types.js";
|
|
|
8
8
|
export declare class AdoMigrationSource implements IMigrationSource {
|
|
9
9
|
private readonly executor;
|
|
10
10
|
private readonly retries;
|
|
11
|
+
private readonly cwd;
|
|
11
12
|
readonly platform: LifecyclePlatform;
|
|
12
|
-
constructor(executor
|
|
13
|
+
constructor(executor: ICommandExecutor, retries: number | undefined, cwd: string);
|
|
13
14
|
private assertAdo;
|
|
14
15
|
cloneForMigration(repoInfo: RepoInfo, workDir: string): Promise<void>;
|
|
15
16
|
}
|