@aspruyt/xfg 4.0.0 → 4.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -2
- package/dist/cli/index.d.ts +1 -2
- package/dist/cli/index.js +0 -1
- package/dist/cli/program.js +7 -2
- package/dist/cli/{settings/results-collector.d.ts → results-collector.d.ts} +1 -1
- package/dist/cli/{settings/results-collector.js → results-collector.js} +2 -1
- package/dist/cli/settings-report-builder.d.ts +1 -3
- package/dist/cli/sync-command.d.ts +2 -24
- package/dist/cli/sync-command.js +295 -301
- package/dist/cli/types.d.ts +60 -40
- package/dist/cli/types.js +1 -12
- package/dist/config/errors.d.ts +9 -0
- package/dist/config/errors.js +11 -0
- package/dist/config/file-reference-resolver.d.ts +2 -1
- package/dist/config/file-reference-resolver.js +10 -8
- package/dist/config/formatter.d.ts +3 -2
- package/dist/config/index.d.ts +4 -6
- package/dist/config/index.js +4 -8
- package/dist/config/loader.js +4 -2
- package/dist/config/merge.d.ts +0 -9
- package/dist/config/merge.js +2 -7
- package/dist/config/normalizer.d.ts +4 -0
- package/dist/config/normalizer.js +61 -110
- package/dist/config/types.d.ts +15 -19
- package/dist/config/types.js +1 -1
- package/dist/config/validator.d.ts +0 -4
- package/dist/config/validator.js +286 -363
- package/dist/config/validators/file-validator.d.ts +2 -8
- package/dist/config/validators/file-validator.js +6 -17
- package/dist/config/validators/index.d.ts +3 -3
- package/dist/config/validators/index.js +3 -3
- package/dist/config/validators/repo-settings-validator.d.ts +0 -6
- package/dist/config/validators/repo-settings-validator.js +9 -9
- package/dist/config/validators/ruleset-validator.d.ts +0 -14
- package/dist/config/validators/ruleset-validator.js +28 -28
- package/dist/lifecycle/ado-migration-source.js +2 -1
- package/dist/lifecycle/github-lifecycle-provider.d.ts +6 -5
- package/dist/lifecycle/github-lifecycle-provider.js +79 -90
- package/dist/lifecycle/index.d.ts +2 -6
- package/dist/lifecycle/index.js +0 -4
- package/dist/lifecycle/lifecycle-formatter.d.ts +2 -1
- package/dist/lifecycle/lifecycle-formatter.js +4 -0
- package/dist/lifecycle/lifecycle-helpers.d.ts +3 -2
- package/dist/lifecycle/repo-lifecycle-manager.js +4 -11
- package/dist/lifecycle/types.d.ts +0 -8
- package/dist/output/github-summary.d.ts +5 -0
- package/dist/output/github-summary.js +9 -2
- package/dist/output/index.d.ts +2 -2
- package/dist/output/index.js +1 -1
- package/dist/output/lifecycle-report.js +5 -23
- package/dist/output/settings-report.d.ts +14 -3
- package/dist/output/settings-report.js +137 -197
- package/dist/output/summary-utils.d.ts +1 -1
- package/dist/output/summary-utils.js +2 -1
- package/dist/output/sync-report.js +5 -8
- package/dist/output/unified-summary.d.ts +2 -1
- package/dist/output/unified-summary.js +71 -133
- package/dist/settings/base-processor.d.ts +67 -0
- package/dist/settings/base-processor.js +91 -0
- package/dist/settings/index.d.ts +4 -3
- package/dist/settings/index.js +3 -3
- package/dist/settings/labels/converter.d.ts +2 -1
- package/dist/settings/labels/github-labels-strategy.d.ts +9 -18
- package/dist/settings/labels/github-labels-strategy.js +17 -73
- package/dist/settings/labels/index.d.ts +2 -6
- package/dist/settings/labels/index.js +1 -9
- package/dist/settings/labels/processor.d.ts +6 -30
- package/dist/settings/labels/processor.js +62 -152
- package/dist/settings/labels/types.d.ts +5 -8
- package/dist/settings/repo-settings/formatter.d.ts +2 -2
- package/dist/settings/repo-settings/formatter.js +6 -6
- package/dist/settings/repo-settings/github-repo-settings-strategy.d.ts +11 -12
- package/dist/settings/repo-settings/github-repo-settings-strategy.js +32 -79
- package/dist/settings/repo-settings/index.d.ts +2 -5
- package/dist/settings/repo-settings/index.js +1 -9
- package/dist/settings/repo-settings/processor.d.ts +6 -27
- package/dist/settings/repo-settings/processor.js +51 -104
- package/dist/settings/repo-settings/types.d.ts +7 -9
- package/dist/settings/rulesets/diff-algorithm.d.ts +0 -4
- package/dist/settings/rulesets/diff-algorithm.js +1 -10
- package/dist/settings/rulesets/diff.d.ts +1 -1
- package/dist/settings/rulesets/diff.js +2 -21
- package/dist/settings/rulesets/formatter.d.ts +1 -3
- package/dist/settings/rulesets/formatter.js +1 -8
- package/dist/settings/rulesets/github-ruleset-strategy.d.ts +11 -51
- package/dist/settings/rulesets/github-ruleset-strategy.js +24 -85
- package/dist/settings/rulesets/index.d.ts +3 -6
- package/dist/settings/rulesets/index.js +5 -9
- package/dist/settings/rulesets/processor.d.ts +8 -33
- package/dist/settings/rulesets/processor.js +58 -151
- package/dist/settings/rulesets/types.d.ts +35 -6
- package/dist/shared/command-executor.d.ts +2 -22
- package/dist/shared/command-executor.js +8 -7
- package/dist/shared/env.d.ts +0 -8
- package/dist/shared/env.js +14 -70
- package/dist/shared/file-status.d.ts +2 -0
- package/dist/shared/file-status.js +13 -0
- package/dist/shared/gh-api-utils.d.ts +46 -0
- package/dist/shared/gh-api-utils.js +107 -0
- package/dist/shared/index.d.ts +5 -5
- package/dist/shared/index.js +3 -3
- package/dist/shared/interpolation-engine.d.ts +31 -0
- package/dist/shared/interpolation-engine.js +50 -0
- package/dist/shared/logger.d.ts +3 -7
- package/dist/shared/logger.js +4 -1
- package/dist/shared/repo-detector.d.ts +17 -2
- package/dist/shared/repo-detector.js +27 -0
- package/dist/shared/retry-utils.d.ts +9 -17
- package/dist/shared/retry-utils.js +22 -28
- package/dist/shared/sanitize-utils.d.ts +0 -7
- package/dist/shared/sanitize-utils.js +0 -7
- package/dist/shared/shell-utils.d.ts +1 -0
- package/dist/shared/shell-utils.js +3 -0
- package/dist/shared/string-utils.d.ts +4 -0
- package/dist/shared/string-utils.js +6 -0
- package/dist/shared/type-guards.d.ts +17 -0
- package/dist/shared/type-guards.js +26 -0
- package/dist/shared/workspace-utils.d.ts +0 -4
- package/dist/shared/workspace-utils.js +0 -4
- package/dist/{sync → shared}/xfg-template.d.ts +3 -2
- package/dist/{sync → shared}/xfg-template.js +13 -54
- package/dist/sync/auth-options-builder.d.ts +4 -5
- package/dist/sync/auth-options-builder.js +15 -26
- package/dist/sync/branch-manager.d.ts +5 -0
- package/dist/sync/branch-manager.js +12 -10
- package/dist/sync/commit-push-manager.d.ts +1 -1
- package/dist/sync/commit-push-manager.js +22 -18
- package/dist/sync/diff-utils.d.ts +4 -9
- package/dist/sync/diff-utils.js +2 -19
- package/dist/sync/file-sync-orchestrator.js +9 -8
- package/dist/sync/file-writer.d.ts +2 -1
- package/dist/sync/file-writer.js +3 -6
- package/dist/sync/index.d.ts +2 -15
- package/dist/sync/index.js +0 -19
- package/dist/sync/manifest-manager.d.ts +4 -0
- package/dist/sync/manifest-manager.js +5 -1
- package/dist/sync/manifest.d.ts +10 -41
- package/dist/sync/manifest.js +11 -56
- package/dist/sync/pr-merge-handler.d.ts +2 -6
- package/dist/sync/pr-merge-handler.js +6 -3
- package/dist/sync/repository-processor.d.ts +1 -2
- package/dist/sync/repository-processor.js +20 -12
- package/dist/sync/repository-session.js +5 -14
- package/dist/sync/sync-workflow.js +31 -38
- package/dist/sync/types.d.ts +43 -178
- package/dist/vcs/authenticated-git-ops.d.ts +27 -70
- package/dist/vcs/authenticated-git-ops.js +70 -96
- package/dist/vcs/azure-pr-strategy.d.ts +6 -4
- package/dist/vcs/azure-pr-strategy.js +34 -82
- package/dist/vcs/branch-utils.d.ts +6 -0
- package/dist/vcs/branch-utils.js +29 -0
- package/dist/vcs/commit-strategy-selector.d.ts +5 -0
- package/dist/vcs/commit-strategy-selector.js +10 -0
- package/dist/vcs/git-commit-strategy.js +1 -2
- package/dist/vcs/git-ops.d.ts +15 -59
- package/dist/vcs/git-ops.js +46 -110
- package/dist/vcs/github-app-token-manager.d.ts +0 -6
- package/dist/vcs/github-app-token-manager.js +5 -12
- package/dist/vcs/github-pr-strategy.d.ts +5 -5
- package/dist/vcs/github-pr-strategy.js +44 -122
- package/dist/vcs/gitlab-pr-strategy.d.ts +6 -4
- package/dist/vcs/gitlab-pr-strategy.js +39 -87
- package/dist/vcs/graphql-commit-strategy.d.ts +3 -4
- package/dist/vcs/graphql-commit-strategy.js +31 -63
- package/dist/vcs/index.d.ts +3 -16
- package/dist/vcs/index.js +2 -33
- package/dist/vcs/pr-creator.d.ts +9 -9
- package/dist/vcs/pr-creator.js +11 -10
- package/dist/vcs/pr-strategy-factory.d.ts +5 -0
- package/dist/vcs/pr-strategy-factory.js +17 -0
- package/dist/vcs/pr-strategy.d.ts +13 -26
- package/dist/vcs/pr-strategy.js +20 -25
- package/dist/vcs/types.d.ts +87 -21
- package/package.json +2 -1
package/dist/cli/sync-command.js
CHANGED
|
@@ -1,20 +1,22 @@
|
|
|
1
1
|
import { resolve, join } from "node:path";
|
|
2
2
|
import { existsSync } from "node:fs";
|
|
3
|
-
import { loadRawConfig, normalizeConfig
|
|
3
|
+
import { loadRawConfig, normalizeConfig } from "../config/index.js";
|
|
4
4
|
import { validateForSync } from "../config/validator.js";
|
|
5
5
|
import { parseGitUrl, getRepoDisplayName, isGitHubRepo, } from "../shared/repo-detector.js";
|
|
6
|
-
import { sanitizeBranchName, validateBranchName } from "../vcs/
|
|
7
|
-
import {
|
|
6
|
+
import { sanitizeBranchName, validateBranchName } from "../vcs/branch-utils.js";
|
|
7
|
+
import { createTokenManager } from "../vcs/index.js";
|
|
8
8
|
import { logger } from "../shared/logger.js";
|
|
9
9
|
import { generateWorkspaceName } from "../shared/workspace-utils.js";
|
|
10
10
|
import { defaultProcessorFactory, defaultRulesetProcessorFactory, defaultRepoSettingsProcessorFactory, defaultLabelsProcessorFactory, } from "./types.js";
|
|
11
|
-
import { ResultsCollector } from "./
|
|
11
|
+
import { ResultsCollector } from "./results-collector.js";
|
|
12
12
|
import { buildSettingsReport } from "./settings-report-builder.js";
|
|
13
13
|
import { formatSettingsReportCLI } from "../output/settings-report.js";
|
|
14
14
|
import { buildSyncReport } from "./sync-report-builder.js";
|
|
15
15
|
import { formatSyncReportCLI } from "../output/sync-report.js";
|
|
16
16
|
import { buildLifecycleReport, formatLifecycleReportCLI, hasLifecycleChanges, } from "../output/lifecycle-report.js";
|
|
17
17
|
import { writeUnifiedSummary } from "../output/unified-summary.js";
|
|
18
|
+
import { toErrorMessage } from "../shared/type-guards.js";
|
|
19
|
+
import { resolveGitHubToken } from "../shared/gh-api-utils.js";
|
|
18
20
|
import { RepoLifecycleManager, runLifecycleCheck, toCreateRepoSettings, } from "../lifecycle/index.js";
|
|
19
21
|
/**
|
|
20
22
|
* Get unique file names from all repos in the config
|
|
@@ -49,9 +51,6 @@ function formatFileNames(fileNames) {
|
|
|
49
51
|
}
|
|
50
52
|
return `${fileNames.length} files`;
|
|
51
53
|
}
|
|
52
|
-
/**
|
|
53
|
-
* Determine merge outcome from processor result
|
|
54
|
-
*/
|
|
55
54
|
function determineMergeOutcome(result) {
|
|
56
55
|
if (!result.success)
|
|
57
56
|
return undefined;
|
|
@@ -63,311 +62,100 @@ function determineMergeOutcome(result) {
|
|
|
63
62
|
return "auto";
|
|
64
63
|
return "manual";
|
|
65
64
|
}
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
if (!existsSync(configPath)) {
|
|
73
|
-
console.error(`Config file not found: ${configPath}`);
|
|
74
|
-
process.exit(1);
|
|
75
|
-
}
|
|
76
|
-
console.log(`Loading config from: ${configPath}`);
|
|
77
|
-
if (options.dryRun) {
|
|
78
|
-
console.log("Running in DRY RUN mode - no changes will be made\n");
|
|
79
|
-
}
|
|
80
|
-
const rawConfig = loadRawConfig(configPath);
|
|
81
|
-
try {
|
|
82
|
-
validateForSync(rawConfig);
|
|
83
|
-
}
|
|
84
|
-
catch (error) {
|
|
85
|
-
console.error(error instanceof Error ? error.message : String(error));
|
|
86
|
-
process.exit(1);
|
|
87
|
-
}
|
|
88
|
-
const config = normalizeConfig(rawConfig);
|
|
89
|
-
const fileNames = getUniqueFileNames(config);
|
|
90
|
-
let branchName;
|
|
91
|
-
if (options.branch) {
|
|
92
|
-
validateBranchName(options.branch);
|
|
93
|
-
branchName = options.branch;
|
|
94
|
-
}
|
|
95
|
-
else {
|
|
96
|
-
branchName = generateBranchName(fileNames);
|
|
97
|
-
}
|
|
98
|
-
logger.setTotal(config.repos.length);
|
|
99
|
-
console.log(`Found ${config.repos.length} repositories to process`);
|
|
100
|
-
console.log(`Target files: ${formatFileNames(fileNames)}`);
|
|
101
|
-
console.log(`Branch: ${branchName}\n`);
|
|
102
|
-
const processor = processorFactory();
|
|
103
|
-
const lm = lifecycleManager ?? new RepoLifecycleManager(undefined, options.retries);
|
|
104
|
-
const tokenManager = hasGitHubAppCredentials()
|
|
105
|
-
? new GitHubAppTokenManager(process.env.XFG_GITHUB_APP_ID, process.env.XFG_GITHUB_APP_PRIVATE_KEY)
|
|
106
|
-
: null;
|
|
107
|
-
const reportResults = [];
|
|
108
|
-
const lifecycleReportInputs = [];
|
|
109
|
-
const settingsCollector = new ResultsCollector();
|
|
110
|
-
for (let i = 0; i < config.repos.length; i++) {
|
|
111
|
-
const repoConfig = config.repos[i];
|
|
112
|
-
if (options.merge || options.mergeStrategy || options.deleteBranch) {
|
|
113
|
-
repoConfig.prOptions = {
|
|
114
|
-
...repoConfig.prOptions,
|
|
115
|
-
merge: options.merge ?? repoConfig.prOptions?.merge,
|
|
116
|
-
mergeStrategy: options.mergeStrategy ?? repoConfig.prOptions?.mergeStrategy,
|
|
117
|
-
deleteBranch: options.deleteBranch ?? repoConfig.prOptions?.deleteBranch,
|
|
118
|
-
};
|
|
119
|
-
}
|
|
120
|
-
const current = i + 1;
|
|
121
|
-
let repoInfo;
|
|
122
|
-
try {
|
|
123
|
-
repoInfo = parseGitUrl(repoConfig.git, {
|
|
124
|
-
githubHosts: config.githubHosts,
|
|
125
|
-
});
|
|
65
|
+
function logSettingsResult(result, label, current, repoName, settingsCollector) {
|
|
66
|
+
if (result.planOutput?.lines?.length) {
|
|
67
|
+
logger.info("");
|
|
68
|
+
logger.info(`${repoName} - ${label}:`);
|
|
69
|
+
for (const line of result.planOutput.lines) {
|
|
70
|
+
logger.info(line);
|
|
126
71
|
}
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
repoName: repoConfig.git,
|
|
131
|
-
success: false,
|
|
132
|
-
fileChanges: [],
|
|
133
|
-
error: error instanceof Error ? error.message : String(error),
|
|
134
|
-
});
|
|
135
|
-
continue;
|
|
136
|
-
}
|
|
137
|
-
const repoName = getRepoDisplayName(repoInfo);
|
|
138
|
-
const workDir = resolve(join(options.workDir ?? "./tmp", generateWorkspaceName(i)));
|
|
139
|
-
// Resolve auth token for lifecycle gh commands
|
|
140
|
-
let lifecycleToken;
|
|
141
|
-
if (isGitHubRepo(repoInfo)) {
|
|
142
|
-
try {
|
|
143
|
-
lifecycleToken =
|
|
144
|
-
(await tokenManager?.getTokenForRepo(repoInfo)) ??
|
|
145
|
-
process.env.GH_TOKEN;
|
|
146
|
-
}
|
|
147
|
-
catch {
|
|
148
|
-
lifecycleToken = process.env.GH_TOKEN;
|
|
72
|
+
if (result.warnings?.length) {
|
|
73
|
+
for (const warning of result.warnings) {
|
|
74
|
+
logger.warn(warning);
|
|
149
75
|
}
|
|
150
76
|
}
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
}
|
|
174
|
-
: undefined,
|
|
175
|
-
});
|
|
176
|
-
// In dry-run, skip processing repos that don't exist yet
|
|
177
|
-
if (options.dryRun && lifecycleResult.action !== "existed") {
|
|
178
|
-
reportResults.push({
|
|
179
|
-
repoName,
|
|
180
|
-
success: true,
|
|
181
|
-
fileChanges: [],
|
|
77
|
+
}
|
|
78
|
+
else if (!result.skipped && result.success) {
|
|
79
|
+
logger.success(current, repoName, `${label}: ${result.message}`);
|
|
80
|
+
}
|
|
81
|
+
if (!result.success && !result.skipped) {
|
|
82
|
+
logger.error(current, repoName, `${label}: ${result.message}`);
|
|
83
|
+
settingsCollector.appendError(repoName, result.message);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
async function applyRepoSettings(ctx) {
|
|
87
|
+
const { repoConfig, repoInfo, repoName, current, options, token, settingsCollector, rulesetProcessorFactory, repoSettingsProcessorFactory, labelsProcessorFactory, } = ctx;
|
|
88
|
+
if (!repoConfig.settings || !isGitHubRepo(repoInfo))
|
|
89
|
+
return;
|
|
90
|
+
const settingsDescriptors = [
|
|
91
|
+
{
|
|
92
|
+
key: "rulesets",
|
|
93
|
+
label: "Rulesets",
|
|
94
|
+
run: async () => {
|
|
95
|
+
const result = await rulesetProcessorFactory().process(repoConfig, repoInfo, {
|
|
96
|
+
dryRun: options.dryRun,
|
|
97
|
+
noDelete: options.noDelete,
|
|
98
|
+
token,
|
|
182
99
|
});
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
100
|
+
if (!result.skipped) {
|
|
101
|
+
settingsCollector.getOrCreate(repoName).rulesetResult = result;
|
|
102
|
+
}
|
|
103
|
+
return result;
|
|
104
|
+
},
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
key: "labels",
|
|
108
|
+
label: "Labels",
|
|
109
|
+
run: async () => {
|
|
110
|
+
const result = await labelsProcessorFactory().process(repoConfig, repoInfo, {
|
|
111
|
+
dryRun: options.dryRun,
|
|
112
|
+
noDelete: options.noDelete,
|
|
113
|
+
token,
|
|
114
|
+
});
|
|
115
|
+
if (!result.skipped) {
|
|
116
|
+
settingsCollector.getOrCreate(repoName).labelsResult = result;
|
|
117
|
+
}
|
|
118
|
+
return result;
|
|
119
|
+
},
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
key: "repo",
|
|
123
|
+
label: "Repo Settings",
|
|
124
|
+
run: async () => {
|
|
125
|
+
const result = await repoSettingsProcessorFactory().process(repoConfig, repoInfo, { dryRun: options.dryRun, token });
|
|
126
|
+
if (!result.skipped) {
|
|
127
|
+
settingsCollector.getOrCreate(repoName).settingsResult = result;
|
|
128
|
+
}
|
|
129
|
+
return result;
|
|
130
|
+
},
|
|
131
|
+
},
|
|
132
|
+
];
|
|
133
|
+
for (const desc of settingsDescriptors) {
|
|
134
|
+
const settingsValue = repoConfig.settings[desc.key];
|
|
135
|
+
if (!settingsValue || Object.keys(settingsValue).length === 0)
|
|
194
136
|
continue;
|
|
195
|
-
}
|
|
196
137
|
try {
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
branchName,
|
|
200
|
-
workDir,
|
|
201
|
-
configId: config.id,
|
|
202
|
-
dryRun: options.dryRun,
|
|
203
|
-
retries: options.retries,
|
|
204
|
-
prTemplate: config.prTemplate,
|
|
205
|
-
noDelete: options.noDelete,
|
|
206
|
-
});
|
|
207
|
-
const mergeOutcome = determineMergeOutcome(result);
|
|
208
|
-
reportResults.push({
|
|
209
|
-
repoName,
|
|
210
|
-
success: result.success,
|
|
211
|
-
fileChanges: (result.fileChanges ?? []).map((f) => ({
|
|
212
|
-
path: f.path,
|
|
213
|
-
action: f.action,
|
|
214
|
-
})),
|
|
215
|
-
prUrl: result.prUrl,
|
|
216
|
-
mergeOutcome,
|
|
217
|
-
error: result.success ? undefined : result.message,
|
|
218
|
-
});
|
|
219
|
-
if (result.skipped) {
|
|
220
|
-
logger.skip(current, repoName, result.message);
|
|
221
|
-
}
|
|
222
|
-
else if (result.success) {
|
|
223
|
-
logger.success(current, repoName, result.message);
|
|
224
|
-
}
|
|
225
|
-
else {
|
|
226
|
-
logger.error(current, repoName, result.message);
|
|
227
|
-
}
|
|
138
|
+
const result = await desc.run();
|
|
139
|
+
logSettingsResult(result, desc.label, current, repoName, settingsCollector);
|
|
228
140
|
}
|
|
229
141
|
catch (error) {
|
|
230
|
-
logger.error(current, repoName,
|
|
231
|
-
|
|
232
|
-
repoName,
|
|
233
|
-
success: false,
|
|
234
|
-
fileChanges: [],
|
|
235
|
-
error: error instanceof Error ? error.message : String(error),
|
|
236
|
-
});
|
|
237
|
-
}
|
|
238
|
-
// After file sync, apply settings via API (GitHub-only — ADO and GitLab repos are skipped)
|
|
239
|
-
if (repoConfig.settings && isGitHubRepo(repoInfo)) {
|
|
240
|
-
const githubRepo = repoInfo;
|
|
241
|
-
let settingsToken;
|
|
242
|
-
try {
|
|
243
|
-
settingsToken =
|
|
244
|
-
(await tokenManager?.getTokenForRepo(githubRepo)) ??
|
|
245
|
-
process.env.GH_TOKEN;
|
|
246
|
-
}
|
|
247
|
-
catch {
|
|
248
|
-
settingsToken = process.env.GH_TOKEN;
|
|
249
|
-
}
|
|
250
|
-
// Apply rulesets
|
|
251
|
-
if (repoConfig.settings.rulesets &&
|
|
252
|
-
Object.keys(repoConfig.settings.rulesets).length > 0) {
|
|
253
|
-
try {
|
|
254
|
-
const rulesetProcessor = rulesetProcessorFactory();
|
|
255
|
-
const rulesetResult = await rulesetProcessor.process(repoConfig, repoInfo, {
|
|
256
|
-
dryRun: options.dryRun,
|
|
257
|
-
noDelete: options.noDelete,
|
|
258
|
-
token: settingsToken,
|
|
259
|
-
});
|
|
260
|
-
if (rulesetResult.planOutput?.lines?.length) {
|
|
261
|
-
logger.info("");
|
|
262
|
-
logger.info(`${repoName} - Rulesets:`);
|
|
263
|
-
for (const line of rulesetResult.planOutput.lines) {
|
|
264
|
-
logger.info(line);
|
|
265
|
-
}
|
|
266
|
-
}
|
|
267
|
-
else if (!rulesetResult.skipped && rulesetResult.success) {
|
|
268
|
-
logger.success(current, repoName, `Rulesets: ${rulesetResult.message}`);
|
|
269
|
-
}
|
|
270
|
-
if (!rulesetResult.skipped) {
|
|
271
|
-
settingsCollector.getOrCreate(repoName).rulesetResult =
|
|
272
|
-
rulesetResult;
|
|
273
|
-
}
|
|
274
|
-
if (!rulesetResult.success && !rulesetResult.skipped) {
|
|
275
|
-
logger.error(current, repoName, `Rulesets: ${rulesetResult.message}`);
|
|
276
|
-
settingsCollector.appendError(repoName, rulesetResult.message);
|
|
277
|
-
}
|
|
278
|
-
}
|
|
279
|
-
catch (error) {
|
|
280
|
-
logger.error(current, repoName, `Rulesets: ${String(error)}`);
|
|
281
|
-
settingsCollector.appendError(repoName, error);
|
|
282
|
-
}
|
|
283
|
-
}
|
|
284
|
-
// Apply labels
|
|
285
|
-
if (repoConfig.settings.labels &&
|
|
286
|
-
Object.keys(repoConfig.settings.labels).length > 0) {
|
|
287
|
-
try {
|
|
288
|
-
const labelsProcessor = labelsProcessorFactory();
|
|
289
|
-
const labelsResult = await labelsProcessor.process(repoConfig, repoInfo, {
|
|
290
|
-
dryRun: options.dryRun,
|
|
291
|
-
noDelete: options.noDelete,
|
|
292
|
-
token: settingsToken,
|
|
293
|
-
});
|
|
294
|
-
if (labelsResult.planOutput?.lines?.length) {
|
|
295
|
-
logger.info("");
|
|
296
|
-
logger.info(`${repoName} - Labels:`);
|
|
297
|
-
for (const line of labelsResult.planOutput.lines) {
|
|
298
|
-
logger.info(line);
|
|
299
|
-
}
|
|
300
|
-
}
|
|
301
|
-
else if (!labelsResult.skipped && labelsResult.success) {
|
|
302
|
-
logger.success(current, repoName, `Labels: ${labelsResult.message}`);
|
|
303
|
-
}
|
|
304
|
-
if (!labelsResult.skipped) {
|
|
305
|
-
settingsCollector.getOrCreate(repoName).labelsResult = labelsResult;
|
|
306
|
-
}
|
|
307
|
-
if (!labelsResult.success && !labelsResult.skipped) {
|
|
308
|
-
logger.error(current, repoName, `Labels: ${labelsResult.message}`);
|
|
309
|
-
settingsCollector.appendError(repoName, labelsResult.message);
|
|
310
|
-
}
|
|
311
|
-
}
|
|
312
|
-
catch (error) {
|
|
313
|
-
logger.error(current, repoName, `Labels: ${String(error)}`);
|
|
314
|
-
settingsCollector.appendError(repoName, error);
|
|
315
|
-
}
|
|
316
|
-
}
|
|
317
|
-
// Apply repo settings
|
|
318
|
-
if (repoConfig.settings.repo &&
|
|
319
|
-
Object.keys(repoConfig.settings.repo).length > 0) {
|
|
320
|
-
try {
|
|
321
|
-
const repoSettingsProcessor = repoSettingsProcessorFactory();
|
|
322
|
-
const repoSettingsResult = await repoSettingsProcessor.process(repoConfig, repoInfo, {
|
|
323
|
-
dryRun: options.dryRun,
|
|
324
|
-
token: settingsToken,
|
|
325
|
-
});
|
|
326
|
-
if (repoSettingsResult.planOutput?.lines?.length) {
|
|
327
|
-
logger.info("");
|
|
328
|
-
logger.info(`${repoName} - Repo Settings:`);
|
|
329
|
-
for (const line of repoSettingsResult.planOutput.lines) {
|
|
330
|
-
logger.info(line);
|
|
331
|
-
}
|
|
332
|
-
if (repoSettingsResult.warnings?.length) {
|
|
333
|
-
for (const warning of repoSettingsResult.warnings) {
|
|
334
|
-
logger.info(`Warning: ${warning}`);
|
|
335
|
-
}
|
|
336
|
-
}
|
|
337
|
-
}
|
|
338
|
-
else if (!repoSettingsResult.skipped &&
|
|
339
|
-
repoSettingsResult.success) {
|
|
340
|
-
logger.success(current, repoName, `Repo settings: ${repoSettingsResult.message}`);
|
|
341
|
-
}
|
|
342
|
-
if (!repoSettingsResult.skipped) {
|
|
343
|
-
settingsCollector.getOrCreate(repoName).settingsResult =
|
|
344
|
-
repoSettingsResult;
|
|
345
|
-
}
|
|
346
|
-
if (!repoSettingsResult.success && !repoSettingsResult.skipped) {
|
|
347
|
-
logger.error(current, repoName, `Repo settings: ${repoSettingsResult.message}`);
|
|
348
|
-
settingsCollector.appendError(repoName, repoSettingsResult.message);
|
|
349
|
-
}
|
|
350
|
-
}
|
|
351
|
-
catch (error) {
|
|
352
|
-
logger.error(current, repoName, `Repo settings: ${String(error)}`);
|
|
353
|
-
settingsCollector.appendError(repoName, error);
|
|
354
|
-
}
|
|
355
|
-
}
|
|
142
|
+
logger.error(current, repoName, `${desc.label}: ${toErrorMessage(error)}`);
|
|
143
|
+
settingsCollector.appendError(repoName, error);
|
|
356
144
|
}
|
|
357
145
|
}
|
|
358
|
-
|
|
146
|
+
}
|
|
147
|
+
function displayReports(reportResults, lifecycleReportInputs, settingsCollector, dryRun) {
|
|
359
148
|
const lifecycleReport = buildLifecycleReport(lifecycleReportInputs);
|
|
360
149
|
if (hasLifecycleChanges(lifecycleReport)) {
|
|
361
|
-
|
|
150
|
+
logger.log("");
|
|
362
151
|
for (const line of formatLifecycleReportCLI(lifecycleReport)) {
|
|
363
|
-
|
|
152
|
+
logger.log(line);
|
|
364
153
|
}
|
|
365
154
|
}
|
|
366
|
-
// Build and display sync report
|
|
367
155
|
const report = buildSyncReport(reportResults);
|
|
368
|
-
|
|
156
|
+
logger.log("");
|
|
369
157
|
for (const line of formatSyncReportCLI(report)) {
|
|
370
|
-
|
|
158
|
+
logger.log(line);
|
|
371
159
|
}
|
|
372
160
|
// Build and display settings report (if any settings were processed)
|
|
373
161
|
const settingsResults = settingsCollector.getAll();
|
|
@@ -376,9 +164,9 @@ export async function runSync(options, deps = {}) {
|
|
|
376
164
|
settingsReport = buildSettingsReport(settingsResults);
|
|
377
165
|
const settingsLines = formatSettingsReportCLI(settingsReport);
|
|
378
166
|
if (settingsLines.length > 0) {
|
|
379
|
-
|
|
167
|
+
logger.log("");
|
|
380
168
|
for (const line of settingsLines) {
|
|
381
|
-
|
|
169
|
+
logger.log(line);
|
|
382
170
|
}
|
|
383
171
|
}
|
|
384
172
|
}
|
|
@@ -387,12 +175,218 @@ export async function runSync(options, deps = {}) {
|
|
|
387
175
|
lifecycle: lifecycleReport,
|
|
388
176
|
sync: report,
|
|
389
177
|
settings: settingsReport,
|
|
390
|
-
dryRun
|
|
178
|
+
dryRun,
|
|
391
179
|
});
|
|
392
|
-
|
|
393
|
-
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Process a single repository: resolve URL, run lifecycle check, sync files, apply settings.
|
|
183
|
+
* Pushes results into ctx.reportResults, ctx.lifecycleReportInputs, and ctx.settingsCollector.
|
|
184
|
+
*/
|
|
185
|
+
async function processSingleRepo(repoConfig, index, ctx) {
|
|
186
|
+
const { config, options } = ctx;
|
|
187
|
+
const current = index + 1;
|
|
188
|
+
// Apply CLI-level PR option overrides
|
|
189
|
+
if (options.merge || options.mergeStrategy || options.deleteBranch) {
|
|
190
|
+
repoConfig.prOptions = {
|
|
191
|
+
...repoConfig.prOptions,
|
|
192
|
+
merge: options.merge ?? repoConfig.prOptions?.merge,
|
|
193
|
+
mergeStrategy: options.mergeStrategy ?? repoConfig.prOptions?.mergeStrategy,
|
|
194
|
+
deleteBranch: options.deleteBranch ?? repoConfig.prOptions?.deleteBranch,
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
const mergeMode = repoConfig.prOptions?.merge ?? "auto";
|
|
198
|
+
if (mergeMode === "direct" && repoConfig.prOptions?.mergeStrategy) {
|
|
199
|
+
logger.warn(`mergeStrategy '${repoConfig.prOptions.mergeStrategy}' is ignored in direct mode for ${repoConfig.git}`);
|
|
200
|
+
}
|
|
201
|
+
let repoInfo;
|
|
202
|
+
try {
|
|
203
|
+
repoInfo = parseGitUrl(repoConfig.git, {
|
|
204
|
+
githubHosts: config.githubHosts,
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
catch (error) {
|
|
208
|
+
logger.error(current, repoConfig.git, toErrorMessage(error));
|
|
209
|
+
ctx.reportResults.push({
|
|
210
|
+
repoName: repoConfig.git,
|
|
211
|
+
success: false,
|
|
212
|
+
fileChanges: [],
|
|
213
|
+
error: toErrorMessage(error),
|
|
214
|
+
});
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
const repoName = getRepoDisplayName(repoInfo);
|
|
218
|
+
const workDir = resolve(join(options.workDir ?? "./tmp", generateWorkspaceName(index)));
|
|
219
|
+
const repoToken = isGitHubRepo(repoInfo)
|
|
220
|
+
? (await resolveGitHubToken(repoInfo, ctx.tokenManager, repoName, logger, process.env.GH_TOKEN)).token
|
|
221
|
+
: undefined;
|
|
222
|
+
const skipFileSync = await runLifecyclePhase(repoConfig, repoInfo, repoName, index, workDir, repoToken, ctx);
|
|
223
|
+
if (skipFileSync)
|
|
224
|
+
return;
|
|
225
|
+
// Sync files via processor
|
|
226
|
+
await runFileSyncPhase(repoConfig, repoInfo, repoName, current, workDir, repoToken, ctx);
|
|
227
|
+
// Apply settings via API (GitHub-only — ADO and GitLab repos are skipped)
|
|
228
|
+
await applyRepoSettings({
|
|
229
|
+
repoConfig,
|
|
230
|
+
repoInfo,
|
|
231
|
+
repoName,
|
|
232
|
+
current,
|
|
233
|
+
options,
|
|
234
|
+
token: repoToken,
|
|
235
|
+
settingsCollector: ctx.settingsCollector,
|
|
236
|
+
rulesetProcessorFactory: ctx.rulesetProcessorFactory,
|
|
237
|
+
repoSettingsProcessorFactory: ctx.repoSettingsProcessorFactory,
|
|
238
|
+
labelsProcessorFactory: ctx.labelsProcessorFactory,
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
/**
|
|
242
|
+
* Run lifecycle check (repo existence, creation, forking).
|
|
243
|
+
* Returns true if the main loop should skip file sync for this repo.
|
|
244
|
+
*/
|
|
245
|
+
async function runLifecyclePhase(repoConfig, repoInfo, repoName, index, workDir, lifecycleToken, ctx) {
|
|
246
|
+
const current = index + 1;
|
|
247
|
+
try {
|
|
248
|
+
const { outputLines, lifecycleResult } = await runLifecycleCheck(repoConfig, repoInfo, index, {
|
|
249
|
+
dryRun: ctx.options.dryRun ?? false,
|
|
250
|
+
resolvedWorkDir: workDir,
|
|
251
|
+
githubHosts: ctx.config.githubHosts,
|
|
252
|
+
token: lifecycleToken,
|
|
253
|
+
}, ctx.lifecycleManager, ctx.config.settings?.repo);
|
|
254
|
+
for (const line of outputLines) {
|
|
255
|
+
logger.info(line);
|
|
256
|
+
}
|
|
257
|
+
const createSettings = toCreateRepoSettings(ctx.config.settings?.repo);
|
|
258
|
+
ctx.lifecycleReportInputs.push({
|
|
259
|
+
repoName,
|
|
260
|
+
action: lifecycleResult.action,
|
|
261
|
+
upstream: repoConfig.upstream,
|
|
262
|
+
source: repoConfig.source,
|
|
263
|
+
settings: createSettings
|
|
264
|
+
? {
|
|
265
|
+
visibility: createSettings.visibility,
|
|
266
|
+
description: createSettings.description,
|
|
267
|
+
}
|
|
268
|
+
: undefined,
|
|
269
|
+
});
|
|
270
|
+
// In dry-run, skip processing repos that don't exist yet
|
|
271
|
+
if (ctx.options.dryRun && lifecycleResult.action !== "existed") {
|
|
272
|
+
ctx.reportResults.push({
|
|
273
|
+
repoName,
|
|
274
|
+
success: true,
|
|
275
|
+
fileChanges: [],
|
|
276
|
+
});
|
|
277
|
+
return true;
|
|
278
|
+
}
|
|
279
|
+
return false;
|
|
280
|
+
}
|
|
281
|
+
catch (error) {
|
|
282
|
+
logger.error(current, repoName, `Lifecycle error: ${toErrorMessage(error)}`);
|
|
283
|
+
ctx.reportResults.push({
|
|
284
|
+
repoName,
|
|
285
|
+
success: false,
|
|
286
|
+
fileChanges: [],
|
|
287
|
+
error: toErrorMessage(error),
|
|
288
|
+
});
|
|
289
|
+
return true;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
/**
|
|
293
|
+
* Run the file sync processor for a single repo and collect results.
|
|
294
|
+
*/
|
|
295
|
+
async function runFileSyncPhase(repoConfig, repoInfo, repoName, current, workDir, token, ctx) {
|
|
296
|
+
try {
|
|
297
|
+
logger.progress(current, repoName, "Processing...");
|
|
298
|
+
const result = await ctx.processor.process(repoConfig, repoInfo, {
|
|
299
|
+
branchName: ctx.branchName,
|
|
300
|
+
workDir,
|
|
301
|
+
configId: ctx.config.id,
|
|
302
|
+
dryRun: ctx.options.dryRun,
|
|
303
|
+
retries: ctx.options.retries,
|
|
304
|
+
prTemplate: ctx.config.prTemplate,
|
|
305
|
+
noDelete: ctx.options.noDelete,
|
|
306
|
+
token,
|
|
307
|
+
isGraphQLCommitMode: isGitHubRepo(repoInfo) && ctx.tokenManager !== null,
|
|
308
|
+
});
|
|
309
|
+
const mergeOutcome = determineMergeOutcome(result);
|
|
310
|
+
ctx.reportResults.push({
|
|
311
|
+
repoName,
|
|
312
|
+
success: result.success,
|
|
313
|
+
fileChanges: (result.fileChanges ?? []).map((f) => ({
|
|
314
|
+
path: f.path,
|
|
315
|
+
action: f.action,
|
|
316
|
+
})),
|
|
317
|
+
prUrl: result.prUrl,
|
|
318
|
+
mergeOutcome,
|
|
319
|
+
error: result.success ? undefined : result.message,
|
|
320
|
+
});
|
|
321
|
+
if (result.skipped) {
|
|
322
|
+
logger.skip(current, repoName, result.message);
|
|
323
|
+
}
|
|
324
|
+
else if (result.success) {
|
|
325
|
+
logger.success(current, repoName, result.message);
|
|
326
|
+
}
|
|
327
|
+
else {
|
|
328
|
+
logger.error(current, repoName, result.message);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
catch (error) {
|
|
332
|
+
logger.error(current, repoName, toErrorMessage(error));
|
|
333
|
+
ctx.reportResults.push({
|
|
334
|
+
repoName,
|
|
335
|
+
success: false,
|
|
336
|
+
fileChanges: [],
|
|
337
|
+
error: toErrorMessage(error),
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
export async function runSync(options, deps = {}) {
|
|
342
|
+
const { processorFactory = defaultProcessorFactory, lifecycleManager, rulesetProcessorFactory = defaultRulesetProcessorFactory, repoSettingsProcessorFactory = defaultRepoSettingsProcessorFactory, labelsProcessorFactory = defaultLabelsProcessorFactory, } = deps;
|
|
343
|
+
const configPath = resolve(options.config);
|
|
344
|
+
if (!existsSync(configPath)) {
|
|
345
|
+
throw new Error(`Config file not found: ${configPath}`);
|
|
346
|
+
}
|
|
347
|
+
logger.log(`Loading config from: ${configPath}`);
|
|
348
|
+
if (options.dryRun) {
|
|
349
|
+
logger.log("Running in DRY RUN mode - no changes will be made\n");
|
|
350
|
+
}
|
|
351
|
+
const rawConfig = loadRawConfig(configPath);
|
|
352
|
+
validateForSync(rawConfig);
|
|
353
|
+
const config = normalizeConfig(rawConfig);
|
|
354
|
+
const fileNames = getUniqueFileNames(config);
|
|
355
|
+
let branchName;
|
|
356
|
+
if (options.branch) {
|
|
357
|
+
validateBranchName(options.branch);
|
|
358
|
+
branchName = options.branch;
|
|
359
|
+
}
|
|
360
|
+
else {
|
|
361
|
+
branchName = generateBranchName(fileNames);
|
|
362
|
+
}
|
|
363
|
+
logger.setTotal(config.repos.length);
|
|
364
|
+
logger.log(`Found ${config.repos.length} repositories to process`);
|
|
365
|
+
logger.log(`Target files: ${formatFileNames(fileNames)}`);
|
|
366
|
+
logger.log(`Branch: ${branchName}\n`);
|
|
367
|
+
const ctx = {
|
|
368
|
+
config,
|
|
369
|
+
options,
|
|
370
|
+
branchName,
|
|
371
|
+
processor: processorFactory(),
|
|
372
|
+
lifecycleManager: lifecycleManager ?? new RepoLifecycleManager(undefined, options.retries),
|
|
373
|
+
tokenManager: createTokenManager(),
|
|
374
|
+
reportResults: [],
|
|
375
|
+
lifecycleReportInputs: [],
|
|
376
|
+
settingsCollector: new ResultsCollector(),
|
|
377
|
+
rulesetProcessorFactory,
|
|
378
|
+
repoSettingsProcessorFactory,
|
|
379
|
+
labelsProcessorFactory,
|
|
380
|
+
};
|
|
381
|
+
for (let i = 0; i < config.repos.length; i++) {
|
|
382
|
+
await processSingleRepo(config.repos[i], i, ctx);
|
|
383
|
+
}
|
|
384
|
+
displayReports(ctx.reportResults, ctx.lifecycleReportInputs, ctx.settingsCollector, options.dryRun ?? false);
|
|
385
|
+
// Propagate failures to caller (CLI entry handles process.exit)
|
|
386
|
+
const settingsResults = ctx.settingsCollector.getAll();
|
|
387
|
+
const hasErrors = ctx.reportResults.some((r) => r.error);
|
|
394
388
|
const hasSettingsErrors = settingsResults.some((r) => r.error);
|
|
395
389
|
if (hasErrors || hasSettingsErrors) {
|
|
396
|
-
|
|
390
|
+
throw new Error("One or more repositories had errors during sync");
|
|
397
391
|
}
|
|
398
392
|
}
|