@aspruyt/xfg 3.9.0 → 3.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -9,8 +9,9 @@ import { logger } from "../shared/logger.js";
9
9
  import { generateWorkspaceName } from "../shared/workspace-utils.js";
10
10
  import { defaultProcessorFactory } from "./types.js";
11
11
  import { buildSyncReport } from "./sync-report-builder.js";
12
- import { formatSyncReportCLI, writeSyncReportSummary, } from "../output/sync-report.js";
13
- import { buildLifecycleReport, formatLifecycleReportCLI, writeLifecycleReportSummary, hasLifecycleChanges, } from "../output/lifecycle-report.js";
12
+ import { formatSyncReportCLI } from "../output/sync-report.js";
13
+ import { buildLifecycleReport, formatLifecycleReportCLI, hasLifecycleChanges, } from "../output/lifecycle-report.js";
14
+ import { writeUnifiedSummary } from "../output/unified-summary.js";
14
15
  import { RepoLifecycleManager, runLifecycleCheck, toCreateRepoSettings, } from "../lifecycle/index.js";
15
16
  /**
16
17
  * Get unique file names from all repos in the config
@@ -238,14 +239,14 @@ export async function runSync(options, processorFactory = defaultProcessorFactor
238
239
  console.log(line);
239
240
  }
240
241
  }
241
- writeLifecycleReportSummary(lifecycleReport, options.dryRun ?? false);
242
242
  // Build and display sync report
243
243
  const report = buildSyncReport(reportResults);
244
244
  console.log("");
245
245
  for (const line of formatSyncReportCLI(report)) {
246
246
  console.log(line);
247
247
  }
248
- writeSyncReportSummary(report, options.dryRun ?? false);
248
+ // Write unified summary to GITHUB_STEP_SUMMARY
249
+ writeUnifiedSummary(lifecycleReport, report, options.dryRun ?? false);
249
250
  // Exit with error if any failures
250
251
  const hasErrors = reportResults.some((r) => r.error);
251
252
  if (hasErrors) {
@@ -1,12 +1,6 @@
1
1
  // src/output/lifecycle-report.ts
2
2
  import { appendFileSync } from "node:fs";
3
3
  import chalk from "chalk";
4
- function escapeHtml(text) {
5
- return text
6
- .replace(/&/g, "&")
7
- .replace(/</g, "&lt;")
8
- .replace(/>/g, "&gt;");
9
- }
10
4
  // =============================================================================
11
5
  // Builder
12
6
  // =============================================================================
@@ -103,35 +97,35 @@ export function formatLifecycleReportMarkdown(report, dryRun) {
103
97
  lines.push("> This was a dry run — no changes were applied");
104
98
  lines.push("");
105
99
  }
106
- // Colored diff output using HTML <pre> with inline styles
100
+ // Diff block
107
101
  const diffLines = [];
108
102
  for (const action of report.actions) {
109
103
  if (action.action === "existed")
110
104
  continue;
111
105
  switch (action.action) {
112
106
  case "created":
113
- diffLines.push(`<span style="color:#3fb950">+ CREATE ${escapeHtml(action.repoName)}</span>`);
107
+ diffLines.push(`+ CREATE ${action.repoName}`);
114
108
  break;
115
109
  case "forked":
116
- diffLines.push(`<span style="color:#3fb950">+ FORK ${escapeHtml(action.upstream ?? "upstream")} -&gt; ${escapeHtml(action.repoName)}</span>`);
110
+ diffLines.push(`+ FORK ${action.upstream ?? "upstream"} -> ${action.repoName}`);
117
111
  break;
118
112
  case "migrated":
119
- diffLines.push(`<span style="color:#3fb950">+ MIGRATE ${escapeHtml(action.source ?? "source")} -&gt; ${escapeHtml(action.repoName)}</span>`);
113
+ diffLines.push(`+ MIGRATE ${action.source ?? "source"} -> ${action.repoName}`);
120
114
  break;
121
115
  }
122
116
  if (action.settings) {
123
117
  if (action.settings.visibility) {
124
- diffLines.push(`<span style="color:#3fb950"> visibility: ${escapeHtml(action.settings.visibility)}</span>`);
118
+ diffLines.push(` visibility: ${action.settings.visibility}`);
125
119
  }
126
120
  if (action.settings.description) {
127
- diffLines.push(`<span style="color:#3fb950"> description: "${escapeHtml(action.settings.description)}"</span>`);
121
+ diffLines.push(` description: "${action.settings.description}"`);
128
122
  }
129
123
  }
130
124
  }
131
125
  if (diffLines.length > 0) {
132
- lines.push("<pre>");
126
+ lines.push("```diff");
133
127
  lines.push(...diffLines);
134
- lines.push("</pre>");
128
+ lines.push("```");
135
129
  lines.push("");
136
130
  }
137
131
  // Summary
@@ -1,12 +1,6 @@
1
1
  import { appendFileSync } from "node:fs";
2
2
  import chalk from "chalk";
3
3
  import { formatPropertyTree, } from "../settings/rulesets/formatter.js";
4
- function escapeHtml(text) {
5
- return text
6
- .replace(/&/g, "&amp;")
7
- .replace(/</g, "&lt;")
8
- .replace(/>/g, "&gt;");
9
- }
10
4
  // =============================================================================
11
5
  // Helpers
12
6
  // =============================================================================
@@ -239,57 +233,55 @@ export function formatSettingsReportMarkdown(report, dryRun) {
239
233
  !repo.error) {
240
234
  continue;
241
235
  }
242
- diffLines.push(`<span style="color:#d29922">~ ${escapeHtml(repo.repoName)}</span>`);
236
+ diffLines.push(`@@ ${repo.repoName} @@`);
243
237
  for (const setting of repo.settings) {
244
238
  // Skip settings where both values are undefined
245
239
  if (setting.oldValue === undefined && setting.newValue === undefined) {
246
240
  continue;
247
241
  }
248
242
  if (setting.action === "add") {
249
- diffLines.push(`<span style="color:#3fb950"> + ${escapeHtml(setting.name)}: ${escapeHtml(formatValuePlain(setting.newValue))}</span>`);
243
+ diffLines.push(`+ ${setting.name}: ${formatValuePlain(setting.newValue)}`);
250
244
  }
251
245
  else {
252
- diffLines.push(`<span style="color:#d29922"> ~ ${escapeHtml(setting.name)}: ${escapeHtml(formatValuePlain(setting.oldValue))} → ${escapeHtml(formatValuePlain(setting.newValue))}</span>`);
246
+ diffLines.push(`! ${setting.name}: ${formatValuePlain(setting.oldValue)} → ${formatValuePlain(setting.newValue)}`);
253
247
  }
254
248
  }
255
249
  for (const ruleset of repo.rulesets) {
256
250
  if (ruleset.action === "create") {
257
- diffLines.push(`<span style="color:#3fb950"> + ruleset "${escapeHtml(ruleset.name)}"</span>`);
251
+ diffLines.push(`+ ruleset "${ruleset.name}"`);
258
252
  if (ruleset.config) {
259
- for (const line of formatRulesetConfigPlain(ruleset.config)) {
260
- diffLines.push(`<span style="color:#3fb950">${escapeHtml(line)}</span>`);
261
- }
253
+ diffLines.push(...formatRulesetConfigPlain(ruleset.config));
262
254
  }
263
255
  }
264
256
  else if (ruleset.action === "update") {
265
- diffLines.push(`<span style="color:#d29922"> ~ ruleset "${escapeHtml(ruleset.name)}"</span>`);
257
+ diffLines.push(`! ruleset "${ruleset.name}"`);
266
258
  if (ruleset.propertyDiffs && ruleset.propertyDiffs.length > 0) {
267
259
  for (const diff of ruleset.propertyDiffs) {
268
260
  const path = diff.path.join(".");
269
261
  if (diff.action === "add") {
270
- diffLines.push(`<span style="color:#3fb950"> + ${escapeHtml(path)}: ${escapeHtml(formatValuePlain(diff.newValue))}</span>`);
262
+ diffLines.push(`+ ${path}: ${formatValuePlain(diff.newValue)}`);
271
263
  }
272
264
  else if (diff.action === "change") {
273
- diffLines.push(`<span style="color:#d29922"> ~ ${escapeHtml(path)}: ${escapeHtml(formatValuePlain(diff.oldValue))} → ${escapeHtml(formatValuePlain(diff.newValue))}</span>`);
265
+ diffLines.push(`! ${path}: ${formatValuePlain(diff.oldValue)} → ${formatValuePlain(diff.newValue)}`);
274
266
  }
275
267
  else if (diff.action === "remove") {
276
- diffLines.push(`<span style="color:#f85149"> - ${escapeHtml(path)}</span>`);
268
+ diffLines.push(`- ${path}`);
277
269
  }
278
270
  }
279
271
  }
280
272
  }
281
273
  else if (ruleset.action === "delete") {
282
- diffLines.push(`<span style="color:#f85149"> - ruleset "${escapeHtml(ruleset.name)}"</span>`);
274
+ diffLines.push(`- ruleset "${ruleset.name}"`);
283
275
  }
284
276
  }
285
277
  if (repo.error) {
286
- diffLines.push(`<span style="color:#f85149"> ! Error: ${escapeHtml(repo.error)}</span>`);
278
+ diffLines.push(`- Error: ${repo.error}`);
287
279
  }
288
280
  }
289
281
  if (diffLines.length > 0) {
290
- lines.push("<pre>");
282
+ lines.push("```diff");
291
283
  lines.push(...diffLines);
292
- lines.push("</pre>");
284
+ lines.push("```");
293
285
  lines.push("");
294
286
  }
295
287
  // Summary
@@ -1,12 +1,6 @@
1
1
  // src/output/sync-report.ts
2
2
  import { appendFileSync } from "node:fs";
3
3
  import chalk from "chalk";
4
- function escapeHtml(text) {
5
- return text
6
- .replace(/&/g, "&amp;")
7
- .replace(/</g, "&lt;")
8
- .replace(/>/g, "&gt;");
9
- }
10
4
  function formatSummary(totals) {
11
5
  const total = totals.files.create + totals.files.update + totals.files.delete;
12
6
  if (total === 0) {
@@ -64,33 +58,32 @@ export function formatSyncReportMarkdown(report, dryRun) {
64
58
  lines.push("> This was a dry run — no changes were applied");
65
59
  lines.push("");
66
60
  }
67
- // Colored diff output using HTML <pre> with inline styles
68
- // Colors: green (#3fb950) for creates, yellow (#d29922) for changes, red (#f85149) for deletes
61
+ // Diff block
69
62
  const diffLines = [];
70
63
  for (const repo of report.repos) {
71
64
  if (repo.files.length === 0 && !repo.error) {
72
65
  continue;
73
66
  }
74
- diffLines.push(`<span style="color:#d29922">~ ${escapeHtml(repo.repoName)}</span>`);
67
+ diffLines.push(`@@ ${repo.repoName} @@`);
75
68
  for (const file of repo.files) {
76
69
  if (file.action === "create") {
77
- diffLines.push(`<span style="color:#3fb950"> + ${escapeHtml(file.path)}</span>`);
70
+ diffLines.push(`+ ${file.path}`);
78
71
  }
79
72
  else if (file.action === "update") {
80
- diffLines.push(`<span style="color:#d29922"> ~ ${escapeHtml(file.path)}</span>`);
73
+ diffLines.push(`! ${file.path}`);
81
74
  }
82
75
  else if (file.action === "delete") {
83
- diffLines.push(`<span style="color:#f85149"> - ${escapeHtml(file.path)}</span>`);
76
+ diffLines.push(`- ${file.path}`);
84
77
  }
85
78
  }
86
79
  if (repo.error) {
87
- diffLines.push(`<span style="color:#f85149"> ! Error: ${escapeHtml(repo.error)}</span>`);
80
+ diffLines.push(`- Error: ${repo.error}`);
88
81
  }
89
82
  }
90
83
  if (diffLines.length > 0) {
91
- lines.push("<pre>");
84
+ lines.push("```diff");
92
85
  lines.push(...diffLines);
93
- lines.push("</pre>");
86
+ lines.push("```");
94
87
  lines.push("");
95
88
  }
96
89
  // Summary
@@ -0,0 +1,4 @@
1
+ import type { LifecycleReport } from "./lifecycle-report.js";
2
+ import type { SyncReport } from "./sync-report.js";
3
+ export declare function formatUnifiedSummaryMarkdown(lifecycle: LifecycleReport, sync: SyncReport, dryRun: boolean): string;
4
+ export declare function writeUnifiedSummary(lifecycle: LifecycleReport, sync: SyncReport, dryRun: boolean): void;
@@ -0,0 +1,147 @@
1
+ // src/output/unified-summary.ts
2
+ import { appendFileSync } from "node:fs";
3
+ import { hasLifecycleChanges } from "./lifecycle-report.js";
4
+ // =============================================================================
5
+ // Helpers
6
+ // =============================================================================
7
+ function formatCombinedSummary(lifecycleTotals, syncTotals) {
8
+ const parts = [];
9
+ // Lifecycle totals
10
+ const repoTotal = lifecycleTotals.created + lifecycleTotals.forked + lifecycleTotals.migrated;
11
+ if (repoTotal > 0) {
12
+ const repoParts = [];
13
+ if (lifecycleTotals.created > 0)
14
+ repoParts.push(`${lifecycleTotals.created} to create`);
15
+ if (lifecycleTotals.forked > 0)
16
+ repoParts.push(`${lifecycleTotals.forked} to fork`);
17
+ if (lifecycleTotals.migrated > 0)
18
+ repoParts.push(`${lifecycleTotals.migrated} to migrate`);
19
+ const repoWord = repoTotal === 1 ? "repo" : "repos";
20
+ parts.push(`${repoTotal} ${repoWord} (${repoParts.join(", ")})`);
21
+ }
22
+ // Sync totals
23
+ const fileTotal = syncTotals.files.create + syncTotals.files.update + syncTotals.files.delete;
24
+ if (fileTotal > 0) {
25
+ const fileParts = [];
26
+ if (syncTotals.files.create > 0)
27
+ fileParts.push(`${syncTotals.files.create} to create`);
28
+ if (syncTotals.files.update > 0)
29
+ fileParts.push(`${syncTotals.files.update} to update`);
30
+ if (syncTotals.files.delete > 0)
31
+ fileParts.push(`${syncTotals.files.delete} to delete`);
32
+ const fileWord = fileTotal === 1 ? "file" : "files";
33
+ parts.push(`${fileTotal} ${fileWord} (${fileParts.join(", ")})`);
34
+ }
35
+ if (parts.length === 0) {
36
+ return "No changes";
37
+ }
38
+ return `Plan: ${parts.join(", ")}`;
39
+ }
40
+ // =============================================================================
41
+ // Markdown Formatter
42
+ // =============================================================================
43
+ export function formatUnifiedSummaryMarkdown(lifecycle, sync, dryRun) {
44
+ const hasLifecycle = hasLifecycleChanges(lifecycle);
45
+ const hasSync = sync.repos.some((r) => r.files.length > 0 || r.error);
46
+ if (!hasLifecycle && !hasSync) {
47
+ return "";
48
+ }
49
+ const lines = [];
50
+ // Title
51
+ const titleSuffix = dryRun ? " (Dry Run)" : "";
52
+ lines.push(`## xfg Sync Summary${titleSuffix}`);
53
+ lines.push("");
54
+ // Dry-run warning
55
+ if (dryRun) {
56
+ lines.push("> [!WARNING]");
57
+ lines.push("> This was a dry run — no changes were applied");
58
+ lines.push("");
59
+ }
60
+ // Build lookup maps by repoName
61
+ const lifecycleByRepo = new Map(lifecycle.actions.map((a) => [a.repoName, a]));
62
+ const syncByRepo = new Map(sync.repos.map((r) => [r.repoName, r]));
63
+ // Collect all repo names in order (lifecycle first, then sync-only)
64
+ const allRepos = [];
65
+ for (const action of lifecycle.actions) {
66
+ if (!allRepos.includes(action.repoName)) {
67
+ allRepos.push(action.repoName);
68
+ }
69
+ }
70
+ for (const repo of sync.repos) {
71
+ if (!allRepos.includes(repo.repoName)) {
72
+ allRepos.push(repo.repoName);
73
+ }
74
+ }
75
+ // Diff block
76
+ const diffLines = [];
77
+ for (const repoName of allRepos) {
78
+ const lcAction = lifecycleByRepo.get(repoName);
79
+ const syncRepo = syncByRepo.get(repoName);
80
+ const hasLcChange = lcAction && lcAction.action !== "existed";
81
+ const hasSyncChanges = syncRepo && (syncRepo.files.length > 0 || syncRepo.error);
82
+ if (!hasLcChange && !hasSyncChanges)
83
+ continue;
84
+ // Repo header
85
+ diffLines.push(`@@ ${repoName} @@`);
86
+ // Lifecycle action
87
+ if (lcAction && lcAction.action !== "existed") {
88
+ switch (lcAction.action) {
89
+ case "created":
90
+ diffLines.push(`+ CREATE`);
91
+ break;
92
+ case "forked":
93
+ diffLines.push(`+ FORK ${lcAction.upstream ?? "upstream"} -> ${repoName}`);
94
+ break;
95
+ case "migrated":
96
+ diffLines.push(`+ MIGRATE ${lcAction.source ?? "source"} -> ${repoName}`);
97
+ break;
98
+ }
99
+ if (lcAction.settings) {
100
+ if (lcAction.settings.visibility) {
101
+ diffLines.push(`+ visibility: ${lcAction.settings.visibility}`);
102
+ }
103
+ if (lcAction.settings.description) {
104
+ diffLines.push(`+ description: "${lcAction.settings.description}"`);
105
+ }
106
+ }
107
+ }
108
+ // File changes
109
+ if (syncRepo) {
110
+ for (const file of syncRepo.files) {
111
+ if (file.action === "create") {
112
+ diffLines.push(`+ ${file.path}`);
113
+ }
114
+ else if (file.action === "update") {
115
+ diffLines.push(`! ${file.path}`);
116
+ }
117
+ else if (file.action === "delete") {
118
+ diffLines.push(`- ${file.path}`);
119
+ }
120
+ }
121
+ if (syncRepo.error) {
122
+ diffLines.push(`- Error: ${syncRepo.error}`);
123
+ }
124
+ }
125
+ }
126
+ if (diffLines.length > 0) {
127
+ lines.push("```diff");
128
+ lines.push(...diffLines);
129
+ lines.push("```");
130
+ lines.push("");
131
+ }
132
+ // Combined summary
133
+ lines.push(`**${formatCombinedSummary(lifecycle.totals, sync.totals)}**`);
134
+ return lines.join("\n");
135
+ }
136
+ // =============================================================================
137
+ // File Writer
138
+ // =============================================================================
139
+ export function writeUnifiedSummary(lifecycle, sync, dryRun) {
140
+ const summaryPath = process.env.GITHUB_STEP_SUMMARY;
141
+ if (!summaryPath)
142
+ return;
143
+ const markdown = formatUnifiedSummaryMarkdown(lifecycle, sync, dryRun);
144
+ if (!markdown)
145
+ return;
146
+ appendFileSync(summaryPath, "\n" + markdown + "\n");
147
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aspruyt/xfg",
3
- "version": "3.9.0",
3
+ "version": "3.9.1",
4
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",