@aspruyt/xfg 2.0.2 → 2.1.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/LICENSE +1 -1
- package/PR.md +2 -0
- package/dist/git-ops.js +1 -1
- package/dist/github-summary.d.ts +25 -0
- package/dist/github-summary.js +83 -0
- package/dist/index.js +21 -14
- package/dist/logger.js +1 -2
- package/dist/repository-processor.d.ts +2 -0
- package/dist/repository-processor.js +4 -0
- package/dist/summary-utils.d.ts +20 -0
- package/dist/summary-utils.js +78 -0
- package/package.json +1 -1
package/LICENSE
CHANGED
package/PR.md
CHANGED
package/dist/git-ops.js
CHANGED
|
@@ -283,7 +283,7 @@ export function validateBranchName(branchName) {
|
|
|
283
283
|
throw new Error('Branch name cannot start with "." or "-"');
|
|
284
284
|
}
|
|
285
285
|
// Git disallows: space, ~, ^, :, ?, *, [, \, and consecutive dots (..)
|
|
286
|
-
if (/[\s
|
|
286
|
+
if (/[\s~^:?*[\\]/.test(branchName) || branchName.includes("..")) {
|
|
287
287
|
throw new Error("Branch name contains invalid characters");
|
|
288
288
|
}
|
|
289
289
|
if (branchName.endsWith("/") ||
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export type MergeOutcome = "manual" | "auto" | "force" | "direct";
|
|
2
|
+
export interface FileChanges {
|
|
3
|
+
added: number;
|
|
4
|
+
modified: number;
|
|
5
|
+
deleted: number;
|
|
6
|
+
unchanged: number;
|
|
7
|
+
}
|
|
8
|
+
export interface RepoResult {
|
|
9
|
+
repoName: string;
|
|
10
|
+
status: "succeeded" | "skipped" | "failed";
|
|
11
|
+
message: string;
|
|
12
|
+
prUrl?: string;
|
|
13
|
+
mergeOutcome?: MergeOutcome;
|
|
14
|
+
fileChanges?: FileChanges;
|
|
15
|
+
}
|
|
16
|
+
export interface SummaryData {
|
|
17
|
+
total: number;
|
|
18
|
+
succeeded: number;
|
|
19
|
+
skipped: number;
|
|
20
|
+
failed: number;
|
|
21
|
+
results: RepoResult[];
|
|
22
|
+
}
|
|
23
|
+
export declare function formatSummary(data: SummaryData): string;
|
|
24
|
+
export declare function isGitHubActions(): boolean;
|
|
25
|
+
export declare function writeSummary(data: SummaryData): void;
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { appendFileSync } from "node:fs";
|
|
2
|
+
function escapeMarkdown(text) {
|
|
3
|
+
// Escape backslashes first, then pipes (order matters to prevent double-escaping)
|
|
4
|
+
return text.replace(/\\/g, "\\\\").replace(/\|/g, "\\|");
|
|
5
|
+
}
|
|
6
|
+
function formatFileChanges(changes) {
|
|
7
|
+
if (!changes)
|
|
8
|
+
return "-";
|
|
9
|
+
return `+${changes.added} ~${changes.modified} -${changes.deleted}`;
|
|
10
|
+
}
|
|
11
|
+
function formatStatus(result) {
|
|
12
|
+
if (result.status === "skipped")
|
|
13
|
+
return "⏭️ Skipped";
|
|
14
|
+
if (result.status === "failed")
|
|
15
|
+
return "❌ Failed";
|
|
16
|
+
// Succeeded - format based on merge outcome
|
|
17
|
+
switch (result.mergeOutcome) {
|
|
18
|
+
case "manual":
|
|
19
|
+
return "✅ Open";
|
|
20
|
+
case "auto":
|
|
21
|
+
return "✅ Auto-merge";
|
|
22
|
+
case "force":
|
|
23
|
+
return "✅ Merged";
|
|
24
|
+
case "direct":
|
|
25
|
+
return "✅ Pushed";
|
|
26
|
+
default:
|
|
27
|
+
return "✅ Succeeded";
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
function formatResult(result) {
|
|
31
|
+
if (result.prUrl) {
|
|
32
|
+
// Extract PR number from URL
|
|
33
|
+
const prMatch = result.prUrl.match(/\/pull\/(\d+)/);
|
|
34
|
+
const prNum = prMatch ? prMatch[1] : "PR";
|
|
35
|
+
return `[PR #${prNum}](${result.prUrl})`;
|
|
36
|
+
}
|
|
37
|
+
if (result.mergeOutcome === "direct") {
|
|
38
|
+
return "Direct to main";
|
|
39
|
+
}
|
|
40
|
+
return escapeMarkdown(result.message);
|
|
41
|
+
}
|
|
42
|
+
export function formatSummary(data) {
|
|
43
|
+
const lines = [];
|
|
44
|
+
// Header
|
|
45
|
+
lines.push("## Config Sync Summary");
|
|
46
|
+
lines.push("");
|
|
47
|
+
// Stats table
|
|
48
|
+
lines.push("| Status | Count |");
|
|
49
|
+
lines.push("|--------|-------|");
|
|
50
|
+
lines.push(`| ✅ Succeeded | ${data.succeeded} |`);
|
|
51
|
+
lines.push(`| ⏭️ Skipped | ${data.skipped} |`);
|
|
52
|
+
lines.push(`| ❌ Failed | ${data.failed} |`);
|
|
53
|
+
lines.push(`| **Total** | **${data.total}** |`);
|
|
54
|
+
// Repo details table (only if there are results)
|
|
55
|
+
if (data.results.length > 0) {
|
|
56
|
+
lines.push("");
|
|
57
|
+
lines.push("<details>");
|
|
58
|
+
lines.push("<summary>Repository Details</summary>");
|
|
59
|
+
lines.push("");
|
|
60
|
+
lines.push("| Repository | Status | Changes | Result |");
|
|
61
|
+
lines.push("|------------|--------|---------|--------|");
|
|
62
|
+
for (const result of data.results) {
|
|
63
|
+
const repo = result.repoName;
|
|
64
|
+
const status = formatStatus(result);
|
|
65
|
+
const changes = formatFileChanges(result.fileChanges);
|
|
66
|
+
const resultText = formatResult(result);
|
|
67
|
+
lines.push(`| ${repo} | ${status} | ${changes} | ${resultText} |`);
|
|
68
|
+
}
|
|
69
|
+
lines.push("");
|
|
70
|
+
lines.push("</details>");
|
|
71
|
+
}
|
|
72
|
+
return lines.join("\n");
|
|
73
|
+
}
|
|
74
|
+
export function isGitHubActions() {
|
|
75
|
+
return !!process.env.GITHUB_STEP_SUMMARY;
|
|
76
|
+
}
|
|
77
|
+
export function writeSummary(data) {
|
|
78
|
+
const summaryPath = process.env.GITHUB_STEP_SUMMARY;
|
|
79
|
+
if (!summaryPath)
|
|
80
|
+
return;
|
|
81
|
+
const markdown = formatSummary(data);
|
|
82
|
+
appendFileSync(summaryPath, markdown + "\n");
|
|
83
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -13,6 +13,8 @@ import { sanitizeBranchName, validateBranchName } from "./git-ops.js";
|
|
|
13
13
|
import { logger } from "./logger.js";
|
|
14
14
|
import { generateWorkspaceName } from "./workspace-utils.js";
|
|
15
15
|
import { RepositoryProcessor, } from "./repository-processor.js";
|
|
16
|
+
import { writeSummary } from "./github-summary.js";
|
|
17
|
+
import { buildRepoResult, buildErrorResult } from "./summary-utils.js";
|
|
16
18
|
/**
|
|
17
19
|
* Default factory that creates a real RepositoryProcessor.
|
|
18
20
|
*/
|
|
@@ -102,6 +104,7 @@ async function main() {
|
|
|
102
104
|
console.log(`Target files: ${formatFileNames(fileNames)}`);
|
|
103
105
|
console.log(`Branch: ${branchName}\n`);
|
|
104
106
|
const processor = defaultProcessorFactory();
|
|
107
|
+
const results = [];
|
|
105
108
|
for (let i = 0; i < config.repos.length; i++) {
|
|
106
109
|
const repoConfig = config.repos[i];
|
|
107
110
|
// Apply CLI merge overrides to repo config
|
|
@@ -121,8 +124,8 @@ async function main() {
|
|
|
121
124
|
});
|
|
122
125
|
}
|
|
123
126
|
catch (error) {
|
|
124
|
-
|
|
125
|
-
|
|
127
|
+
logger.error(current, repoConfig.git, String(error));
|
|
128
|
+
results.push(buildErrorResult(repoConfig.git, error));
|
|
126
129
|
continue;
|
|
127
130
|
}
|
|
128
131
|
const repoName = getRepoDisplayName(repoInfo);
|
|
@@ -138,31 +141,35 @@ async function main() {
|
|
|
138
141
|
prTemplate: config.prTemplate,
|
|
139
142
|
noDelete: options.noDelete,
|
|
140
143
|
});
|
|
144
|
+
const repoResult = buildRepoResult(repoName, repoConfig, result);
|
|
145
|
+
results.push(repoResult);
|
|
141
146
|
if (result.skipped) {
|
|
142
147
|
logger.skip(current, repoName, result.message);
|
|
143
148
|
}
|
|
144
149
|
else if (result.success) {
|
|
145
|
-
|
|
146
|
-
if (result.mergeResult) {
|
|
147
|
-
if (result.mergeResult.merged) {
|
|
148
|
-
message += " (merged)";
|
|
149
|
-
}
|
|
150
|
-
else if (result.mergeResult.autoMergeEnabled) {
|
|
151
|
-
message += " (auto-merge enabled)";
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
logger.success(current, repoName, message);
|
|
150
|
+
logger.success(current, repoName, repoResult.message);
|
|
155
151
|
}
|
|
156
152
|
else {
|
|
157
153
|
logger.error(current, repoName, result.message);
|
|
158
154
|
}
|
|
159
155
|
}
|
|
160
156
|
catch (error) {
|
|
161
|
-
|
|
162
|
-
|
|
157
|
+
logger.error(current, repoName, String(error));
|
|
158
|
+
results.push(buildErrorResult(repoName, error));
|
|
163
159
|
}
|
|
164
160
|
}
|
|
165
161
|
logger.summary();
|
|
162
|
+
// Write GitHub Actions job summary if running in GitHub Actions
|
|
163
|
+
const succeeded = results.filter((r) => r.status === "succeeded").length;
|
|
164
|
+
const skipped = results.filter((r) => r.status === "skipped").length;
|
|
165
|
+
const failed = results.filter((r) => r.status === "failed").length;
|
|
166
|
+
writeSummary({
|
|
167
|
+
total: config.repos.length,
|
|
168
|
+
succeeded,
|
|
169
|
+
skipped,
|
|
170
|
+
failed,
|
|
171
|
+
results,
|
|
172
|
+
});
|
|
166
173
|
if (logger.hasFailures()) {
|
|
167
174
|
process.exit(1);
|
|
168
175
|
}
|
package/dist/logger.js
CHANGED
|
@@ -29,8 +29,7 @@ export class Logger {
|
|
|
29
29
|
}
|
|
30
30
|
error(current, repoName, error) {
|
|
31
31
|
this.stats.failed++;
|
|
32
|
-
console.log(chalk.red(`[${current}/${this.stats.total}] ✗`) +
|
|
33
|
-
` ${repoName}: ${error}`);
|
|
32
|
+
console.log(chalk.red(`[${current}/${this.stats.total}] ✗`) + ` ${repoName}: ${error}`);
|
|
34
33
|
}
|
|
35
34
|
/**
|
|
36
35
|
* Display a file diff with status badge.
|
|
@@ -3,6 +3,7 @@ import { RepoInfo } from "./repo-detector.js";
|
|
|
3
3
|
import { GitOps, GitOpsOptions } from "./git-ops.js";
|
|
4
4
|
import { ILogger } from "./logger.js";
|
|
5
5
|
import { CommandExecutor } from "./command-executor.js";
|
|
6
|
+
import { DiffStats } from "./diff-utils.js";
|
|
6
7
|
export interface ProcessorOptions {
|
|
7
8
|
branchName: string;
|
|
8
9
|
workDir: string;
|
|
@@ -34,6 +35,7 @@ export interface ProcessorResult {
|
|
|
34
35
|
autoMergeEnabled?: boolean;
|
|
35
36
|
message: string;
|
|
36
37
|
};
|
|
38
|
+
diffStats?: DiffStats;
|
|
37
39
|
}
|
|
38
40
|
export declare class RepositoryProcessor {
|
|
39
41
|
private gitOps;
|
|
@@ -268,6 +268,7 @@ export class RepositoryProcessor {
|
|
|
268
268
|
repoName,
|
|
269
269
|
message: "No changes detected",
|
|
270
270
|
skipped: true,
|
|
271
|
+
diffStats,
|
|
271
272
|
};
|
|
272
273
|
}
|
|
273
274
|
// Step 7: Commit
|
|
@@ -281,6 +282,7 @@ export class RepositoryProcessor {
|
|
|
281
282
|
repoName,
|
|
282
283
|
message: "No changes detected after staging",
|
|
283
284
|
skipped: true,
|
|
285
|
+
diffStats,
|
|
284
286
|
};
|
|
285
287
|
}
|
|
286
288
|
this.log.info(`Committed: ${commitMessage}`);
|
|
@@ -316,6 +318,7 @@ export class RepositoryProcessor {
|
|
|
316
318
|
success: true,
|
|
317
319
|
repoName,
|
|
318
320
|
message: `Pushed directly to ${baseBranch}`,
|
|
321
|
+
diffStats,
|
|
319
322
|
};
|
|
320
323
|
}
|
|
321
324
|
// Step 9: Create PR (non-direct modes only)
|
|
@@ -368,6 +371,7 @@ export class RepositoryProcessor {
|
|
|
368
371
|
message: prResult.message,
|
|
369
372
|
prUrl: prResult.url,
|
|
370
373
|
mergeResult,
|
|
374
|
+
diffStats,
|
|
371
375
|
};
|
|
372
376
|
}
|
|
373
377
|
finally {
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { ProcessorResult } from "./repository-processor.js";
|
|
2
|
+
import { RepoConfig } from "./config.js";
|
|
3
|
+
import { MergeOutcome, FileChanges, RepoResult } from "./github-summary.js";
|
|
4
|
+
import { DiffStats } from "./diff-utils.js";
|
|
5
|
+
/**
|
|
6
|
+
* Determine merge outcome from repo config and processor result
|
|
7
|
+
*/
|
|
8
|
+
export declare function getMergeOutcome(repoConfig: RepoConfig, result: ProcessorResult): MergeOutcome | undefined;
|
|
9
|
+
/**
|
|
10
|
+
* Convert DiffStats to FileChanges for summary output
|
|
11
|
+
*/
|
|
12
|
+
export declare function toFileChanges(diffStats?: DiffStats): FileChanges | undefined;
|
|
13
|
+
/**
|
|
14
|
+
* Build a RepoResult from a ProcessorResult for the summary
|
|
15
|
+
*/
|
|
16
|
+
export declare function buildRepoResult(repoName: string, repoConfig: RepoConfig, result: ProcessorResult): RepoResult;
|
|
17
|
+
/**
|
|
18
|
+
* Build a RepoResult for an error case
|
|
19
|
+
*/
|
|
20
|
+
export declare function buildErrorResult(repoName: string, error: unknown): RepoResult;
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Determine merge outcome from repo config and processor result
|
|
3
|
+
*/
|
|
4
|
+
export function getMergeOutcome(repoConfig, result) {
|
|
5
|
+
if (!result.success || result.skipped)
|
|
6
|
+
return undefined;
|
|
7
|
+
const mergeMode = repoConfig.prOptions?.merge ?? "auto";
|
|
8
|
+
if (mergeMode === "direct")
|
|
9
|
+
return "direct";
|
|
10
|
+
if (result.mergeResult?.merged)
|
|
11
|
+
return "force";
|
|
12
|
+
if (result.mergeResult?.autoMergeEnabled)
|
|
13
|
+
return "auto";
|
|
14
|
+
if (result.prUrl)
|
|
15
|
+
return "manual";
|
|
16
|
+
return undefined;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Convert DiffStats to FileChanges for summary output
|
|
20
|
+
*/
|
|
21
|
+
export function toFileChanges(diffStats) {
|
|
22
|
+
if (!diffStats)
|
|
23
|
+
return undefined;
|
|
24
|
+
return {
|
|
25
|
+
added: diffStats.newCount,
|
|
26
|
+
modified: diffStats.modifiedCount,
|
|
27
|
+
deleted: diffStats.deletedCount,
|
|
28
|
+
unchanged: diffStats.unchangedCount,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Build a RepoResult from a ProcessorResult for the summary
|
|
33
|
+
*/
|
|
34
|
+
export function buildRepoResult(repoName, repoConfig, result) {
|
|
35
|
+
if (result.skipped) {
|
|
36
|
+
return {
|
|
37
|
+
repoName,
|
|
38
|
+
status: "skipped",
|
|
39
|
+
message: result.message,
|
|
40
|
+
fileChanges: toFileChanges(result.diffStats),
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
if (result.success) {
|
|
44
|
+
let message = result.prUrl ? `PR: ${result.prUrl}` : result.message;
|
|
45
|
+
if (result.mergeResult) {
|
|
46
|
+
if (result.mergeResult.merged) {
|
|
47
|
+
message += " (merged)";
|
|
48
|
+
}
|
|
49
|
+
else if (result.mergeResult.autoMergeEnabled) {
|
|
50
|
+
message += " (auto-merge enabled)";
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return {
|
|
54
|
+
repoName,
|
|
55
|
+
status: "succeeded",
|
|
56
|
+
message,
|
|
57
|
+
prUrl: result.prUrl,
|
|
58
|
+
mergeOutcome: getMergeOutcome(repoConfig, result),
|
|
59
|
+
fileChanges: toFileChanges(result.diffStats),
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
return {
|
|
63
|
+
repoName,
|
|
64
|
+
status: "failed",
|
|
65
|
+
message: result.message,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Build a RepoResult for an error case
|
|
70
|
+
*/
|
|
71
|
+
export function buildErrorResult(repoName, error) {
|
|
72
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
73
|
+
return {
|
|
74
|
+
repoName,
|
|
75
|
+
status: "failed",
|
|
76
|
+
message,
|
|
77
|
+
};
|
|
78
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aspruyt/xfg",
|
|
3
|
-
"version": "2.0
|
|
3
|
+
"version": "2.1.0",
|
|
4
4
|
"description": "CLI tool to sync JSON, JSON5, YAML, or text configuration files across multiple Git repositories via pull requests or direct push",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|