@aspruyt/xfg 3.7.7 → 3.8.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/settings-command.js +48 -39
- package/dist/cli/settings-report-builder.d.ts +19 -0
- package/dist/cli/settings-report-builder.js +64 -0
- package/dist/cli/sync-command.js +49 -24
- package/dist/cli/sync-report-builder.d.ts +15 -0
- package/dist/cli/sync-report-builder.js +29 -0
- package/dist/output/index.d.ts +2 -2
- package/dist/output/index.js +4 -4
- package/dist/output/settings-report.d.ts +37 -0
- package/dist/output/settings-report.js +300 -0
- package/dist/output/sync-report.d.ts +24 -0
- package/dist/output/sync-report.js +99 -0
- package/dist/settings/index.d.ts +0 -1
- package/dist/settings/index.js +0 -2
- package/dist/settings/repo-settings/formatter.d.ts +2 -0
- package/dist/settings/repo-settings/formatter.js +11 -2
- package/dist/settings/repo-settings/processor.js +8 -2
- package/dist/settings/rulesets/formatter.d.ts +3 -0
- package/dist/settings/rulesets/formatter.js +7 -1
- package/dist/sync/index.d.ts +1 -1
- package/dist/sync/pr-merge-handler.d.ts +2 -2
- package/dist/sync/pr-merge-handler.js +2 -1
- package/dist/sync/repository-processor.js +22 -2
- package/dist/sync/types.d.ts +9 -1
- package/package.json +1 -1
- package/dist/output/plan-formatter.d.ts +0 -39
- package/dist/output/plan-formatter.js +0 -84
- package/dist/output/plan-summary.d.ts +0 -8
- package/dist/output/plan-summary.js +0 -110
- package/dist/settings/resource-converters.d.ts +0 -28
- package/dist/settings/resource-converters.js +0 -107
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
import { appendFileSync } from "node:fs";
|
|
2
|
+
import chalk from "chalk";
|
|
3
|
+
import { formatPropertyTree, } from "../settings/rulesets/formatter.js";
|
|
4
|
+
// =============================================================================
|
|
5
|
+
// Helpers
|
|
6
|
+
// =============================================================================
|
|
7
|
+
function formatValue(val) {
|
|
8
|
+
if (val === null)
|
|
9
|
+
return "null";
|
|
10
|
+
if (val === undefined)
|
|
11
|
+
return "undefined";
|
|
12
|
+
if (typeof val === "string")
|
|
13
|
+
return `"${val}"`;
|
|
14
|
+
if (typeof val === "boolean")
|
|
15
|
+
return val ? "true" : "false";
|
|
16
|
+
return String(val);
|
|
17
|
+
}
|
|
18
|
+
function formatRulesetConfig(config, indent) {
|
|
19
|
+
const lines = [];
|
|
20
|
+
function renderObject(obj, currentIndent) {
|
|
21
|
+
for (const [k, v] of Object.entries(obj)) {
|
|
22
|
+
renderValue(k, v, currentIndent);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
function renderValue(key, value, currentIndent) {
|
|
26
|
+
const pad = " ".repeat(currentIndent);
|
|
27
|
+
if (value === null || value === undefined)
|
|
28
|
+
return;
|
|
29
|
+
if (Array.isArray(value)) {
|
|
30
|
+
if (value.length === 0) {
|
|
31
|
+
lines.push(chalk.green(`${pad}+ ${key}: []`));
|
|
32
|
+
}
|
|
33
|
+
else if (value.every((v) => typeof v !== "object")) {
|
|
34
|
+
lines.push(chalk.green(`${pad}+ ${key}: [${value.map((v) => (typeof v === "string" ? `"${v}"` : String(v))).join(", ")}]`));
|
|
35
|
+
}
|
|
36
|
+
else {
|
|
37
|
+
lines.push(chalk.green(`${pad}+ ${key}:`));
|
|
38
|
+
for (let i = 0; i < value.length; i++) {
|
|
39
|
+
const item = value[i];
|
|
40
|
+
if (typeof item === "object" && item !== null) {
|
|
41
|
+
const obj = item;
|
|
42
|
+
const typeLabel = "type" in obj ? ` (${obj.type})` : "";
|
|
43
|
+
lines.push(chalk.green(`${pad} + [${i}]${typeLabel}:`));
|
|
44
|
+
renderObject(obj, currentIndent + 2);
|
|
45
|
+
}
|
|
46
|
+
else {
|
|
47
|
+
lines.push(chalk.green(`${pad} + ${formatValue(item)}`));
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
else if (typeof value === "object") {
|
|
53
|
+
lines.push(chalk.green(`${pad}+ ${key}:`));
|
|
54
|
+
renderObject(value, currentIndent + 1);
|
|
55
|
+
}
|
|
56
|
+
else {
|
|
57
|
+
lines.push(chalk.green(`${pad}+ ${key}: ${formatValue(value)}`));
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
for (const [key, value] of Object.entries(config)) {
|
|
61
|
+
if (key === "name")
|
|
62
|
+
continue; // Name is in the header
|
|
63
|
+
renderValue(key, value, indent);
|
|
64
|
+
}
|
|
65
|
+
return lines;
|
|
66
|
+
}
|
|
67
|
+
function formatSummary(totals) {
|
|
68
|
+
const parts = [];
|
|
69
|
+
const settingsTotal = totals.settings.add + totals.settings.change;
|
|
70
|
+
const rulesetsTotal = totals.rulesets.create + totals.rulesets.update + totals.rulesets.delete;
|
|
71
|
+
if (settingsTotal > 0) {
|
|
72
|
+
const settingWord = settingsTotal === 1 ? "setting" : "settings";
|
|
73
|
+
const actions = [];
|
|
74
|
+
if (totals.settings.add > 0)
|
|
75
|
+
actions.push(`${totals.settings.add} to add`);
|
|
76
|
+
if (totals.settings.change > 0)
|
|
77
|
+
actions.push(`${totals.settings.change} to change`);
|
|
78
|
+
parts.push(`${settingsTotal} ${settingWord} (${actions.join(", ")})`);
|
|
79
|
+
}
|
|
80
|
+
if (rulesetsTotal > 0) {
|
|
81
|
+
const rulesetWord = rulesetsTotal === 1 ? "ruleset" : "rulesets";
|
|
82
|
+
const actions = [];
|
|
83
|
+
if (totals.rulesets.create > 0)
|
|
84
|
+
actions.push(`${totals.rulesets.create} to create`);
|
|
85
|
+
if (totals.rulesets.update > 0)
|
|
86
|
+
actions.push(`${totals.rulesets.update} to update`);
|
|
87
|
+
if (totals.rulesets.delete > 0)
|
|
88
|
+
actions.push(`${totals.rulesets.delete} to delete`);
|
|
89
|
+
parts.push(`${rulesetsTotal} ${rulesetWord} (${actions.join(", ")})`);
|
|
90
|
+
}
|
|
91
|
+
if (parts.length === 0) {
|
|
92
|
+
return "No changes";
|
|
93
|
+
}
|
|
94
|
+
return `Plan: ${parts.join(", ")}`;
|
|
95
|
+
}
|
|
96
|
+
// =============================================================================
|
|
97
|
+
// CLI Formatter
|
|
98
|
+
// =============================================================================
|
|
99
|
+
export function formatSettingsReportCLI(report) {
|
|
100
|
+
const lines = [];
|
|
101
|
+
for (const repo of report.repos) {
|
|
102
|
+
if (repo.settings.length === 0 &&
|
|
103
|
+
repo.rulesets.length === 0 &&
|
|
104
|
+
!repo.error) {
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
// Repo header
|
|
108
|
+
lines.push(chalk.yellow(`~ ${repo.repoName}`));
|
|
109
|
+
// Settings
|
|
110
|
+
for (const setting of repo.settings) {
|
|
111
|
+
// Skip settings where both values are undefined
|
|
112
|
+
if (setting.oldValue === undefined && setting.newValue === undefined) {
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
if (setting.action === "add") {
|
|
116
|
+
lines.push(chalk.green(` + ${setting.name}: ${formatValue(setting.newValue)}`));
|
|
117
|
+
}
|
|
118
|
+
else {
|
|
119
|
+
lines.push(chalk.yellow(` ~ ${setting.name}: ${formatValue(setting.oldValue)} → ${formatValue(setting.newValue)}`));
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
// Rulesets
|
|
123
|
+
for (const ruleset of repo.rulesets) {
|
|
124
|
+
if (ruleset.action === "create") {
|
|
125
|
+
lines.push(chalk.green(` + ruleset "${ruleset.name}"`));
|
|
126
|
+
if (ruleset.config) {
|
|
127
|
+
lines.push(...formatRulesetConfig(ruleset.config, 2));
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
else if (ruleset.action === "update") {
|
|
131
|
+
lines.push(chalk.yellow(` ~ ruleset "${ruleset.name}"`));
|
|
132
|
+
if (ruleset.propertyDiffs && ruleset.propertyDiffs.length > 0) {
|
|
133
|
+
const treeLines = formatPropertyTree(ruleset.propertyDiffs);
|
|
134
|
+
for (const line of treeLines) {
|
|
135
|
+
lines.push(` ${line}`);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
else if (ruleset.action === "delete") {
|
|
140
|
+
lines.push(chalk.red(` - ruleset "${ruleset.name}"`));
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
// Error
|
|
144
|
+
if (repo.error) {
|
|
145
|
+
lines.push(chalk.red(` Error: ${repo.error}`));
|
|
146
|
+
}
|
|
147
|
+
lines.push(""); // Blank line between repos
|
|
148
|
+
}
|
|
149
|
+
// Summary
|
|
150
|
+
lines.push(formatSummary(report.totals));
|
|
151
|
+
return lines;
|
|
152
|
+
}
|
|
153
|
+
// =============================================================================
|
|
154
|
+
// Markdown Formatter
|
|
155
|
+
// =============================================================================
|
|
156
|
+
function formatValuePlain(val) {
|
|
157
|
+
if (val === null)
|
|
158
|
+
return "null";
|
|
159
|
+
if (val === undefined)
|
|
160
|
+
return "undefined";
|
|
161
|
+
if (typeof val === "string")
|
|
162
|
+
return `"${val}"`;
|
|
163
|
+
if (typeof val === "boolean")
|
|
164
|
+
return val ? "true" : "false";
|
|
165
|
+
return String(val);
|
|
166
|
+
}
|
|
167
|
+
function formatRulesetConfigPlain(config, indent) {
|
|
168
|
+
const lines = [];
|
|
169
|
+
function renderObject(obj, currentIndent) {
|
|
170
|
+
for (const [k, v] of Object.entries(obj)) {
|
|
171
|
+
renderValue(k, v, currentIndent);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
function renderValue(key, value, currentIndent) {
|
|
175
|
+
const pad = " ".repeat(currentIndent);
|
|
176
|
+
if (value === null || value === undefined)
|
|
177
|
+
return;
|
|
178
|
+
if (Array.isArray(value)) {
|
|
179
|
+
if (value.length === 0) {
|
|
180
|
+
lines.push(`${pad}+ ${key}: []`);
|
|
181
|
+
}
|
|
182
|
+
else if (value.every((v) => typeof v !== "object")) {
|
|
183
|
+
lines.push(`${pad}+ ${key}: [${value.map((v) => (typeof v === "string" ? `"${v}"` : String(v))).join(", ")}]`);
|
|
184
|
+
}
|
|
185
|
+
else {
|
|
186
|
+
lines.push(`${pad}+ ${key}:`);
|
|
187
|
+
for (let i = 0; i < value.length; i++) {
|
|
188
|
+
const item = value[i];
|
|
189
|
+
if (typeof item === "object" && item !== null) {
|
|
190
|
+
const obj = item;
|
|
191
|
+
const typeLabel = "type" in obj ? ` (${obj.type})` : "";
|
|
192
|
+
lines.push(`${pad} + [${i}]${typeLabel}:`);
|
|
193
|
+
renderObject(obj, currentIndent + 2);
|
|
194
|
+
}
|
|
195
|
+
else {
|
|
196
|
+
lines.push(`${pad} + ${formatValuePlain(item)}`);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
else if (typeof value === "object") {
|
|
202
|
+
lines.push(`${pad}+ ${key}:`);
|
|
203
|
+
renderObject(value, currentIndent + 1);
|
|
204
|
+
}
|
|
205
|
+
else {
|
|
206
|
+
lines.push(`${pad}+ ${key}: ${formatValuePlain(value)}`);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
for (const [key, value] of Object.entries(config)) {
|
|
210
|
+
if (key === "name")
|
|
211
|
+
continue;
|
|
212
|
+
renderValue(key, value, indent);
|
|
213
|
+
}
|
|
214
|
+
return lines;
|
|
215
|
+
}
|
|
216
|
+
export function formatSettingsReportMarkdown(report, dryRun) {
|
|
217
|
+
const lines = [];
|
|
218
|
+
// Title
|
|
219
|
+
const titleSuffix = dryRun ? " (Dry Run)" : "";
|
|
220
|
+
lines.push(`## Repository Settings Summary${titleSuffix}`);
|
|
221
|
+
lines.push("");
|
|
222
|
+
// Dry-run warning
|
|
223
|
+
if (dryRun) {
|
|
224
|
+
lines.push("> [!WARNING]");
|
|
225
|
+
lines.push("> This was a dry run — no changes were applied");
|
|
226
|
+
lines.push("");
|
|
227
|
+
}
|
|
228
|
+
// Diff block
|
|
229
|
+
const diffLines = [];
|
|
230
|
+
for (const repo of report.repos) {
|
|
231
|
+
if (repo.settings.length === 0 &&
|
|
232
|
+
repo.rulesets.length === 0 &&
|
|
233
|
+
!repo.error) {
|
|
234
|
+
continue;
|
|
235
|
+
}
|
|
236
|
+
diffLines.push(`~ ${repo.repoName}`);
|
|
237
|
+
for (const setting of repo.settings) {
|
|
238
|
+
// Skip settings where both values are undefined
|
|
239
|
+
if (setting.oldValue === undefined && setting.newValue === undefined) {
|
|
240
|
+
continue;
|
|
241
|
+
}
|
|
242
|
+
if (setting.action === "add") {
|
|
243
|
+
diffLines.push(` + ${setting.name}: ${formatValuePlain(setting.newValue)}`);
|
|
244
|
+
}
|
|
245
|
+
else {
|
|
246
|
+
diffLines.push(` ~ ${setting.name}: ${formatValuePlain(setting.oldValue)} → ${formatValuePlain(setting.newValue)}`);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
for (const ruleset of repo.rulesets) {
|
|
250
|
+
if (ruleset.action === "create") {
|
|
251
|
+
diffLines.push(` + ruleset "${ruleset.name}"`);
|
|
252
|
+
if (ruleset.config) {
|
|
253
|
+
diffLines.push(...formatRulesetConfigPlain(ruleset.config, 2));
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
else if (ruleset.action === "update") {
|
|
257
|
+
diffLines.push(` ~ ruleset "${ruleset.name}"`);
|
|
258
|
+
if (ruleset.propertyDiffs && ruleset.propertyDiffs.length > 0) {
|
|
259
|
+
for (const diff of ruleset.propertyDiffs) {
|
|
260
|
+
const path = diff.path.join(".");
|
|
261
|
+
if (diff.action === "add") {
|
|
262
|
+
diffLines.push(` + ${path}: ${formatValuePlain(diff.newValue)}`);
|
|
263
|
+
}
|
|
264
|
+
else if (diff.action === "change") {
|
|
265
|
+
diffLines.push(` ~ ${path}: ${formatValuePlain(diff.oldValue)} → ${formatValuePlain(diff.newValue)}`);
|
|
266
|
+
}
|
|
267
|
+
else if (diff.action === "remove") {
|
|
268
|
+
diffLines.push(` - ${path}`);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
else if (ruleset.action === "delete") {
|
|
274
|
+
diffLines.push(` - ruleset "${ruleset.name}"`);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
if (repo.error) {
|
|
278
|
+
diffLines.push(` ! Error: ${repo.error}`);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
if (diffLines.length > 0) {
|
|
282
|
+
lines.push("```diff");
|
|
283
|
+
lines.push(...diffLines);
|
|
284
|
+
lines.push("```");
|
|
285
|
+
lines.push("");
|
|
286
|
+
}
|
|
287
|
+
// Summary
|
|
288
|
+
lines.push(`**${formatSummary(report.totals)}**`);
|
|
289
|
+
return lines.join("\n");
|
|
290
|
+
}
|
|
291
|
+
// =============================================================================
|
|
292
|
+
// File Writer
|
|
293
|
+
// =============================================================================
|
|
294
|
+
export function writeSettingsReportSummary(report, dryRun) {
|
|
295
|
+
const summaryPath = process.env.GITHUB_STEP_SUMMARY;
|
|
296
|
+
if (!summaryPath)
|
|
297
|
+
return;
|
|
298
|
+
const markdown = formatSettingsReportMarkdown(report, dryRun);
|
|
299
|
+
appendFileSync(summaryPath, "\n" + markdown + "\n");
|
|
300
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export interface SyncReport {
|
|
2
|
+
repos: RepoFileChanges[];
|
|
3
|
+
totals: {
|
|
4
|
+
files: {
|
|
5
|
+
create: number;
|
|
6
|
+
update: number;
|
|
7
|
+
delete: number;
|
|
8
|
+
};
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
export interface RepoFileChanges {
|
|
12
|
+
repoName: string;
|
|
13
|
+
files: FileChange[];
|
|
14
|
+
prUrl?: string;
|
|
15
|
+
mergeOutcome?: "manual" | "auto" | "force" | "direct";
|
|
16
|
+
error?: string;
|
|
17
|
+
}
|
|
18
|
+
export interface FileChange {
|
|
19
|
+
path: string;
|
|
20
|
+
action: "create" | "update" | "delete";
|
|
21
|
+
}
|
|
22
|
+
export declare function formatSyncReportCLI(report: SyncReport): string[];
|
|
23
|
+
export declare function formatSyncReportMarkdown(report: SyncReport, dryRun: boolean): string;
|
|
24
|
+
export declare function writeSyncReportSummary(report: SyncReport, dryRun: boolean): void;
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
// src/output/sync-report.ts
|
|
2
|
+
import { appendFileSync } from "node:fs";
|
|
3
|
+
import chalk from "chalk";
|
|
4
|
+
function formatSummary(totals) {
|
|
5
|
+
const total = totals.files.create + totals.files.update + totals.files.delete;
|
|
6
|
+
if (total === 0) {
|
|
7
|
+
return "No changes";
|
|
8
|
+
}
|
|
9
|
+
const parts = [];
|
|
10
|
+
if (totals.files.create > 0)
|
|
11
|
+
parts.push(`${totals.files.create} to create`);
|
|
12
|
+
if (totals.files.update > 0)
|
|
13
|
+
parts.push(`${totals.files.update} to update`);
|
|
14
|
+
if (totals.files.delete > 0)
|
|
15
|
+
parts.push(`${totals.files.delete} to delete`);
|
|
16
|
+
const fileWord = total === 1 ? "file" : "files";
|
|
17
|
+
return `Plan: ${total} ${fileWord} (${parts.join(", ")})`;
|
|
18
|
+
}
|
|
19
|
+
export function formatSyncReportCLI(report) {
|
|
20
|
+
const lines = [];
|
|
21
|
+
for (const repo of report.repos) {
|
|
22
|
+
if (repo.files.length === 0 && !repo.error) {
|
|
23
|
+
continue;
|
|
24
|
+
}
|
|
25
|
+
// Repo header
|
|
26
|
+
lines.push(chalk.yellow(`~ ${repo.repoName}`));
|
|
27
|
+
// Files
|
|
28
|
+
for (const file of repo.files) {
|
|
29
|
+
if (file.action === "create") {
|
|
30
|
+
lines.push(chalk.green(` + ${file.path}`));
|
|
31
|
+
}
|
|
32
|
+
else if (file.action === "update") {
|
|
33
|
+
lines.push(chalk.yellow(` ~ ${file.path}`));
|
|
34
|
+
}
|
|
35
|
+
else if (file.action === "delete") {
|
|
36
|
+
lines.push(chalk.red(` - ${file.path}`));
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
// Error
|
|
40
|
+
if (repo.error) {
|
|
41
|
+
lines.push(chalk.red(` Error: ${repo.error}`));
|
|
42
|
+
}
|
|
43
|
+
lines.push(""); // Blank line between repos
|
|
44
|
+
}
|
|
45
|
+
// Summary
|
|
46
|
+
lines.push(formatSummary(report.totals));
|
|
47
|
+
return lines;
|
|
48
|
+
}
|
|
49
|
+
export function formatSyncReportMarkdown(report, dryRun) {
|
|
50
|
+
const lines = [];
|
|
51
|
+
// Title
|
|
52
|
+
const titleSuffix = dryRun ? " (Dry Run)" : "";
|
|
53
|
+
lines.push(`## Config Sync Summary${titleSuffix}`);
|
|
54
|
+
lines.push("");
|
|
55
|
+
// Dry-run warning
|
|
56
|
+
if (dryRun) {
|
|
57
|
+
lines.push("> [!WARNING]");
|
|
58
|
+
lines.push("> This was a dry run — no changes were applied");
|
|
59
|
+
lines.push("");
|
|
60
|
+
}
|
|
61
|
+
// Diff block
|
|
62
|
+
const diffLines = [];
|
|
63
|
+
for (const repo of report.repos) {
|
|
64
|
+
if (repo.files.length === 0 && !repo.error) {
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
diffLines.push(`~ ${repo.repoName}`);
|
|
68
|
+
for (const file of repo.files) {
|
|
69
|
+
if (file.action === "create") {
|
|
70
|
+
diffLines.push(` + ${file.path}`);
|
|
71
|
+
}
|
|
72
|
+
else if (file.action === "update") {
|
|
73
|
+
diffLines.push(` ~ ${file.path}`);
|
|
74
|
+
}
|
|
75
|
+
else if (file.action === "delete") {
|
|
76
|
+
diffLines.push(` - ${file.path}`);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
if (repo.error) {
|
|
80
|
+
diffLines.push(` ! Error: ${repo.error}`);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
if (diffLines.length > 0) {
|
|
84
|
+
lines.push("```diff");
|
|
85
|
+
lines.push(...diffLines);
|
|
86
|
+
lines.push("```");
|
|
87
|
+
lines.push("");
|
|
88
|
+
}
|
|
89
|
+
// Summary
|
|
90
|
+
lines.push(`**${formatSummary(report.totals)}**`);
|
|
91
|
+
return lines.join("\n");
|
|
92
|
+
}
|
|
93
|
+
export function writeSyncReportSummary(report, dryRun) {
|
|
94
|
+
const summaryPath = process.env.GITHUB_STEP_SUMMARY;
|
|
95
|
+
if (!summaryPath)
|
|
96
|
+
return;
|
|
97
|
+
const markdown = formatSyncReportMarkdown(report, dryRun);
|
|
98
|
+
appendFileSync(summaryPath, "\n" + markdown + "\n");
|
|
99
|
+
}
|
package/dist/settings/index.d.ts
CHANGED
package/dist/settings/index.js
CHANGED
|
@@ -54,12 +54,21 @@ export function formatRepoSettingsPlan(changes) {
|
|
|
54
54
|
if (change.action === "add") {
|
|
55
55
|
lines.push(chalk.green(` + ${change.property}: ${formatValue(change.newValue)}`));
|
|
56
56
|
adds++;
|
|
57
|
-
entries.push({
|
|
57
|
+
entries.push({
|
|
58
|
+
property: change.property,
|
|
59
|
+
action: "add",
|
|
60
|
+
newValue: change.newValue,
|
|
61
|
+
});
|
|
58
62
|
}
|
|
59
63
|
else if (change.action === "change") {
|
|
60
64
|
lines.push(chalk.yellow(` ~ ${change.property}: ${formatValue(change.oldValue)} → ${formatValue(change.newValue)}`));
|
|
61
65
|
changesCount++;
|
|
62
|
-
entries.push({
|
|
66
|
+
entries.push({
|
|
67
|
+
property: change.property,
|
|
68
|
+
action: "change",
|
|
69
|
+
oldValue: change.oldValue,
|
|
70
|
+
newValue: change.newValue,
|
|
71
|
+
});
|
|
63
72
|
}
|
|
64
73
|
}
|
|
65
74
|
return { lines, adds, changes: changesCount, warnings, entries };
|
|
@@ -69,8 +69,14 @@ export class RepoSettingsProcessor {
|
|
|
69
69
|
planOutput,
|
|
70
70
|
};
|
|
71
71
|
}
|
|
72
|
-
// Apply changes
|
|
73
|
-
|
|
72
|
+
// Apply changes - only send settings that actually changed
|
|
73
|
+
const changedSettings = changes.reduce((acc, change) => {
|
|
74
|
+
if (change.action !== "unchanged") {
|
|
75
|
+
acc[change.property] = change.newValue;
|
|
76
|
+
}
|
|
77
|
+
return acc;
|
|
78
|
+
}, {});
|
|
79
|
+
await this.applyChanges(githubRepo, changedSettings, strategyOptions);
|
|
74
80
|
return {
|
|
75
81
|
success: true,
|
|
76
82
|
repoName,
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type RulesetChange, type RulesetAction } from "./diff.js";
|
|
2
|
+
import type { Ruleset } from "../../config/index.js";
|
|
2
3
|
import { type PropertyDiff } from "./index.js";
|
|
3
4
|
export type { DiffAction, PropertyDiff } from "./index.js";
|
|
4
5
|
export { computePropertyDiffs } from "./index.js";
|
|
@@ -11,6 +12,8 @@ export interface RulesetPlanEntry {
|
|
|
11
12
|
changed: number;
|
|
12
13
|
removed: number;
|
|
13
14
|
};
|
|
15
|
+
propertyDiffs?: PropertyDiff[];
|
|
16
|
+
config?: Ruleset;
|
|
14
17
|
}
|
|
15
18
|
export interface RulesetPlanResult {
|
|
16
19
|
lines: string[];
|
|
@@ -264,7 +264,12 @@ export function formatRulesetPlan(changes) {
|
|
|
264
264
|
const propertyCount = change.desired
|
|
265
265
|
? Object.keys(change.desired).length
|
|
266
266
|
: 0;
|
|
267
|
-
entries.push({
|
|
267
|
+
entries.push({
|
|
268
|
+
name: change.name,
|
|
269
|
+
action: "create",
|
|
270
|
+
propertyCount,
|
|
271
|
+
config: change.desired,
|
|
272
|
+
});
|
|
268
273
|
lines.push(""); // Blank line between rulesets
|
|
269
274
|
}
|
|
270
275
|
}
|
|
@@ -289,6 +294,7 @@ export function formatRulesetPlan(changes) {
|
|
|
289
294
|
name: change.name,
|
|
290
295
|
action: "update",
|
|
291
296
|
propertyChanges: { added, changed, removed },
|
|
297
|
+
propertyDiffs: diffs,
|
|
292
298
|
});
|
|
293
299
|
}
|
|
294
300
|
else {
|
package/dist/sync/index.d.ts
CHANGED
|
@@ -7,7 +7,7 @@ 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 type { IFileWriter, FileWriteContext, FileWriterDeps, FileWriteAllResult, FileWriteResult, IManifestManager, OrphanProcessResult, OrphanDeleteOptions, OrphanDeleteDeps, IBranchManager, BranchSetupOptions, IAuthOptionsBuilder, AuthResult, IRepositorySession, SessionOptions, SessionContext, ICommitPushManager, CommitPushOptions, CommitPushResult, GitOpsFactory, IRepositoryProcessor, ProcessorOptions, ProcessorResult, IFileSyncOrchestrator, FileSyncResult, IPRMergeHandler, PRHandlerOptions, } from "./types.js";
|
|
10
|
+
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, } from "./types.js";
|
|
11
11
|
export { RepositoryProcessor } from "./repository-processor.js";
|
|
12
12
|
export { createEmptyManifest, loadManifest, saveManifest, getManagedFiles, getManagedRulesets, updateManifest, updateManifestRulesets, MANIFEST_FILENAME, type XfgManifest, type XfgManifestConfigEntry, } from "./manifest.js";
|
|
13
13
|
export { getFileStatus, formatStatusBadge, formatDiffLine, generateDiff, createDiffStats, incrementDiffStats, type FileStatus, type DiffStats, } from "./diff-utils.js";
|
|
@@ -3,9 +3,9 @@ import type { RepoInfo } from "../shared/repo-detector.js";
|
|
|
3
3
|
import type { ILogger } from "../shared/logger.js";
|
|
4
4
|
import { type FileAction } from "../vcs/pr-creator.js";
|
|
5
5
|
import type { DiffStats } from "./diff-utils.js";
|
|
6
|
-
import type { ProcessorResult, PRHandlerOptions, IPRMergeHandler } from "./types.js";
|
|
6
|
+
import type { ProcessorResult, PRHandlerOptions, IPRMergeHandler, FileChangeDetail } from "./types.js";
|
|
7
7
|
export declare class PRMergeHandler implements IPRMergeHandler {
|
|
8
8
|
private readonly log;
|
|
9
9
|
constructor(log: ILogger);
|
|
10
|
-
createAndMerge(repoInfo: RepoInfo, repoConfig: RepoConfig, options: PRHandlerOptions, changedFiles: FileAction[], repoName: string, diffStats?: DiffStats): Promise<ProcessorResult>;
|
|
10
|
+
createAndMerge(repoInfo: RepoInfo, repoConfig: RepoConfig, options: PRHandlerOptions, changedFiles: FileAction[], repoName: string, diffStats?: DiffStats, fileChanges?: FileChangeDetail[]): Promise<ProcessorResult>;
|
|
11
11
|
}
|
|
@@ -4,7 +4,7 @@ export class PRMergeHandler {
|
|
|
4
4
|
constructor(log) {
|
|
5
5
|
this.log = log;
|
|
6
6
|
}
|
|
7
|
-
async createAndMerge(repoInfo, repoConfig, options, changedFiles, repoName, diffStats) {
|
|
7
|
+
async createAndMerge(repoInfo, repoConfig, options, changedFiles, repoName, diffStats, fileChanges) {
|
|
8
8
|
this.log.info("Creating pull request...");
|
|
9
9
|
const prResult = await createPR({
|
|
10
10
|
repoInfo,
|
|
@@ -57,6 +57,7 @@ export class PRMergeHandler {
|
|
|
57
57
|
prUrl: prResult.url,
|
|
58
58
|
mergeResult,
|
|
59
59
|
diffStats,
|
|
60
|
+
fileChanges,
|
|
60
61
|
};
|
|
61
62
|
}
|
|
62
63
|
}
|
|
@@ -8,6 +8,14 @@ import { loadManifest, saveManifest, updateManifestRulesets, MANIFEST_FILENAME,
|
|
|
8
8
|
import { GitHubAppTokenManager } from "../vcs/github-app-token-manager.js";
|
|
9
9
|
import { formatCommitMessage } from "./commit-message.js";
|
|
10
10
|
import { FileWriter, ManifestManager, BranchManager, AuthOptionsBuilder, RepositorySession, CommitPushManager, FileSyncOrchestrator, PRMergeHandler, } from "./index.js";
|
|
11
|
+
function mapToFileChangeDetails(changedFiles) {
|
|
12
|
+
return changedFiles
|
|
13
|
+
.filter((f) => f.action !== "skip")
|
|
14
|
+
.map((f) => ({
|
|
15
|
+
path: f.fileName,
|
|
16
|
+
action: f.action,
|
|
17
|
+
}));
|
|
18
|
+
}
|
|
11
19
|
export class RepositoryProcessor {
|
|
12
20
|
gitOpsFactory;
|
|
13
21
|
log;
|
|
@@ -87,6 +95,8 @@ export class RepositoryProcessor {
|
|
|
87
95
|
});
|
|
88
96
|
// Process files and manifest
|
|
89
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);
|
|
90
100
|
if (!hasChanges) {
|
|
91
101
|
return {
|
|
92
102
|
success: true,
|
|
@@ -94,6 +104,7 @@ export class RepositoryProcessor {
|
|
|
94
104
|
message: "No changes detected",
|
|
95
105
|
skipped: true,
|
|
96
106
|
diffStats,
|
|
107
|
+
fileChanges: fileChangeDetails,
|
|
97
108
|
};
|
|
98
109
|
}
|
|
99
110
|
// Commit and push
|
|
@@ -122,6 +133,7 @@ export class RepositoryProcessor {
|
|
|
122
133
|
message: "No changes detected after staging",
|
|
123
134
|
skipped: true,
|
|
124
135
|
diffStats,
|
|
136
|
+
fileChanges: fileChangeDetails,
|
|
125
137
|
};
|
|
126
138
|
}
|
|
127
139
|
// Direct mode: no PR
|
|
@@ -132,6 +144,7 @@ export class RepositoryProcessor {
|
|
|
132
144
|
repoName,
|
|
133
145
|
message: `Pushed directly to ${session.baseBranch}`,
|
|
134
146
|
diffStats,
|
|
147
|
+
fileChanges: fileChangeDetails,
|
|
135
148
|
};
|
|
136
149
|
}
|
|
137
150
|
// Create and merge PR
|
|
@@ -144,7 +157,7 @@ export class RepositoryProcessor {
|
|
|
144
157
|
prTemplate,
|
|
145
158
|
token: authResult.token,
|
|
146
159
|
executor,
|
|
147
|
-
}, changedFiles, repoName, diffStats);
|
|
160
|
+
}, changedFiles, repoName, diffStats, fileChangeDetails);
|
|
148
161
|
}
|
|
149
162
|
finally {
|
|
150
163
|
try {
|
|
@@ -190,12 +203,17 @@ export class RepositoryProcessor {
|
|
|
190
203
|
skipped: true,
|
|
191
204
|
};
|
|
192
205
|
}
|
|
206
|
+
// Prepare manifest file change details for reporting
|
|
207
|
+
const manifestFileChange = [
|
|
208
|
+
{ path: MANIFEST_FILENAME, action: "update" },
|
|
209
|
+
];
|
|
193
210
|
if (dryRun) {
|
|
194
211
|
this.log.info(`Would update ${MANIFEST_FILENAME} with rulesets`);
|
|
195
212
|
return {
|
|
196
213
|
success: true,
|
|
197
214
|
repoName,
|
|
198
215
|
message: "Would update manifest (dry-run)",
|
|
216
|
+
fileChanges: manifestFileChange,
|
|
199
217
|
};
|
|
200
218
|
}
|
|
201
219
|
// Setup branch and commit
|
|
@@ -246,6 +264,7 @@ export class RepositoryProcessor {
|
|
|
246
264
|
repoName,
|
|
247
265
|
message: "No changes detected after staging",
|
|
248
266
|
skipped: true,
|
|
267
|
+
fileChanges: manifestFileChange,
|
|
249
268
|
};
|
|
250
269
|
}
|
|
251
270
|
if (isDirectMode) {
|
|
@@ -253,6 +272,7 @@ export class RepositoryProcessor {
|
|
|
253
272
|
success: true,
|
|
254
273
|
repoName,
|
|
255
274
|
message: `Manifest updated directly on ${session.baseBranch}`,
|
|
275
|
+
fileChanges: manifestFileChange,
|
|
256
276
|
};
|
|
257
277
|
}
|
|
258
278
|
// Create and merge PR
|
|
@@ -264,7 +284,7 @@ export class RepositoryProcessor {
|
|
|
264
284
|
retries,
|
|
265
285
|
token: authResult.token,
|
|
266
286
|
executor,
|
|
267
|
-
}, [{ fileName: MANIFEST_FILENAME, action: "update" }], repoName);
|
|
287
|
+
}, [{ fileName: MANIFEST_FILENAME, action: "update" }], repoName, undefined, manifestFileChange);
|
|
268
288
|
}
|
|
269
289
|
finally {
|
|
270
290
|
try {
|