@aspruyt/xfg 1.0.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.
Files changed (46) hide show
  1. package/LICENSE +21 -0
  2. package/PR.md +15 -0
  3. package/README.md +991 -0
  4. package/dist/command-executor.d.ts +25 -0
  5. package/dist/command-executor.js +32 -0
  6. package/dist/config-formatter.d.ts +17 -0
  7. package/dist/config-formatter.js +100 -0
  8. package/dist/config-normalizer.d.ts +6 -0
  9. package/dist/config-normalizer.js +136 -0
  10. package/dist/config-validator.d.ts +6 -0
  11. package/dist/config-validator.js +173 -0
  12. package/dist/config.d.ts +54 -0
  13. package/dist/config.js +27 -0
  14. package/dist/env.d.ts +39 -0
  15. package/dist/env.js +144 -0
  16. package/dist/file-reference-resolver.d.ts +20 -0
  17. package/dist/file-reference-resolver.js +135 -0
  18. package/dist/git-ops.d.ts +75 -0
  19. package/dist/git-ops.js +229 -0
  20. package/dist/index.d.ts +20 -0
  21. package/dist/index.js +167 -0
  22. package/dist/logger.d.ts +21 -0
  23. package/dist/logger.js +46 -0
  24. package/dist/merge.d.ts +47 -0
  25. package/dist/merge.js +196 -0
  26. package/dist/pr-creator.d.ts +40 -0
  27. package/dist/pr-creator.js +129 -0
  28. package/dist/repo-detector.d.ts +22 -0
  29. package/dist/repo-detector.js +98 -0
  30. package/dist/repository-processor.d.ts +47 -0
  31. package/dist/repository-processor.js +245 -0
  32. package/dist/retry-utils.d.ts +53 -0
  33. package/dist/retry-utils.js +143 -0
  34. package/dist/shell-utils.d.ts +8 -0
  35. package/dist/shell-utils.js +12 -0
  36. package/dist/strategies/azure-pr-strategy.d.ts +16 -0
  37. package/dist/strategies/azure-pr-strategy.js +221 -0
  38. package/dist/strategies/github-pr-strategy.d.ts +17 -0
  39. package/dist/strategies/github-pr-strategy.js +215 -0
  40. package/dist/strategies/index.d.ts +13 -0
  41. package/dist/strategies/index.js +22 -0
  42. package/dist/strategies/pr-strategy.d.ts +112 -0
  43. package/dist/strategies/pr-strategy.js +60 -0
  44. package/dist/workspace-utils.d.ts +5 -0
  45. package/dist/workspace-utils.js +10 -0
  46. package/package.json +58 -0
package/README.md ADDED
@@ -0,0 +1,991 @@
1
+ # xfg
2
+
3
+ [![CI](https://github.com/anthony-spruyt/xfg/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/anthony-spruyt/xfg/actions/workflows/ci.yml)
4
+ [![npm version](https://img.shields.io/npm/v/@aspruyt/xfg.svg)](https://www.npmjs.com/package/@aspruyt/xfg)
5
+ [![npm downloads](https://img.shields.io/npm/dw/@aspruyt/xfg.svg)](https://www.npmjs.com/package/@aspruyt/xfg)
6
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
7
+
8
+ A CLI tool that syncs JSON, JSON5, YAML, or text configuration files across multiple GitHub and Azure DevOps repositories by creating pull requests. Output format is automatically detected from the target filename extension (`.json` → JSON, `.json5` → JSON5, `.yaml`/`.yml` → YAML, other → text).
9
+
10
+ ## Table of Contents
11
+
12
+ - [Quick Start](#quick-start)
13
+ - [Features](#features)
14
+ - [How It Works](#how-it-works)
15
+ - [Installation](#installation)
16
+ - [Prerequisites](#prerequisites)
17
+ - [Usage](#usage)
18
+ - [Configuration Format](#configuration-format)
19
+ - [Examples](#examples)
20
+ - [Supported Git URL Formats](#supported-git-url-formats)
21
+ - [CI/CD Integration](#cicd-integration)
22
+ - [Output Examples](#output-examples)
23
+ - [Troubleshooting](#troubleshooting)
24
+ - [IDE Integration](#ide-integration)
25
+ - [Development](#development)
26
+ - [License](#license)
27
+
28
+ ## Quick Start
29
+
30
+ ```bash
31
+ # Install
32
+ npm install -g @aspruyt/xfg
33
+
34
+ # Authenticate (GitHub)
35
+ gh auth login
36
+
37
+ # Create config.yaml
38
+ cat > config.yaml << 'EOF'
39
+ files:
40
+ .prettierrc.json:
41
+ content:
42
+ semi: false
43
+ singleQuote: true
44
+ tabWidth: 2
45
+ trailingComma: es5
46
+
47
+ repos:
48
+ # Multiple repos can share the same config
49
+ - git:
50
+ - git@github.com:your-org/frontend-app.git
51
+ - git@github.com:your-org/backend-api.git
52
+ - git@github.com:your-org/shared-lib.git
53
+ EOF
54
+
55
+ # Run
56
+ xfg --config ./config.yaml
57
+ ```
58
+
59
+ **Result:** PRs are created in all three repos with identical `.prettierrc.json` files.
60
+
61
+ ## Features
62
+
63
+ - **Multi-File Sync** - Sync multiple config files in a single run
64
+ - **Multi-Format Output** - JSON, YAML, or plain text based on filename extension
65
+ - **Subdirectory Support** - Sync files to any path (e.g., `.github/workflows/ci.yml`)
66
+ - **Text Files** - Sync `.gitignore`, `.markdownlintignore`, etc. with string or lines array
67
+ - **File References** - Use `@path/to/file` to load content from external template files
68
+ - **Content Inheritance** - Define base config once, override per-repo as needed
69
+ - **Multi-Repo Targeting** - Apply same config to multiple repos with array syntax
70
+ - **Environment Variables** - Use `${VAR}` syntax for dynamic values
71
+ - **Merge Strategies** - Control how arrays merge (replace, append, prepend)
72
+ - **Override Mode** - Skip merging entirely for specific repos
73
+ - **Empty Files** - Create files with no content (e.g., `.prettierignore`)
74
+ - **YAML Comments** - Add header comments and schema directives to YAML files
75
+ - **GitHub & Azure DevOps** - Works with both platforms
76
+ - **Auto-Merge PRs** - Automatically merge PRs when checks pass, or force merge with admin privileges
77
+ - **Dry-Run Mode** - Preview changes without creating PRs
78
+ - **Error Resilience** - Continues processing if individual repos fail
79
+ - **Automatic Retries** - Retries transient network errors with exponential backoff
80
+
81
+ ## Installation
82
+
83
+ ### From npm
84
+
85
+ ```bash
86
+ npm install -g @aspruyt/xfg
87
+ ```
88
+
89
+ ### From Source
90
+
91
+ ```bash
92
+ git clone https://github.com/anthony-spruyt/xfg.git
93
+ cd xfg
94
+ npm install
95
+ npm run build
96
+ ```
97
+
98
+ ### Using Dev Container
99
+
100
+ Open this repository in VS Code with the Dev Containers extension. The container includes all dependencies pre-installed and the project pre-built.
101
+
102
+ ## Prerequisites
103
+
104
+ ### GitHub Authentication
105
+
106
+ Before using with GitHub repositories, authenticate with the GitHub CLI:
107
+
108
+ ```bash
109
+ gh auth login
110
+ ```
111
+
112
+ ### Azure DevOps Authentication
113
+
114
+ Before using with Azure DevOps repositories, authenticate with the Azure CLI:
115
+
116
+ ```bash
117
+ az login
118
+ az devops configure --defaults organization=https://dev.azure.com/YOUR_ORG project=YOUR_PROJECT
119
+ ```
120
+
121
+ ## Usage
122
+
123
+ ```bash
124
+ # Basic usage
125
+ xfg --config ./config.yaml
126
+
127
+ # Dry run (no changes made)
128
+ xfg --config ./config.yaml --dry-run
129
+
130
+ # Custom work directory
131
+ xfg --config ./config.yaml --work-dir ./my-temp
132
+
133
+ # Custom branch name
134
+ xfg --config ./config.yaml --branch feature/update-eslint
135
+ ```
136
+
137
+ ### Options
138
+
139
+ | Option | Alias | Description | Required |
140
+ | ------------------ | ----- | ------------------------------------------------------------------------------------ | -------- |
141
+ | `--config` | `-c` | Path to YAML config file | Yes |
142
+ | `--dry-run` | `-d` | Show what would be done without making changes | No |
143
+ | `--work-dir` | `-w` | Temporary directory for cloning (default: `./tmp`) | No |
144
+ | `--retries` | `-r` | Number of retries for network operations (default: 3) | No |
145
+ | `--branch` | `-b` | Override branch name (default: `chore/sync-config`) | No |
146
+ | `--merge` | `-m` | PR merge mode: `manual` (default), `auto` (merge when checks pass), `force` (bypass) | No |
147
+ | `--merge-strategy` | | Merge strategy: `merge` (default), `squash`, `rebase` | No |
148
+ | `--delete-branch` | | Delete source branch after merge | No |
149
+
150
+ ## Configuration Format
151
+
152
+ ### Basic Structure
153
+
154
+ ```yaml
155
+ files:
156
+ my.config.json: # Target file (.json outputs JSON, .yaml/.yml outputs YAML)
157
+ mergeStrategy: replace # Array merge strategy for this file (optional)
158
+ content: # Base config content
159
+ key: value
160
+
161
+ repos: # List of repositories
162
+ - git: git@github.com:org/repo.git
163
+ files: # Per-repo file overrides (optional)
164
+ my.config.json:
165
+ content:
166
+ key: override
167
+ ```
168
+
169
+ ### Root-Level Fields
170
+
171
+ | Field | Description | Required |
172
+ | ----------- | ---------------------------------------------------- | -------- |
173
+ | `files` | Map of target filenames to configs | Yes |
174
+ | `repos` | Array of repository configurations | Yes |
175
+ | `prOptions` | Global PR merge options (can be overridden per-repo) | No |
176
+
177
+ ### Per-File Fields
178
+
179
+ | Field | Description | Required |
180
+ | --------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- | -------- |
181
+ | `content` | Base config: object for JSON/YAML files, string or string[] for text files, or `@path/to/file` to load from external template (omit for empty file) | No |
182
+ | `mergeStrategy` | Merge strategy: `replace`, `append`, `prepend` (for arrays and text lines) | No |
183
+ | `createOnly` | If `true`, only create file if it doesn't exist | No |
184
+ | `executable` | Mark file as executable. `.sh` files are auto-executable unless set to `false`. Set to `true` for non-.sh files. | No |
185
+ | `header` | Comment line(s) at top of YAML files (string or array) | No |
186
+ | `schemaUrl` | Adds `# yaml-language-server: $schema=<url>` to YAML files | No |
187
+
188
+ ### Per-Repo Fields
189
+
190
+ | Field | Description | Required |
191
+ | ----------- | -------------------------------------------- | -------- |
192
+ | `git` | Git URL (string) or array of URLs | Yes |
193
+ | `files` | Per-repo file overrides (optional) | No |
194
+ | `prOptions` | Per-repo PR merge options (overrides global) | No |
195
+
196
+ ### PR Options Fields
197
+
198
+ | Field | Description | Default |
199
+ | --------------- | --------------------------------------------------------------------------- | -------- |
200
+ | `merge` | Merge mode: `manual` (leave open), `auto` (merge when checks pass), `force` | `manual` |
201
+ | `mergeStrategy` | How to merge: `merge`, `squash`, `rebase` | `merge` |
202
+ | `deleteBranch` | Delete source branch after merge | `false` |
203
+ | `bypassReason` | Reason for bypassing policies (Azure DevOps only, required for `force`) | - |
204
+
205
+ ### Per-Repo File Override Fields
206
+
207
+ | Field | Description | Required |
208
+ | ------------ | ------------------------------------------------------- | -------- |
209
+ | `content` | Content overlay merged onto file's base content | No |
210
+ | `override` | If `true`, ignore base content and use only this repo's | No |
211
+ | `createOnly` | Override root-level `createOnly` for this repo | No |
212
+ | `executable` | Override root-level `executable` for this repo | No |
213
+ | `header` | Override root-level `header` for this repo | No |
214
+ | `schemaUrl` | Override root-level `schemaUrl` for this repo | No |
215
+
216
+ **File Exclusion:** Set a file to `false` to exclude it from a specific repo:
217
+
218
+ ```yaml
219
+ repos:
220
+ - git: git@github.com:org/repo.git
221
+ files:
222
+ eslint.json: false # This repo won't receive eslint.json
223
+ ```
224
+
225
+ ### Environment Variables
226
+
227
+ Use `${VAR}` syntax in string values:
228
+
229
+ ```yaml
230
+ files:
231
+ app.config.json:
232
+ content:
233
+ apiUrl: ${API_URL} # Required - errors if not set
234
+ environment: ${ENV:-development} # With default value
235
+ secretKey: ${SECRET:?Secret required} # Required with custom error message
236
+
237
+ repos:
238
+ - git: git@github.com:org/backend.git
239
+ ```
240
+
241
+ #### Escaping Variable Syntax
242
+
243
+ If your target file needs literal `${VAR}` syntax (e.g., for devcontainer.json, shell scripts, or other templating systems), use `$$` to escape:
244
+
245
+ ```yaml
246
+ files:
247
+ .devcontainer/devcontainer.json:
248
+ content:
249
+ name: my-dev-container
250
+ remoteEnv:
251
+ # Escaped - outputs literal ${localWorkspaceFolder} for VS Code
252
+ LOCAL_WORKSPACE_FOLDER: "$${localWorkspaceFolder}"
253
+ CONTAINER_WORKSPACE: "$${containerWorkspaceFolder}"
254
+ # Interpolated - replaced with actual env value
255
+ API_KEY: "${API_KEY}"
256
+ ```
257
+
258
+ Output:
259
+
260
+ ```json
261
+ {
262
+ "name": "my-dev-container",
263
+ "remoteEnv": {
264
+ "LOCAL_WORKSPACE_FOLDER": "${localWorkspaceFolder}",
265
+ "CONTAINER_WORKSPACE": "${containerWorkspaceFolder}",
266
+ "API_KEY": "actual-api-key-value"
267
+ }
268
+ }
269
+ ```
270
+
271
+ This follows the same escape convention used by Docker Compose.
272
+
273
+ ### Merge Directives
274
+
275
+ Control array merging with the `$arrayMerge` directive:
276
+
277
+ ```yaml
278
+ files:
279
+ config.json:
280
+ content:
281
+ features:
282
+ - core
283
+ - monitoring
284
+
285
+ repos:
286
+ - git: git@github.com:org/repo.git
287
+ files:
288
+ config.json:
289
+ content:
290
+ features:
291
+ $arrayMerge: append # append | prepend | replace
292
+ values:
293
+ - custom-feature # Results in: [core, monitoring, custom-feature]
294
+ ```
295
+
296
+ ## Examples
297
+
298
+ ### Multi-File Sync
299
+
300
+ Sync multiple configuration files to all repos:
301
+
302
+ ```yaml
303
+ files:
304
+ .eslintrc.json:
305
+ content:
306
+ extends: ["@org/eslint-config"]
307
+ rules:
308
+ no-console: warn
309
+
310
+ .prettierrc.yaml:
311
+ content:
312
+ semi: false
313
+ singleQuote: true
314
+
315
+ tsconfig.json:
316
+ content:
317
+ compilerOptions:
318
+ strict: true
319
+ target: ES2022
320
+
321
+ repos:
322
+ - git:
323
+ - git@github.com:org/frontend.git
324
+ - git@github.com:org/backend.git
325
+ - git@github.com:org/shared-lib.git
326
+ ```
327
+
328
+ ### Shared Config Across Teams
329
+
330
+ Define common settings once, customize per team:
331
+
332
+ ```yaml
333
+ files:
334
+ service.config.json:
335
+ content:
336
+ version: "2.0"
337
+ logging:
338
+ level: info
339
+ format: json
340
+ features:
341
+ - health-check
342
+ - metrics
343
+
344
+ repos:
345
+ # Platform team repos - add extra features
346
+ - git:
347
+ - git@github.com:org/api-gateway.git
348
+ - git@github.com:org/auth-service.git
349
+ files:
350
+ service.config.json:
351
+ content:
352
+ team: platform
353
+ features:
354
+ $arrayMerge: append
355
+ values:
356
+ - tracing
357
+ - rate-limiting
358
+
359
+ # Data team repos - different logging
360
+ - git:
361
+ - git@github.com:org/data-pipeline.git
362
+ - git@github.com:org/analytics.git
363
+ files:
364
+ service.config.json:
365
+ content:
366
+ team: data
367
+ logging:
368
+ level: debug
369
+
370
+ # Legacy service - completely different config
371
+ - git: git@github.com:org/legacy-api.git
372
+ files:
373
+ service.config.json:
374
+ override: true
375
+ content:
376
+ version: "1.0"
377
+ legacy: true
378
+ ```
379
+
380
+ ### Environment-Specific Values
381
+
382
+ Use environment variables for secrets and environment-specific values:
383
+
384
+ ```yaml
385
+ files:
386
+ app.config.json:
387
+ content:
388
+ database:
389
+ host: ${DB_HOST:-localhost}
390
+ port: ${DB_PORT:-5432}
391
+ password: ${DB_PASSWORD:?Database password required}
392
+
393
+ api:
394
+ baseUrl: ${API_BASE_URL}
395
+ timeout: 30000
396
+
397
+ repos:
398
+ - git: git@github.com:org/backend.git
399
+ ```
400
+
401
+ ### Per-File Merge Strategies
402
+
403
+ Different files can use different array merge strategies:
404
+
405
+ ```yaml
406
+ files:
407
+ eslint.config.json:
408
+ mergeStrategy: append # Extends will append
409
+ content:
410
+ extends: ["@company/base"]
411
+
412
+ tsconfig.json:
413
+ mergeStrategy: replace # Lib will replace entirely
414
+ content:
415
+ compilerOptions:
416
+ lib: ["ES2022"]
417
+
418
+ repos:
419
+ - git: git@github.com:org/frontend.git
420
+ files:
421
+ eslint.config.json:
422
+ content:
423
+ extends: ["plugin:react/recommended"]
424
+ # Results in extends: ["@company/base", "plugin:react/recommended"]
425
+ ```
426
+
427
+ ### Create-Only Files (Defaults That Can Be Customized)
428
+
429
+ Some files should only be created once as defaults, allowing repos to maintain their own versions:
430
+
431
+ ```yaml
432
+ files:
433
+ .trivyignore.yaml:
434
+ createOnly: true # Only create if doesn't exist
435
+ content:
436
+ vulnerabilities: []
437
+
438
+ .prettierignore:
439
+ createOnly: true
440
+ content:
441
+ patterns:
442
+ - "dist/"
443
+ - "node_modules/"
444
+
445
+ eslint.config.json:
446
+ content: # Always synced (no createOnly)
447
+ extends: ["@company/base"]
448
+
449
+ repos:
450
+ - git: git@github.com:org/repo.git
451
+ # .trivyignore.yaml and .prettierignore only created if missing
452
+ # eslint.config.json always updated
453
+
454
+ - git: git@github.com:org/special-repo.git
455
+ files:
456
+ .trivyignore.yaml:
457
+ createOnly: false # Override: always sync this file
458
+ ```
459
+
460
+ ### YAML Comments and Empty Files
461
+
462
+ Add schema directives and comments to YAML files, or create empty files:
463
+
464
+ ```yaml
465
+ files:
466
+ # YAML file with schema directive and header comment
467
+ trivy.yaml:
468
+ schemaUrl: https://trivy.dev/latest/docs/references/configuration/config-file/
469
+ header: "Trivy security scanner configuration"
470
+ content:
471
+ exit-code: 1
472
+ scan:
473
+ scanners:
474
+ - vuln
475
+
476
+ # Empty file (content omitted)
477
+ .prettierignore:
478
+ createOnly: true
479
+ # No content = empty file
480
+
481
+ # YAML with multi-line header
482
+ config.yaml:
483
+ header:
484
+ - "Auto-generated configuration"
485
+ - "Do not edit manually"
486
+ content:
487
+ version: 1
488
+
489
+ repos:
490
+ - git: git@github.com:org/repo.git
491
+ ```
492
+
493
+ **Output for trivy.yaml:**
494
+
495
+ ```yaml
496
+ # yaml-language-server: $schema=https://trivy.dev/latest/docs/references/configuration/config-file/
497
+ # Trivy security scanner configuration
498
+ exit-code: 1
499
+ scan:
500
+ scanners:
501
+ - vuln
502
+ ```
503
+
504
+ **Note:** `header` and `schemaUrl` only apply to YAML output files (`.yaml`, `.yml`). They are ignored for JSON files.
505
+
506
+ ### Text Files
507
+
508
+ Sync text files like `.gitignore`, `.markdownlintignore`, or `.env.example` using string or lines array content:
509
+
510
+ ```yaml
511
+ files:
512
+ # String content (multiline text)
513
+ .markdownlintignore:
514
+ createOnly: true
515
+ content: |-
516
+ # Claude Code generated files
517
+ .claude/
518
+
519
+ # Lines array with merge strategy
520
+ .gitignore:
521
+ mergeStrategy: append
522
+ content:
523
+ - "node_modules/"
524
+ - "dist/"
525
+
526
+ repos:
527
+ - git: git@github.com:org/repo.git
528
+ files:
529
+ .gitignore:
530
+ content:
531
+ - "coverage/" # Appended to base lines
532
+ ```
533
+
534
+ **Content Types:**
535
+
536
+ - **String content** (`content: |-`) - Direct text output with environment variable interpolation. Merging always replaces the base.
537
+ - **Lines array** (`content: ['line1', 'line2']`) - Each line joined with newlines. Supports merge strategies (`append`, `prepend`, `replace`).
538
+
539
+ **Validation:** JSON/JSON5/YAML file extensions (`.json`, `.json5`, `.yaml`, `.yml`) require object content. Other extensions require string or string[] content.
540
+
541
+ ### Executable Files
542
+
543
+ Shell scripts (`.sh` files) are automatically marked as executable using `git update-index --add --chmod=+x`. You can control this behavior:
544
+
545
+ ```yaml
546
+ files:
547
+ # .sh files are auto-executable (no config needed)
548
+ deploy.sh:
549
+ content: |-
550
+ #!/bin/bash
551
+ echo "Deploying..."
552
+
553
+ # Disable auto-executable for a specific .sh file
554
+ template.sh:
555
+ executable: false
556
+ content: "# This is just a template"
557
+
558
+ # Make a non-.sh file executable
559
+ run:
560
+ executable: true
561
+ content: |-
562
+ #!/usr/bin/env python3
563
+ print("Hello")
564
+
565
+ repos:
566
+ - git: git@github.com:org/repo.git
567
+ files:
568
+ # Override executable per-repo
569
+ deploy.sh:
570
+ executable: false # Disable for this repo
571
+ ```
572
+
573
+ **Behavior:**
574
+
575
+ - `.sh` files: Automatically executable unless `executable: false`
576
+ - Other files: Not executable unless `executable: true`
577
+ - Per-repo settings override root-level settings
578
+
579
+ ### Subdirectory Paths
580
+
581
+ Sync files to any subdirectory path - parent directories are created automatically:
582
+
583
+ ```yaml
584
+ files:
585
+ # GitHub Actions workflow
586
+ ".github/workflows/ci.yml":
587
+ content:
588
+ name: CI
589
+ on: [push, pull_request]
590
+ jobs:
591
+ build:
592
+ runs-on: ubuntu-latest
593
+ steps:
594
+ - uses: actions/checkout@v4
595
+
596
+ # Nested config directory
597
+ "config/settings/app.json":
598
+ content:
599
+ environment: production
600
+ debug: false
601
+
602
+ repos:
603
+ - git:
604
+ - git@github.com:org/frontend.git
605
+ - git@github.com:org/backend.git
606
+ ```
607
+
608
+ **Note:** Quote file paths containing `/` in YAML keys. Parent directories are created if they don't exist.
609
+
610
+ ### File References
611
+
612
+ Instead of inline content, you can reference external template files using the `@path/to/file` syntax:
613
+
614
+ ```yaml
615
+ files:
616
+ .prettierrc.json:
617
+ content: "@templates/prettierrc.json"
618
+ .eslintrc.yaml:
619
+ content: "@templates/eslintrc.yaml"
620
+ header: "Auto-generated - do not edit"
621
+ schemaUrl: "https://json.schemastore.org/eslintrc"
622
+ .gitignore:
623
+ content: "@templates/gitignore.txt"
624
+
625
+ repos:
626
+ - git: git@github.com:org/repo.git
627
+ ```
628
+
629
+ **How it works:**
630
+
631
+ - File references start with `@` followed by a relative path
632
+ - Paths are resolved relative to the config file's directory
633
+ - JSON/JSON5/YAML files are parsed as objects, other files as strings
634
+ - Metadata fields (`header`, `schemaUrl`, `createOnly`, `mergeStrategy`) remain in the config
635
+ - Per-repo overlays still work - they merge onto the resolved file content
636
+
637
+ **Example directory structure:**
638
+
639
+ ```
640
+ config/
641
+ sync-config.yaml # content: "@templates/prettier.json"
642
+ templates/
643
+ prettier.json # Actual Prettier config
644
+ eslintrc.yaml # Actual ESLint config
645
+ gitignore.txt # Template .gitignore content
646
+ ```
647
+
648
+ **Security:** File references are restricted to the config file's directory tree. Paths like `@../../../etc/passwd` or `@/etc/passwd` are blocked.
649
+
650
+ ### Auto-Merge PRs
651
+
652
+ Configure PRs to merge automatically when checks pass, or force merge using admin privileges:
653
+
654
+ ```yaml
655
+ files:
656
+ .prettierrc.json:
657
+ content:
658
+ semi: false
659
+ singleQuote: true
660
+
661
+ # Global PR options - apply to all repos
662
+ prOptions:
663
+ merge: auto # auto-merge when checks pass
664
+ mergeStrategy: squash # squash commits
665
+ deleteBranch: true # cleanup after merge
666
+
667
+ repos:
668
+ # These repos use global prOptions (auto-merge)
669
+ - git:
670
+ - git@github.com:org/frontend.git
671
+ - git@github.com:org/backend.git
672
+
673
+ # This repo overrides to force merge (bypass required reviews)
674
+ - git: git@github.com:org/internal-tool.git
675
+ prOptions:
676
+ merge: force
677
+ bypassReason: "Automated config sync" # Azure DevOps only
678
+ ```
679
+
680
+ **Merge Modes:**
681
+
682
+ | Mode | GitHub Behavior | Azure DevOps Behavior |
683
+ | -------- | --------------------------------------- | -------------------------------------- |
684
+ | `manual` | Leave PR open for review (default) | Leave PR open for review |
685
+ | `auto` | Enable auto-merge (requires repo setup) | Enable auto-complete |
686
+ | `force` | Merge with `--admin` (bypass checks) | Bypass policies with `--bypass-policy` |
687
+
688
+ **GitHub Auto-Merge Note:** The `auto` mode requires auto-merge to be enabled in the repository settings. If not enabled, the tool will warn and leave the PR open for manual review. Enable it with:
689
+
690
+ ```bash
691
+ gh repo edit org/repo --enable-auto-merge
692
+ ```
693
+
694
+ **CLI Override:** You can override config file settings with CLI flags:
695
+
696
+ ```bash
697
+ # Force merge all PRs (useful for urgent updates)
698
+ xfg --config ./config.yaml --merge force
699
+
700
+ # Enable auto-merge with squash
701
+ xfg --config ./config.yaml --merge auto --merge-strategy squash --delete-branch
702
+ ```
703
+
704
+ ## Supported Git URL Formats
705
+
706
+ ### GitHub
707
+
708
+ - SSH: `git@github.com:owner/repo.git`
709
+ - HTTPS: `https://github.com/owner/repo.git`
710
+
711
+ ### Azure DevOps
712
+
713
+ - SSH: `git@ssh.dev.azure.com:v3/organization/project/repo`
714
+ - HTTPS: `https://dev.azure.com/organization/project/_git/repo`
715
+
716
+ ## How It Works
717
+
718
+ ```mermaid
719
+ flowchart TB
720
+ subgraph Input
721
+ YAML[/"YAML Config File<br/>files{} + repos[]"/]
722
+ end
723
+
724
+ subgraph Normalization
725
+ EXPAND[Expand git arrays] --> MERGE[Merge base + overlay content<br/>for each file]
726
+ MERGE --> ENV[Interpolate env vars]
727
+ end
728
+
729
+ subgraph Processing["For Each Repository"]
730
+ CLONE[Clone Repo] --> BRANCH[Create/Checkout Branch<br/>--branch or chore/sync-config]
731
+ BRANCH --> WRITE[Write All Config Files<br/>JSON, JSON5, or YAML]
732
+ WRITE --> CHECK{Changes?}
733
+ CHECK -->|No| SKIP[Skip - No Changes]
734
+ CHECK -->|Yes| COMMIT[Commit Changes]
735
+ COMMIT --> PUSH[Push to Remote]
736
+ end
737
+
738
+ subgraph Platform["Platform Detection"]
739
+ PUSH --> DETECT{GitHub or<br/>Azure DevOps?}
740
+ end
741
+
742
+ subgraph Output
743
+ DETECT -->|GitHub| GH_PR[Create PR via gh CLI]
744
+ DETECT -->|Azure DevOps| AZ_PR[Create PR via az CLI]
745
+ GH_PR --> GH_URL[/"GitHub PR URL"/]
746
+ AZ_PR --> AZ_URL[/"Azure DevOps PR URL"/]
747
+ end
748
+
749
+ YAML --> EXPAND
750
+ ENV --> CLONE
751
+ ```
752
+
753
+ For each repository in the config, the tool:
754
+
755
+ 1. Expands git URL arrays into individual entries
756
+ 2. For each file, merges base content with per-repo overlay
757
+ 3. Interpolates environment variables
758
+ 4. Cleans the temporary workspace
759
+ 5. Clones the repository
760
+ 6. Creates/checks out branch (custom `--branch` or default `chore/sync-config`)
761
+ 7. Writes all config files (JSON, JSON5, or YAML based on filename extension)
762
+ 8. Checks for changes (skips if no changes)
763
+ 9. Commits and pushes changes
764
+ 10. Creates a pull request
765
+
766
+ ## CI/CD Integration
767
+
768
+ ### GitHub Actions
769
+
770
+ ```yaml
771
+ name: Sync Configs
772
+ on:
773
+ push:
774
+ branches: [main]
775
+ paths: ["config.yaml"]
776
+
777
+ jobs:
778
+ sync:
779
+ runs-on: ubuntu-latest
780
+ steps:
781
+ - uses: actions/checkout@v4
782
+ - uses: actions/setup-node@v4
783
+ with:
784
+ node-version: "20"
785
+ - run: npm install -g @aspruyt/xfg
786
+ - run: xfg --config ./config.yaml
787
+ env:
788
+ GH_TOKEN: ${{ secrets.GH_PAT }}
789
+ ```
790
+
791
+ > **Note:** `GH_PAT` must be a Personal Access Token with `repo` scope to create PRs in target repositories.
792
+
793
+ ### Azure Pipelines
794
+
795
+ ```yaml
796
+ trigger:
797
+ branches:
798
+ include: [main]
799
+ paths:
800
+ include: ["config.yaml"]
801
+
802
+ pool:
803
+ vmImage: "ubuntu-latest"
804
+
805
+ steps:
806
+ - task: NodeTool@0
807
+ inputs:
808
+ versionSpec: "20.x"
809
+ - script: npm install -g @aspruyt/xfg
810
+ displayName: "Install xfg"
811
+ - script: xfg --config ./config.yaml
812
+ displayName: "Sync configs"
813
+ env:
814
+ AZURE_DEVOPS_EXT_PAT: $(System.AccessToken)
815
+ ```
816
+
817
+ > **Note:** Ensure the build service account has permission to create PRs in target repositories.
818
+
819
+ ## Output Examples
820
+
821
+ ### Console Output
822
+
823
+ ```
824
+ [1/3] Processing example-org/repo1...
825
+ ✓ Cloned repository
826
+ ✓ Created branch chore/sync-config
827
+ ✓ Wrote .eslintrc.json
828
+ ✓ Wrote .prettierrc.yaml
829
+ ✓ Committed changes
830
+ ✓ Pushed to remote
831
+ ✓ Created PR: https://github.com/example-org/repo1/pull/42
832
+
833
+ [2/3] Processing example-org/repo2...
834
+ ✓ Cloned repository
835
+ ✓ Checked out existing branch chore/sync-config
836
+ ✓ Wrote .eslintrc.json
837
+ ✓ Wrote .prettierrc.yaml
838
+ ⊘ No changes detected, skipping
839
+
840
+ [3/3] Processing example-org/repo3...
841
+ ✓ Cloned repository
842
+ ✓ Created branch chore/sync-config
843
+ ✓ Wrote .eslintrc.json
844
+ ✓ Wrote .prettierrc.yaml
845
+ ✓ Committed changes
846
+ ✓ Pushed to remote
847
+ ✓ PR already exists: https://github.com/example-org/repo3/pull/15
848
+
849
+ Summary: 2 succeeded, 1 skipped, 0 failed
850
+ ```
851
+
852
+ ### Created PR
853
+
854
+ The tool creates PRs with:
855
+
856
+ - **Title:** `chore: sync config files` (or lists files if ≤3)
857
+ - **Branch:** `chore/sync-config` (or custom `--branch`)
858
+ - **Body:** Describes the sync action and lists changed files
859
+
860
+ ## Troubleshooting
861
+
862
+ ### Authentication Errors
863
+
864
+ **GitHub:**
865
+
866
+ ```bash
867
+ # Check authentication status
868
+ gh auth status
869
+
870
+ # Re-authenticate if needed
871
+ gh auth login
872
+ ```
873
+
874
+ **Azure DevOps:**
875
+
876
+ ```bash
877
+ # Check authentication status
878
+ az account show
879
+
880
+ # Re-authenticate if needed
881
+ az login
882
+ az devops configure --defaults organization=https://dev.azure.com/YOUR_ORG
883
+ ```
884
+
885
+ ### Permission Denied
886
+
887
+ - Ensure your token has write access to the target repositories
888
+ - For GitHub, the token needs `repo` scope
889
+ - For Azure DevOps, ensure the user/service account has "Contribute to pull requests" permission
890
+
891
+ ### Branch Already Exists
892
+
893
+ The tool automatically reuses existing branches. If you see unexpected behavior:
894
+
895
+ ```bash
896
+ # Delete the remote branch to start fresh
897
+ git push origin --delete chore/sync-config
898
+ ```
899
+
900
+ ### Missing Environment Variables
901
+
902
+ If you see "Missing required environment variable" errors:
903
+
904
+ ```bash
905
+ # Set the variable before running
906
+ export MY_VAR=value
907
+ xfg --config ./config.yaml
908
+
909
+ # Or use default values in config
910
+ # ${MY_VAR:-default-value}
911
+ ```
912
+
913
+ ### Network/Proxy Issues
914
+
915
+ If cloning fails behind a corporate proxy:
916
+
917
+ ```bash
918
+ # Configure git proxy
919
+ git config --global http.proxy http://proxy.example.com:8080
920
+ git config --global https.proxy http://proxy.example.com:8080
921
+ ```
922
+
923
+ ### Transient Network Errors
924
+
925
+ The tool automatically retries transient errors (timeouts, connection resets, rate limits) with exponential backoff. By default, it retries 3 times before failing.
926
+
927
+ ```bash
928
+ # Increase retries for unreliable networks
929
+ xfg --config ./config.yaml --retries 5
930
+
931
+ # Disable retries
932
+ xfg --config ./config.yaml --retries 0
933
+ ```
934
+
935
+ Permanent errors (authentication failures, permission denied, repository not found) are not retried.
936
+
937
+ ## IDE Integration
938
+
939
+ ### VS Code YAML Schema Support
940
+
941
+ For autocomplete and validation in VS Code, install the [YAML extension](https://marketplace.visualstudio.com/items?itemName=redhat.vscode-yaml) and add a schema reference to your config file:
942
+
943
+ **Option 1: Inline comment**
944
+
945
+ ```yaml
946
+ # yaml-language-server: $schema=https://raw.githubusercontent.com/anthony-spruyt/xfg/main/config-schema.json
947
+ files:
948
+ my.config.json:
949
+ content:
950
+ key: value
951
+
952
+ repos:
953
+ - git: git@github.com:org/repo.git
954
+ ```
955
+
956
+ **Option 2: VS Code settings** (`.vscode/settings.json`)
957
+
958
+ ```json
959
+ {
960
+ "yaml.schemas": {
961
+ "https://raw.githubusercontent.com/anthony-spruyt/xfg/main/config-schema.json": [
962
+ "**/sync-config.yaml",
963
+ "**/config-sync.yaml"
964
+ ]
965
+ }
966
+ }
967
+ ```
968
+
969
+ This enables:
970
+
971
+ - Autocomplete for `files`, `repos`, `content`, `mergeStrategy`, `git`, `override`
972
+ - Enum suggestions for `mergeStrategy` values (`replace`, `append`, `prepend`)
973
+ - Validation of required fields
974
+ - Hover documentation for each field
975
+
976
+ ## Development
977
+
978
+ ```bash
979
+ # Run in development mode
980
+ npm run dev -- --config ./fixtures/test-repos-input.yaml --dry-run
981
+
982
+ # Run tests
983
+ npm test
984
+
985
+ # Build
986
+ npm run build
987
+ ```
988
+
989
+ ## License
990
+
991
+ MIT