@aspruyt/json-config-sync 3.0.0 → 3.1.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 +39 -4
- package/dist/config-normalizer.js +3 -0
- package/dist/config-validator.js +8 -0
- package/dist/config.d.ts +3 -0
- 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 +25 -17
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -169,6 +169,7 @@ repos: # List of repositories
|
|
|
169
169
|
| --------------- | ---------------------------------------------------- | -------- |
|
|
170
170
|
| `content` | Base config inherited by all repos | Yes |
|
|
171
171
|
| `mergeStrategy` | Array merge strategy: `replace`, `append`, `prepend` | No |
|
|
172
|
+
| `createOnly` | If `true`, only create file if it doesn't exist | No |
|
|
172
173
|
|
|
173
174
|
### Per-Repo Fields
|
|
174
175
|
|
|
@@ -179,10 +180,11 @@ repos: # List of repositories
|
|
|
179
180
|
|
|
180
181
|
### Per-Repo File Override Fields
|
|
181
182
|
|
|
182
|
-
| Field
|
|
183
|
-
|
|
|
184
|
-
| `content`
|
|
185
|
-
| `override`
|
|
183
|
+
| Field | Description | Required |
|
|
184
|
+
| ------------ | ------------------------------------------------------- | -------- |
|
|
185
|
+
| `content` | Content overlay merged onto file's base content | No |
|
|
186
|
+
| `override` | If `true`, ignore base content and use only this repo's | No |
|
|
187
|
+
| `createOnly` | Override root-level `createOnly` for this repo | No |
|
|
186
188
|
|
|
187
189
|
**File Exclusion:** Set a file to `false` to exclude it from a specific repo:
|
|
188
190
|
|
|
@@ -363,6 +365,39 @@ repos:
|
|
|
363
365
|
# Results in extends: ["@company/base", "plugin:react/recommended"]
|
|
364
366
|
```
|
|
365
367
|
|
|
368
|
+
### Create-Only Files (Defaults That Can Be Customized)
|
|
369
|
+
|
|
370
|
+
Some files should only be created once as defaults, allowing repos to maintain their own versions:
|
|
371
|
+
|
|
372
|
+
```yaml
|
|
373
|
+
files:
|
|
374
|
+
.trivyignore.yaml:
|
|
375
|
+
createOnly: true # Only create if doesn't exist
|
|
376
|
+
content:
|
|
377
|
+
vulnerabilities: []
|
|
378
|
+
|
|
379
|
+
.prettierignore:
|
|
380
|
+
createOnly: true
|
|
381
|
+
content:
|
|
382
|
+
patterns:
|
|
383
|
+
- "dist/"
|
|
384
|
+
- "node_modules/"
|
|
385
|
+
|
|
386
|
+
eslint.config.json:
|
|
387
|
+
content: # Always synced (no createOnly)
|
|
388
|
+
extends: ["@company/base"]
|
|
389
|
+
|
|
390
|
+
repos:
|
|
391
|
+
- git: git@github.com:org/repo.git
|
|
392
|
+
# .trivyignore.yaml and .prettierignore only created if missing
|
|
393
|
+
# eslint.config.json always updated
|
|
394
|
+
|
|
395
|
+
- git: git@github.com:org/special-repo.git
|
|
396
|
+
files:
|
|
397
|
+
.trivyignore.yaml:
|
|
398
|
+
createOnly: false # Override: always sync this file
|
|
399
|
+
```
|
|
400
|
+
|
|
366
401
|
## Supported Git URL Formats
|
|
367
402
|
|
|
368
403
|
### GitHub
|
|
@@ -40,9 +40,12 @@ export function normalizeConfig(raw) {
|
|
|
40
40
|
}
|
|
41
41
|
// Step 4: Interpolate env vars
|
|
42
42
|
mergedContent = interpolateEnvVars(mergedContent, { strict: true });
|
|
43
|
+
// Resolve createOnly: per-repo overrides root level
|
|
44
|
+
const createOnly = repoOverride?.createOnly ?? fileConfig.createOnly;
|
|
43
45
|
files.push({
|
|
44
46
|
fileName,
|
|
45
47
|
content: mergedContent,
|
|
48
|
+
createOnly,
|
|
46
49
|
});
|
|
47
50
|
}
|
|
48
51
|
expandedRepos.push({
|
package/dist/config-validator.js
CHANGED
|
@@ -29,6 +29,10 @@ 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
|
+
}
|
|
32
36
|
}
|
|
33
37
|
if (!config.repos || !Array.isArray(config.repos)) {
|
|
34
38
|
throw new Error("Config missing required field: repos (must be an array)");
|
|
@@ -66,6 +70,10 @@ export function validateRawConfig(config) {
|
|
|
66
70
|
Array.isArray(fileOverride.content))) {
|
|
67
71
|
throw new Error(`Repo at index ${i}: file '${fileName}' content must be an object`);
|
|
68
72
|
}
|
|
73
|
+
if (fileOverride.createOnly !== undefined &&
|
|
74
|
+
typeof fileOverride.createOnly !== "boolean") {
|
|
75
|
+
throw new Error(`Repo ${getGitDisplayName(repo.git)}: file '${fileName}' createOnly must be a boolean`);
|
|
76
|
+
}
|
|
69
77
|
}
|
|
70
78
|
}
|
|
71
79
|
}
|
package/dist/config.d.ts
CHANGED
|
@@ -3,10 +3,12 @@ export { convertContentToString } from "./config-formatter.js";
|
|
|
3
3
|
export interface RawFileConfig {
|
|
4
4
|
content: Record<string, unknown>;
|
|
5
5
|
mergeStrategy?: ArrayMergeStrategy;
|
|
6
|
+
createOnly?: boolean;
|
|
6
7
|
}
|
|
7
8
|
export interface RawRepoFileOverride {
|
|
8
9
|
content?: Record<string, unknown>;
|
|
9
10
|
override?: boolean;
|
|
11
|
+
createOnly?: boolean;
|
|
10
12
|
}
|
|
11
13
|
export interface RawRepoConfig {
|
|
12
14
|
git: string | string[];
|
|
@@ -19,6 +21,7 @@ export interface RawConfig {
|
|
|
19
21
|
export interface FileContent {
|
|
20
22
|
fileName: string;
|
|
21
23
|
content: Record<string, unknown>;
|
|
24
|
+
createOnly?: boolean;
|
|
22
25
|
}
|
|
23
26
|
export interface RepoConfig {
|
|
24
27
|
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,18 @@ 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
|
+
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
|
+
}
|
|
41
49
|
this.log.info(`Writing ${file.fileName}...`);
|
|
42
50
|
const fileContent = convertContentToString(file.content, file.fileName);
|
|
43
|
-
const filePath = join(workDir, file.fileName);
|
|
44
51
|
// Determine action type (create vs update)
|
|
45
|
-
const action =
|
|
46
|
-
? "update"
|
|
47
|
-
: "create";
|
|
52
|
+
const action = fileExists ? "update" : "create";
|
|
48
53
|
if (dryRun) {
|
|
49
54
|
// In dry-run, check if file would change without writing
|
|
50
55
|
if (this.gitOps.wouldChange(file.fileName, fileContent)) {
|
|
@@ -56,23 +61,25 @@ export class RepositoryProcessor {
|
|
|
56
61
|
this.gitOps.writeFile(file.fileName, fileContent);
|
|
57
62
|
}
|
|
58
63
|
}
|
|
59
|
-
// Step 6: Check for changes
|
|
64
|
+
// Step 6: Check for changes (exclude skipped files)
|
|
60
65
|
let hasChanges;
|
|
61
66
|
if (dryRun) {
|
|
62
|
-
hasChanges = changedFiles.length > 0;
|
|
67
|
+
hasChanges = changedFiles.filter((f) => f.action !== "skip").length > 0;
|
|
63
68
|
}
|
|
64
69
|
else {
|
|
65
70
|
hasChanges = await this.gitOps.hasChanges();
|
|
66
71
|
// If there are changes, determine which files changed
|
|
67
72
|
if (hasChanges) {
|
|
68
73
|
// Rebuild the changed files list by checking git status
|
|
69
|
-
//
|
|
74
|
+
// Skip files that were already marked as skipped (createOnly)
|
|
75
|
+
const skippedFiles = new Set(changedFiles
|
|
76
|
+
.filter((f) => f.action === "skip")
|
|
77
|
+
.map((f) => f.fileName));
|
|
70
78
|
for (const file of repoConfig.files) {
|
|
79
|
+
if (skippedFiles.has(file.fileName)) {
|
|
80
|
+
continue; // Already tracked as skipped
|
|
81
|
+
}
|
|
71
82
|
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
83
|
const action = existsSync(filePath)
|
|
77
84
|
? "update"
|
|
78
85
|
: "create";
|
|
@@ -126,16 +133,17 @@ export class RepositoryProcessor {
|
|
|
126
133
|
}
|
|
127
134
|
}
|
|
128
135
|
/**
|
|
129
|
-
* Format commit message based on files changed
|
|
136
|
+
* Format commit message based on files changed (excludes skipped files)
|
|
130
137
|
*/
|
|
131
138
|
formatCommitMessage(files) {
|
|
132
|
-
|
|
133
|
-
|
|
139
|
+
const changedFiles = files.filter((f) => f.action !== "skip");
|
|
140
|
+
if (changedFiles.length === 1) {
|
|
141
|
+
return `chore: sync ${changedFiles[0].fileName}`;
|
|
134
142
|
}
|
|
135
|
-
if (
|
|
136
|
-
const fileNames =
|
|
143
|
+
if (changedFiles.length <= 3) {
|
|
144
|
+
const fileNames = changedFiles.map((f) => f.fileName).join(", ");
|
|
137
145
|
return `chore: sync ${fileNames}`;
|
|
138
146
|
}
|
|
139
|
-
return `chore: sync ${
|
|
147
|
+
return `chore: sync ${changedFiles.length} config files`;
|
|
140
148
|
}
|
|
141
149
|
}
|
package/package.json
CHANGED