@aspruyt/xfg 4.0.5 → 5.0.1
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 -1
- package/dist/cli/program.d.ts +3 -0
- package/dist/cli/program.js +18 -13
- package/dist/cli/sync-command.js +63 -39
- package/dist/cli/sync-report-builder.js +7 -4
- package/dist/cli/types.d.ts +1 -0
- package/dist/config/formatter.js +14 -9
- package/dist/config/merge.d.ts +2 -4
- package/dist/config/merge.js +15 -67
- package/dist/config/validator.js +2 -9
- package/dist/lifecycle/repo-lifecycle-factory.js +0 -4
- package/dist/output/github-summary.d.ts +3 -2
- package/dist/output/github-summary.js +1 -7
- package/dist/output/lifecycle-report.js +7 -14
- package/dist/output/sync-report.d.ts +2 -19
- package/dist/output/sync-report.js +16 -28
- package/dist/output/types.d.ts +19 -0
- package/dist/output/types.js +1 -0
- package/dist/output/unified-summary.d.ts +2 -1
- package/dist/output/unified-summary.js +4 -1
- package/dist/settings/base-processor.d.ts +3 -1
- package/dist/settings/base-processor.js +9 -5
- package/dist/settings/index.d.ts +1 -1
- package/dist/settings/labels/diff.d.ts +2 -1
- package/dist/settings/labels/formatter.js +2 -4
- package/dist/settings/labels/github-labels-strategy.js +2 -1
- package/dist/settings/labels/processor.js +0 -1
- package/dist/settings/repo-settings/github-repo-settings-strategy.js +2 -1
- package/dist/settings/rulesets/diff-algorithm.js +0 -1
- package/dist/settings/rulesets/diff.d.ts +2 -1
- package/dist/settings/rulesets/diff.js +37 -21
- package/dist/settings/rulesets/formatter.js +44 -38
- package/dist/settings/rulesets/github-ruleset-strategy.js +2 -1
- package/dist/settings/rulesets/processor.js +0 -1
- package/dist/shared/gh-api-utils.d.ts +8 -7
- package/dist/shared/gh-api-utils.js +2 -16
- package/dist/shared/interpolation-engine.d.ts +3 -0
- package/dist/shared/interpolation-engine.js +0 -3
- package/dist/shared/json-utils.d.ts +6 -0
- package/dist/shared/json-utils.js +16 -0
- package/dist/shared/repo-detector.js +0 -4
- package/dist/shared/xfg-template.d.ts +3 -0
- package/dist/shared/xfg-template.js +0 -20
- package/dist/sync/auth-options-builder.js +7 -1
- package/dist/sync/branch-manager.d.ts +1 -1
- package/dist/sync/commit-message.d.ts +1 -1
- package/dist/sync/commit-push-manager.d.ts +1 -1
- package/dist/sync/commit-push-manager.js +2 -2
- package/dist/sync/diff-utils.d.ts +15 -2
- package/dist/sync/diff-utils.js +50 -14
- package/dist/sync/file-sync-orchestrator.js +2 -4
- package/dist/sync/file-sync-strategy.js +11 -4
- package/dist/sync/file-writer.js +9 -4
- package/dist/sync/index.d.ts +2 -1
- package/dist/sync/index.js +1 -0
- package/dist/sync/manifest-manager.d.ts +1 -1
- package/dist/sync/manifest-manager.js +20 -6
- package/dist/sync/pr-merge-handler.js +6 -1
- package/dist/sync/repository-processor.js +8 -1
- package/dist/sync/types.d.ts +5 -4
- package/dist/vcs/authenticated-git-ops.d.ts +9 -1
- package/dist/vcs/authenticated-git-ops.js +7 -14
- package/dist/vcs/git-ops.js +29 -12
- package/dist/vcs/github-pr-strategy.js +6 -1
- package/dist/vcs/gitlab-pr-strategy.js +7 -2
- package/dist/vcs/graphql-commit-strategy.js +2 -1
- package/dist/vcs/index.d.ts +1 -0
- package/dist/vcs/index.js +2 -0
- package/dist/vcs/pr-creator.d.ts +5 -1
- package/dist/vcs/pr-creator.js +4 -4
- package/package.json +1 -1
- package/dist/output/index.d.ts +0 -5
- package/dist/output/index.js +0 -10
- package/dist/shared/index.d.ts +0 -15
- package/dist/shared/index.js +0 -30
package/README.md
CHANGED
|
@@ -31,7 +31,7 @@ jobs:
|
|
|
31
31
|
runs-on: ubuntu-latest
|
|
32
32
|
steps:
|
|
33
33
|
- uses: actions/checkout@v4
|
|
34
|
-
- uses: anthony-spruyt/xfg@
|
|
34
|
+
- uses: anthony-spruyt/xfg@v5
|
|
35
35
|
with:
|
|
36
36
|
config: ./sync-config.yaml
|
|
37
37
|
github-token: ${{ secrets.GH_PAT }} # PAT with repo scope for cross-repo access
|
package/dist/cli/program.d.ts
CHANGED
package/dist/cli/program.js
CHANGED
|
@@ -28,30 +28,35 @@ function addSharedOptions(cmd) {
|
|
|
28
28
|
.option("--no-delete", "Skip deletion of orphaned resources even if deleteOrphaned is configured");
|
|
29
29
|
}
|
|
30
30
|
// =============================================================================
|
|
31
|
-
//
|
|
31
|
+
// Validators
|
|
32
32
|
// =============================================================================
|
|
33
|
-
|
|
34
|
-
.name("xfg")
|
|
35
|
-
.description("Manage files, settings, and repositories across GitHub, Azure DevOps, and GitLab")
|
|
36
|
-
.version(packageJson.version);
|
|
37
|
-
// Sync command (file synchronization)
|
|
38
|
-
const syncCommand = new Command("sync")
|
|
39
|
-
.description("Sync configuration files across repositories")
|
|
40
|
-
.option("-b, --branch <name>", "Override the branch name (default: chore/sync-{filename} or chore/sync-config)")
|
|
41
|
-
.option("-m, --merge <mode>", "PR merge mode: manual, auto (default, merge when checks pass), force (bypass requirements), direct (push to default branch, no PR)", (value) => {
|
|
33
|
+
export function parseMergeMode(value) {
|
|
42
34
|
const valid = ["manual", "auto", "force", "direct"];
|
|
43
35
|
if (!valid.includes(value)) {
|
|
44
36
|
throw new ValidationError(`Invalid merge mode: ${value}. Valid: ${valid.join(", ")}`);
|
|
45
37
|
}
|
|
46
38
|
return value;
|
|
47
|
-
}
|
|
48
|
-
|
|
39
|
+
}
|
|
40
|
+
export function parseMergeStrategy(value) {
|
|
49
41
|
const valid = ["merge", "squash", "rebase"];
|
|
50
42
|
if (!valid.includes(value)) {
|
|
51
43
|
throw new ValidationError(`Invalid merge strategy: ${value}. Valid: ${valid.join(", ")}`);
|
|
52
44
|
}
|
|
53
45
|
return value;
|
|
54
|
-
}
|
|
46
|
+
}
|
|
47
|
+
// =============================================================================
|
|
48
|
+
// CLI Program
|
|
49
|
+
// =============================================================================
|
|
50
|
+
program
|
|
51
|
+
.name("xfg")
|
|
52
|
+
.description("Manage files, settings, and repositories across GitHub, Azure DevOps, and GitLab")
|
|
53
|
+
.version(packageJson.version);
|
|
54
|
+
// Sync command (file synchronization)
|
|
55
|
+
const syncCommand = new Command("sync")
|
|
56
|
+
.description("Sync configuration files across repositories")
|
|
57
|
+
.option("-b, --branch <name>", "Override the branch name (default: chore/sync-{filename} or chore/sync-config)")
|
|
58
|
+
.option("-m, --merge <mode>", "PR merge mode: manual, auto (default, merge when checks pass), force (bypass requirements), direct (push to default branch, no PR)", parseMergeMode)
|
|
59
|
+
.option("--merge-strategy <strategy>", "Merge strategy: merge, squash (default), rebase", parseMergeStrategy)
|
|
55
60
|
.option("--delete-branch", "Delete source branch after merge")
|
|
56
61
|
.action((opts) => {
|
|
57
62
|
runSync(opts).catch((error) => {
|
package/dist/cli/sync-command.js
CHANGED
|
@@ -11,12 +11,26 @@ import { RulesetProcessor, RepoSettingsProcessor, LabelsProcessor, GitHubRuleset
|
|
|
11
11
|
import { ShellCommandExecutor } from "../shared/command-executor.js";
|
|
12
12
|
import { Logger } from "../shared/logger.js";
|
|
13
13
|
import { generateWorkspaceName } from "../shared/workspace-utils.js";
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
14
|
+
let _defaultExecutor;
|
|
15
|
+
let _logger;
|
|
16
|
+
function getDefaultExecutor() {
|
|
17
|
+
return (_defaultExecutor ??= new ShellCommandExecutor(process.env));
|
|
18
|
+
}
|
|
19
|
+
function getLogger() {
|
|
20
|
+
return (_logger ??= new Logger(!!(process.env.DEBUG || process.env.XFG_DEBUG)));
|
|
21
|
+
}
|
|
22
|
+
function createDefaultRulesetProcessorFactory() {
|
|
23
|
+
const cwd = process.cwd();
|
|
24
|
+
return () => new RulesetProcessor(new GitHubRulesetStrategy(getDefaultExecutor(), { cwd }));
|
|
25
|
+
}
|
|
26
|
+
function createDefaultRepoSettingsProcessorFactory() {
|
|
27
|
+
const cwd = process.cwd();
|
|
28
|
+
return () => new RepoSettingsProcessor(new GitHubRepoSettingsStrategy(getDefaultExecutor(), { cwd }));
|
|
29
|
+
}
|
|
30
|
+
function createDefaultLabelsProcessorFactory() {
|
|
31
|
+
const cwd = process.cwd();
|
|
32
|
+
return () => new LabelsProcessor(new GitHubLabelsStrategy(getDefaultExecutor(), { cwd }));
|
|
33
|
+
}
|
|
20
34
|
import { ResultsCollector } from "./results-collector.js";
|
|
21
35
|
import { buildSettingsReport } from "./settings-report-builder.js";
|
|
22
36
|
import { formatSettingsReportCLI } from "../output/settings-report.js";
|
|
@@ -73,22 +87,22 @@ function determineMergeOutcome(result) {
|
|
|
73
87
|
}
|
|
74
88
|
function logSettingsResult(result, label, current, repoName, settingsCollector) {
|
|
75
89
|
if (result.planOutput?.lines?.length) {
|
|
76
|
-
|
|
77
|
-
|
|
90
|
+
getLogger().info("");
|
|
91
|
+
getLogger().info(`${repoName} - ${label}:`);
|
|
78
92
|
for (const line of result.planOutput.lines) {
|
|
79
|
-
|
|
93
|
+
getLogger().info(line);
|
|
80
94
|
}
|
|
81
95
|
if (result.warnings?.length) {
|
|
82
96
|
for (const warning of result.warnings) {
|
|
83
|
-
|
|
97
|
+
getLogger().warn(warning);
|
|
84
98
|
}
|
|
85
99
|
}
|
|
86
100
|
}
|
|
87
101
|
else if (!result.skipped && result.success) {
|
|
88
|
-
|
|
102
|
+
getLogger().success(current, repoName, `${label}: ${result.message}`);
|
|
89
103
|
}
|
|
90
104
|
if (!result.success && !result.skipped) {
|
|
91
|
-
|
|
105
|
+
getLogger().error(current, repoName, `${label}: ${result.message}`);
|
|
92
106
|
settingsCollector.appendError(repoName, result.message);
|
|
93
107
|
}
|
|
94
108
|
}
|
|
@@ -148,7 +162,7 @@ async function applyRepoSettings(ctx) {
|
|
|
148
162
|
logSettingsResult(result, desc.label, current, repoName, settingsCollector);
|
|
149
163
|
}
|
|
150
164
|
catch (error) {
|
|
151
|
-
|
|
165
|
+
getLogger().error(current, repoName, `${desc.label}: ${toErrorMessage(error)}`);
|
|
152
166
|
settingsCollector.appendError(repoName, error);
|
|
153
167
|
}
|
|
154
168
|
}
|
|
@@ -156,15 +170,15 @@ async function applyRepoSettings(ctx) {
|
|
|
156
170
|
function displayReports(reportResults, lifecycleReportInputs, settingsCollector, dryRun) {
|
|
157
171
|
const lifecycleReport = buildLifecycleReport(lifecycleReportInputs);
|
|
158
172
|
if (hasLifecycleChanges(lifecycleReport)) {
|
|
159
|
-
|
|
173
|
+
getLogger().log("");
|
|
160
174
|
for (const line of formatLifecycleReportCLI(lifecycleReport)) {
|
|
161
|
-
|
|
175
|
+
getLogger().log(line);
|
|
162
176
|
}
|
|
163
177
|
}
|
|
164
178
|
const report = buildSyncReport(reportResults);
|
|
165
|
-
|
|
179
|
+
getLogger().log("");
|
|
166
180
|
for (const line of formatSyncReportCLI(report)) {
|
|
167
|
-
|
|
181
|
+
getLogger().log(line);
|
|
168
182
|
}
|
|
169
183
|
// Build and display settings report (if any settings were processed)
|
|
170
184
|
const settingsResults = settingsCollector.getAll();
|
|
@@ -173,9 +187,9 @@ function displayReports(reportResults, lifecycleReportInputs, settingsCollector,
|
|
|
173
187
|
settingsReport = buildSettingsReport(settingsResults);
|
|
174
188
|
const settingsLines = formatSettingsReportCLI(settingsReport);
|
|
175
189
|
if (settingsLines.length > 0) {
|
|
176
|
-
|
|
190
|
+
getLogger().log("");
|
|
177
191
|
for (const line of settingsLines) {
|
|
178
|
-
|
|
192
|
+
getLogger().log(line);
|
|
179
193
|
}
|
|
180
194
|
}
|
|
181
195
|
}
|
|
@@ -206,7 +220,7 @@ async function processSingleRepo(repoConfig, index, ctx) {
|
|
|
206
220
|
}
|
|
207
221
|
const mergeMode = repoConfig.prOptions?.merge ?? "auto";
|
|
208
222
|
if (mergeMode === "direct" && repoConfig.prOptions?.mergeStrategy) {
|
|
209
|
-
|
|
223
|
+
getLogger().warn(`mergeStrategy '${repoConfig.prOptions.mergeStrategy}' is ignored in direct mode for ${repoConfig.git}`);
|
|
210
224
|
}
|
|
211
225
|
let repoInfo;
|
|
212
226
|
try {
|
|
@@ -215,7 +229,7 @@ async function processSingleRepo(repoConfig, index, ctx) {
|
|
|
215
229
|
});
|
|
216
230
|
}
|
|
217
231
|
catch (error) {
|
|
218
|
-
|
|
232
|
+
getLogger().error(current, repoConfig.git, toErrorMessage(error));
|
|
219
233
|
ctx.reportResults.push({
|
|
220
234
|
repoName: repoConfig.git,
|
|
221
235
|
success: false,
|
|
@@ -227,7 +241,13 @@ async function processSingleRepo(repoConfig, index, ctx) {
|
|
|
227
241
|
const repoName = getRepoDisplayName(repoInfo);
|
|
228
242
|
const workDir = resolve(join(options.workDir ?? "./tmp", generateWorkspaceName(index)));
|
|
229
243
|
const repoToken = isGitHubRepo(repoInfo)
|
|
230
|
-
? (await resolveGitHubToken(
|
|
244
|
+
? (await resolveGitHubToken({
|
|
245
|
+
repoInfo: repoInfo,
|
|
246
|
+
tokenManager: ctx.tokenManager,
|
|
247
|
+
context: repoName,
|
|
248
|
+
log: getLogger(),
|
|
249
|
+
envToken: process.env.GH_TOKEN,
|
|
250
|
+
})).token
|
|
231
251
|
: undefined;
|
|
232
252
|
const repo = {
|
|
233
253
|
repoConfig,
|
|
@@ -273,7 +293,7 @@ async function runLifecyclePhase(repo, ctx) {
|
|
|
273
293
|
repoSettings: ctx.config.settings?.repo,
|
|
274
294
|
});
|
|
275
295
|
for (const line of outputLines) {
|
|
276
|
-
|
|
296
|
+
getLogger().info(line);
|
|
277
297
|
}
|
|
278
298
|
const createSettings = toCreateRepoSettings(ctx.config.settings?.repo);
|
|
279
299
|
ctx.lifecycleReportInputs.push({
|
|
@@ -300,7 +320,7 @@ async function runLifecyclePhase(repo, ctx) {
|
|
|
300
320
|
return false;
|
|
301
321
|
}
|
|
302
322
|
catch (error) {
|
|
303
|
-
|
|
323
|
+
getLogger().error(current, repo.repoName, `Lifecycle error: ${toErrorMessage(error)}`);
|
|
304
324
|
ctx.reportResults.push({
|
|
305
325
|
repoName: repo.repoName,
|
|
306
326
|
success: false,
|
|
@@ -316,14 +336,14 @@ async function runLifecyclePhase(repo, ctx) {
|
|
|
316
336
|
async function runFileSyncPhase(repo, ctx) {
|
|
317
337
|
const current = repo.index + 1;
|
|
318
338
|
try {
|
|
319
|
-
|
|
339
|
+
getLogger().progress(current, repo.repoName, "Processing...");
|
|
320
340
|
const result = await ctx.processor.process(repo.repoConfig, repo.repoInfo, {
|
|
321
341
|
branchName: ctx.branchName,
|
|
322
342
|
workDir: repo.workDir,
|
|
323
343
|
configId: ctx.config.id,
|
|
324
344
|
dryRun: ctx.options.dryRun,
|
|
325
345
|
retries: ctx.options.retries,
|
|
326
|
-
executor:
|
|
346
|
+
executor: getDefaultExecutor(),
|
|
327
347
|
prTemplate: ctx.config.prTemplate,
|
|
328
348
|
noDelete: ctx.options.noDelete,
|
|
329
349
|
token: repo.token,
|
|
@@ -336,23 +356,24 @@ async function runFileSyncPhase(repo, ctx) {
|
|
|
336
356
|
fileChanges: (result.fileChanges ?? []).map((f) => ({
|
|
337
357
|
path: f.path,
|
|
338
358
|
action: f.action,
|
|
359
|
+
...(f.diffLines ? { diffLines: f.diffLines } : {}),
|
|
339
360
|
})),
|
|
340
361
|
prUrl: result.prUrl,
|
|
341
362
|
mergeOutcome,
|
|
342
363
|
error: result.success ? undefined : result.message,
|
|
343
364
|
});
|
|
344
365
|
if (result.skipped) {
|
|
345
|
-
|
|
366
|
+
getLogger().skip(current, repo.repoName, result.message);
|
|
346
367
|
}
|
|
347
368
|
else if (result.success) {
|
|
348
|
-
|
|
369
|
+
getLogger().success(current, repo.repoName, result.message);
|
|
349
370
|
}
|
|
350
371
|
else {
|
|
351
|
-
|
|
372
|
+
getLogger().error(current, repo.repoName, result.message);
|
|
352
373
|
}
|
|
353
374
|
}
|
|
354
375
|
catch (error) {
|
|
355
|
-
|
|
376
|
+
getLogger().error(current, repo.repoName, toErrorMessage(error));
|
|
356
377
|
ctx.reportResults.push({
|
|
357
378
|
repoName: repo.repoName,
|
|
358
379
|
success: false,
|
|
@@ -362,14 +383,17 @@ async function runFileSyncPhase(repo, ctx) {
|
|
|
362
383
|
}
|
|
363
384
|
}
|
|
364
385
|
export async function runSync(options, deps = {}) {
|
|
365
|
-
|
|
386
|
+
// Reset module-level singletons to ensure fresh state per invocation
|
|
387
|
+
_defaultExecutor = undefined;
|
|
388
|
+
_logger = undefined;
|
|
389
|
+
const { lifecycleManager, rulesetProcessorFactory = createDefaultRulesetProcessorFactory(), repoSettingsProcessorFactory = createDefaultRepoSettingsProcessorFactory(), labelsProcessorFactory = createDefaultLabelsProcessorFactory(), } = deps;
|
|
366
390
|
const configPath = resolve(options.config);
|
|
367
391
|
if (!existsSync(configPath)) {
|
|
368
392
|
throw new ValidationError(`Config file not found: ${configPath}`);
|
|
369
393
|
}
|
|
370
|
-
|
|
394
|
+
getLogger().log(`Loading config from: ${configPath}`);
|
|
371
395
|
if (options.dryRun) {
|
|
372
|
-
|
|
396
|
+
getLogger().log("Running in DRY RUN mode - no changes will be made\n");
|
|
373
397
|
}
|
|
374
398
|
const rawConfig = loadRawConfig(configPath);
|
|
375
399
|
validateForSync(rawConfig);
|
|
@@ -383,10 +407,10 @@ export async function runSync(options, deps = {}) {
|
|
|
383
407
|
else {
|
|
384
408
|
branchName = generateBranchName(fileNames);
|
|
385
409
|
}
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
410
|
+
getLogger().setTotal(config.repos.length);
|
|
411
|
+
getLogger().log(`Found ${config.repos.length} repositories to process`);
|
|
412
|
+
getLogger().log(`Target files: ${formatFileNames(fileNames)}`);
|
|
413
|
+
getLogger().log(`Branch: ${branchName}\n`);
|
|
390
414
|
const tokenManager = createTokenManager(process.env.XFG_GITHUB_APP_ID && process.env.XFG_GITHUB_APP_PRIVATE_KEY
|
|
391
415
|
? {
|
|
392
416
|
appId: process.env.XFG_GITHUB_APP_ID,
|
|
@@ -395,7 +419,7 @@ export async function runSync(options, deps = {}) {
|
|
|
395
419
|
: undefined);
|
|
396
420
|
const processor = deps.processorFactory
|
|
397
421
|
? deps.processorFactory()
|
|
398
|
-
: new RepositoryProcessor(undefined,
|
|
422
|
+
: new RepositoryProcessor(undefined, getLogger(), {
|
|
399
423
|
tokenManager,
|
|
400
424
|
envToken: process.env.GH_TOKEN,
|
|
401
425
|
});
|
|
@@ -405,7 +429,7 @@ export async function runSync(options, deps = {}) {
|
|
|
405
429
|
branchName,
|
|
406
430
|
processor,
|
|
407
431
|
lifecycleManager: lifecycleManager ??
|
|
408
|
-
new RepoLifecycleManager(undefined,
|
|
432
|
+
new RepoLifecycleManager(undefined, getDefaultExecutor(), options.retries, process.cwd(), getLogger()),
|
|
409
433
|
tokenManager,
|
|
410
434
|
reportResults: [],
|
|
411
435
|
lifecycleReportInputs: [],
|
|
@@ -4,10 +4,13 @@ export function buildSyncReport(results) {
|
|
|
4
4
|
files: { create: 0, update: 0, delete: 0 },
|
|
5
5
|
};
|
|
6
6
|
for (const result of results) {
|
|
7
|
-
const files = result.fileChanges.map((f) =>
|
|
8
|
-
path: f.path,
|
|
9
|
-
|
|
10
|
-
|
|
7
|
+
const files = result.fileChanges.map((f) => {
|
|
8
|
+
const entry = { path: f.path, action: f.action };
|
|
9
|
+
if (f.diffLines) {
|
|
10
|
+
entry.diffLines = f.diffLines;
|
|
11
|
+
}
|
|
12
|
+
return entry;
|
|
13
|
+
});
|
|
11
14
|
// Count totals
|
|
12
15
|
for (const file of files) {
|
|
13
16
|
if (file.action === "create")
|
package/dist/cli/types.d.ts
CHANGED
package/dist/config/formatter.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Document, stringify } from "yaml";
|
|
1
|
+
import { Document, isScalar, Scalar, stringify, visit } from "yaml";
|
|
2
2
|
export function detectOutputFormat(fileName) {
|
|
3
3
|
const ext = fileName.toLowerCase().split(".").pop();
|
|
4
4
|
if (ext === "yaml" || ext === "yml") {
|
|
@@ -16,11 +16,9 @@ export function detectOutputFormat(fileName) {
|
|
|
16
16
|
*/
|
|
17
17
|
function buildHeaderComment(header, schemaUrl) {
|
|
18
18
|
const lines = [];
|
|
19
|
-
// Add yaml-language-server schema directive first (if present)
|
|
20
19
|
if (schemaUrl) {
|
|
21
20
|
lines.push(` yaml-language-server: $schema=${schemaUrl}`);
|
|
22
21
|
}
|
|
23
|
-
// Add custom header lines (with space prefix for proper formatting)
|
|
24
22
|
if (header && header.length > 0) {
|
|
25
23
|
lines.push(...header.map((h) => ` ${h}`));
|
|
26
24
|
}
|
|
@@ -34,11 +32,9 @@ function buildHeaderComment(header, schemaUrl) {
|
|
|
34
32
|
*/
|
|
35
33
|
function buildCommentOnlyYaml(header, schemaUrl) {
|
|
36
34
|
const lines = [];
|
|
37
|
-
// Add yaml-language-server schema directive first (if present)
|
|
38
35
|
if (schemaUrl) {
|
|
39
36
|
lines.push(`# yaml-language-server: $schema=${schemaUrl}`);
|
|
40
37
|
}
|
|
41
|
-
// Add custom header lines
|
|
42
38
|
if (header && header.length > 0) {
|
|
43
39
|
lines.push(...header.map((h) => `# ${h}`));
|
|
44
40
|
}
|
|
@@ -79,10 +75,19 @@ export function convertContentToString(content, fileName, options) {
|
|
|
79
75
|
doc.commentBefore = headerComment;
|
|
80
76
|
}
|
|
81
77
|
}
|
|
82
|
-
//
|
|
83
|
-
//
|
|
84
|
-
//
|
|
85
|
-
|
|
78
|
+
// Use BLOCK_LITERAL (|) for multi-line string values to preserve readability.
|
|
79
|
+
// Single-line strings remain QUOTE_DOUBLE via defaultStringType for YAML 1.1
|
|
80
|
+
// compatibility (prevents "06:00" as sexagesimal, "yes"/"no" as booleans).
|
|
81
|
+
visit(doc, {
|
|
82
|
+
Scalar(key, node) {
|
|
83
|
+
if (key === "value" &&
|
|
84
|
+
isScalar(node) &&
|
|
85
|
+
typeof node.value === "string" &&
|
|
86
|
+
node.value.includes("\n")) {
|
|
87
|
+
node.type = Scalar.BLOCK_LITERAL;
|
|
88
|
+
}
|
|
89
|
+
},
|
|
90
|
+
});
|
|
86
91
|
return stringify(doc, {
|
|
87
92
|
indent: 2,
|
|
88
93
|
defaultStringType: "QUOTE_DOUBLE",
|
package/dist/config/merge.d.ts
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Deep merge utilities for JSON configuration objects.
|
|
3
|
-
* Supports
|
|
3
|
+
* Supports per-field array merge strategies via $arrayMerge + $values directives.
|
|
4
4
|
*/
|
|
5
5
|
export type ArrayMergeStrategy = "replace" | "append" | "prepend";
|
|
6
6
|
export interface MergeContext {
|
|
7
|
-
arrayStrategies: Map<string, ArrayMergeStrategy>;
|
|
8
7
|
defaultArrayStrategy: ArrayMergeStrategy;
|
|
9
8
|
}
|
|
10
9
|
/**
|
|
@@ -13,9 +12,8 @@ export interface MergeContext {
|
|
|
13
12
|
* @param base - The base object
|
|
14
13
|
* @param overlay - The overlay object (values override base)
|
|
15
14
|
* @param ctx - Merge context with array strategies
|
|
16
|
-
* @param path - Current path for strategy lookup (internal)
|
|
17
15
|
*/
|
|
18
|
-
export declare function deepMerge(base: Record<string, unknown>, overlay: Record<string, unknown>, ctx: MergeContext
|
|
16
|
+
export declare function deepMerge(base: Record<string, unknown>, overlay: Record<string, unknown>, ctx: MergeContext): Record<string, unknown>;
|
|
19
17
|
/**
|
|
20
18
|
* Strip merge directive keys ($arrayMerge, $override, etc.) from an object.
|
|
21
19
|
* Works recursively on nested objects and arrays.
|
package/dist/config/merge.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Deep merge utilities for JSON configuration objects.
|
|
3
|
-
* Supports
|
|
3
|
+
* Supports per-field array merge strategies via $arrayMerge + $values directives.
|
|
4
4
|
*/
|
|
5
5
|
import { isPlainObject } from "../shared/type-guards.js";
|
|
6
6
|
/**
|
|
@@ -23,92 +23,41 @@ function mergeArrays(base, overlay, strategy) {
|
|
|
23
23
|
// Fallback to replace for unknown strategies
|
|
24
24
|
return overlay;
|
|
25
25
|
}
|
|
26
|
-
/**
|
|
27
|
-
* Extract array values from an overlay object that uses the directive syntax:
|
|
28
|
-
* { $arrayMerge: 'append', values: [1, 2, 3] }
|
|
29
|
-
*
|
|
30
|
-
* Or just return the array if it's already an array.
|
|
31
|
-
*/
|
|
32
|
-
function extractArrayFromOverlay(overlay) {
|
|
33
|
-
if (Array.isArray(overlay)) {
|
|
34
|
-
return overlay;
|
|
35
|
-
}
|
|
36
|
-
if (isPlainObject(overlay) && "values" in overlay) {
|
|
37
|
-
const values = overlay.values;
|
|
38
|
-
if (Array.isArray(values)) {
|
|
39
|
-
return values;
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
return null;
|
|
43
|
-
}
|
|
44
|
-
/**
|
|
45
|
-
* Get merge strategy from an overlay object's $arrayMerge directive.
|
|
46
|
-
*/
|
|
47
|
-
function getStrategyFromOverlay(overlay) {
|
|
48
|
-
if (isPlainObject(overlay) && "$arrayMerge" in overlay) {
|
|
49
|
-
const strategy = overlay.$arrayMerge;
|
|
50
|
-
if (strategy === "replace" ||
|
|
51
|
-
strategy === "append" ||
|
|
52
|
-
strategy === "prepend") {
|
|
53
|
-
return strategy;
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
return null;
|
|
57
|
-
}
|
|
58
26
|
/**
|
|
59
27
|
* Deep merge two objects with configurable array handling.
|
|
60
28
|
*
|
|
61
29
|
* @param base - The base object
|
|
62
30
|
* @param overlay - The overlay object (values override base)
|
|
63
31
|
* @param ctx - Merge context with array strategies
|
|
64
|
-
* @param path - Current path for strategy lookup (internal)
|
|
65
32
|
*/
|
|
66
|
-
export function deepMerge(base, overlay, ctx
|
|
33
|
+
export function deepMerge(base, overlay, ctx) {
|
|
67
34
|
const result = { ...base };
|
|
68
|
-
// Check for $arrayMerge directive at this level (applies to child arrays)
|
|
69
|
-
const levelStrategy = getStrategyFromOverlay(overlay);
|
|
70
35
|
for (const [key, overlayValue] of Object.entries(overlay)) {
|
|
71
36
|
// Skip directive keys in output
|
|
72
37
|
if (key.startsWith("$"))
|
|
73
38
|
continue;
|
|
74
|
-
const currentPath = path ? `${path}.${key}` : key;
|
|
75
39
|
const baseValue = base[key];
|
|
76
|
-
//
|
|
40
|
+
// Per-field $arrayMerge + $values directive
|
|
77
41
|
if (isPlainObject(overlayValue) && "$arrayMerge" in overlayValue) {
|
|
78
|
-
const strategy =
|
|
79
|
-
const
|
|
80
|
-
if (strategy
|
|
81
|
-
|
|
42
|
+
const strategy = overlayValue.$arrayMerge;
|
|
43
|
+
const values = overlayValue.$values;
|
|
44
|
+
if ((strategy === "replace" ||
|
|
45
|
+
strategy === "append" ||
|
|
46
|
+
strategy === "prepend") &&
|
|
47
|
+
Array.isArray(values) &&
|
|
48
|
+
Array.isArray(baseValue)) {
|
|
49
|
+
result[key] = mergeArrays(baseValue, values, strategy);
|
|
82
50
|
continue;
|
|
83
51
|
}
|
|
84
52
|
}
|
|
85
|
-
// Both are arrays
|
|
53
|
+
// Both are arrays — use default strategy
|
|
86
54
|
if (Array.isArray(baseValue) && Array.isArray(overlayValue)) {
|
|
87
|
-
|
|
88
|
-
const strategy = levelStrategy ??
|
|
89
|
-
ctx.arrayStrategies.get(currentPath) ??
|
|
90
|
-
ctx.defaultArrayStrategy;
|
|
91
|
-
result[key] = mergeArrays(baseValue, overlayValue, strategy);
|
|
55
|
+
result[key] = mergeArrays(baseValue, overlayValue, ctx.defaultArrayStrategy);
|
|
92
56
|
continue;
|
|
93
57
|
}
|
|
94
|
-
// Both are plain objects
|
|
58
|
+
// Both are plain objects — recurse
|
|
95
59
|
if (isPlainObject(baseValue) && isPlainObject(overlayValue)) {
|
|
96
|
-
|
|
97
|
-
if ("$arrayMerge" in overlayValue) {
|
|
98
|
-
const childStrategy = getStrategyFromOverlay(overlayValue);
|
|
99
|
-
if (childStrategy) {
|
|
100
|
-
// Apply to all immediate child arrays
|
|
101
|
-
for (const childKey of Object.keys(overlayValue)) {
|
|
102
|
-
if (!childKey.startsWith("$")) {
|
|
103
|
-
const childPath = currentPath
|
|
104
|
-
? `${currentPath}.${childKey}`
|
|
105
|
-
: childKey;
|
|
106
|
-
ctx.arrayStrategies.set(childPath, childStrategy);
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
result[key] = deepMerge(baseValue, overlayValue, ctx, currentPath);
|
|
60
|
+
result[key] = deepMerge(baseValue, overlayValue, ctx);
|
|
112
61
|
continue;
|
|
113
62
|
}
|
|
114
63
|
// Otherwise, overlay wins (including null values)
|
|
@@ -143,7 +92,6 @@ export function stripMergeDirectives(obj) {
|
|
|
143
92
|
*/
|
|
144
93
|
export function createMergeContext(defaultStrategy = "replace") {
|
|
145
94
|
return {
|
|
146
|
-
arrayStrategies: new Map(),
|
|
147
95
|
defaultArrayStrategy: defaultStrategy,
|
|
148
96
|
};
|
|
149
97
|
}
|
package/dist/config/validator.js
CHANGED
|
@@ -12,15 +12,8 @@ const CONFIG_ID_MAX_LENGTH = 64;
|
|
|
12
12
|
* Supports SSH (git@host:path) and HTTPS (https://host/path) formats.
|
|
13
13
|
*/
|
|
14
14
|
function isValidGitUrl(url) {
|
|
15
|
-
// SSH format: git@hostname:path
|
|
16
|
-
|
|
17
|
-
return true;
|
|
18
|
-
}
|
|
19
|
-
// HTTPS format: https://hostname/path
|
|
20
|
-
if (/^https?:\/\/[^/]+\/.+$/.test(url)) {
|
|
21
|
-
return true;
|
|
22
|
-
}
|
|
23
|
-
return false;
|
|
15
|
+
// SSH format: git@hostname:path OR HTTPS format: https://hostname/path
|
|
16
|
+
return /^git@[^:]+:.+$/.test(url) || /^https?:\/\/[^/]+\/.+$/.test(url);
|
|
24
17
|
}
|
|
25
18
|
/**
|
|
26
19
|
* Check if a git URL points to GitHub (github.com).
|
|
@@ -15,12 +15,10 @@ export class RepoLifecycleFactory {
|
|
|
15
15
|
this.log = log;
|
|
16
16
|
}
|
|
17
17
|
getProvider(platform) {
|
|
18
|
-
// Check cache first
|
|
19
18
|
const cached = this.providers.get(platform);
|
|
20
19
|
if (cached) {
|
|
21
20
|
return cached;
|
|
22
21
|
}
|
|
23
|
-
// Create provider
|
|
24
22
|
let provider;
|
|
25
23
|
switch (platform) {
|
|
26
24
|
case "github":
|
|
@@ -39,12 +37,10 @@ export class RepoLifecycleFactory {
|
|
|
39
37
|
return provider;
|
|
40
38
|
}
|
|
41
39
|
getMigrationSource(platform) {
|
|
42
|
-
// Check cache first
|
|
43
40
|
const cached = this.sources.get(platform);
|
|
44
41
|
if (cached) {
|
|
45
42
|
return cached;
|
|
46
43
|
}
|
|
47
|
-
// Create source
|
|
48
44
|
let source;
|
|
49
45
|
switch (platform) {
|
|
50
46
|
case "azure-devops":
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { DebugLog } from "../shared/logger.js";
|
|
2
|
+
import type { SettingsAction } from "../settings/index.js";
|
|
2
3
|
export type MergeOutcome = "manual" | "auto" | "force" | "direct";
|
|
3
4
|
export interface FileChanges {
|
|
4
5
|
added: number;
|
|
@@ -8,7 +9,7 @@ export interface FileChanges {
|
|
|
8
9
|
}
|
|
9
10
|
export interface RulesetPlanDetail {
|
|
10
11
|
name: string;
|
|
11
|
-
action:
|
|
12
|
+
action: SettingsAction;
|
|
12
13
|
propertyCount?: number;
|
|
13
14
|
propertyChanges?: {
|
|
14
15
|
added: number;
|
|
@@ -22,7 +23,7 @@ export interface RepoSettingsPlanDetail {
|
|
|
22
23
|
}
|
|
23
24
|
export interface LabelsPlanDetail {
|
|
24
25
|
name: string;
|
|
25
|
-
action:
|
|
26
|
+
action: SettingsAction;
|
|
26
27
|
newName?: string;
|
|
27
28
|
}
|
|
28
29
|
export interface RepoResult {
|
|
@@ -179,13 +179,7 @@ export function formatSummary(data) {
|
|
|
179
179
|
lines.push("| Label | Action |");
|
|
180
180
|
lines.push("|-------|--------|");
|
|
181
181
|
for (const detail of result.labelsPlanDetails) {
|
|
182
|
-
const action = detail.action
|
|
183
|
-
? "+ Create"
|
|
184
|
-
: detail.action === "update"
|
|
185
|
-
? "~ Update"
|
|
186
|
-
: detail.action === "delete"
|
|
187
|
-
? "- Delete"
|
|
188
|
-
: "No change";
|
|
182
|
+
const action = formatRulesetAction(detail.action);
|
|
189
183
|
const name = detail.newName
|
|
190
184
|
? `${detail.name} \u2192 ${detail.newName}`
|
|
191
185
|
: detail.name;
|