@aspruyt/json-config-sync 3.0.0 → 3.2.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 +95 -8
- package/dist/config-formatter.d.ts +9 -1
- package/dist/config-formatter.js +61 -3
- package/dist/config-normalizer.js +39 -7
- package/dist/config-validator.js +30 -0
- package/dist/config.d.ts +11 -2
- package/dist/pr-creator.d.ts +2 -2
- package/dist/pr-creator.js +24 -22
- package/dist/repository-processor.d.ts +1 -1
- package/dist/repository-processor.js +29 -18
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -66,6 +66,8 @@ json-config-sync --config ./config.yaml
|
|
|
66
66
|
- **Environment Variables** - Use `${VAR}` syntax for dynamic values
|
|
67
67
|
- **Merge Strategies** - Control how arrays merge (replace, append, prepend)
|
|
68
68
|
- **Override Mode** - Skip merging entirely for specific repos
|
|
69
|
+
- **Empty Files** - Create files with no content (e.g., `.prettierignore`)
|
|
70
|
+
- **YAML Comments** - Add header comments and schema directives to YAML files
|
|
69
71
|
- **GitHub & Azure DevOps** - Works with both platforms
|
|
70
72
|
- **Dry-Run Mode** - Preview changes without creating PRs
|
|
71
73
|
- **Error Resilience** - Continues processing if individual repos fail
|
|
@@ -165,10 +167,13 @@ repos: # List of repositories
|
|
|
165
167
|
|
|
166
168
|
### Per-File Fields
|
|
167
169
|
|
|
168
|
-
| Field | Description
|
|
169
|
-
| --------------- |
|
|
170
|
-
| `content` | Base config inherited by all repos
|
|
171
|
-
| `mergeStrategy` | Array merge strategy: `replace`, `append`, `prepend`
|
|
170
|
+
| Field | Description | Required |
|
|
171
|
+
| --------------- | ---------------------------------------------------------- | -------- |
|
|
172
|
+
| `content` | Base config inherited by all repos (omit for empty file) | No |
|
|
173
|
+
| `mergeStrategy` | Array merge strategy: `replace`, `append`, `prepend` | No |
|
|
174
|
+
| `createOnly` | If `true`, only create file if it doesn't exist | No |
|
|
175
|
+
| `header` | Comment line(s) at top of YAML files (string or array) | No |
|
|
176
|
+
| `schemaUrl` | Adds `# yaml-language-server: $schema=<url>` to YAML files | No |
|
|
172
177
|
|
|
173
178
|
### Per-Repo Fields
|
|
174
179
|
|
|
@@ -179,10 +184,13 @@ repos: # List of repositories
|
|
|
179
184
|
|
|
180
185
|
### Per-Repo File Override Fields
|
|
181
186
|
|
|
182
|
-
| Field
|
|
183
|
-
|
|
|
184
|
-
| `content`
|
|
185
|
-
| `override`
|
|
187
|
+
| Field | Description | Required |
|
|
188
|
+
| ------------ | ------------------------------------------------------- | -------- |
|
|
189
|
+
| `content` | Content overlay merged onto file's base content | No |
|
|
190
|
+
| `override` | If `true`, ignore base content and use only this repo's | No |
|
|
191
|
+
| `createOnly` | Override root-level `createOnly` for this repo | No |
|
|
192
|
+
| `header` | Override root-level `header` for this repo | No |
|
|
193
|
+
| `schemaUrl` | Override root-level `schemaUrl` for this repo | No |
|
|
186
194
|
|
|
187
195
|
**File Exclusion:** Set a file to `false` to exclude it from a specific repo:
|
|
188
196
|
|
|
@@ -363,6 +371,85 @@ repos:
|
|
|
363
371
|
# Results in extends: ["@company/base", "plugin:react/recommended"]
|
|
364
372
|
```
|
|
365
373
|
|
|
374
|
+
### Create-Only Files (Defaults That Can Be Customized)
|
|
375
|
+
|
|
376
|
+
Some files should only be created once as defaults, allowing repos to maintain their own versions:
|
|
377
|
+
|
|
378
|
+
```yaml
|
|
379
|
+
files:
|
|
380
|
+
.trivyignore.yaml:
|
|
381
|
+
createOnly: true # Only create if doesn't exist
|
|
382
|
+
content:
|
|
383
|
+
vulnerabilities: []
|
|
384
|
+
|
|
385
|
+
.prettierignore:
|
|
386
|
+
createOnly: true
|
|
387
|
+
content:
|
|
388
|
+
patterns:
|
|
389
|
+
- "dist/"
|
|
390
|
+
- "node_modules/"
|
|
391
|
+
|
|
392
|
+
eslint.config.json:
|
|
393
|
+
content: # Always synced (no createOnly)
|
|
394
|
+
extends: ["@company/base"]
|
|
395
|
+
|
|
396
|
+
repos:
|
|
397
|
+
- git: git@github.com:org/repo.git
|
|
398
|
+
# .trivyignore.yaml and .prettierignore only created if missing
|
|
399
|
+
# eslint.config.json always updated
|
|
400
|
+
|
|
401
|
+
- git: git@github.com:org/special-repo.git
|
|
402
|
+
files:
|
|
403
|
+
.trivyignore.yaml:
|
|
404
|
+
createOnly: false # Override: always sync this file
|
|
405
|
+
```
|
|
406
|
+
|
|
407
|
+
### YAML Comments and Empty Files
|
|
408
|
+
|
|
409
|
+
Add schema directives and comments to YAML files, or create empty files:
|
|
410
|
+
|
|
411
|
+
```yaml
|
|
412
|
+
files:
|
|
413
|
+
# YAML file with schema directive and header comment
|
|
414
|
+
trivy.yaml:
|
|
415
|
+
schemaUrl: https://trivy.dev/latest/docs/references/configuration/config-file/
|
|
416
|
+
header: "Trivy security scanner configuration"
|
|
417
|
+
content:
|
|
418
|
+
exit-code: 1
|
|
419
|
+
scan:
|
|
420
|
+
scanners:
|
|
421
|
+
- vuln
|
|
422
|
+
|
|
423
|
+
# Empty file (content omitted)
|
|
424
|
+
.prettierignore:
|
|
425
|
+
createOnly: true
|
|
426
|
+
# No content = empty file
|
|
427
|
+
|
|
428
|
+
# YAML with multi-line header
|
|
429
|
+
config.yaml:
|
|
430
|
+
header:
|
|
431
|
+
- "Auto-generated configuration"
|
|
432
|
+
- "Do not edit manually"
|
|
433
|
+
content:
|
|
434
|
+
version: 1
|
|
435
|
+
|
|
436
|
+
repos:
|
|
437
|
+
- git: git@github.com:org/repo.git
|
|
438
|
+
```
|
|
439
|
+
|
|
440
|
+
**Output for trivy.yaml:**
|
|
441
|
+
|
|
442
|
+
```yaml
|
|
443
|
+
# yaml-language-server: $schema=https://trivy.dev/latest/docs/references/configuration/config-file/
|
|
444
|
+
# Trivy security scanner configuration
|
|
445
|
+
exit-code: 1
|
|
446
|
+
scan:
|
|
447
|
+
scanners:
|
|
448
|
+
- vuln
|
|
449
|
+
```
|
|
450
|
+
|
|
451
|
+
**Note:** `header` and `schemaUrl` only apply to YAML output files (`.yaml`, `.yml`). They are ignored for JSON files.
|
|
452
|
+
|
|
366
453
|
## Supported Git URL Formats
|
|
367
454
|
|
|
368
455
|
### GitHub
|
|
@@ -1,9 +1,17 @@
|
|
|
1
1
|
export type OutputFormat = "json" | "yaml";
|
|
2
|
+
/**
|
|
3
|
+
* Options for content conversion.
|
|
4
|
+
*/
|
|
5
|
+
export interface ConvertOptions {
|
|
6
|
+
header?: string[];
|
|
7
|
+
schemaUrl?: string;
|
|
8
|
+
}
|
|
2
9
|
/**
|
|
3
10
|
* Detects output format from file extension.
|
|
4
11
|
*/
|
|
5
12
|
export declare function detectOutputFormat(fileName: string): OutputFormat;
|
|
6
13
|
/**
|
|
7
14
|
* Converts content object to string in the appropriate format.
|
|
15
|
+
* Handles null content (empty files) and comments (YAML only).
|
|
8
16
|
*/
|
|
9
|
-
export declare function convertContentToString(content: Record<string, unknown
|
|
17
|
+
export declare function convertContentToString(content: Record<string, unknown> | null, fileName: string, options?: ConvertOptions): string;
|
package/dist/config-formatter.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { stringify } from "yaml";
|
|
1
|
+
import { Document, stringify } from "yaml";
|
|
2
2
|
/**
|
|
3
3
|
* Detects output format from file extension.
|
|
4
4
|
*/
|
|
@@ -9,13 +9,71 @@ export function detectOutputFormat(fileName) {
|
|
|
9
9
|
}
|
|
10
10
|
return "json";
|
|
11
11
|
}
|
|
12
|
+
/**
|
|
13
|
+
* Builds header comment string from header lines and schemaUrl.
|
|
14
|
+
* Returns undefined if no comments to add.
|
|
15
|
+
* Each line gets a space prefix since yaml library adds # directly.
|
|
16
|
+
*/
|
|
17
|
+
function buildHeaderComment(header, schemaUrl) {
|
|
18
|
+
const lines = [];
|
|
19
|
+
// Add yaml-language-server schema directive first (if present)
|
|
20
|
+
if (schemaUrl) {
|
|
21
|
+
lines.push(` yaml-language-server: $schema=${schemaUrl}`);
|
|
22
|
+
}
|
|
23
|
+
// Add custom header lines (with space prefix for proper formatting)
|
|
24
|
+
if (header && header.length > 0) {
|
|
25
|
+
lines.push(...header.map((h) => ` ${h}`));
|
|
26
|
+
}
|
|
27
|
+
if (lines.length === 0)
|
|
28
|
+
return undefined;
|
|
29
|
+
// Join with newlines - the yaml library adds # prefix to each line
|
|
30
|
+
return lines.join("\n");
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Builds comment-only output for empty YAML files with headers.
|
|
34
|
+
*/
|
|
35
|
+
function buildCommentOnlyYaml(header, schemaUrl) {
|
|
36
|
+
const lines = [];
|
|
37
|
+
// Add yaml-language-server schema directive first (if present)
|
|
38
|
+
if (schemaUrl) {
|
|
39
|
+
lines.push(`# yaml-language-server: $schema=${schemaUrl}`);
|
|
40
|
+
}
|
|
41
|
+
// Add custom header lines
|
|
42
|
+
if (header && header.length > 0) {
|
|
43
|
+
lines.push(...header.map((h) => `# ${h}`));
|
|
44
|
+
}
|
|
45
|
+
if (lines.length === 0)
|
|
46
|
+
return undefined;
|
|
47
|
+
return lines.join("\n") + "\n";
|
|
48
|
+
}
|
|
12
49
|
/**
|
|
13
50
|
* Converts content object to string in the appropriate format.
|
|
51
|
+
* Handles null content (empty files) and comments (YAML only).
|
|
14
52
|
*/
|
|
15
|
-
export function convertContentToString(content, fileName) {
|
|
53
|
+
export function convertContentToString(content, fileName, options) {
|
|
16
54
|
const format = detectOutputFormat(fileName);
|
|
55
|
+
// Handle empty file case
|
|
56
|
+
if (content === null) {
|
|
57
|
+
if (format === "yaml" && options) {
|
|
58
|
+
const commentOnly = buildCommentOnlyYaml(options.header, options.schemaUrl);
|
|
59
|
+
if (commentOnly) {
|
|
60
|
+
return commentOnly;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return "";
|
|
64
|
+
}
|
|
17
65
|
if (format === "yaml") {
|
|
18
|
-
|
|
66
|
+
// Use Document API for YAML to support comments
|
|
67
|
+
const doc = new Document(content);
|
|
68
|
+
// Add header comment if present
|
|
69
|
+
if (options) {
|
|
70
|
+
const headerComment = buildHeaderComment(options.header, options.schemaUrl);
|
|
71
|
+
if (headerComment) {
|
|
72
|
+
doc.commentBefore = headerComment;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return stringify(doc, { indent: 2 });
|
|
19
76
|
}
|
|
77
|
+
// JSON format - comments not supported, ignore header/schemaUrl
|
|
20
78
|
return JSON.stringify(content, null, 2);
|
|
21
79
|
}
|
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
import { deepMerge, stripMergeDirectives, createMergeContext, } from "./merge.js";
|
|
2
2
|
import { interpolateEnvVars } from "./env.js";
|
|
3
|
+
/**
|
|
4
|
+
* Normalizes header to array format.
|
|
5
|
+
*/
|
|
6
|
+
function normalizeHeader(header) {
|
|
7
|
+
if (header === undefined)
|
|
8
|
+
return undefined;
|
|
9
|
+
if (typeof header === "string")
|
|
10
|
+
return [header];
|
|
11
|
+
return header;
|
|
12
|
+
}
|
|
3
13
|
/**
|
|
4
14
|
* Normalizes raw config into expanded, merged config.
|
|
5
15
|
* Pipeline: expand git arrays -> merge content -> interpolate env vars
|
|
@@ -20,29 +30,51 @@ export function normalizeConfig(raw) {
|
|
|
20
30
|
continue;
|
|
21
31
|
}
|
|
22
32
|
const fileConfig = raw.files[fileName];
|
|
23
|
-
const baseContent = fileConfig.content ?? {};
|
|
24
33
|
const fileStrategy = fileConfig.mergeStrategy ?? "replace";
|
|
25
34
|
// Step 3: Compute merged content for this file
|
|
26
35
|
let mergedContent;
|
|
27
36
|
if (repoOverride?.override) {
|
|
28
|
-
// Override mode: use only repo file content
|
|
29
|
-
|
|
37
|
+
// Override mode: use only repo file content (may be undefined for empty file)
|
|
38
|
+
if (repoOverride.content === undefined) {
|
|
39
|
+
mergedContent = null;
|
|
40
|
+
}
|
|
41
|
+
else {
|
|
42
|
+
mergedContent = stripMergeDirectives(structuredClone(repoOverride.content));
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
else if (fileConfig.content === undefined) {
|
|
46
|
+
// Root file has no content = empty file (unless repo provides content)
|
|
47
|
+
if (repoOverride?.content) {
|
|
48
|
+
mergedContent = stripMergeDirectives(structuredClone(repoOverride.content));
|
|
49
|
+
}
|
|
50
|
+
else {
|
|
51
|
+
mergedContent = null;
|
|
52
|
+
}
|
|
30
53
|
}
|
|
31
54
|
else if (!repoOverride?.content) {
|
|
32
55
|
// No repo override: use file base content as-is
|
|
33
|
-
mergedContent = structuredClone(
|
|
56
|
+
mergedContent = structuredClone(fileConfig.content);
|
|
34
57
|
}
|
|
35
58
|
else {
|
|
36
59
|
// Merge mode: deep merge file base + repo overlay
|
|
37
60
|
const ctx = createMergeContext(fileStrategy);
|
|
38
|
-
mergedContent = deepMerge(structuredClone(
|
|
61
|
+
mergedContent = deepMerge(structuredClone(fileConfig.content), repoOverride.content, ctx);
|
|
39
62
|
mergedContent = stripMergeDirectives(mergedContent);
|
|
40
63
|
}
|
|
41
|
-
// Step 4: Interpolate env vars
|
|
42
|
-
|
|
64
|
+
// Step 4: Interpolate env vars (only if content exists)
|
|
65
|
+
if (mergedContent !== null) {
|
|
66
|
+
mergedContent = interpolateEnvVars(mergedContent, { strict: true });
|
|
67
|
+
}
|
|
68
|
+
// Resolve fields: per-repo overrides root level
|
|
69
|
+
const createOnly = repoOverride?.createOnly ?? fileConfig.createOnly;
|
|
70
|
+
const header = normalizeHeader(repoOverride?.header ?? fileConfig.header);
|
|
71
|
+
const schemaUrl = repoOverride?.schemaUrl ?? fileConfig.schemaUrl;
|
|
43
72
|
files.push({
|
|
44
73
|
fileName,
|
|
45
74
|
content: mergedContent,
|
|
75
|
+
createOnly,
|
|
76
|
+
header,
|
|
77
|
+
schemaUrl,
|
|
46
78
|
});
|
|
47
79
|
}
|
|
48
80
|
expandedRepos.push({
|
package/dist/config-validator.js
CHANGED
|
@@ -29,6 +29,21 @@ export function validateRawConfig(config) {
|
|
|
29
29
|
!VALID_STRATEGIES.includes(fileConfig.mergeStrategy)) {
|
|
30
30
|
throw new Error(`File '${fileName}' has invalid mergeStrategy: ${fileConfig.mergeStrategy}. Must be one of: ${VALID_STRATEGIES.join(", ")}`);
|
|
31
31
|
}
|
|
32
|
+
if (fileConfig.createOnly !== undefined &&
|
|
33
|
+
typeof fileConfig.createOnly !== "boolean") {
|
|
34
|
+
throw new Error(`File '${fileName}' createOnly must be a boolean`);
|
|
35
|
+
}
|
|
36
|
+
if (fileConfig.header !== undefined) {
|
|
37
|
+
if (typeof fileConfig.header !== "string" &&
|
|
38
|
+
(!Array.isArray(fileConfig.header) ||
|
|
39
|
+
!fileConfig.header.every((h) => typeof h === "string"))) {
|
|
40
|
+
throw new Error(`File '${fileName}' header must be a string or array of strings`);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
if (fileConfig.schemaUrl !== undefined &&
|
|
44
|
+
typeof fileConfig.schemaUrl !== "string") {
|
|
45
|
+
throw new Error(`File '${fileName}' schemaUrl must be a string`);
|
|
46
|
+
}
|
|
32
47
|
}
|
|
33
48
|
if (!config.repos || !Array.isArray(config.repos)) {
|
|
34
49
|
throw new Error("Config missing required field: repos (must be an array)");
|
|
@@ -66,6 +81,21 @@ export function validateRawConfig(config) {
|
|
|
66
81
|
Array.isArray(fileOverride.content))) {
|
|
67
82
|
throw new Error(`Repo at index ${i}: file '${fileName}' content must be an object`);
|
|
68
83
|
}
|
|
84
|
+
if (fileOverride.createOnly !== undefined &&
|
|
85
|
+
typeof fileOverride.createOnly !== "boolean") {
|
|
86
|
+
throw new Error(`Repo ${getGitDisplayName(repo.git)}: file '${fileName}' createOnly must be a boolean`);
|
|
87
|
+
}
|
|
88
|
+
if (fileOverride.header !== undefined) {
|
|
89
|
+
if (typeof fileOverride.header !== "string" &&
|
|
90
|
+
(!Array.isArray(fileOverride.header) ||
|
|
91
|
+
!fileOverride.header.every((h) => typeof h === "string"))) {
|
|
92
|
+
throw new Error(`Repo ${getGitDisplayName(repo.git)}: file '${fileName}' header must be a string or array of strings`);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
if (fileOverride.schemaUrl !== undefined &&
|
|
96
|
+
typeof fileOverride.schemaUrl !== "string") {
|
|
97
|
+
throw new Error(`Repo ${getGitDisplayName(repo.git)}: file '${fileName}' schemaUrl must be a string`);
|
|
98
|
+
}
|
|
69
99
|
}
|
|
70
100
|
}
|
|
71
101
|
}
|
package/dist/config.d.ts
CHANGED
|
@@ -1,12 +1,18 @@
|
|
|
1
1
|
import type { ArrayMergeStrategy } from "./merge.js";
|
|
2
2
|
export { convertContentToString } from "./config-formatter.js";
|
|
3
3
|
export interface RawFileConfig {
|
|
4
|
-
content
|
|
4
|
+
content?: Record<string, unknown>;
|
|
5
5
|
mergeStrategy?: ArrayMergeStrategy;
|
|
6
|
+
createOnly?: boolean;
|
|
7
|
+
header?: string | string[];
|
|
8
|
+
schemaUrl?: string;
|
|
6
9
|
}
|
|
7
10
|
export interface RawRepoFileOverride {
|
|
8
11
|
content?: Record<string, unknown>;
|
|
9
12
|
override?: boolean;
|
|
13
|
+
createOnly?: boolean;
|
|
14
|
+
header?: string | string[];
|
|
15
|
+
schemaUrl?: string;
|
|
10
16
|
}
|
|
11
17
|
export interface RawRepoConfig {
|
|
12
18
|
git: string | string[];
|
|
@@ -18,7 +24,10 @@ export interface RawConfig {
|
|
|
18
24
|
}
|
|
19
25
|
export interface FileContent {
|
|
20
26
|
fileName: string;
|
|
21
|
-
content: Record<string, unknown
|
|
27
|
+
content: Record<string, unknown> | null;
|
|
28
|
+
createOnly?: boolean;
|
|
29
|
+
header?: string[];
|
|
30
|
+
schemaUrl?: string;
|
|
22
31
|
}
|
|
23
32
|
export interface RepoConfig {
|
|
24
33
|
git: string;
|
package/dist/pr-creator.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { RepoInfo } from "./repo-detector.js";
|
|
|
2
2
|
export { escapeShellArg } from "./shell-utils.js";
|
|
3
3
|
export interface FileAction {
|
|
4
4
|
fileName: string;
|
|
5
|
-
action: "create" | "update";
|
|
5
|
+
action: "create" | "update" | "skip";
|
|
6
6
|
}
|
|
7
7
|
export interface PROptions {
|
|
8
8
|
repoInfo: RepoInfo;
|
|
@@ -24,7 +24,7 @@ export interface PRResult {
|
|
|
24
24
|
*/
|
|
25
25
|
export declare function formatPRBody(files: FileAction[]): string;
|
|
26
26
|
/**
|
|
27
|
-
* Generate PR title based on files changed
|
|
27
|
+
* Generate PR title based on files changed (excludes skipped files)
|
|
28
28
|
*/
|
|
29
29
|
export declare function formatPRTitle(files: FileAction[]): string;
|
|
30
30
|
export declare function createPR(options: PROptions): Promise<PRResult>;
|
package/dist/pr-creator.js
CHANGED
|
@@ -25,36 +25,37 @@ Configuration synced using [json-config-sync](https://github.com/anthony-spruyt/
|
|
|
25
25
|
---
|
|
26
26
|
_This PR was automatically generated by [json-config-sync](https://github.com/anthony-spruyt/json-config-sync)_`;
|
|
27
27
|
}
|
|
28
|
+
/**
|
|
29
|
+
* Format file changes list, excluding skipped files
|
|
30
|
+
*/
|
|
31
|
+
function formatFileChanges(files) {
|
|
32
|
+
const changedFiles = files.filter((f) => f.action !== "skip");
|
|
33
|
+
return changedFiles
|
|
34
|
+
.map((f) => {
|
|
35
|
+
const actionText = f.action === "create" ? "Created" : "Updated";
|
|
36
|
+
return `- ${actionText} \`${f.fileName}\``;
|
|
37
|
+
})
|
|
38
|
+
.join("\n");
|
|
39
|
+
}
|
|
28
40
|
/**
|
|
29
41
|
* Format PR body for multiple files
|
|
30
42
|
*/
|
|
31
43
|
export function formatPRBody(files) {
|
|
32
44
|
const template = loadPRTemplate();
|
|
45
|
+
const fileChanges = formatFileChanges(files);
|
|
33
46
|
// Check if template supports multi-file format
|
|
34
47
|
if (template.includes("{{FILE_CHANGES}}")) {
|
|
35
|
-
// Multi-file template
|
|
36
|
-
const fileChanges = files
|
|
37
|
-
.map((f) => {
|
|
38
|
-
const actionText = f.action === "create" ? "Created" : "Updated";
|
|
39
|
-
return `- ${actionText} \`${f.fileName}\``;
|
|
40
|
-
})
|
|
41
|
-
.join("\n");
|
|
42
48
|
return template.replace(/\{\{FILE_CHANGES\}\}/g, fileChanges);
|
|
43
49
|
}
|
|
44
50
|
// Legacy single-file template - adapt it for multiple files
|
|
45
|
-
|
|
46
|
-
|
|
51
|
+
const changedFiles = files.filter((f) => f.action !== "skip");
|
|
52
|
+
if (changedFiles.length === 1) {
|
|
53
|
+
const actionText = changedFiles[0].action === "create" ? "Created" : "Updated";
|
|
47
54
|
return template
|
|
48
|
-
.replace(/\{\{FILE_NAME\}\}/g,
|
|
55
|
+
.replace(/\{\{FILE_NAME\}\}/g, changedFiles[0].fileName)
|
|
49
56
|
.replace(/\{\{ACTION\}\}/g, actionText);
|
|
50
57
|
}
|
|
51
58
|
// Multiple files with legacy template - generate custom body
|
|
52
|
-
const fileChanges = files
|
|
53
|
-
.map((f) => {
|
|
54
|
-
const actionText = f.action === "create" ? "Created" : "Updated";
|
|
55
|
-
return `- ${actionText} \`${f.fileName}\``;
|
|
56
|
-
})
|
|
57
|
-
.join("\n");
|
|
58
59
|
return `## Summary
|
|
59
60
|
Automated sync of configuration files.
|
|
60
61
|
|
|
@@ -68,17 +69,18 @@ Configuration synced using [json-config-sync](https://github.com/anthony-spruyt/
|
|
|
68
69
|
_This PR was automatically generated by [json-config-sync](https://github.com/anthony-spruyt/json-config-sync)_`;
|
|
69
70
|
}
|
|
70
71
|
/**
|
|
71
|
-
* Generate PR title based on files changed
|
|
72
|
+
* Generate PR title based on files changed (excludes skipped files)
|
|
72
73
|
*/
|
|
73
74
|
export function formatPRTitle(files) {
|
|
74
|
-
|
|
75
|
-
|
|
75
|
+
const changedFiles = files.filter((f) => f.action !== "skip");
|
|
76
|
+
if (changedFiles.length === 1) {
|
|
77
|
+
return `chore: sync ${changedFiles[0].fileName}`;
|
|
76
78
|
}
|
|
77
|
-
if (
|
|
78
|
-
const fileNames =
|
|
79
|
+
if (changedFiles.length <= 3) {
|
|
80
|
+
const fileNames = changedFiles.map((f) => f.fileName).join(", ");
|
|
79
81
|
return `chore: sync ${fileNames}`;
|
|
80
82
|
}
|
|
81
|
-
return `chore: sync ${
|
|
83
|
+
return `chore: sync ${changedFiles.length} config files`;
|
|
82
84
|
}
|
|
83
85
|
export async function createPR(options) {
|
|
84
86
|
const { repoInfo, branchName, baseBranch, files, workDir, dryRun, retries } = options;
|
|
@@ -33,7 +33,7 @@ export declare class RepositoryProcessor {
|
|
|
33
33
|
constructor(gitOpsFactory?: GitOpsFactory, log?: ILogger);
|
|
34
34
|
process(repoConfig: RepoConfig, repoInfo: RepoInfo, options: ProcessorOptions): Promise<ProcessorResult>;
|
|
35
35
|
/**
|
|
36
|
-
* Format commit message based on files changed
|
|
36
|
+
* Format commit message based on files changed (excludes skipped files)
|
|
37
37
|
*/
|
|
38
38
|
private formatCommitMessage;
|
|
39
39
|
}
|
|
@@ -38,13 +38,21 @@ export class RepositoryProcessor {
|
|
|
38
38
|
// Step 5: Write all config files and track changes
|
|
39
39
|
const changedFiles = [];
|
|
40
40
|
for (const file of repoConfig.files) {
|
|
41
|
-
this.log.info(`Writing ${file.fileName}...`);
|
|
42
|
-
const fileContent = convertContentToString(file.content, file.fileName);
|
|
43
41
|
const filePath = join(workDir, file.fileName);
|
|
42
|
+
const fileExists = existsSync(filePath);
|
|
43
|
+
// Handle createOnly - skip if file already exists
|
|
44
|
+
if (file.createOnly && fileExists) {
|
|
45
|
+
this.log.info(`Skipping ${file.fileName} (createOnly: already exists)`);
|
|
46
|
+
changedFiles.push({ fileName: file.fileName, action: "skip" });
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
this.log.info(`Writing ${file.fileName}...`);
|
|
50
|
+
const fileContent = convertContentToString(file.content, file.fileName, {
|
|
51
|
+
header: file.header,
|
|
52
|
+
schemaUrl: file.schemaUrl,
|
|
53
|
+
});
|
|
44
54
|
// Determine action type (create vs update)
|
|
45
|
-
const action =
|
|
46
|
-
? "update"
|
|
47
|
-
: "create";
|
|
55
|
+
const action = fileExists ? "update" : "create";
|
|
48
56
|
if (dryRun) {
|
|
49
57
|
// In dry-run, check if file would change without writing
|
|
50
58
|
if (this.gitOps.wouldChange(file.fileName, fileContent)) {
|
|
@@ -56,23 +64,25 @@ export class RepositoryProcessor {
|
|
|
56
64
|
this.gitOps.writeFile(file.fileName, fileContent);
|
|
57
65
|
}
|
|
58
66
|
}
|
|
59
|
-
// Step 6: Check for changes
|
|
67
|
+
// Step 6: Check for changes (exclude skipped files)
|
|
60
68
|
let hasChanges;
|
|
61
69
|
if (dryRun) {
|
|
62
|
-
hasChanges = changedFiles.length > 0;
|
|
70
|
+
hasChanges = changedFiles.filter((f) => f.action !== "skip").length > 0;
|
|
63
71
|
}
|
|
64
72
|
else {
|
|
65
73
|
hasChanges = await this.gitOps.hasChanges();
|
|
66
74
|
// If there are changes, determine which files changed
|
|
67
75
|
if (hasChanges) {
|
|
68
76
|
// Rebuild the changed files list by checking git status
|
|
69
|
-
//
|
|
77
|
+
// Skip files that were already marked as skipped (createOnly)
|
|
78
|
+
const skippedFiles = new Set(changedFiles
|
|
79
|
+
.filter((f) => f.action === "skip")
|
|
80
|
+
.map((f) => f.fileName));
|
|
70
81
|
for (const file of repoConfig.files) {
|
|
82
|
+
if (skippedFiles.has(file.fileName)) {
|
|
83
|
+
continue; // Already tracked as skipped
|
|
84
|
+
}
|
|
71
85
|
const filePath = join(workDir, file.fileName);
|
|
72
|
-
// We check if file existed before writing (action was determined above)
|
|
73
|
-
// Since we don't have pre-write state, we'll mark all files that are in the commit
|
|
74
|
-
// A more accurate approach would track this before writing, but for now
|
|
75
|
-
// we'll assume all files are being synced and include them all
|
|
76
86
|
const action = existsSync(filePath)
|
|
77
87
|
? "update"
|
|
78
88
|
: "create";
|
|
@@ -126,16 +136,17 @@ export class RepositoryProcessor {
|
|
|
126
136
|
}
|
|
127
137
|
}
|
|
128
138
|
/**
|
|
129
|
-
* Format commit message based on files changed
|
|
139
|
+
* Format commit message based on files changed (excludes skipped files)
|
|
130
140
|
*/
|
|
131
141
|
formatCommitMessage(files) {
|
|
132
|
-
|
|
133
|
-
|
|
142
|
+
const changedFiles = files.filter((f) => f.action !== "skip");
|
|
143
|
+
if (changedFiles.length === 1) {
|
|
144
|
+
return `chore: sync ${changedFiles[0].fileName}`;
|
|
134
145
|
}
|
|
135
|
-
if (
|
|
136
|
-
const fileNames =
|
|
146
|
+
if (changedFiles.length <= 3) {
|
|
147
|
+
const fileNames = changedFiles.map((f) => f.fileName).join(", ");
|
|
137
148
|
return `chore: sync ${fileNames}`;
|
|
138
149
|
}
|
|
139
|
-
return `chore: sync ${
|
|
150
|
+
return `chore: sync ${changedFiles.length} config files`;
|
|
140
151
|
}
|
|
141
152
|
}
|
package/package.json
CHANGED