@aspruyt/xfg 3.8.0 → 3.8.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 -1
- package/dist/cli/program.js +2 -2
- package/dist/cli/settings-command.js +137 -123
- package/dist/cli/settings-report-builder.d.ts +5 -2
- package/dist/sync/file-sync-strategy.d.ts +12 -0
- package/dist/sync/file-sync-strategy.js +30 -0
- package/dist/sync/index.d.ts +4 -1
- package/dist/sync/index.js +4 -0
- package/dist/sync/manifest-strategy.d.ts +20 -0
- package/dist/sync/manifest-strategy.js +47 -0
- package/dist/sync/repository-processor.d.ts +10 -12
- package/dist/sync/repository-processor.js +50 -271
- package/dist/sync/sync-workflow.d.ts +18 -0
- package/dist/sync/sync-workflow.js +134 -0
- package/dist/sync/types.d.ts +35 -0
- package/package.json +2 -2
package/README.md
CHANGED
package/dist/cli/program.js
CHANGED
|
@@ -56,9 +56,9 @@ const syncCommand = new Command("sync")
|
|
|
56
56
|
});
|
|
57
57
|
addSharedOptions(syncCommand);
|
|
58
58
|
program.addCommand(syncCommand);
|
|
59
|
-
// Settings command (
|
|
59
|
+
// Settings command (repository settings and rulesets)
|
|
60
60
|
const settingsCommand = new Command("settings")
|
|
61
|
-
.description("Manage GitHub
|
|
61
|
+
.description("Manage GitHub repository settings and rulesets")
|
|
62
62
|
.action((opts) => {
|
|
63
63
|
runSettings(opts).catch((error) => {
|
|
64
64
|
console.error("Fatal error:", error);
|
|
@@ -9,58 +9,42 @@ import { generateWorkspaceName } from "../shared/workspace-utils.js";
|
|
|
9
9
|
import { buildErrorResult } from "../output/summary-utils.js";
|
|
10
10
|
import { getManagedRulesets } from "../sync/manifest.js";
|
|
11
11
|
import { formatSettingsReportCLI, writeSettingsReportSummary, } from "../output/settings-report.js";
|
|
12
|
-
import { buildSettingsReport } from "./settings-report-builder.js";
|
|
12
|
+
import { buildSettingsReport, } from "./settings-report-builder.js";
|
|
13
13
|
import { defaultProcessorFactory, defaultRulesetProcessorFactory, defaultRepoSettingsProcessorFactory, } from "./types.js";
|
|
14
14
|
/**
|
|
15
|
-
*
|
|
15
|
+
* Collects processing results for the SettingsReport.
|
|
16
|
+
* Provides a centralized way to track results across rulesets and repo settings.
|
|
16
17
|
*/
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
process.exit(1);
|
|
22
|
-
}
|
|
23
|
-
console.log(`Loading config from: ${configPath}`);
|
|
24
|
-
if (options.dryRun) {
|
|
25
|
-
console.log("Running in DRY RUN mode - no changes will be made\n");
|
|
26
|
-
}
|
|
27
|
-
const rawConfig = loadRawConfig(configPath);
|
|
28
|
-
try {
|
|
29
|
-
validateForSettings(rawConfig);
|
|
30
|
-
}
|
|
31
|
-
catch (error) {
|
|
32
|
-
console.error(error instanceof Error ? error.message : String(error));
|
|
33
|
-
process.exit(1);
|
|
34
|
-
}
|
|
35
|
-
const config = normalizeConfig(rawConfig);
|
|
36
|
-
const reposWithRulesets = config.repos.filter((r) => r.settings?.rulesets && Object.keys(r.settings.rulesets).length > 0);
|
|
37
|
-
const reposWithRepoSettings = config.repos.filter((r) => r.settings?.repo && Object.keys(r.settings.repo).length > 0);
|
|
38
|
-
if (reposWithRulesets.length === 0 && reposWithRepoSettings.length === 0) {
|
|
39
|
-
console.log("No settings configured. Add settings.rulesets or settings.repo to your config.");
|
|
40
|
-
return;
|
|
41
|
-
}
|
|
42
|
-
if (reposWithRulesets.length > 0) {
|
|
43
|
-
console.log(`Found ${reposWithRulesets.length} repositories with rulesets`);
|
|
44
|
-
}
|
|
45
|
-
if (reposWithRepoSettings.length > 0) {
|
|
46
|
-
console.log(`Found ${reposWithRepoSettings.length} repositories with repo settings`);
|
|
47
|
-
}
|
|
48
|
-
console.log("");
|
|
49
|
-
logger.setTotal(reposWithRulesets.length + reposWithRepoSettings.length);
|
|
50
|
-
const processor = processorFactory();
|
|
51
|
-
const repoProcessor = repoProcessorFactory();
|
|
52
|
-
const results = [];
|
|
53
|
-
const processingResults = [];
|
|
54
|
-
function getOrCreateResult(repoName) {
|
|
55
|
-
let result = processingResults.find((r) => r.repoName === repoName);
|
|
18
|
+
class ResultsCollector {
|
|
19
|
+
results = [];
|
|
20
|
+
getOrCreate(repoName) {
|
|
21
|
+
let result = this.results.find((r) => r.repoName === repoName);
|
|
56
22
|
if (!result) {
|
|
57
23
|
result = { repoName };
|
|
58
|
-
|
|
24
|
+
this.results.push(result);
|
|
59
25
|
}
|
|
60
26
|
return result;
|
|
61
27
|
}
|
|
62
|
-
|
|
63
|
-
const
|
|
28
|
+
appendError(repoName, error) {
|
|
29
|
+
const existing = this.getOrCreate(repoName);
|
|
30
|
+
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
31
|
+
if (existing.error) {
|
|
32
|
+
existing.error += `; ${errorMsg}`;
|
|
33
|
+
}
|
|
34
|
+
else {
|
|
35
|
+
existing.error = errorMsg;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
getAll() {
|
|
39
|
+
return this.results;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Process rulesets for all configured repositories.
|
|
44
|
+
*/
|
|
45
|
+
async function processRulesets(repos, config, options, processor, repoProcessor, results, collector) {
|
|
46
|
+
for (let i = 0; i < repos.length; i++) {
|
|
47
|
+
const repoConfig = repos[i];
|
|
64
48
|
let repoInfo;
|
|
65
49
|
try {
|
|
66
50
|
repoInfo = parseGitUrl(repoConfig.git, {
|
|
@@ -70,14 +54,12 @@ export async function runSettings(options, processorFactory = defaultRulesetProc
|
|
|
70
54
|
catch (error) {
|
|
71
55
|
logger.error(i + 1, repoConfig.git, String(error));
|
|
72
56
|
results.push(buildErrorResult(repoConfig.git, error));
|
|
73
|
-
|
|
74
|
-
error instanceof Error ? error.message : String(error);
|
|
57
|
+
collector.appendError(repoConfig.git, error);
|
|
75
58
|
continue;
|
|
76
59
|
}
|
|
77
60
|
const repoName = getRepoDisplayName(repoInfo);
|
|
78
61
|
if (!isGitHubRepo(repoInfo)) {
|
|
79
62
|
logger.skip(i + 1, repoName, "GitHub Rulesets only supported for GitHub repos");
|
|
80
|
-
// Skipped repos don't appear in the report
|
|
81
63
|
continue;
|
|
82
64
|
}
|
|
83
65
|
const managedRulesets = getManagedRulesets(null, config.id);
|
|
@@ -130,101 +112,133 @@ export async function runSettings(options, processorFactory = defaultRulesetProc
|
|
|
130
112
|
message: result.message,
|
|
131
113
|
rulesetPlanDetails: result.planOutput?.entries,
|
|
132
114
|
});
|
|
133
|
-
// Collect result for SettingsReport
|
|
134
115
|
if (!result.skipped) {
|
|
135
|
-
|
|
116
|
+
collector.getOrCreate(repoName).rulesetResult = result;
|
|
136
117
|
}
|
|
137
118
|
}
|
|
138
119
|
catch (error) {
|
|
139
120
|
logger.error(i + 1, repoName, String(error));
|
|
140
121
|
results.push(buildErrorResult(repoName, error));
|
|
141
|
-
|
|
142
|
-
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
143
|
-
if (existingResult.error) {
|
|
144
|
-
existingResult.error += `; ${errorMsg}`;
|
|
145
|
-
}
|
|
146
|
-
else {
|
|
147
|
-
existingResult.error = errorMsg;
|
|
148
|
-
}
|
|
122
|
+
collector.appendError(repoName, error);
|
|
149
123
|
}
|
|
150
124
|
}
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
}
|
|
185
|
-
if (result.skipped) {
|
|
186
|
-
// Silent skip
|
|
187
|
-
}
|
|
188
|
-
else if (result.success) {
|
|
189
|
-
console.log(chalk.green(` ✓ ${repoName}: ${result.message}`));
|
|
190
|
-
}
|
|
191
|
-
else {
|
|
192
|
-
console.log(chalk.red(` ✗ ${repoName}: ${result.message}`));
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Process repo settings for all configured repositories.
|
|
128
|
+
*/
|
|
129
|
+
async function processRepoSettings(repos, config, options, processorFactory, results, collector) {
|
|
130
|
+
if (repos.length === 0) {
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
const processor = processorFactory();
|
|
134
|
+
console.log(`\nProcessing repo settings for ${repos.length} repositories\n`);
|
|
135
|
+
for (let i = 0; i < repos.length; i++) {
|
|
136
|
+
const repoConfig = repos[i];
|
|
137
|
+
let repoInfo;
|
|
138
|
+
try {
|
|
139
|
+
repoInfo = parseGitUrl(repoConfig.git, {
|
|
140
|
+
githubHosts: config.githubHosts,
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
catch (error) {
|
|
144
|
+
console.error(`Failed to parse ${repoConfig.git}: ${error}`);
|
|
145
|
+
collector.appendError(repoConfig.git, error);
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
const repoName = getRepoDisplayName(repoInfo);
|
|
149
|
+
try {
|
|
150
|
+
const result = await processor.process(repoConfig, repoInfo, {
|
|
151
|
+
dryRun: options.dryRun,
|
|
152
|
+
});
|
|
153
|
+
if (result.planOutput && result.planOutput.lines.length > 0) {
|
|
154
|
+
console.log(`\n ${chalk.bold(repoName)}:`);
|
|
155
|
+
console.log(" Repo Settings:");
|
|
156
|
+
for (const line of result.planOutput.lines) {
|
|
157
|
+
console.log(line);
|
|
193
158
|
}
|
|
194
|
-
if (
|
|
195
|
-
const
|
|
196
|
-
|
|
197
|
-
existing.repoSettingsPlanDetails = result.planOutput?.entries;
|
|
198
|
-
}
|
|
199
|
-
else {
|
|
200
|
-
results.push({
|
|
201
|
-
repoName,
|
|
202
|
-
status: result.success ? "succeeded" : "failed",
|
|
203
|
-
message: result.message,
|
|
204
|
-
repoSettingsPlanDetails: result.planOutput?.entries,
|
|
205
|
-
});
|
|
159
|
+
if (result.warnings && result.warnings.length > 0) {
|
|
160
|
+
for (const warning of result.warnings) {
|
|
161
|
+
console.log(chalk.yellow(` ⚠️ Warning: ${warning}`));
|
|
206
162
|
}
|
|
207
163
|
}
|
|
208
|
-
// Collect result for SettingsReport
|
|
209
|
-
if (!result.skipped) {
|
|
210
|
-
getOrCreateResult(repoName).settingsResult = result;
|
|
211
|
-
}
|
|
212
164
|
}
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
165
|
+
if (result.skipped) {
|
|
166
|
+
// Silent skip
|
|
167
|
+
}
|
|
168
|
+
else if (result.success) {
|
|
169
|
+
console.log(chalk.green(` ✓ ${repoName}: ${result.message}`));
|
|
170
|
+
}
|
|
171
|
+
else {
|
|
172
|
+
console.log(chalk.red(` ✗ ${repoName}: ${result.message}`));
|
|
173
|
+
}
|
|
174
|
+
if (!result.skipped) {
|
|
175
|
+
const existing = results.find((r) => r.repoName === repoName);
|
|
176
|
+
if (existing) {
|
|
177
|
+
existing.repoSettingsPlanDetails = result.planOutput?.entries;
|
|
219
178
|
}
|
|
220
179
|
else {
|
|
221
|
-
|
|
180
|
+
results.push({
|
|
181
|
+
repoName,
|
|
182
|
+
status: result.success ? "succeeded" : "failed",
|
|
183
|
+
message: result.message,
|
|
184
|
+
repoSettingsPlanDetails: result.planOutput?.entries,
|
|
185
|
+
});
|
|
222
186
|
}
|
|
223
187
|
}
|
|
188
|
+
if (!result.skipped) {
|
|
189
|
+
collector.getOrCreate(repoName).settingsResult = result;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
catch (error) {
|
|
193
|
+
console.error(` ✗ ${repoName}: ${error}`);
|
|
194
|
+
collector.appendError(repoName, error);
|
|
224
195
|
}
|
|
225
196
|
}
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* Run the settings command - manages GitHub Rulesets and repo settings.
|
|
200
|
+
*/
|
|
201
|
+
export async function runSettings(options, processorFactory = defaultRulesetProcessorFactory, repoProcessorFactory = defaultProcessorFactory, repoSettingsProcessorFactory = defaultRepoSettingsProcessorFactory) {
|
|
202
|
+
const configPath = resolve(options.config);
|
|
203
|
+
if (!existsSync(configPath)) {
|
|
204
|
+
console.error(`Config file not found: ${configPath}`);
|
|
205
|
+
process.exit(1);
|
|
206
|
+
}
|
|
207
|
+
console.log(`Loading config from: ${configPath}`);
|
|
208
|
+
if (options.dryRun) {
|
|
209
|
+
console.log("Running in DRY RUN mode - no changes will be made\n");
|
|
210
|
+
}
|
|
211
|
+
const rawConfig = loadRawConfig(configPath);
|
|
212
|
+
try {
|
|
213
|
+
validateForSettings(rawConfig);
|
|
214
|
+
}
|
|
215
|
+
catch (error) {
|
|
216
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
217
|
+
process.exit(1);
|
|
218
|
+
}
|
|
219
|
+
const config = normalizeConfig(rawConfig);
|
|
220
|
+
const reposWithRulesets = config.repos.filter((r) => r.settings?.rulesets && Object.keys(r.settings.rulesets).length > 0);
|
|
221
|
+
const reposWithRepoSettings = config.repos.filter((r) => r.settings?.repo && Object.keys(r.settings.repo).length > 0);
|
|
222
|
+
if (reposWithRulesets.length === 0 && reposWithRepoSettings.length === 0) {
|
|
223
|
+
console.log("No settings configured. Add settings.rulesets or settings.repo to your config.");
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
if (reposWithRulesets.length > 0) {
|
|
227
|
+
console.log(`Found ${reposWithRulesets.length} repositories with rulesets`);
|
|
228
|
+
}
|
|
229
|
+
if (reposWithRepoSettings.length > 0) {
|
|
230
|
+
console.log(`Found ${reposWithRepoSettings.length} repositories with repo settings`);
|
|
231
|
+
}
|
|
232
|
+
console.log("");
|
|
233
|
+
logger.setTotal(reposWithRulesets.length + reposWithRepoSettings.length);
|
|
234
|
+
const processor = processorFactory();
|
|
235
|
+
const repoProcessor = repoProcessorFactory();
|
|
236
|
+
const results = [];
|
|
237
|
+
const collector = new ResultsCollector();
|
|
238
|
+
await processRulesets(reposWithRulesets, config, options, processor, repoProcessor, results, collector);
|
|
239
|
+
await processRepoSettings(reposWithRepoSettings, config, options, repoSettingsProcessorFactory, results, collector);
|
|
226
240
|
console.log("");
|
|
227
|
-
const report = buildSettingsReport(
|
|
241
|
+
const report = buildSettingsReport(collector.getAll());
|
|
228
242
|
const lines = formatSettingsReportCLI(report);
|
|
229
243
|
for (const line of lines) {
|
|
230
244
|
console.log(line);
|
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
import type { SettingsReport } from "../output/settings-report.js";
|
|
2
2
|
import type { RepoSettingsPlanEntry } from "../settings/repo-settings/formatter.js";
|
|
3
3
|
import type { RulesetPlanEntry } from "../settings/rulesets/formatter.js";
|
|
4
|
-
|
|
4
|
+
/**
|
|
5
|
+
* Result from processing a repository's settings and rulesets.
|
|
6
|
+
* Used to collect results during settings command execution.
|
|
7
|
+
*/
|
|
8
|
+
export interface ProcessorResults {
|
|
5
9
|
repoName: string;
|
|
6
10
|
settingsResult?: {
|
|
7
11
|
planOutput?: {
|
|
@@ -16,4 +20,3 @@ interface ProcessorResults {
|
|
|
16
20
|
error?: string;
|
|
17
21
|
}
|
|
18
22
|
export declare function buildSettingsReport(results: ProcessorResults[]): SettingsReport;
|
|
19
|
-
export {};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { RepoConfig } from "../config/types.js";
|
|
2
|
+
import type { RepoInfo } from "../shared/repo-detector.js";
|
|
3
|
+
import type { IWorkStrategy, WorkResult, SessionContext, ProcessorOptions, IFileSyncOrchestrator } from "./types.js";
|
|
4
|
+
/**
|
|
5
|
+
* Strategy that performs full file synchronization.
|
|
6
|
+
* Wraps FileSyncOrchestrator to fit the IWorkStrategy interface.
|
|
7
|
+
*/
|
|
8
|
+
export declare class FileSyncStrategy implements IWorkStrategy {
|
|
9
|
+
private readonly fileSyncOrchestrator;
|
|
10
|
+
constructor(fileSyncOrchestrator: IFileSyncOrchestrator);
|
|
11
|
+
execute(repoConfig: RepoConfig, repoInfo: RepoInfo, session: SessionContext, options: ProcessorOptions): Promise<WorkResult | null>;
|
|
12
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { formatCommitMessage } from "./commit-message.js";
|
|
2
|
+
/**
|
|
3
|
+
* Strategy that performs full file synchronization.
|
|
4
|
+
* Wraps FileSyncOrchestrator to fit the IWorkStrategy interface.
|
|
5
|
+
*/
|
|
6
|
+
export class FileSyncStrategy {
|
|
7
|
+
fileSyncOrchestrator;
|
|
8
|
+
constructor(fileSyncOrchestrator) {
|
|
9
|
+
this.fileSyncOrchestrator = fileSyncOrchestrator;
|
|
10
|
+
}
|
|
11
|
+
async execute(repoConfig, repoInfo, session, options) {
|
|
12
|
+
const { fileChanges, diffStats, changedFiles, hasChanges } = await this.fileSyncOrchestrator.sync(repoConfig, repoInfo, session, options);
|
|
13
|
+
if (!hasChanges) {
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
const fileChangeDetails = changedFiles
|
|
17
|
+
.filter((f) => f.action !== "skip")
|
|
18
|
+
.map((f) => ({
|
|
19
|
+
path: f.fileName,
|
|
20
|
+
action: f.action,
|
|
21
|
+
}));
|
|
22
|
+
return {
|
|
23
|
+
fileChanges,
|
|
24
|
+
changedFiles,
|
|
25
|
+
diffStats,
|
|
26
|
+
commitMessage: formatCommitMessage(changedFiles),
|
|
27
|
+
fileChangeDetails,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
}
|
package/dist/sync/index.d.ts
CHANGED
|
@@ -7,7 +7,10 @@ export { CommitPushManager } from "./commit-push-manager.js";
|
|
|
7
7
|
export { formatCommitMessage } from "./commit-message.js";
|
|
8
8
|
export { FileSyncOrchestrator } from "./file-sync-orchestrator.js";
|
|
9
9
|
export { PRMergeHandler } from "./pr-merge-handler.js";
|
|
10
|
-
export
|
|
10
|
+
export { FileSyncStrategy } from "./file-sync-strategy.js";
|
|
11
|
+
export { ManifestStrategy, type ManifestUpdateParams, } from "./manifest-strategy.js";
|
|
12
|
+
export { SyncWorkflow } from "./sync-workflow.js";
|
|
13
|
+
export type { IFileWriter, FileWriteContext, FileWriterDeps, FileWriteAllResult, FileWriteResult, IManifestManager, OrphanProcessResult, OrphanDeleteOptions, OrphanDeleteDeps, IBranchManager, BranchSetupOptions, IAuthOptionsBuilder, AuthResult, IRepositorySession, SessionOptions, SessionContext, ICommitPushManager, CommitPushOptions, CommitPushResult, GitOpsFactory, IRepositoryProcessor, ProcessorOptions, ProcessorResult, FileChangeDetail, IFileSyncOrchestrator, FileSyncResult, IPRMergeHandler, PRHandlerOptions, WorkResult, IWorkStrategy, ISyncWorkflow, } from "./types.js";
|
|
11
14
|
export { RepositoryProcessor } from "./repository-processor.js";
|
|
12
15
|
export { createEmptyManifest, loadManifest, saveManifest, getManagedFiles, getManagedRulesets, updateManifest, updateManifestRulesets, MANIFEST_FILENAME, type XfgManifest, type XfgManifestConfigEntry, } from "./manifest.js";
|
|
13
16
|
export { getFileStatus, formatStatusBadge, formatDiffLine, generateDiff, createDiffStats, incrementDiffStats, type FileStatus, type DiffStats, } from "./diff-utils.js";
|
package/dist/sync/index.js
CHANGED
|
@@ -7,6 +7,10 @@ export { CommitPushManager } from "./commit-push-manager.js";
|
|
|
7
7
|
export { formatCommitMessage } from "./commit-message.js";
|
|
8
8
|
export { FileSyncOrchestrator } from "./file-sync-orchestrator.js";
|
|
9
9
|
export { PRMergeHandler } from "./pr-merge-handler.js";
|
|
10
|
+
// Strategy pattern components
|
|
11
|
+
export { FileSyncStrategy } from "./file-sync-strategy.js";
|
|
12
|
+
export { ManifestStrategy, } from "./manifest-strategy.js";
|
|
13
|
+
export { SyncWorkflow } from "./sync-workflow.js";
|
|
10
14
|
// Repository processor
|
|
11
15
|
export { RepositoryProcessor } from "./repository-processor.js";
|
|
12
16
|
// Manifest handling
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { RepoConfig } from "../config/types.js";
|
|
2
|
+
import type { RepoInfo } from "../shared/repo-detector.js";
|
|
3
|
+
import type { ILogger } from "../shared/logger.js";
|
|
4
|
+
import type { IWorkStrategy, WorkResult, SessionContext, ProcessorOptions } from "./types.js";
|
|
5
|
+
/**
|
|
6
|
+
* Parameters for manifest-only updates
|
|
7
|
+
*/
|
|
8
|
+
export interface ManifestUpdateParams {
|
|
9
|
+
rulesets: string[];
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Strategy that only updates the manifest with ruleset tracking.
|
|
13
|
+
* Used by updateManifestOnly() for settings command ruleset sync.
|
|
14
|
+
*/
|
|
15
|
+
export declare class ManifestStrategy implements IWorkStrategy {
|
|
16
|
+
private readonly params;
|
|
17
|
+
private readonly log;
|
|
18
|
+
constructor(params: ManifestUpdateParams, log: ILogger);
|
|
19
|
+
execute(_repoConfig: RepoConfig, _repoInfo: RepoInfo, _session: SessionContext, options: ProcessorOptions): Promise<WorkResult | null>;
|
|
20
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { loadManifest, saveManifest, updateManifestRulesets, MANIFEST_FILENAME, } from "./manifest.js";
|
|
2
|
+
/**
|
|
3
|
+
* Strategy that only updates the manifest with ruleset tracking.
|
|
4
|
+
* Used by updateManifestOnly() for settings command ruleset sync.
|
|
5
|
+
*/
|
|
6
|
+
export class ManifestStrategy {
|
|
7
|
+
params;
|
|
8
|
+
log;
|
|
9
|
+
constructor(params, log) {
|
|
10
|
+
this.params = params;
|
|
11
|
+
this.log = log;
|
|
12
|
+
}
|
|
13
|
+
async execute(_repoConfig, _repoInfo, _session, options) {
|
|
14
|
+
const { workDir, dryRun, configId } = options;
|
|
15
|
+
// Load and update manifest
|
|
16
|
+
const existingManifest = loadManifest(workDir);
|
|
17
|
+
const rulesetsWithDeleteOrphaned = new Map(this.params.rulesets.map((name) => [name, true]));
|
|
18
|
+
const { manifest: newManifest } = updateManifestRulesets(existingManifest, configId, rulesetsWithDeleteOrphaned);
|
|
19
|
+
// Check if changed
|
|
20
|
+
const existingConfigs = existingManifest?.configs ?? {};
|
|
21
|
+
if (JSON.stringify(existingConfigs) === JSON.stringify(newManifest.configs)) {
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
if (dryRun) {
|
|
25
|
+
this.log.info(`Would update ${MANIFEST_FILENAME} with rulesets`);
|
|
26
|
+
}
|
|
27
|
+
saveManifest(workDir, newManifest);
|
|
28
|
+
const fileChanges = new Map([
|
|
29
|
+
[
|
|
30
|
+
MANIFEST_FILENAME,
|
|
31
|
+
{
|
|
32
|
+
fileName: MANIFEST_FILENAME,
|
|
33
|
+
content: JSON.stringify(newManifest, null, 2) + "\n",
|
|
34
|
+
action: "update",
|
|
35
|
+
},
|
|
36
|
+
],
|
|
37
|
+
]);
|
|
38
|
+
return {
|
|
39
|
+
fileChanges,
|
|
40
|
+
changedFiles: [
|
|
41
|
+
{ fileName: MANIFEST_FILENAME, action: "update" },
|
|
42
|
+
],
|
|
43
|
+
commitMessage: "chore: update manifest with ruleset tracking",
|
|
44
|
+
fileChangeDetails: [{ path: MANIFEST_FILENAME, action: "update" }],
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -1,18 +1,15 @@
|
|
|
1
|
-
import { RepoConfig } from "../config/index.js";
|
|
2
|
-
import { RepoInfo } from "../shared/repo-detector.js";
|
|
1
|
+
import type { RepoConfig } from "../config/index.js";
|
|
2
|
+
import type { RepoInfo } from "../shared/repo-detector.js";
|
|
3
3
|
import { ILogger } from "../shared/logger.js";
|
|
4
|
-
import { type IFileWriter, type IManifestManager, type IBranchManager, type IAuthOptionsBuilder, type IRepositorySession, type ICommitPushManager, type IFileSyncOrchestrator, type IPRMergeHandler, type IRepositoryProcessor, type GitOpsFactory, type ProcessorOptions, type ProcessorResult } from "./index.js";
|
|
4
|
+
import { type IFileWriter, type IManifestManager, type IBranchManager, type IAuthOptionsBuilder, type IRepositorySession, type ICommitPushManager, type IFileSyncOrchestrator, type IPRMergeHandler, type ISyncWorkflow, type IRepositoryProcessor, type GitOpsFactory, type ProcessorOptions, type ProcessorResult } from "./index.js";
|
|
5
|
+
/**
|
|
6
|
+
* Thin facade that delegates to SyncWorkflow with appropriate strategy.
|
|
7
|
+
* process() uses FileSyncStrategy, updateManifestOnly() uses ManifestStrategy.
|
|
8
|
+
*/
|
|
5
9
|
export declare class RepositoryProcessor implements IRepositoryProcessor {
|
|
6
|
-
private readonly
|
|
7
|
-
private readonly log;
|
|
8
|
-
private readonly authOptionsBuilder;
|
|
9
|
-
private readonly repositorySession;
|
|
10
|
-
private readonly commitPushManager;
|
|
11
|
-
private readonly fileWriter;
|
|
12
|
-
private readonly manifestManager;
|
|
13
|
-
private readonly branchManager;
|
|
10
|
+
private readonly syncWorkflow;
|
|
14
11
|
private readonly fileSyncOrchestrator;
|
|
15
|
-
private readonly
|
|
12
|
+
private readonly log;
|
|
16
13
|
constructor(gitOpsFactory?: GitOpsFactory, log?: ILogger, components?: {
|
|
17
14
|
fileWriter?: IFileWriter;
|
|
18
15
|
manifestManager?: IManifestManager;
|
|
@@ -22,6 +19,7 @@ export declare class RepositoryProcessor implements IRepositoryProcessor {
|
|
|
22
19
|
commitPushManager?: ICommitPushManager;
|
|
23
20
|
fileSyncOrchestrator?: IFileSyncOrchestrator;
|
|
24
21
|
prMergeHandler?: IPRMergeHandler;
|
|
22
|
+
syncWorkflow?: ISyncWorkflow;
|
|
25
23
|
});
|
|
26
24
|
process(repoConfig: RepoConfig, repoInfo: RepoInfo, options: ProcessorOptions): Promise<ProcessorResult>;
|
|
27
25
|
updateManifestOnly(repoInfo: RepoInfo, repoConfig: RepoConfig, options: ProcessorOptions, manifestUpdate: {
|
|
@@ -1,298 +1,77 @@
|
|
|
1
|
-
import { getRepoDisplayName } from "../shared/repo-detector.js";
|
|
2
1
|
import { GitOps } from "../vcs/git-ops.js";
|
|
3
2
|
import { AuthenticatedGitOps } from "../vcs/authenticated-git-ops.js";
|
|
4
3
|
import { logger } from "../shared/logger.js";
|
|
5
4
|
import { hasGitHubAppCredentials } from "../vcs/index.js";
|
|
6
|
-
import { defaultExecutor } from "../shared/command-executor.js";
|
|
7
|
-
import { loadManifest, saveManifest, updateManifestRulesets, MANIFEST_FILENAME, } from "./manifest.js";
|
|
8
5
|
import { GitHubAppTokenManager } from "../vcs/github-app-token-manager.js";
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
path: f.fileName,
|
|
16
|
-
action: f.action,
|
|
17
|
-
}));
|
|
18
|
-
}
|
|
6
|
+
import { FileWriter, ManifestManager, BranchManager, AuthOptionsBuilder, RepositorySession, CommitPushManager, FileSyncOrchestrator, PRMergeHandler, FileSyncStrategy, ManifestStrategy, SyncWorkflow, loadManifest, updateManifestRulesets, MANIFEST_FILENAME, } from "./index.js";
|
|
7
|
+
import { getRepoDisplayName } from "../shared/repo-detector.js";
|
|
8
|
+
/**
|
|
9
|
+
* Thin facade that delegates to SyncWorkflow with appropriate strategy.
|
|
10
|
+
* process() uses FileSyncStrategy, updateManifestOnly() uses ManifestStrategy.
|
|
11
|
+
*/
|
|
19
12
|
export class RepositoryProcessor {
|
|
20
|
-
|
|
21
|
-
log;
|
|
22
|
-
authOptionsBuilder;
|
|
23
|
-
repositorySession;
|
|
24
|
-
commitPushManager;
|
|
25
|
-
fileWriter;
|
|
26
|
-
manifestManager;
|
|
27
|
-
branchManager;
|
|
13
|
+
syncWorkflow;
|
|
28
14
|
fileSyncOrchestrator;
|
|
29
|
-
|
|
15
|
+
log;
|
|
30
16
|
constructor(gitOpsFactory, log, components) {
|
|
31
17
|
const factory = gitOpsFactory ??
|
|
32
18
|
((opts, auth) => new AuthenticatedGitOps(new GitOps(opts), auth));
|
|
33
19
|
const logInstance = log ?? logger;
|
|
34
|
-
this.gitOpsFactory = factory;
|
|
35
20
|
this.log = logInstance;
|
|
36
|
-
this.fileWriter = components?.fileWriter ?? new FileWriter();
|
|
37
|
-
this.manifestManager = components?.manifestManager ?? new ManifestManager();
|
|
38
|
-
this.branchManager = components?.branchManager ?? new BranchManager();
|
|
39
21
|
// Initialize token manager for auth builder
|
|
40
22
|
const tokenManager = hasGitHubAppCredentials()
|
|
41
23
|
? new GitHubAppTokenManager(process.env.XFG_GITHUB_APP_ID, process.env.XFG_GITHUB_APP_PRIVATE_KEY)
|
|
42
24
|
: null;
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
25
|
+
const fileWriter = components?.fileWriter ?? new FileWriter();
|
|
26
|
+
const manifestManager = components?.manifestManager ?? new ManifestManager();
|
|
27
|
+
const branchManager = components?.branchManager ?? new BranchManager();
|
|
28
|
+
const authOptionsBuilder = components?.authOptionsBuilder ??
|
|
29
|
+
new AuthOptionsBuilder(tokenManager, logInstance);
|
|
30
|
+
const repositorySession = components?.repositorySession ??
|
|
31
|
+
new RepositorySession(factory, logInstance);
|
|
32
|
+
const commitPushManager = components?.commitPushManager ?? new CommitPushManager(logInstance);
|
|
33
|
+
const prMergeHandler = components?.prMergeHandler ?? new PRMergeHandler(logInstance);
|
|
51
34
|
this.fileSyncOrchestrator =
|
|
52
35
|
components?.fileSyncOrchestrator ??
|
|
53
|
-
new FileSyncOrchestrator(
|
|
54
|
-
this.
|
|
55
|
-
components?.
|
|
36
|
+
new FileSyncOrchestrator(fileWriter, manifestManager, logInstance);
|
|
37
|
+
this.syncWorkflow =
|
|
38
|
+
components?.syncWorkflow ??
|
|
39
|
+
new SyncWorkflow(authOptionsBuilder, repositorySession, branchManager, commitPushManager, prMergeHandler, logInstance);
|
|
56
40
|
}
|
|
57
41
|
async process(repoConfig, repoInfo, options) {
|
|
58
|
-
const
|
|
59
|
-
|
|
60
|
-
const retries = options.retries ?? 3;
|
|
61
|
-
const executor = options.executor ?? defaultExecutor;
|
|
62
|
-
// Resolve auth
|
|
63
|
-
const authResult = await this.authOptionsBuilder.resolve(repoInfo, repoName);
|
|
64
|
-
if (authResult.skipResult) {
|
|
65
|
-
return authResult.skipResult;
|
|
66
|
-
}
|
|
67
|
-
// Determine merge mode
|
|
68
|
-
const mergeMode = repoConfig.prOptions?.merge ?? "auto";
|
|
69
|
-
const isDirectMode = mergeMode === "direct";
|
|
70
|
-
if (isDirectMode && repoConfig.prOptions?.mergeStrategy) {
|
|
71
|
-
this.log.info(`Warning: mergeStrategy '${repoConfig.prOptions.mergeStrategy}' is ignored in direct mode`);
|
|
72
|
-
}
|
|
73
|
-
let session = null;
|
|
74
|
-
try {
|
|
75
|
-
// Setup workspace
|
|
76
|
-
session = await this.repositorySession.setup(repoInfo, {
|
|
77
|
-
workDir,
|
|
78
|
-
dryRun: dryRun ?? false,
|
|
79
|
-
retries,
|
|
80
|
-
authOptions: authResult.authOptions,
|
|
81
|
-
});
|
|
82
|
-
// Setup branch
|
|
83
|
-
await this.branchManager.setupBranch({
|
|
84
|
-
repoInfo,
|
|
85
|
-
branchName,
|
|
86
|
-
baseBranch: session.baseBranch,
|
|
87
|
-
workDir,
|
|
88
|
-
isDirectMode,
|
|
89
|
-
dryRun: dryRun ?? false,
|
|
90
|
-
retries,
|
|
91
|
-
token: authResult.token,
|
|
92
|
-
gitOps: session.gitOps,
|
|
93
|
-
log: this.log,
|
|
94
|
-
executor,
|
|
95
|
-
});
|
|
96
|
-
// Process files and manifest
|
|
97
|
-
const { fileChanges, diffStats, changedFiles, hasChanges } = await this.fileSyncOrchestrator.sync(repoConfig, repoInfo, session, options);
|
|
98
|
-
// Map file changes for reporting
|
|
99
|
-
const fileChangeDetails = mapToFileChangeDetails(changedFiles);
|
|
100
|
-
if (!hasChanges) {
|
|
101
|
-
return {
|
|
102
|
-
success: true,
|
|
103
|
-
repoName,
|
|
104
|
-
message: "No changes detected",
|
|
105
|
-
skipped: true,
|
|
106
|
-
diffStats,
|
|
107
|
-
fileChanges: fileChangeDetails,
|
|
108
|
-
};
|
|
109
|
-
}
|
|
110
|
-
// Commit and push
|
|
111
|
-
const commitMessage = formatCommitMessage(changedFiles);
|
|
112
|
-
const pushBranch = isDirectMode ? session.baseBranch : branchName;
|
|
113
|
-
const commitResult = await this.commitPushManager.commitAndPush({
|
|
114
|
-
repoInfo,
|
|
115
|
-
gitOps: session.gitOps,
|
|
116
|
-
workDir,
|
|
117
|
-
fileChanges,
|
|
118
|
-
commitMessage,
|
|
119
|
-
pushBranch,
|
|
120
|
-
isDirectMode,
|
|
121
|
-
dryRun: dryRun ?? false,
|
|
122
|
-
retries,
|
|
123
|
-
token: authResult.token,
|
|
124
|
-
executor,
|
|
125
|
-
}, repoName);
|
|
126
|
-
if (!commitResult.success && commitResult.errorResult) {
|
|
127
|
-
return commitResult.errorResult;
|
|
128
|
-
}
|
|
129
|
-
if (commitResult.skipped) {
|
|
130
|
-
return {
|
|
131
|
-
success: true,
|
|
132
|
-
repoName,
|
|
133
|
-
message: "No changes detected after staging",
|
|
134
|
-
skipped: true,
|
|
135
|
-
diffStats,
|
|
136
|
-
fileChanges: fileChangeDetails,
|
|
137
|
-
};
|
|
138
|
-
}
|
|
139
|
-
// Direct mode: no PR
|
|
140
|
-
if (isDirectMode) {
|
|
141
|
-
this.log.info(`Changes pushed directly to ${session.baseBranch}`);
|
|
142
|
-
return {
|
|
143
|
-
success: true,
|
|
144
|
-
repoName,
|
|
145
|
-
message: `Pushed directly to ${session.baseBranch}`,
|
|
146
|
-
diffStats,
|
|
147
|
-
fileChanges: fileChangeDetails,
|
|
148
|
-
};
|
|
149
|
-
}
|
|
150
|
-
// Create and merge PR
|
|
151
|
-
return await this.prMergeHandler.createAndMerge(repoInfo, repoConfig, {
|
|
152
|
-
branchName,
|
|
153
|
-
baseBranch: session.baseBranch,
|
|
154
|
-
workDir,
|
|
155
|
-
dryRun: dryRun ?? false,
|
|
156
|
-
retries,
|
|
157
|
-
prTemplate,
|
|
158
|
-
token: authResult.token,
|
|
159
|
-
executor,
|
|
160
|
-
}, changedFiles, repoName, diffStats, fileChangeDetails);
|
|
161
|
-
}
|
|
162
|
-
finally {
|
|
163
|
-
try {
|
|
164
|
-
session?.cleanup();
|
|
165
|
-
}
|
|
166
|
-
catch {
|
|
167
|
-
// Ignore cleanup errors - best effort
|
|
168
|
-
}
|
|
169
|
-
}
|
|
42
|
+
const strategy = new FileSyncStrategy(this.fileSyncOrchestrator);
|
|
43
|
+
return this.syncWorkflow.execute(repoConfig, repoInfo, options, strategy);
|
|
170
44
|
}
|
|
171
45
|
async updateManifestOnly(repoInfo, repoConfig, options, manifestUpdate) {
|
|
172
46
|
const repoName = getRepoDisplayName(repoInfo);
|
|
173
|
-
const {
|
|
174
|
-
|
|
175
|
-
const
|
|
176
|
-
|
|
177
|
-
const
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
session = await this.repositorySession.setup(repoInfo, {
|
|
187
|
-
workDir,
|
|
188
|
-
dryRun: dryRun ?? false,
|
|
189
|
-
retries,
|
|
190
|
-
authOptions: authResult.authOptions,
|
|
191
|
-
});
|
|
192
|
-
// Load and update manifest
|
|
193
|
-
const existingManifest = loadManifest(workDir);
|
|
194
|
-
const rulesetsWithDeleteOrphaned = new Map(manifestUpdate.rulesets.map((name) => [name, true]));
|
|
195
|
-
const { manifest: newManifest } = updateManifestRulesets(existingManifest, options.configId, rulesetsWithDeleteOrphaned);
|
|
196
|
-
// Check if changed
|
|
197
|
-
const existingConfigs = existingManifest?.configs ?? {};
|
|
198
|
-
if (JSON.stringify(existingConfigs) === JSON.stringify(newManifest.configs)) {
|
|
199
|
-
return {
|
|
200
|
-
success: true,
|
|
201
|
-
repoName,
|
|
202
|
-
message: "No manifest changes detected",
|
|
203
|
-
skipped: true,
|
|
204
|
-
};
|
|
205
|
-
}
|
|
206
|
-
// Prepare manifest file change details for reporting
|
|
207
|
-
const manifestFileChange = [
|
|
208
|
-
{ path: MANIFEST_FILENAME, action: "update" },
|
|
209
|
-
];
|
|
210
|
-
if (dryRun) {
|
|
211
|
-
this.log.info(`Would update ${MANIFEST_FILENAME} with rulesets`);
|
|
212
|
-
return {
|
|
213
|
-
success: true,
|
|
214
|
-
repoName,
|
|
215
|
-
message: "Would update manifest (dry-run)",
|
|
216
|
-
fileChanges: manifestFileChange,
|
|
217
|
-
};
|
|
218
|
-
}
|
|
219
|
-
// Setup branch and commit
|
|
220
|
-
await this.branchManager.setupBranch({
|
|
221
|
-
repoInfo,
|
|
222
|
-
branchName,
|
|
223
|
-
baseBranch: session.baseBranch,
|
|
224
|
-
workDir,
|
|
225
|
-
isDirectMode,
|
|
226
|
-
dryRun: false,
|
|
227
|
-
retries,
|
|
228
|
-
token: authResult.token,
|
|
229
|
-
gitOps: session.gitOps,
|
|
230
|
-
log: this.log,
|
|
231
|
-
executor,
|
|
232
|
-
});
|
|
233
|
-
saveManifest(workDir, newManifest);
|
|
234
|
-
const fileChanges = new Map([
|
|
235
|
-
[
|
|
236
|
-
MANIFEST_FILENAME,
|
|
237
|
-
{
|
|
238
|
-
fileName: MANIFEST_FILENAME,
|
|
239
|
-
content: JSON.stringify(newManifest, null, 2) + "\n",
|
|
240
|
-
action: "update",
|
|
241
|
-
},
|
|
242
|
-
],
|
|
243
|
-
]);
|
|
244
|
-
const pushBranch = isDirectMode ? session.baseBranch : branchName;
|
|
245
|
-
const commitResult = await this.commitPushManager.commitAndPush({
|
|
246
|
-
repoInfo,
|
|
247
|
-
gitOps: session.gitOps,
|
|
248
|
-
workDir,
|
|
249
|
-
fileChanges,
|
|
250
|
-
commitMessage: "chore: update manifest with ruleset tracking",
|
|
251
|
-
pushBranch,
|
|
252
|
-
isDirectMode,
|
|
253
|
-
dryRun: false,
|
|
254
|
-
retries,
|
|
255
|
-
token: authResult.token,
|
|
256
|
-
executor,
|
|
257
|
-
}, repoName);
|
|
258
|
-
if (!commitResult.success && commitResult.errorResult) {
|
|
259
|
-
return commitResult.errorResult;
|
|
260
|
-
}
|
|
261
|
-
if (commitResult.skipped) {
|
|
262
|
-
return {
|
|
263
|
-
success: true,
|
|
264
|
-
repoName,
|
|
265
|
-
message: "No changes detected after staging",
|
|
266
|
-
skipped: true,
|
|
267
|
-
fileChanges: manifestFileChange,
|
|
268
|
-
};
|
|
269
|
-
}
|
|
270
|
-
if (isDirectMode) {
|
|
271
|
-
return {
|
|
272
|
-
success: true,
|
|
273
|
-
repoName,
|
|
274
|
-
message: `Manifest updated directly on ${session.baseBranch}`,
|
|
275
|
-
fileChanges: manifestFileChange,
|
|
276
|
-
};
|
|
277
|
-
}
|
|
278
|
-
// Create and merge PR
|
|
279
|
-
return await this.prMergeHandler.createAndMerge(repoInfo, repoConfig, {
|
|
280
|
-
branchName,
|
|
281
|
-
baseBranch: session.baseBranch,
|
|
282
|
-
workDir,
|
|
283
|
-
dryRun: false,
|
|
284
|
-
retries,
|
|
285
|
-
token: authResult.token,
|
|
286
|
-
executor,
|
|
287
|
-
}, [{ fileName: MANIFEST_FILENAME, action: "update" }], repoName, undefined, manifestFileChange);
|
|
47
|
+
const { workDir, dryRun } = options;
|
|
48
|
+
// Pre-check manifest changes (preserves original early-return behavior)
|
|
49
|
+
const existingManifest = loadManifest(workDir);
|
|
50
|
+
const rulesetsWithDeleteOrphaned = new Map(manifestUpdate.rulesets.map((name) => [name, true]));
|
|
51
|
+
const { manifest: newManifest } = updateManifestRulesets(existingManifest, options.configId, rulesetsWithDeleteOrphaned);
|
|
52
|
+
const existingConfigs = existingManifest?.configs ?? {};
|
|
53
|
+
if (JSON.stringify(existingConfigs) === JSON.stringify(newManifest.configs)) {
|
|
54
|
+
return {
|
|
55
|
+
success: true,
|
|
56
|
+
repoName,
|
|
57
|
+
message: "No manifest changes detected",
|
|
58
|
+
skipped: true,
|
|
59
|
+
};
|
|
288
60
|
}
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
61
|
+
const manifestFileChange = [
|
|
62
|
+
{ path: MANIFEST_FILENAME, action: "update" },
|
|
63
|
+
];
|
|
64
|
+
if (dryRun) {
|
|
65
|
+
this.log.info(`Would update ${MANIFEST_FILENAME} with rulesets`);
|
|
66
|
+
return {
|
|
67
|
+
success: true,
|
|
68
|
+
repoName,
|
|
69
|
+
message: "Would update manifest (dry-run)",
|
|
70
|
+
fileChanges: manifestFileChange,
|
|
71
|
+
};
|
|
296
72
|
}
|
|
73
|
+
// Delegate to workflow for actual commit/push/PR
|
|
74
|
+
const strategy = new ManifestStrategy(manifestUpdate, this.log);
|
|
75
|
+
return this.syncWorkflow.execute(repoConfig, repoInfo, options, strategy);
|
|
297
76
|
}
|
|
298
77
|
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { RepoConfig } from "../config/types.js";
|
|
2
|
+
import { RepoInfo } from "../shared/repo-detector.js";
|
|
3
|
+
import type { ILogger } from "../shared/logger.js";
|
|
4
|
+
import type { ISyncWorkflow, IWorkStrategy, IAuthOptionsBuilder, IRepositorySession, IBranchManager, ICommitPushManager, IPRMergeHandler, ProcessorOptions, ProcessorResult } from "./types.js";
|
|
5
|
+
/**
|
|
6
|
+
* Orchestrates the common sync workflow steps.
|
|
7
|
+
* Used by RepositoryProcessor with different strategies for file sync vs manifest.
|
|
8
|
+
*/
|
|
9
|
+
export declare class SyncWorkflow implements ISyncWorkflow {
|
|
10
|
+
private readonly authOptionsBuilder;
|
|
11
|
+
private readonly repositorySession;
|
|
12
|
+
private readonly branchManager;
|
|
13
|
+
private readonly commitPushManager;
|
|
14
|
+
private readonly prMergeHandler;
|
|
15
|
+
private readonly log;
|
|
16
|
+
constructor(authOptionsBuilder: IAuthOptionsBuilder, repositorySession: IRepositorySession, branchManager: IBranchManager, commitPushManager: ICommitPushManager, prMergeHandler: IPRMergeHandler, log: ILogger);
|
|
17
|
+
execute(repoConfig: RepoConfig, repoInfo: RepoInfo, options: ProcessorOptions, workStrategy: IWorkStrategy): Promise<ProcessorResult>;
|
|
18
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { getRepoDisplayName } from "../shared/repo-detector.js";
|
|
2
|
+
import { defaultExecutor } from "../shared/command-executor.js";
|
|
3
|
+
/**
|
|
4
|
+
* Orchestrates the common sync workflow steps.
|
|
5
|
+
* Used by RepositoryProcessor with different strategies for file sync vs manifest.
|
|
6
|
+
*/
|
|
7
|
+
export class SyncWorkflow {
|
|
8
|
+
authOptionsBuilder;
|
|
9
|
+
repositorySession;
|
|
10
|
+
branchManager;
|
|
11
|
+
commitPushManager;
|
|
12
|
+
prMergeHandler;
|
|
13
|
+
log;
|
|
14
|
+
constructor(authOptionsBuilder, repositorySession, branchManager, commitPushManager, prMergeHandler, log) {
|
|
15
|
+
this.authOptionsBuilder = authOptionsBuilder;
|
|
16
|
+
this.repositorySession = repositorySession;
|
|
17
|
+
this.branchManager = branchManager;
|
|
18
|
+
this.commitPushManager = commitPushManager;
|
|
19
|
+
this.prMergeHandler = prMergeHandler;
|
|
20
|
+
this.log = log;
|
|
21
|
+
}
|
|
22
|
+
async execute(repoConfig, repoInfo, options, workStrategy) {
|
|
23
|
+
const repoName = getRepoDisplayName(repoInfo);
|
|
24
|
+
const { branchName, workDir, dryRun } = options;
|
|
25
|
+
const retries = options.retries ?? 3;
|
|
26
|
+
const executor = options.executor ?? defaultExecutor;
|
|
27
|
+
// Step 1: Resolve auth
|
|
28
|
+
const authResult = await this.authOptionsBuilder.resolve(repoInfo, repoName);
|
|
29
|
+
if (authResult.skipResult) {
|
|
30
|
+
return authResult.skipResult;
|
|
31
|
+
}
|
|
32
|
+
// Step 2: Determine merge mode
|
|
33
|
+
const mergeMode = repoConfig.prOptions?.merge ?? "auto";
|
|
34
|
+
const isDirectMode = mergeMode === "direct";
|
|
35
|
+
// Warn if mergeStrategy is set but ignored in direct mode
|
|
36
|
+
if (isDirectMode && repoConfig.prOptions?.mergeStrategy) {
|
|
37
|
+
this.log.info(`Warning: mergeStrategy '${repoConfig.prOptions.mergeStrategy}' is ignored in direct mode`);
|
|
38
|
+
}
|
|
39
|
+
let session = null;
|
|
40
|
+
try {
|
|
41
|
+
// Step 3: Setup session
|
|
42
|
+
session = await this.repositorySession.setup(repoInfo, {
|
|
43
|
+
workDir,
|
|
44
|
+
dryRun: dryRun ?? false,
|
|
45
|
+
retries,
|
|
46
|
+
authOptions: authResult.authOptions,
|
|
47
|
+
});
|
|
48
|
+
// Step 4: Setup branch
|
|
49
|
+
await this.branchManager.setupBranch({
|
|
50
|
+
repoInfo,
|
|
51
|
+
branchName,
|
|
52
|
+
baseBranch: session.baseBranch,
|
|
53
|
+
workDir,
|
|
54
|
+
isDirectMode,
|
|
55
|
+
dryRun: dryRun ?? false,
|
|
56
|
+
retries,
|
|
57
|
+
token: authResult.token,
|
|
58
|
+
gitOps: session.gitOps,
|
|
59
|
+
log: this.log,
|
|
60
|
+
executor,
|
|
61
|
+
});
|
|
62
|
+
// Step 5: Execute work strategy
|
|
63
|
+
const workResult = await workStrategy.execute(repoConfig, repoInfo, session, options);
|
|
64
|
+
// Step 6: No changes - skip
|
|
65
|
+
if (!workResult) {
|
|
66
|
+
return {
|
|
67
|
+
success: true,
|
|
68
|
+
repoName,
|
|
69
|
+
message: "No changes detected",
|
|
70
|
+
skipped: true,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
// Step 7: Commit and push
|
|
74
|
+
const pushBranch = isDirectMode ? session.baseBranch : branchName;
|
|
75
|
+
const commitResult = await this.commitPushManager.commitAndPush({
|
|
76
|
+
repoInfo,
|
|
77
|
+
gitOps: session.gitOps,
|
|
78
|
+
workDir,
|
|
79
|
+
fileChanges: workResult.fileChanges,
|
|
80
|
+
commitMessage: workResult.commitMessage,
|
|
81
|
+
pushBranch,
|
|
82
|
+
isDirectMode,
|
|
83
|
+
dryRun: dryRun ?? false,
|
|
84
|
+
retries,
|
|
85
|
+
token: authResult.token,
|
|
86
|
+
executor,
|
|
87
|
+
}, repoName);
|
|
88
|
+
// Step 8: Handle commit errors
|
|
89
|
+
if (!commitResult.success && commitResult.errorResult) {
|
|
90
|
+
return commitResult.errorResult;
|
|
91
|
+
}
|
|
92
|
+
if (commitResult.skipped) {
|
|
93
|
+
return {
|
|
94
|
+
success: true,
|
|
95
|
+
repoName,
|
|
96
|
+
message: "No changes detected after staging",
|
|
97
|
+
skipped: true,
|
|
98
|
+
diffStats: workResult.diffStats,
|
|
99
|
+
fileChanges: workResult.fileChangeDetails,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
// Step 9: Direct mode - done
|
|
103
|
+
if (isDirectMode) {
|
|
104
|
+
this.log.info(`Changes pushed directly to ${session.baseBranch}`);
|
|
105
|
+
return {
|
|
106
|
+
success: true,
|
|
107
|
+
repoName,
|
|
108
|
+
message: `Pushed directly to ${session.baseBranch}`,
|
|
109
|
+
diffStats: workResult.diffStats,
|
|
110
|
+
fileChanges: workResult.fileChangeDetails,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
// Step 10: Create and merge PR
|
|
114
|
+
return await this.prMergeHandler.createAndMerge(repoInfo, repoConfig, {
|
|
115
|
+
branchName,
|
|
116
|
+
baseBranch: session.baseBranch,
|
|
117
|
+
workDir,
|
|
118
|
+
dryRun: dryRun ?? false,
|
|
119
|
+
retries,
|
|
120
|
+
prTemplate: options.prTemplate,
|
|
121
|
+
token: authResult.token,
|
|
122
|
+
executor,
|
|
123
|
+
}, workResult.changedFiles, repoName, workResult.diffStats, workResult.fileChangeDetails);
|
|
124
|
+
}
|
|
125
|
+
finally {
|
|
126
|
+
try {
|
|
127
|
+
session?.cleanup();
|
|
128
|
+
}
|
|
129
|
+
catch {
|
|
130
|
+
// Ignore cleanup errors - best effort
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
package/dist/sync/types.d.ts
CHANGED
|
@@ -302,3 +302,38 @@ export interface IPRMergeHandler {
|
|
|
302
302
|
*/
|
|
303
303
|
createAndMerge(repoInfo: RepoInfo, repoConfig: RepoConfig, options: PRHandlerOptions, changedFiles: FileAction[], repoName: string, diffStats?: DiffStats, fileChanges?: FileChangeDetail[]): Promise<ProcessorResult>;
|
|
304
304
|
}
|
|
305
|
+
/**
|
|
306
|
+
* Result of executing work within a sync workflow
|
|
307
|
+
*/
|
|
308
|
+
export interface WorkResult {
|
|
309
|
+
/** File changes to commit */
|
|
310
|
+
fileChanges: Map<string, FileWriteResult>;
|
|
311
|
+
/** Changed files for PR body */
|
|
312
|
+
changedFiles: FileAction[];
|
|
313
|
+
/** Diff statistics for reporting */
|
|
314
|
+
diffStats?: DiffStats;
|
|
315
|
+
/** Human-readable commit message */
|
|
316
|
+
commitMessage: string;
|
|
317
|
+
/** File change details for result reporting */
|
|
318
|
+
fileChangeDetails: FileChangeDetail[];
|
|
319
|
+
}
|
|
320
|
+
/**
|
|
321
|
+
* Strategy for executing work within the sync workflow.
|
|
322
|
+
* Implementations define what changes to make (files vs manifest).
|
|
323
|
+
*/
|
|
324
|
+
export interface IWorkStrategy {
|
|
325
|
+
/**
|
|
326
|
+
* Execute work and return changes to commit.
|
|
327
|
+
* Return null if no changes detected (workflow will skip).
|
|
328
|
+
*/
|
|
329
|
+
execute(repoConfig: RepoConfig, repoInfo: RepoInfo, session: SessionContext, options: ProcessorOptions): Promise<WorkResult | null>;
|
|
330
|
+
}
|
|
331
|
+
/**
|
|
332
|
+
* Orchestrates the common sync workflow steps.
|
|
333
|
+
*/
|
|
334
|
+
export interface ISyncWorkflow {
|
|
335
|
+
/**
|
|
336
|
+
* Execute workflow: auth → session → branch → work → commit → PR
|
|
337
|
+
*/
|
|
338
|
+
execute(repoConfig: RepoConfig, repoInfo: RepoInfo, options: ProcessorOptions, workStrategy: IWorkStrategy): Promise<ProcessorResult>;
|
|
339
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aspruyt/xfg",
|
|
3
|
-
"version": "3.8.
|
|
4
|
-
"description": "CLI tool for repository-as-code",
|
|
3
|
+
"version": "3.8.2",
|
|
4
|
+
"description": "CLI tool for repository-as-code: sync files and manage settings across GitHub, Azure DevOps, and GitLab",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
7
7
|
"bin": {
|