@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 CHANGED
@@ -5,7 +5,7 @@
5
5
  [![npm downloads](https://img.shields.io/npm/dw/@aspruyt/xfg.svg)](https://www.npmjs.com/package/@aspruyt/xfg)
6
6
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
7
7
 
8
- A CLI tool that syncs JSON, JSON5, YAML, or text configuration files across multiple GitHub, Azure DevOps, and GitLab repositories by creating pull requests.
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
- if (!dryRun) {
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
- this.log.info(`Creating branch: ${branchName}`);
72
- await this.gitOps.createBranch(branchName);
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
- this.log.info("Pushing to remote...");
209
- await this.gitOps.push(branchName);
210
- // Step 9: Create PR
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.7.0",
4
- "description": "CLI tool to sync JSON, JSON5, YAML, or text configuration files across multiple Git repositories by creating pull requests",
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": {