@aspruyt/xfg 1.7.0 → 1.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/README.md +2 -1
- package/dist/config.d.ts +1 -1
- package/dist/index.js +2 -2
- package/dist/repository-processor.js +49 -7
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
[](https://www.npmjs.com/package/@aspruyt/xfg)
|
|
6
6
|
[](LICENSE)
|
|
7
7
|
|
|
8
|
-
A CLI tool that syncs JSON, JSON5, YAML, or text configuration files across multiple GitHub, Azure DevOps, and GitLab repositories
|
|
8
|
+
A CLI tool that syncs JSON, JSON5, YAML, or text configuration files across multiple GitHub, Azure DevOps, and GitLab repositories. By default, changes are made via pull requests, but you can also push directly to the default branch.
|
|
9
9
|
|
|
10
10
|
**[Full Documentation](https://anthony-spruyt.github.io/xfg/)**
|
|
11
11
|
|
|
@@ -58,6 +58,7 @@ xfg --config ./config.yaml
|
|
|
58
58
|
- **YAML Comments** - Add header comments and schema directives to YAML files
|
|
59
59
|
- **Multi-Platform** - Works with GitHub (including GitHub Enterprise Server), Azure DevOps, and GitLab (including self-hosted)
|
|
60
60
|
- **Auto-Merge PRs** - Automatically merge PRs when checks pass, or force merge with admin privileges
|
|
61
|
+
- **Direct Push Mode** - Push directly to default branch without creating PRs
|
|
61
62
|
- **Dry-Run Mode** - Preview changes without creating PRs
|
|
62
63
|
- **Error Resilience** - Continues processing if individual repos fail
|
|
63
64
|
- **Automatic Retries** - Retries transient network errors with exponential backoff
|
package/dist/config.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { ArrayMergeStrategy } from "./merge.js";
|
|
2
2
|
export { convertContentToString } from "./config-formatter.js";
|
|
3
|
-
export type MergeMode = "manual" | "auto" | "force";
|
|
3
|
+
export type MergeMode = "manual" | "auto" | "force" | "direct";
|
|
4
4
|
export type MergeStrategy = "merge" | "squash" | "rebase";
|
|
5
5
|
export interface PRMergeOptions {
|
|
6
6
|
merge?: MergeMode;
|
package/dist/index.js
CHANGED
|
@@ -26,8 +26,8 @@ program
|
|
|
26
26
|
.option("-w, --work-dir <path>", "Temporary directory for cloning", "./tmp")
|
|
27
27
|
.option("-r, --retries <number>", "Number of retries for network operations (0 to disable)", (v) => parseInt(v, 10), 3)
|
|
28
28
|
.option("-b, --branch <name>", "Override the branch name (default: chore/sync-{filename} or chore/sync-config)")
|
|
29
|
-
.option("-m, --merge <mode>", "PR merge mode: manual, auto (default, merge when checks pass), force (bypass requirements)", (value) => {
|
|
30
|
-
const valid = ["manual", "auto", "force"];
|
|
29
|
+
.option("-m, --merge <mode>", "PR merge mode: manual, auto (default, merge when checks pass), force (bypass requirements), direct (push to default branch, no PR)", (value) => {
|
|
30
|
+
const valid = ["manual", "auto", "force", "direct"];
|
|
31
31
|
if (!valid.includes(value)) {
|
|
32
32
|
throw new Error(`Invalid merge mode: ${value}. Valid: ${valid.join(", ")}`);
|
|
33
33
|
}
|
|
@@ -41,6 +41,13 @@ export class RepositoryProcessor {
|
|
|
41
41
|
const { branchName, workDir, dryRun, retries, prTemplate } = options;
|
|
42
42
|
const executor = options.executor ?? defaultExecutor;
|
|
43
43
|
this.gitOps = this.gitOpsFactory({ workDir, dryRun, retries });
|
|
44
|
+
// Determine merge mode early - affects workflow steps
|
|
45
|
+
const mergeMode = repoConfig.prOptions?.merge ?? "auto";
|
|
46
|
+
const isDirectMode = mergeMode === "direct";
|
|
47
|
+
// Warn if mergeStrategy is set with direct mode (irrelevant)
|
|
48
|
+
if (isDirectMode && repoConfig.prOptions?.mergeStrategy) {
|
|
49
|
+
this.log.info(`Warning: mergeStrategy '${repoConfig.prOptions.mergeStrategy}' is ignored in direct mode (no PR created)`);
|
|
50
|
+
}
|
|
44
51
|
try {
|
|
45
52
|
// Step 1: Clean workspace
|
|
46
53
|
this.log.info("Cleaning workspace...");
|
|
@@ -53,7 +60,8 @@ export class RepositoryProcessor {
|
|
|
53
60
|
this.log.info(`Default branch: ${baseBranch} (detected via ${detectionMethod})`);
|
|
54
61
|
// Step 3.5: Close existing PR if exists (fresh start approach)
|
|
55
62
|
// This ensures isolated sync attempts - each run starts from clean state
|
|
56
|
-
|
|
63
|
+
// Skip for direct mode - no PR involved
|
|
64
|
+
if (!dryRun && !isDirectMode) {
|
|
57
65
|
this.log.info("Checking for existing PR...");
|
|
58
66
|
const strategy = getPRStrategy(repoInfo, executor);
|
|
59
67
|
const closed = await strategy.closeExistingPR({
|
|
@@ -68,8 +76,14 @@ export class RepositoryProcessor {
|
|
|
68
76
|
}
|
|
69
77
|
}
|
|
70
78
|
// Step 4: Create branch (always fresh from base branch)
|
|
71
|
-
|
|
72
|
-
|
|
79
|
+
// Skip for direct mode - stay on default branch
|
|
80
|
+
if (!isDirectMode) {
|
|
81
|
+
this.log.info(`Creating branch: ${branchName}`);
|
|
82
|
+
await this.gitOps.createBranch(branchName);
|
|
83
|
+
}
|
|
84
|
+
else {
|
|
85
|
+
this.log.info(`Direct mode: staying on ${baseBranch}`);
|
|
86
|
+
}
|
|
73
87
|
// Step 5: Write all config files and track changes
|
|
74
88
|
//
|
|
75
89
|
// DESIGN NOTE: Change detection differs between dry-run and normal mode:
|
|
@@ -205,9 +219,38 @@ export class RepositoryProcessor {
|
|
|
205
219
|
}
|
|
206
220
|
this.log.info(`Committed: ${commitMessage}`);
|
|
207
221
|
// Step 8: Push
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
222
|
+
// In direct mode, push to default branch; otherwise push to sync branch
|
|
223
|
+
const pushBranch = isDirectMode ? baseBranch : branchName;
|
|
224
|
+
this.log.info(`Pushing to ${pushBranch}...`);
|
|
225
|
+
try {
|
|
226
|
+
await this.gitOps.push(pushBranch);
|
|
227
|
+
}
|
|
228
|
+
catch (error) {
|
|
229
|
+
// Handle branch protection errors in direct mode
|
|
230
|
+
if (isDirectMode) {
|
|
231
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
232
|
+
if (errorMessage.includes("rejected") ||
|
|
233
|
+
errorMessage.includes("protected") ||
|
|
234
|
+
errorMessage.includes("denied")) {
|
|
235
|
+
return {
|
|
236
|
+
success: false,
|
|
237
|
+
repoName,
|
|
238
|
+
message: `Push to '${baseBranch}' was rejected (likely branch protection). To use 'direct' mode, the target branch must allow direct pushes. Use 'merge: force' to create a PR and merge with admin privileges.`,
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
throw error;
|
|
243
|
+
}
|
|
244
|
+
// Direct mode: no PR creation, return success
|
|
245
|
+
if (isDirectMode) {
|
|
246
|
+
this.log.info(`Changes pushed directly to ${baseBranch}`);
|
|
247
|
+
return {
|
|
248
|
+
success: true,
|
|
249
|
+
repoName,
|
|
250
|
+
message: `Pushed directly to ${baseBranch}`,
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
// Step 9: Create PR (non-direct modes only)
|
|
211
254
|
this.log.info("Creating pull request...");
|
|
212
255
|
const prResult = await createPR({
|
|
213
256
|
repoInfo,
|
|
@@ -220,7 +263,6 @@ export class RepositoryProcessor {
|
|
|
220
263
|
prTemplate,
|
|
221
264
|
});
|
|
222
265
|
// Step 10: Handle merge options if configured
|
|
223
|
-
const mergeMode = repoConfig.prOptions?.merge ?? "auto";
|
|
224
266
|
let mergeResult;
|
|
225
267
|
if (prResult.success && prResult.url && mergeMode !== "manual") {
|
|
226
268
|
this.log.info(`Handling merge (mode: ${mergeMode})...`);
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aspruyt/xfg",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "CLI tool to sync JSON, JSON5, YAML, or text configuration files across multiple Git repositories
|
|
3
|
+
"version": "1.8.0",
|
|
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",
|
|
7
7
|
"bin": {
|