@aspruyt/xfg 4.0.2 → 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/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
|
@@ -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
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { escapeShellArg } from "../shared/shell-utils.js";
|
|
2
|
-
import { defaultExecutor, } from "../shared/command-executor.js";
|
|
3
2
|
import { withRetry } from "../shared/retry-utils.js";
|
|
4
3
|
import { toErrorMessage } from "../shared/type-guards.js";
|
|
4
|
+
import { LifecycleError } from "../shared/errors.js";
|
|
5
5
|
import { isAzureDevOpsRepo, } from "../shared/repo-detector.js";
|
|
6
6
|
/**
|
|
7
7
|
* Azure DevOps implementation of IMigrationSource.
|
|
@@ -10,27 +10,29 @@ import { isAzureDevOpsRepo, } from "../shared/repo-detector.js";
|
|
|
10
10
|
export class AdoMigrationSource {
|
|
11
11
|
executor;
|
|
12
12
|
retries;
|
|
13
|
+
cwd;
|
|
13
14
|
platform = "azure-devops";
|
|
14
|
-
constructor(executor
|
|
15
|
+
constructor(executor, retries = 3, cwd) {
|
|
15
16
|
this.executor = executor;
|
|
16
17
|
this.retries = retries;
|
|
18
|
+
this.cwd = cwd;
|
|
17
19
|
}
|
|
18
20
|
assertAdo(repoInfo) {
|
|
19
21
|
if (!isAzureDevOpsRepo(repoInfo)) {
|
|
20
|
-
throw new
|
|
22
|
+
throw new LifecycleError(`AdoMigrationSource requires Azure DevOps repo, got: ${repoInfo.type}`);
|
|
21
23
|
}
|
|
22
24
|
}
|
|
23
25
|
async cloneForMigration(repoInfo, workDir) {
|
|
24
26
|
this.assertAdo(repoInfo);
|
|
25
27
|
const command = `git clone --mirror ${escapeShellArg(repoInfo.gitUrl)} ${escapeShellArg(workDir)}`;
|
|
26
28
|
try {
|
|
27
|
-
await withRetry(() => this.executor.exec(command,
|
|
29
|
+
await withRetry(() => this.executor.exec(command, this.cwd), {
|
|
28
30
|
retries: this.retries,
|
|
29
31
|
});
|
|
30
32
|
}
|
|
31
33
|
catch (error) {
|
|
32
34
|
const msg = toErrorMessage(error);
|
|
33
|
-
throw new
|
|
35
|
+
throw new LifecycleError(`Failed to clone migration source ${repoInfo.gitUrl}: ${msg}. ` +
|
|
34
36
|
`Ensure you have authentication configured for Azure DevOps ` +
|
|
35
37
|
`(e.g., AZURE_DEVOPS_EXT_PAT or git credential helper).`);
|
|
36
38
|
}
|
|
@@ -1,18 +1,20 @@
|
|
|
1
1
|
import { ICommandExecutor } from "../shared/command-executor.js";
|
|
2
2
|
import { type RepoInfo } from "../shared/repo-detector.js";
|
|
3
|
+
import type { DebugWarnLog } from "../shared/logger.js";
|
|
3
4
|
import type { IRepoLifecycleProvider, LifecyclePlatform, CreateRepoSettings } from "./types.js";
|
|
4
5
|
/**
|
|
5
6
|
* GitHub implementation of IRepoLifecycleProvider.
|
|
6
7
|
* Uses gh CLI for all operations.
|
|
7
8
|
*/
|
|
8
9
|
interface GitHubLifecycleProviderOptions {
|
|
9
|
-
executor
|
|
10
|
+
executor: ICommandExecutor;
|
|
10
11
|
retries?: number;
|
|
11
|
-
cwd
|
|
12
|
+
cwd: string;
|
|
12
13
|
/** Timeout in ms for waiting for fork readiness (default: 60000) */
|
|
13
14
|
forkReadyTimeoutMs?: number;
|
|
14
15
|
/** Poll interval in ms for fork readiness checks (default: 2000) */
|
|
15
16
|
forkPollIntervalMs?: number;
|
|
17
|
+
log?: DebugWarnLog;
|
|
16
18
|
}
|
|
17
19
|
export declare class GitHubLifecycleProvider implements IRepoLifecycleProvider {
|
|
18
20
|
readonly platform: LifecyclePlatform;
|
|
@@ -21,7 +23,8 @@ export declare class GitHubLifecycleProvider implements IRepoLifecycleProvider {
|
|
|
21
23
|
private readonly cwd;
|
|
22
24
|
private readonly forkReadyTimeoutMs;
|
|
23
25
|
private readonly forkPollIntervalMs;
|
|
24
|
-
|
|
26
|
+
private readonly log?;
|
|
27
|
+
constructor(options: GitHubLifecycleProviderOptions);
|
|
25
28
|
/**
|
|
26
29
|
* Check if a GitHub owner is an organization (vs user).
|
|
27
30
|
* Uses gh api to query the user/org endpoint.
|