@aspruyt/xfg 6.0.3 → 6.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/lifecycle-report-builder.d.ts +2 -2
- package/dist/cli/lifecycle-report-builder.js +3 -11
- package/dist/cli/program.d.ts +2 -1
- package/dist/cli/program.js +2 -3
- package/dist/cli/repo-sync-runner.d.ts +24 -0
- package/dist/cli/repo-sync-runner.js +156 -0
- package/dist/cli/results-collector.d.ts +1 -1
- package/dist/cli/results-collector.js +2 -2
- package/dist/cli/settings-factories.d.ts +7 -0
- package/dist/cli/settings-factories.js +27 -0
- package/dist/cli/settings-report-builder.d.ts +1 -1
- package/dist/cli/settings-report-builder.js +12 -23
- package/dist/cli/settings-runner.d.ts +2 -0
- package/dist/cli/settings-runner.js +87 -0
- package/dist/cli/sync-command.d.ts +1 -1
- package/dist/cli/sync-command.js +31 -372
- package/dist/cli/sync-report-builder.d.ts +1 -1
- package/dist/cli/sync-utils.d.ts +8 -0
- package/dist/cli/sync-utils.js +36 -0
- package/dist/cli/types.d.ts +5 -7
- package/dist/cli/unified-summary.d.ts +1 -3
- package/dist/cli/unified-summary.js +7 -5
- package/dist/cli.js +2 -1
- package/dist/{shared → config}/env.js +2 -2
- package/dist/config/extends-resolver.js +4 -3
- package/dist/config/file-reference-resolver.js +4 -2
- package/dist/config/formatter.js +18 -1
- package/dist/config/index.d.ts +1 -1
- package/dist/config/loader.js +30 -6
- package/dist/config/merge.d.ts +11 -1
- package/dist/config/merge.js +78 -6
- package/dist/config/normalizer.js +53 -38
- package/dist/config/validator.d.ts +1 -4
- package/dist/config/validator.js +13 -599
- package/dist/config/validators/file-validator.d.ts +2 -1
- package/dist/config/validators/file-validator.js +9 -1
- package/dist/config/validators/group-validator.d.ts +3 -0
- package/dist/config/validators/group-validator.js +167 -0
- package/dist/config/validators/repo-entry-validator.d.ts +2 -0
- package/dist/config/validators/repo-entry-validator.js +165 -0
- package/dist/config/validators/repo-settings-validator.js +18 -7
- package/dist/config/validators/ruleset-validator.js +2 -5
- package/dist/config/validators/shared.d.ts +11 -0
- package/dist/config/validators/shared.js +242 -0
- package/dist/lifecycle/ado-migration-source.js +2 -4
- package/dist/lifecycle/github-lifecycle-provider.d.ts +7 -11
- package/dist/lifecycle/github-lifecycle-provider.js +125 -136
- package/dist/lifecycle/{lifecycle-helpers.d.ts → helpers.d.ts} +5 -1
- package/dist/lifecycle/{lifecycle-helpers.js → helpers.js} +9 -8
- package/dist/lifecycle/index.d.ts +2 -2
- package/dist/lifecycle/index.js +1 -1
- package/dist/lifecycle/repo-lifecycle-factory.d.ts +2 -2
- package/dist/output/github-summary.js +2 -3
- package/dist/output/index.d.ts +4 -0
- package/dist/output/index.js +4 -0
- package/dist/output/lifecycle-report.d.ts +1 -1
- package/dist/output/lifecycle-report.js +5 -0
- package/dist/output/sync-report.d.ts +25 -3
- package/dist/output/sync-report.js +11 -11
- package/dist/settings/base-processor.d.ts +18 -7
- package/dist/settings/base-processor.js +26 -5
- package/dist/settings/code-scanning/diff.js +2 -2
- package/dist/settings/code-scanning/formatter.d.ts +2 -6
- package/dist/settings/code-scanning/formatter.js +2 -25
- package/dist/settings/code-scanning/github-code-scanning-strategy.d.ts +3 -7
- package/dist/settings/code-scanning/github-code-scanning-strategy.js +2 -2
- package/dist/settings/code-scanning/processor.js +6 -4
- package/dist/settings/code-scanning/types.d.ts +10 -8
- package/dist/settings/labels/github-labels-strategy.d.ts +3 -11
- package/dist/settings/labels/types.d.ts +12 -10
- package/dist/settings/repo-settings/diff.d.ts +1 -1
- package/dist/settings/repo-settings/diff.js +1 -1
- package/dist/settings/repo-settings/formatter.d.ts +2 -6
- package/dist/settings/repo-settings/formatter.js +4 -23
- package/dist/settings/repo-settings/github-repo-settings-strategy.d.ts +2 -2
- package/dist/settings/repo-settings/github-repo-settings-strategy.js +8 -7
- package/dist/settings/repo-settings/processor.js +11 -11
- package/dist/settings/repo-settings/types.d.ts +2 -2
- package/dist/settings/rulesets/diff-algorithm.js +4 -2
- package/dist/settings/rulesets/diff.js +2 -51
- package/dist/settings/rulesets/formatter.js +4 -0
- package/dist/settings/rulesets/github-ruleset-strategy.d.ts +3 -3
- package/dist/settings/rulesets/github-ruleset-strategy.js +4 -6
- package/dist/settings/rulesets/index.d.ts +1 -1
- package/dist/settings/rulesets/index.js +0 -2
- package/dist/settings/rulesets/processor.js +1 -1
- package/dist/settings/rulesets/types.d.ts +6 -2
- package/dist/shared/command-executor.d.ts +4 -4
- package/dist/shared/command-executor.js +9 -7
- package/dist/shared/diff-format.d.ts +1 -0
- package/dist/shared/diff-format.js +10 -0
- package/dist/shared/errors.d.ts +7 -4
- package/dist/shared/errors.js +8 -8
- package/dist/shared/gh-api-utils.d.ts +3 -34
- package/dist/shared/gh-api-utils.js +23 -53
- package/dist/shared/gh-token-utils.d.ts +26 -0
- package/dist/shared/gh-token-utils.js +32 -0
- package/dist/shared/json-utils.js +1 -1
- package/dist/shared/regex-utils.d.ts +1 -0
- package/dist/shared/regex-utils.js +3 -0
- package/dist/shared/retry-utils.d.ts +1 -0
- package/dist/shared/retry-utils.js +13 -7
- package/dist/sync/auth-options-builder.js +1 -1
- package/dist/sync/branch-manager.js +5 -3
- package/dist/sync/commit-push-manager.js +2 -3
- package/dist/sync/diff-utils.d.ts +0 -1
- package/dist/sync/diff-utils.js +5 -10
- package/dist/sync/file-sync-orchestrator.js +0 -2
- package/dist/sync/file-writer.d.ts +3 -0
- package/dist/sync/file-writer.js +84 -81
- package/dist/sync/index.d.ts +0 -1
- package/dist/sync/index.js +0 -1
- package/dist/sync/manifest.js +1 -1
- package/dist/sync/pr-merge-handler.js +6 -6
- package/dist/sync/sync-workflow.js +1 -1
- package/dist/sync/types.d.ts +2 -2
- package/dist/vcs/ado-pr-strategy.d.ts +3 -5
- package/dist/vcs/ado-pr-strategy.js +131 -33
- package/dist/vcs/authenticated-git-ops.js +45 -23
- package/dist/vcs/git-commit-strategy.js +10 -6
- package/dist/vcs/git-ops.js +30 -24
- package/dist/vcs/github-pr-strategy.d.ts +3 -2
- package/dist/vcs/github-pr-strategy.js +80 -30
- package/dist/vcs/gitlab-pr-strategy.d.ts +2 -5
- package/dist/vcs/gitlab-pr-strategy.js +88 -87
- package/dist/vcs/graphql-commit-strategy.d.ts +1 -5
- package/dist/vcs/graphql-commit-strategy.js +21 -37
- package/dist/vcs/pr-creator.js +9 -2
- package/dist/vcs/pr-strategy.d.ts +2 -3
- package/dist/vcs/pr-strategy.js +0 -1
- package/dist/vcs/types.d.ts +9 -5
- package/package.json +5 -5
- package/dist/config/validators/index.d.ts +0 -3
- package/dist/config/validators/index.js +0 -6
- package/dist/output/types.d.ts +0 -20
- package/dist/output/types.js +0 -1
- package/dist/shared/shell-utils.d.ts +0 -6
- package/dist/shared/shell-utils.js +0 -17
- /package/dist/{shared → config}/env.d.ts +0 -0
- /package/dist/lifecycle/{lifecycle-formatter.d.ts → formatter.d.ts} +0 -0
- /package/dist/lifecycle/{lifecycle-formatter.js → formatter.js} +0 -0
- /package/dist/{vcs → shared}/sanitize-utils.d.ts +0 -0
- /package/dist/{vcs → shared}/sanitize-utils.js +0 -0
package/dist/cli/sync-command.js
CHANGED
|
@@ -1,209 +1,47 @@
|
|
|
1
|
-
import { resolve
|
|
1
|
+
import { resolve } from "node:path";
|
|
2
2
|
import { existsSync } from "node:fs";
|
|
3
3
|
import { loadRawConfig, normalizeConfig, validateForSync, } from "../config/index.js";
|
|
4
4
|
import { ValidationError, SyncError } from "../shared/errors.js";
|
|
5
|
-
import {
|
|
6
|
-
import { sanitizeBranchName, validateBranchName } from "./branch-utils.js";
|
|
5
|
+
import { validateBranchName } from "./branch-utils.js";
|
|
7
6
|
import { createTokenManager } from "../vcs/index.js";
|
|
8
7
|
import { RepositoryProcessor } from "../sync/index.js";
|
|
9
|
-
import {
|
|
10
|
-
import { GitHubRepoMetadataProvider } from "../repo/index.js";
|
|
11
|
-
import { ShellCommandExecutor } from "../shared/command-executor.js";
|
|
8
|
+
import { ProcessExecutor } from "../shared/command-executor.js";
|
|
12
9
|
import { Logger } from "../shared/logger.js";
|
|
13
|
-
import { generateWorkspaceName } from "../shared/workspace-utils.js";
|
|
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
|
-
const executor = getDefaultExecutor();
|
|
29
|
-
return () => new RepoSettingsProcessor(new GitHubRepoSettingsStrategy(executor, { cwd }), new GitHubRepoMetadataProvider(executor, { cwd }));
|
|
30
|
-
}
|
|
31
|
-
function createDefaultLabelsProcessorFactory() {
|
|
32
|
-
const cwd = process.cwd();
|
|
33
|
-
return () => new LabelsProcessor(new GitHubLabelsStrategy(getDefaultExecutor(), { cwd }));
|
|
34
|
-
}
|
|
35
|
-
function createDefaultCodeScanningProcessorFactory() {
|
|
36
|
-
const cwd = process.cwd();
|
|
37
|
-
const executor = getDefaultExecutor();
|
|
38
|
-
return () => new CodeScanningProcessor(new GitHubCodeScanningStrategy(executor, { cwd }), new GitHubRepoMetadataProvider(executor, { cwd }));
|
|
39
|
-
}
|
|
40
10
|
import { ResultsCollector } from "./results-collector.js";
|
|
41
|
-
import { buildSettingsReport
|
|
42
|
-
import { formatSettingsReportCLI } from "../output/
|
|
11
|
+
import { buildSettingsReport } from "./settings-report-builder.js";
|
|
12
|
+
import { formatSettingsReportCLI, formatSyncReportCLI, formatLifecycleReportCLI, hasLifecycleChanges, } from "../output/index.js";
|
|
43
13
|
import { buildSyncReport } from "./sync-report-builder.js";
|
|
44
|
-
import { formatSyncReportCLI } from "../output/sync-report.js";
|
|
45
14
|
import { buildLifecycleReport } from "./lifecycle-report-builder.js";
|
|
46
|
-
import { formatLifecycleReportCLI, hasLifecycleChanges, } from "../output/lifecycle-report.js";
|
|
47
15
|
import { writeUnifiedSummary } from "./unified-summary.js";
|
|
48
|
-
import {
|
|
49
|
-
import {
|
|
50
|
-
import {
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
for (const repo of config.repos) {
|
|
54
|
-
for (const file of repo.files) {
|
|
55
|
-
fileNames.add(file.fileName);
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
return Array.from(fileNames);
|
|
59
|
-
}
|
|
60
|
-
function generateBranchName(fileNames) {
|
|
61
|
-
if (fileNames.length === 1) {
|
|
62
|
-
return `chore/sync-${sanitizeBranchName(fileNames[0])}`;
|
|
63
|
-
}
|
|
64
|
-
return "chore/sync-config";
|
|
65
|
-
}
|
|
66
|
-
function formatFileNames(fileNames) {
|
|
67
|
-
if (fileNames.length === 1) {
|
|
68
|
-
return fileNames[0];
|
|
69
|
-
}
|
|
70
|
-
if (fileNames.length <= 3) {
|
|
71
|
-
return fileNames.join(", ");
|
|
72
|
-
}
|
|
73
|
-
return `${fileNames.length} files`;
|
|
74
|
-
}
|
|
75
|
-
function determineMergeOutcome(result) {
|
|
76
|
-
if (!result.success)
|
|
77
|
-
return undefined;
|
|
78
|
-
if (!result.prUrl)
|
|
79
|
-
return "direct";
|
|
80
|
-
if (result.mergeResult?.merged)
|
|
81
|
-
return "force";
|
|
82
|
-
if (result.mergeResult?.autoMergeEnabled)
|
|
83
|
-
return "auto";
|
|
84
|
-
return "manual";
|
|
85
|
-
}
|
|
86
|
-
function logSettingsResult(result, label, repoNumber, repoName, settingsCollector) {
|
|
87
|
-
if (result.planOutput?.lines?.length) {
|
|
88
|
-
getLogger().info("");
|
|
89
|
-
getLogger().info(`${repoName} - ${label}:`);
|
|
90
|
-
for (const line of result.planOutput.lines) {
|
|
91
|
-
getLogger().info(line);
|
|
92
|
-
}
|
|
93
|
-
if (result.warnings?.length) {
|
|
94
|
-
for (const warning of result.warnings) {
|
|
95
|
-
getLogger().warn(warning);
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
else if (!result.skipped && result.success) {
|
|
100
|
-
getLogger().success(repoNumber, repoName, `${label}: ${result.message}`);
|
|
101
|
-
}
|
|
102
|
-
if (!result.success && !result.skipped) {
|
|
103
|
-
getLogger().error(repoNumber, repoName, `${label}: ${result.message}`);
|
|
104
|
-
settingsCollector.appendError(repoName, result.message);
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
// Each processor returns a subtype of BaseProcessorResult whose planOutput
|
|
108
|
-
// contains both `lines` (for CLI display) and `entries` (for report building).
|
|
109
|
-
// ProcessorResults fields capture only the `entries` slice; the runtime object
|
|
110
|
-
// satisfies both views, so we assign with an explicit per-field cast.
|
|
111
|
-
async function runAndStoreResult(factory, repoConfig, repoInfo, opts, repoName, settingsCollector, assign) {
|
|
112
|
-
const result = await runSettingsProcessor(factory, repoConfig, repoInfo, opts);
|
|
113
|
-
if (!result.skipped) {
|
|
114
|
-
assign(settingsCollector.getOrCreate(repoName), result);
|
|
115
|
-
}
|
|
116
|
-
return result;
|
|
117
|
-
}
|
|
118
|
-
function buildSettingsDescriptors(ctx) {
|
|
119
|
-
const { repoConfig, repoInfo, options, token, repoName, settingsCollector } = ctx;
|
|
120
|
-
const { factories } = ctx;
|
|
121
|
-
const sharedOpts = {
|
|
122
|
-
dryRun: options.dryRun,
|
|
123
|
-
noDelete: options.noDelete,
|
|
124
|
-
token,
|
|
125
|
-
};
|
|
126
|
-
return [
|
|
127
|
-
{
|
|
128
|
-
key: "rulesets",
|
|
129
|
-
label: "Rulesets",
|
|
130
|
-
run: () => runAndStoreResult(factories.rulesets, repoConfig, repoInfo, sharedOpts, repoName, settingsCollector, (e, r) => {
|
|
131
|
-
e.rulesetResult = r;
|
|
132
|
-
}),
|
|
133
|
-
},
|
|
134
|
-
{
|
|
135
|
-
key: "labels",
|
|
136
|
-
label: "Labels",
|
|
137
|
-
run: () => runAndStoreResult(factories.labels, repoConfig, repoInfo, sharedOpts, repoName, settingsCollector, (e, r) => {
|
|
138
|
-
e.labelsResult = r;
|
|
139
|
-
}),
|
|
140
|
-
},
|
|
141
|
-
{
|
|
142
|
-
key: "repo",
|
|
143
|
-
label: "Repo Settings",
|
|
144
|
-
run: () => runAndStoreResult(factories.repo, repoConfig, repoInfo, { dryRun: options.dryRun, token }, repoName, settingsCollector, (e, r) => {
|
|
145
|
-
e.settingsResult = r;
|
|
146
|
-
}),
|
|
147
|
-
},
|
|
148
|
-
{
|
|
149
|
-
key: "codeScanning",
|
|
150
|
-
label: "Code Scanning",
|
|
151
|
-
run: () => runAndStoreResult(factories.codeScanning, repoConfig, repoInfo, sharedOpts, repoName, settingsCollector, (e, r) => {
|
|
152
|
-
e.codeScanningResult = r;
|
|
153
|
-
}),
|
|
154
|
-
},
|
|
155
|
-
];
|
|
156
|
-
}
|
|
157
|
-
function runSettingsProcessor(factory, repoConfig, repoInfo, processOptions) {
|
|
158
|
-
return factory()
|
|
159
|
-
.process(repoConfig, repoInfo, processOptions)
|
|
160
|
-
.then((result) => result);
|
|
161
|
-
}
|
|
162
|
-
async function applyRepoSettings(ctx) {
|
|
163
|
-
const { repoConfig, repoInfo, repoName, repoNumber, settingsCollector } = ctx;
|
|
164
|
-
if (!repoConfig.settings || !isGitHubRepo(repoInfo))
|
|
165
|
-
return;
|
|
166
|
-
for (const desc of buildSettingsDescriptors(ctx)) {
|
|
167
|
-
const settingsValue = repoConfig.settings[desc.key];
|
|
168
|
-
if (!settingsValue || Object.keys(settingsValue).length === 0)
|
|
169
|
-
continue;
|
|
170
|
-
try {
|
|
171
|
-
const result = await desc.run();
|
|
172
|
-
logSettingsResult(result, desc.label, repoNumber, repoName, settingsCollector);
|
|
173
|
-
}
|
|
174
|
-
catch (error) {
|
|
175
|
-
getLogger().error(repoNumber, repoName, `${desc.label}: ${toErrorMessage(error)}`);
|
|
176
|
-
settingsCollector.appendError(repoName, error);
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
function displayReports(reportResults, lifecycleReportInputs, settingsCollector, dryRun) {
|
|
16
|
+
import { RepoLifecycleManager } from "../lifecycle/index.js";
|
|
17
|
+
import { createDefaultFactories } from "./settings-factories.js";
|
|
18
|
+
import { getUniqueFileNames, generateBranchName, formatFileNames, } from "./sync-utils.js";
|
|
19
|
+
import { runSingleRepo, } from "./repo-sync-runner.js";
|
|
20
|
+
function displayReports(logger, reportResults, lifecycleReportInputs, settingsCollector, dryRun) {
|
|
181
21
|
const lifecycleReport = buildLifecycleReport(lifecycleReportInputs);
|
|
182
22
|
if (hasLifecycleChanges(lifecycleReport)) {
|
|
183
|
-
|
|
23
|
+
logger.log("");
|
|
184
24
|
for (const line of formatLifecycleReportCLI(lifecycleReport)) {
|
|
185
|
-
|
|
25
|
+
logger.log(line);
|
|
186
26
|
}
|
|
187
27
|
}
|
|
188
28
|
const report = buildSyncReport(reportResults);
|
|
189
|
-
|
|
29
|
+
logger.log("");
|
|
190
30
|
for (const line of formatSyncReportCLI(report)) {
|
|
191
|
-
|
|
31
|
+
logger.log(line);
|
|
192
32
|
}
|
|
193
|
-
// Build and display settings report (if any settings were processed)
|
|
194
33
|
const settingsResults = settingsCollector.getAll();
|
|
195
34
|
let settingsReport;
|
|
196
35
|
if (settingsResults.length > 0) {
|
|
197
36
|
settingsReport = buildSettingsReport(settingsResults);
|
|
198
37
|
const settingsLines = formatSettingsReportCLI(settingsReport);
|
|
199
38
|
if (settingsLines.length > 0) {
|
|
200
|
-
|
|
39
|
+
logger.log("");
|
|
201
40
|
for (const line of settingsLines) {
|
|
202
|
-
|
|
41
|
+
logger.log(line);
|
|
203
42
|
}
|
|
204
43
|
}
|
|
205
44
|
}
|
|
206
|
-
// Write unified summary to GITHUB_STEP_SUMMARY
|
|
207
45
|
writeUnifiedSummary({
|
|
208
46
|
lifecycle: lifecycleReport,
|
|
209
47
|
sync: report,
|
|
@@ -212,198 +50,18 @@ function displayReports(reportResults, lifecycleReportInputs, settingsCollector,
|
|
|
212
50
|
summaryPath: process.env.GITHUB_STEP_SUMMARY,
|
|
213
51
|
});
|
|
214
52
|
}
|
|
215
|
-
function pushFailure(results, repoName, error) {
|
|
216
|
-
results.push({
|
|
217
|
-
repoName,
|
|
218
|
-
success: false,
|
|
219
|
-
fileChanges: [],
|
|
220
|
-
error: toErrorMessage(error),
|
|
221
|
-
});
|
|
222
|
-
}
|
|
223
|
-
/**
|
|
224
|
-
* Process a single repository: resolve URL, run lifecycle check, sync files, apply settings.
|
|
225
|
-
* Pushes results into ctx.reportResults, ctx.lifecycleReportInputs, and ctx.settingsCollector.
|
|
226
|
-
*/
|
|
227
|
-
async function processSingleRepo(repoConfig, index, ctx) {
|
|
228
|
-
const { config, options } = ctx;
|
|
229
|
-
const repoNumber = index + 1;
|
|
230
|
-
// Apply CLI-level PR option overrides
|
|
231
|
-
if (options.merge || options.mergeStrategy || options.deleteBranch) {
|
|
232
|
-
repoConfig.prOptions = {
|
|
233
|
-
...repoConfig.prOptions,
|
|
234
|
-
merge: options.merge ?? repoConfig.prOptions?.merge,
|
|
235
|
-
mergeStrategy: options.mergeStrategy ?? repoConfig.prOptions?.mergeStrategy,
|
|
236
|
-
deleteBranch: options.deleteBranch ?? repoConfig.prOptions?.deleteBranch,
|
|
237
|
-
};
|
|
238
|
-
}
|
|
239
|
-
const mergeMode = repoConfig.prOptions?.merge ?? "auto";
|
|
240
|
-
if (mergeMode === "direct" && repoConfig.prOptions?.mergeStrategy) {
|
|
241
|
-
getLogger().warn(`mergeStrategy '${repoConfig.prOptions.mergeStrategy}' is ignored in direct mode for ${repoConfig.git}`);
|
|
242
|
-
}
|
|
243
|
-
let repoInfo;
|
|
244
|
-
try {
|
|
245
|
-
repoInfo = parseGitUrl(repoConfig.git, {
|
|
246
|
-
githubHosts: config.githubHosts,
|
|
247
|
-
});
|
|
248
|
-
}
|
|
249
|
-
catch (error) {
|
|
250
|
-
getLogger().error(repoNumber, repoConfig.git, toErrorMessage(error));
|
|
251
|
-
pushFailure(ctx.reportResults, repoConfig.git, error);
|
|
252
|
-
return;
|
|
253
|
-
}
|
|
254
|
-
const repoName = getRepoDisplayName(repoInfo);
|
|
255
|
-
const workDir = resolve(join(options.workDir ?? "./tmp", generateWorkspaceName(index)));
|
|
256
|
-
const repoToken = isGitHubRepo(repoInfo)
|
|
257
|
-
? (await resolveGitHubToken({
|
|
258
|
-
repoInfo: repoInfo,
|
|
259
|
-
tokenManager: ctx.tokenManager,
|
|
260
|
-
context: repoName,
|
|
261
|
-
log: getLogger(),
|
|
262
|
-
envToken: process.env.GH_TOKEN,
|
|
263
|
-
})).token
|
|
264
|
-
: undefined;
|
|
265
|
-
const repo = {
|
|
266
|
-
repoConfig,
|
|
267
|
-
repoInfo,
|
|
268
|
-
repoName,
|
|
269
|
-
index,
|
|
270
|
-
workDir,
|
|
271
|
-
token: repoToken,
|
|
272
|
-
};
|
|
273
|
-
const skipFileSync = await runLifecyclePhase(repo, ctx);
|
|
274
|
-
if (skipFileSync)
|
|
275
|
-
return;
|
|
276
|
-
// Sync files via processor
|
|
277
|
-
await runFileSyncPhase(repo, ctx);
|
|
278
|
-
// Apply settings via API (GitHub-only — ADO and GitLab repos are skipped)
|
|
279
|
-
await applyRepoSettings({
|
|
280
|
-
repoConfig,
|
|
281
|
-
repoInfo,
|
|
282
|
-
repoName,
|
|
283
|
-
repoNumber,
|
|
284
|
-
options,
|
|
285
|
-
token: repoToken,
|
|
286
|
-
settingsCollector: ctx.settingsCollector,
|
|
287
|
-
factories: ctx.factories,
|
|
288
|
-
});
|
|
289
|
-
}
|
|
290
|
-
/**
|
|
291
|
-
* Run lifecycle check (repo existence, creation, forking).
|
|
292
|
-
* Returns true if the main loop should skip file sync for this repo.
|
|
293
|
-
*/
|
|
294
|
-
async function runLifecyclePhase(repo, ctx) {
|
|
295
|
-
const repoNumber = repo.index + 1;
|
|
296
|
-
try {
|
|
297
|
-
const { outputLines, lifecycleResult, createSettings } = await runLifecycleCheck(repo.repoConfig, repo.repoInfo, {
|
|
298
|
-
dryRun: ctx.options.dryRun ?? false,
|
|
299
|
-
resolvedWorkDir: repo.workDir,
|
|
300
|
-
githubHosts: ctx.config.githubHosts,
|
|
301
|
-
token: repo.token,
|
|
302
|
-
repoIndex: repo.index,
|
|
303
|
-
lifecycleManager: ctx.lifecycleManager,
|
|
304
|
-
repoSettings: ctx.config.settings?.repo,
|
|
305
|
-
});
|
|
306
|
-
for (const line of outputLines) {
|
|
307
|
-
getLogger().info(line);
|
|
308
|
-
}
|
|
309
|
-
ctx.lifecycleReportInputs.push({
|
|
310
|
-
repoName: repo.repoName,
|
|
311
|
-
action: lifecycleResult.action,
|
|
312
|
-
upstream: repo.repoConfig.upstream,
|
|
313
|
-
source: repo.repoConfig.source,
|
|
314
|
-
settings: createSettings
|
|
315
|
-
? {
|
|
316
|
-
visibility: createSettings.visibility,
|
|
317
|
-
description: createSettings.description,
|
|
318
|
-
}
|
|
319
|
-
: undefined,
|
|
320
|
-
});
|
|
321
|
-
// In dry-run, skip processing repos that don't exist yet
|
|
322
|
-
if (ctx.options.dryRun && lifecycleResult.action !== "existed") {
|
|
323
|
-
ctx.reportResults.push({
|
|
324
|
-
repoName: repo.repoName,
|
|
325
|
-
success: true,
|
|
326
|
-
fileChanges: [],
|
|
327
|
-
});
|
|
328
|
-
return true;
|
|
329
|
-
}
|
|
330
|
-
return false;
|
|
331
|
-
}
|
|
332
|
-
catch (error) {
|
|
333
|
-
getLogger().error(repoNumber, repo.repoName, `Lifecycle error: ${toErrorMessage(error)}`);
|
|
334
|
-
pushFailure(ctx.reportResults, repo.repoName, error);
|
|
335
|
-
return true;
|
|
336
|
-
}
|
|
337
|
-
}
|
|
338
|
-
/**
|
|
339
|
-
* Run the file sync processor for a single repo and collect results.
|
|
340
|
-
*/
|
|
341
|
-
async function runFileSyncPhase(repo, ctx) {
|
|
342
|
-
const repoNumber = repo.index + 1;
|
|
343
|
-
try {
|
|
344
|
-
getLogger().progress(repoNumber, repo.repoName, "Processing...");
|
|
345
|
-
const result = await ctx.processor.process(repo.repoConfig, repo.repoInfo, {
|
|
346
|
-
branchName: ctx.branchName,
|
|
347
|
-
workDir: repo.workDir,
|
|
348
|
-
configId: ctx.config.id,
|
|
349
|
-
dryRun: ctx.options.dryRun,
|
|
350
|
-
retries: ctx.options.retries,
|
|
351
|
-
executor: getDefaultExecutor(),
|
|
352
|
-
prTemplate: ctx.config.prTemplate,
|
|
353
|
-
noDelete: ctx.options.noDelete,
|
|
354
|
-
token: repo.token,
|
|
355
|
-
hasAppCredentials: isGitHubRepo(repo.repoInfo) && ctx.tokenManager !== null,
|
|
356
|
-
});
|
|
357
|
-
const mergeOutcome = determineMergeOutcome(result);
|
|
358
|
-
ctx.reportResults.push({
|
|
359
|
-
repoName: repo.repoName,
|
|
360
|
-
success: result.success,
|
|
361
|
-
fileChanges: (result.fileChanges ?? []).map((f) => ({
|
|
362
|
-
path: f.path,
|
|
363
|
-
action: f.action,
|
|
364
|
-
...(f.diffLines ? { diffLines: f.diffLines } : {}),
|
|
365
|
-
})),
|
|
366
|
-
prUrl: result.prUrl,
|
|
367
|
-
mergeOutcome,
|
|
368
|
-
error: result.success ? undefined : result.message,
|
|
369
|
-
});
|
|
370
|
-
if (result.skipped) {
|
|
371
|
-
getLogger().skip(repoNumber, repo.repoName, result.message);
|
|
372
|
-
}
|
|
373
|
-
else if (result.success) {
|
|
374
|
-
getLogger().success(repoNumber, repo.repoName, result.message);
|
|
375
|
-
}
|
|
376
|
-
else {
|
|
377
|
-
getLogger().error(repoNumber, repo.repoName, result.message);
|
|
378
|
-
}
|
|
379
|
-
}
|
|
380
|
-
catch (error) {
|
|
381
|
-
getLogger().error(repoNumber, repo.repoName, toErrorMessage(error));
|
|
382
|
-
pushFailure(ctx.reportResults, repo.repoName, error);
|
|
383
|
-
}
|
|
384
|
-
}
|
|
385
53
|
export async function runSync(options, deps = {}) {
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
_logger = undefined;
|
|
54
|
+
const executor = new ProcessExecutor(process.env);
|
|
55
|
+
const logger = new Logger(!!(process.env.DEBUG || process.env.XFG_DEBUG));
|
|
389
56
|
const { lifecycleManager, settingsProcessorFactories } = deps;
|
|
390
|
-
const factories =
|
|
391
|
-
rulesets: settingsProcessorFactories?.rulesets ??
|
|
392
|
-
createDefaultRulesetProcessorFactory(),
|
|
393
|
-
labels: settingsProcessorFactories?.labels ??
|
|
394
|
-
createDefaultLabelsProcessorFactory(),
|
|
395
|
-
repo: settingsProcessorFactories?.repo ??
|
|
396
|
-
createDefaultRepoSettingsProcessorFactory(),
|
|
397
|
-
codeScanning: settingsProcessorFactories?.codeScanning ??
|
|
398
|
-
createDefaultCodeScanningProcessorFactory(),
|
|
399
|
-
};
|
|
57
|
+
const factories = createDefaultFactories(executor, settingsProcessorFactories);
|
|
400
58
|
const configPath = resolve(options.config);
|
|
401
59
|
if (!existsSync(configPath)) {
|
|
402
60
|
throw new ValidationError(`Config path not found: ${configPath}`);
|
|
403
61
|
}
|
|
404
|
-
|
|
62
|
+
logger.log(`Loading config from: ${configPath}`);
|
|
405
63
|
if (options.dryRun) {
|
|
406
|
-
|
|
64
|
+
logger.log("Running in DRY RUN mode - no changes will be made\n");
|
|
407
65
|
}
|
|
408
66
|
const rawConfig = loadRawConfig(configPath);
|
|
409
67
|
validateForSync(rawConfig);
|
|
@@ -417,10 +75,10 @@ export async function runSync(options, deps = {}) {
|
|
|
417
75
|
else {
|
|
418
76
|
branchName = generateBranchName(fileNames);
|
|
419
77
|
}
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
78
|
+
logger.setTotal(config.repos.length);
|
|
79
|
+
logger.log(`Found ${config.repos.length} repositories to process`);
|
|
80
|
+
logger.log(`Target files: ${formatFileNames(fileNames)}`);
|
|
81
|
+
logger.log(`Branch: ${branchName}\n`);
|
|
424
82
|
const tokenManager = createTokenManager(process.env.XFG_GITHUB_CLIENT_ID && process.env.XFG_GITHUB_APP_PRIVATE_KEY
|
|
425
83
|
? {
|
|
426
84
|
clientId: process.env.XFG_GITHUB_CLIENT_ID,
|
|
@@ -429,7 +87,7 @@ export async function runSync(options, deps = {}) {
|
|
|
429
87
|
: undefined);
|
|
430
88
|
const processor = deps.processorFactory
|
|
431
89
|
? deps.processorFactory()
|
|
432
|
-
: new RepositoryProcessor(undefined,
|
|
90
|
+
: new RepositoryProcessor(undefined, logger, {
|
|
433
91
|
tokenManager,
|
|
434
92
|
envToken: process.env.GH_TOKEN,
|
|
435
93
|
});
|
|
@@ -439,18 +97,19 @@ export async function runSync(options, deps = {}) {
|
|
|
439
97
|
branchName,
|
|
440
98
|
processor,
|
|
441
99
|
lifecycleManager: lifecycleManager ??
|
|
442
|
-
new RepoLifecycleManager(undefined,
|
|
100
|
+
new RepoLifecycleManager(undefined, executor, options.retries, process.cwd(), logger),
|
|
443
101
|
tokenManager,
|
|
444
102
|
reportResults: [],
|
|
445
103
|
lifecycleReportInputs: [],
|
|
446
104
|
settingsCollector: new ResultsCollector(),
|
|
447
105
|
factories,
|
|
106
|
+
logger,
|
|
107
|
+
executor,
|
|
448
108
|
};
|
|
449
109
|
for (let i = 0; i < config.repos.length; i++) {
|
|
450
|
-
await
|
|
110
|
+
await runSingleRepo(config.repos[i], i, ctx);
|
|
451
111
|
}
|
|
452
|
-
displayReports(ctx.reportResults, ctx.lifecycleReportInputs, ctx.settingsCollector, options.dryRun ?? false);
|
|
453
|
-
// Propagate failures to caller (CLI entry handles process.exit)
|
|
112
|
+
displayReports(logger, ctx.reportResults, ctx.lifecycleReportInputs, ctx.settingsCollector, options.dryRun ?? false);
|
|
454
113
|
const settingsResults = ctx.settingsCollector.getAll();
|
|
455
114
|
const hasErrors = ctx.reportResults.some((r) => r.error);
|
|
456
115
|
const hasSettingsErrors = settingsResults.some((r) => r.error);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { MergeMode } from "../config/index.js";
|
|
2
|
-
import type { SyncReport, ReportFileChange } from "../output/
|
|
2
|
+
import type { SyncReport, ReportFileChange } from "../output/index.js";
|
|
3
3
|
interface SyncResultInput {
|
|
4
4
|
repoName: string;
|
|
5
5
|
success: boolean;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { RepoConfig, MergeMode } from "../config/index.js";
|
|
2
|
+
import type { ProcessorResult } from "../sync/index.js";
|
|
3
|
+
export declare function getUniqueFileNames(config: {
|
|
4
|
+
repos: RepoConfig[];
|
|
5
|
+
}): string[];
|
|
6
|
+
export declare function generateBranchName(fileNames: string[]): string;
|
|
7
|
+
export declare function formatFileNames(fileNames: string[]): string;
|
|
8
|
+
export declare function determineMergeOutcome(result: ProcessorResult): MergeMode | undefined;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { sanitizeBranchName } from "./branch-utils.js";
|
|
2
|
+
export function getUniqueFileNames(config) {
|
|
3
|
+
const fileNames = new Set();
|
|
4
|
+
for (const repo of config.repos) {
|
|
5
|
+
for (const file of repo.files) {
|
|
6
|
+
fileNames.add(file.fileName);
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
return Array.from(fileNames);
|
|
10
|
+
}
|
|
11
|
+
export function generateBranchName(fileNames) {
|
|
12
|
+
if (fileNames.length === 1) {
|
|
13
|
+
return `chore/sync-${sanitizeBranchName(fileNames[0])}`;
|
|
14
|
+
}
|
|
15
|
+
return "chore/sync-config";
|
|
16
|
+
}
|
|
17
|
+
export function formatFileNames(fileNames) {
|
|
18
|
+
if (fileNames.length === 1) {
|
|
19
|
+
return fileNames[0];
|
|
20
|
+
}
|
|
21
|
+
if (fileNames.length <= 3) {
|
|
22
|
+
return fileNames.join(", ");
|
|
23
|
+
}
|
|
24
|
+
return `${fileNames.length} files`;
|
|
25
|
+
}
|
|
26
|
+
export function determineMergeOutcome(result) {
|
|
27
|
+
if (!result.success)
|
|
28
|
+
return undefined;
|
|
29
|
+
if (!result.prUrl)
|
|
30
|
+
return "direct";
|
|
31
|
+
if (result.mergeResult?.merged)
|
|
32
|
+
return "force";
|
|
33
|
+
if (result.mergeResult?.autoMergeEnabled)
|
|
34
|
+
return "auto";
|
|
35
|
+
return "manual";
|
|
36
|
+
}
|
package/dist/cli/types.d.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import type { MergeMode, MergeStrategy, RepoConfig } from "../config/index.js";
|
|
2
2
|
import type { IRepoLifecycleManager } from "../lifecycle/index.js";
|
|
3
|
-
import type { IRepositoryProcessor } from "../sync/index.js";
|
|
4
|
-
import type { ISettingsProcessor, IRulesetProcessor, IRepoSettingsProcessor, ILabelsProcessor, ICodeScanningProcessor, BaseProcessorResult
|
|
3
|
+
import type { IRepositoryProcessor, FileChangeDetail } from "../sync/index.js";
|
|
4
|
+
import type { ISettingsProcessor, IRulesetProcessor, IRepoSettingsProcessor, ILabelsProcessor, ICodeScanningProcessor, BaseProcessorResult } from "../settings/index.js";
|
|
5
5
|
import type { RepoInfo } from "../repo/index.js";
|
|
6
6
|
import type { ResultsCollector } from "./results-collector.js";
|
|
7
|
+
import type { Logger } from "../shared/logger.js";
|
|
7
8
|
export type ProcessorFactory = () => IRepositoryProcessor;
|
|
8
9
|
export type SettingsProcessorFactory<T extends ISettingsProcessor> = () => T;
|
|
9
10
|
export type RulesetProcessorFactory = SettingsProcessorFactory<IRulesetProcessor>;
|
|
@@ -41,11 +42,7 @@ export interface SyncOptions extends SharedOptions {
|
|
|
41
42
|
export interface SyncResultEntry {
|
|
42
43
|
repoName: string;
|
|
43
44
|
success: boolean;
|
|
44
|
-
fileChanges:
|
|
45
|
-
path: string;
|
|
46
|
-
action: ActiveAction;
|
|
47
|
-
diffLines?: string[];
|
|
48
|
-
}>;
|
|
45
|
+
fileChanges: FileChangeDetail[];
|
|
49
46
|
prUrl?: string;
|
|
50
47
|
mergeOutcome?: MergeMode;
|
|
51
48
|
error?: string;
|
|
@@ -69,4 +66,5 @@ export interface ApplyRepoSettingsContext {
|
|
|
69
66
|
token: string | undefined;
|
|
70
67
|
settingsCollector: ResultsCollector;
|
|
71
68
|
factories: SettingsProcessorFactories;
|
|
69
|
+
logger: Logger;
|
|
72
70
|
}
|
|
@@ -1,6 +1,4 @@
|
|
|
1
|
-
import type
|
|
2
|
-
import type { SyncReport } from "../output/types.js";
|
|
3
|
-
import type { SettingsReport } from "../output/settings-report.js";
|
|
1
|
+
import { type LifecycleReport, type SyncReport, type SettingsReport } from "../output/index.js";
|
|
4
2
|
interface UnifiedSummaryInput {
|
|
5
3
|
lifecycle?: LifecycleReport;
|
|
6
4
|
sync?: SyncReport;
|
|
@@ -1,7 +1,4 @@
|
|
|
1
|
-
import { hasLifecycleChanges } from "../output/
|
|
2
|
-
import { writeGitHubStepSummary } from "../output/github-summary.js";
|
|
3
|
-
import { renderSyncLines } from "../output/sync-report.js";
|
|
4
|
-
import { renderRepoSettingsDiffLines, formatCountEntry, } from "../output/settings-report.js";
|
|
1
|
+
import { hasLifecycleChanges, writeGitHubStepSummary, renderSyncLines, renderRepoSettingsDiffLines, formatCountEntry, } from "../output/index.js";
|
|
5
2
|
// =============================================================================
|
|
6
3
|
// Helpers
|
|
7
4
|
// =============================================================================
|
|
@@ -121,6 +118,11 @@ function renderLifecycleLines(lcAction, diffLines) {
|
|
|
121
118
|
case "migrated":
|
|
122
119
|
diffLines.push(`+ MIGRATE ${lcAction.source ?? "source"} -> ${lcAction.repoName}`);
|
|
123
120
|
break;
|
|
121
|
+
/* c8 ignore next 4 */
|
|
122
|
+
default: {
|
|
123
|
+
const _exhaustive = lcAction.action;
|
|
124
|
+
throw new Error(`Unexpected lifecycle action: ${_exhaustive}`);
|
|
125
|
+
}
|
|
124
126
|
}
|
|
125
127
|
if (lcAction.settings) {
|
|
126
128
|
if (lcAction.settings.visibility) {
|
|
@@ -188,7 +190,7 @@ export function formatUnifiedSummaryMarkdown(input) {
|
|
|
188
190
|
if (hasLcChange && hasSyncChanges)
|
|
189
191
|
diffLines.push("");
|
|
190
192
|
if (syncRepo)
|
|
191
|
-
renderSyncLines(syncRepo
|
|
193
|
+
diffLines.push(...renderSyncLines(syncRepo));
|
|
192
194
|
// Blank line between files and settings sections
|
|
193
195
|
if (hasSyncChanges && hasSettingsChanges)
|
|
194
196
|
diffLines.push("");
|
package/dist/cli.js
CHANGED
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
* Supports ${VAR}, ${VAR:-default}, and ${VAR:?message} syntax.
|
|
4
4
|
* Use $${VAR} to escape and output literal ${VAR}.
|
|
5
5
|
*/
|
|
6
|
-
import { interpolateString, interpolateValue, } from "
|
|
7
|
-
import { ValidationError } from "
|
|
6
|
+
import { interpolateString, interpolateValue, } from "../shared/interpolation-engine.js";
|
|
7
|
+
import { ValidationError } from "../shared/errors.js";
|
|
8
8
|
/**
|
|
9
9
|
* Regex to match environment variable placeholders.
|
|
10
10
|
* Captures:
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { ValidationError } from "../shared/errors.js";
|
|
1
2
|
const MAX_EXTENDS_DEPTH = 100;
|
|
2
3
|
/**
|
|
3
4
|
* Resolves a single group's extends chain into an ordered list of group names.
|
|
@@ -7,16 +8,16 @@ const MAX_EXTENDS_DEPTH = 100;
|
|
|
7
8
|
export function resolveExtendsChain(groupName, groupDefs) {
|
|
8
9
|
function walk(name, visited, depth) {
|
|
9
10
|
if (depth > MAX_EXTENDS_DEPTH) {
|
|
10
|
-
throw new
|
|
11
|
+
throw new ValidationError(`Extends chain exceeds maximum depth of ${MAX_EXTENDS_DEPTH} — likely misconfigured`);
|
|
11
12
|
}
|
|
12
13
|
if (visited.has(name)) {
|
|
13
14
|
const cycle = [...visited, name].join(" -> ");
|
|
14
|
-
throw new
|
|
15
|
+
throw new ValidationError(`Circular extends detected: ${cycle}`);
|
|
15
16
|
}
|
|
16
17
|
visited.add(name);
|
|
17
18
|
const group = groupDefs[name];
|
|
18
19
|
if (!group) {
|
|
19
|
-
throw new
|
|
20
|
+
throw new ValidationError(`Group '${name}' referenced in extends chain does not exist`);
|
|
20
21
|
}
|
|
21
22
|
if (!group.extends) {
|
|
22
23
|
return [name];
|
|
@@ -44,7 +44,7 @@ export function resolveFileReference(reference, configDir) {
|
|
|
44
44
|
}
|
|
45
45
|
catch (error) {
|
|
46
46
|
const msg = toErrorMessage(error);
|
|
47
|
-
throw new ValidationError(`Failed to load file reference "${reference}": ${msg}
|
|
47
|
+
throw new ValidationError(`Failed to load file reference "${reference}": ${msg}`, { cause: error });
|
|
48
48
|
}
|
|
49
49
|
// Parse based on extension
|
|
50
50
|
const ext = extname(relativePath).toLowerCase();
|
|
@@ -65,7 +65,9 @@ function parseWithContext(fn, errorPrefix) {
|
|
|
65
65
|
return fn();
|
|
66
66
|
}
|
|
67
67
|
catch (error) {
|
|
68
|
-
throw new ValidationError(`${errorPrefix}: ${toErrorMessage(error)}
|
|
68
|
+
throw new ValidationError(`${errorPrefix}: ${toErrorMessage(error)}`, {
|
|
69
|
+
cause: error,
|
|
70
|
+
});
|
|
69
71
|
}
|
|
70
72
|
}
|
|
71
73
|
/**
|