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