@aspruyt/json-config-sync 3.5.0 → 3.6.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 +40 -0
- package/dist/config-normalizer.js +2 -0
- package/dist/config-validator.js +8 -0
- package/dist/config.d.ts +3 -0
- package/dist/git-ops.d.ts +6 -0
- package/dist/git-ops.js +14 -0
- package/dist/repository-processor.js +26 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -176,6 +176,7 @@ repos: # List of repositories
|
|
|
176
176
|
| `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 |
|
|
177
177
|
| `mergeStrategy` | Merge strategy: `replace`, `append`, `prepend` (for arrays and text lines) | No |
|
|
178
178
|
| `createOnly` | If `true`, only create file if it doesn't exist | No |
|
|
179
|
+
| `executable` | Mark file as executable. `.sh` files are auto-executable unless set to `false`. Set to `true` for non-.sh files. | No |
|
|
179
180
|
| `header` | Comment line(s) at top of YAML files (string or array) | No |
|
|
180
181
|
| `schemaUrl` | Adds `# yaml-language-server: $schema=<url>` to YAML files | No |
|
|
181
182
|
|
|
@@ -193,6 +194,7 @@ repos: # List of repositories
|
|
|
193
194
|
| `content` | Content overlay merged onto file's base content | No |
|
|
194
195
|
| `override` | If `true`, ignore base content and use only this repo's | No |
|
|
195
196
|
| `createOnly` | Override root-level `createOnly` for this repo | No |
|
|
197
|
+
| `executable` | Override root-level `executable` for this repo | No |
|
|
196
198
|
| `header` | Override root-level `header` for this repo | No |
|
|
197
199
|
| `schemaUrl` | Override root-level `schemaUrl` for this repo | No |
|
|
198
200
|
|
|
@@ -489,6 +491,44 @@ repos:
|
|
|
489
491
|
|
|
490
492
|
**Validation:** JSON/YAML file extensions (`.json`, `.yaml`, `.yml`) require object content. Other extensions require string or string[] content.
|
|
491
493
|
|
|
494
|
+
### Executable Files
|
|
495
|
+
|
|
496
|
+
Shell scripts (`.sh` files) are automatically marked as executable using `git update-index --chmod=+x`. You can control this behavior:
|
|
497
|
+
|
|
498
|
+
```yaml
|
|
499
|
+
files:
|
|
500
|
+
# .sh files are auto-executable (no config needed)
|
|
501
|
+
deploy.sh:
|
|
502
|
+
content: |-
|
|
503
|
+
#!/bin/bash
|
|
504
|
+
echo "Deploying..."
|
|
505
|
+
|
|
506
|
+
# Disable auto-executable for a specific .sh file
|
|
507
|
+
template.sh:
|
|
508
|
+
executable: false
|
|
509
|
+
content: "# This is just a template"
|
|
510
|
+
|
|
511
|
+
# Make a non-.sh file executable
|
|
512
|
+
run:
|
|
513
|
+
executable: true
|
|
514
|
+
content: |-
|
|
515
|
+
#!/usr/bin/env python3
|
|
516
|
+
print("Hello")
|
|
517
|
+
|
|
518
|
+
repos:
|
|
519
|
+
- git: git@github.com:org/repo.git
|
|
520
|
+
files:
|
|
521
|
+
# Override executable per-repo
|
|
522
|
+
deploy.sh:
|
|
523
|
+
executable: false # Disable for this repo
|
|
524
|
+
```
|
|
525
|
+
|
|
526
|
+
**Behavior:**
|
|
527
|
+
|
|
528
|
+
- `.sh` files: Automatically executable unless `executable: false`
|
|
529
|
+
- Other files: Not executable unless `executable: true`
|
|
530
|
+
- Per-repo settings override root-level settings
|
|
531
|
+
|
|
492
532
|
### Subdirectory Paths
|
|
493
533
|
|
|
494
534
|
Sync files to any subdirectory path - parent directories are created automatically:
|
|
@@ -83,12 +83,14 @@ export function normalizeConfig(raw) {
|
|
|
83
83
|
}
|
|
84
84
|
// Resolve fields: per-repo overrides root level
|
|
85
85
|
const createOnly = repoOverride?.createOnly ?? fileConfig.createOnly;
|
|
86
|
+
const executable = repoOverride?.executable ?? fileConfig.executable;
|
|
86
87
|
const header = normalizeHeader(repoOverride?.header ?? fileConfig.header);
|
|
87
88
|
const schemaUrl = repoOverride?.schemaUrl ?? fileConfig.schemaUrl;
|
|
88
89
|
files.push({
|
|
89
90
|
fileName,
|
|
90
91
|
content: mergedContent,
|
|
91
92
|
createOnly,
|
|
93
|
+
executable,
|
|
92
94
|
header,
|
|
93
95
|
schemaUrl,
|
|
94
96
|
});
|
package/dist/config-validator.js
CHANGED
|
@@ -64,6 +64,10 @@ export function validateRawConfig(config) {
|
|
|
64
64
|
typeof fileConfig.createOnly !== "boolean") {
|
|
65
65
|
throw new Error(`File '${fileName}' createOnly must be a boolean`);
|
|
66
66
|
}
|
|
67
|
+
if (fileConfig.executable !== undefined &&
|
|
68
|
+
typeof fileConfig.executable !== "boolean") {
|
|
69
|
+
throw new Error(`File '${fileName}' executable must be a boolean`);
|
|
70
|
+
}
|
|
67
71
|
if (fileConfig.header !== undefined) {
|
|
68
72
|
if (typeof fileConfig.header !== "string" &&
|
|
69
73
|
(!Array.isArray(fileConfig.header) ||
|
|
@@ -126,6 +130,10 @@ export function validateRawConfig(config) {
|
|
|
126
130
|
typeof fileOverride.createOnly !== "boolean") {
|
|
127
131
|
throw new Error(`Repo ${getGitDisplayName(repo.git)}: file '${fileName}' createOnly must be a boolean`);
|
|
128
132
|
}
|
|
133
|
+
if (fileOverride.executable !== undefined &&
|
|
134
|
+
typeof fileOverride.executable !== "boolean") {
|
|
135
|
+
throw new Error(`Repo ${getGitDisplayName(repo.git)}: file '${fileName}' executable must be a boolean`);
|
|
136
|
+
}
|
|
129
137
|
if (fileOverride.header !== undefined) {
|
|
130
138
|
if (typeof fileOverride.header !== "string" &&
|
|
131
139
|
(!Array.isArray(fileOverride.header) ||
|
package/dist/config.d.ts
CHANGED
|
@@ -5,6 +5,7 @@ export interface RawFileConfig {
|
|
|
5
5
|
content?: ContentValue;
|
|
6
6
|
mergeStrategy?: ArrayMergeStrategy;
|
|
7
7
|
createOnly?: boolean;
|
|
8
|
+
executable?: boolean;
|
|
8
9
|
header?: string | string[];
|
|
9
10
|
schemaUrl?: string;
|
|
10
11
|
}
|
|
@@ -12,6 +13,7 @@ export interface RawRepoFileOverride {
|
|
|
12
13
|
content?: ContentValue;
|
|
13
14
|
override?: boolean;
|
|
14
15
|
createOnly?: boolean;
|
|
16
|
+
executable?: boolean;
|
|
15
17
|
header?: string | string[];
|
|
16
18
|
schemaUrl?: string;
|
|
17
19
|
}
|
|
@@ -27,6 +29,7 @@ export interface FileContent {
|
|
|
27
29
|
fileName: string;
|
|
28
30
|
content: ContentValue | null;
|
|
29
31
|
createOnly?: boolean;
|
|
32
|
+
executable?: boolean;
|
|
30
33
|
header?: string[];
|
|
31
34
|
schemaUrl?: string;
|
|
32
35
|
}
|
package/dist/git-ops.d.ts
CHANGED
|
@@ -28,6 +28,12 @@ export declare class GitOps {
|
|
|
28
28
|
clone(gitUrl: string): Promise<void>;
|
|
29
29
|
createBranch(branchName: string): Promise<void>;
|
|
30
30
|
writeFile(fileName: string, content: string): void;
|
|
31
|
+
/**
|
|
32
|
+
* Marks a file as executable in git using update-index --chmod=+x.
|
|
33
|
+
* This modifies the file mode in git's index, not the filesystem.
|
|
34
|
+
* @param fileName - The file path relative to the work directory
|
|
35
|
+
*/
|
|
36
|
+
setExecutable(fileName: string): Promise<void>;
|
|
31
37
|
/**
|
|
32
38
|
* Checks if writing the given content would result in changes.
|
|
33
39
|
* Works in both normal and dry-run modes by comparing content directly.
|
package/dist/git-ops.js
CHANGED
|
@@ -103,6 +103,20 @@ export class GitOps {
|
|
|
103
103
|
const normalized = content.endsWith("\n") ? content : content + "\n";
|
|
104
104
|
writeFileSync(filePath, normalized, "utf-8");
|
|
105
105
|
}
|
|
106
|
+
/**
|
|
107
|
+
* Marks a file as executable in git using update-index --chmod=+x.
|
|
108
|
+
* This modifies the file mode in git's index, not the filesystem.
|
|
109
|
+
* @param fileName - The file path relative to the work directory
|
|
110
|
+
*/
|
|
111
|
+
async setExecutable(fileName) {
|
|
112
|
+
if (this.dryRun) {
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
const filePath = this.validatePath(fileName);
|
|
116
|
+
// Use relative path from workDir for git command
|
|
117
|
+
const relativePath = relative(this.workDir, filePath);
|
|
118
|
+
await this.exec(`git update-index --chmod=+x ${escapeShellArg(relativePath)}`, this.workDir);
|
|
119
|
+
}
|
|
106
120
|
/**
|
|
107
121
|
* Checks if writing the given content would result in changes.
|
|
108
122
|
* Works in both normal and dry-run modes by comparing content directly.
|
|
@@ -5,6 +5,20 @@ import { getRepoDisplayName } from "./repo-detector.js";
|
|
|
5
5
|
import { GitOps } from "./git-ops.js";
|
|
6
6
|
import { createPR } from "./pr-creator.js";
|
|
7
7
|
import { logger } from "./logger.js";
|
|
8
|
+
/**
|
|
9
|
+
* Determines if a file should be marked as executable.
|
|
10
|
+
* .sh files are auto-executable unless explicit executable: false is set.
|
|
11
|
+
* Non-.sh files are executable only if executable: true is explicitly set.
|
|
12
|
+
*/
|
|
13
|
+
function shouldBeExecutable(file) {
|
|
14
|
+
const isShellScript = file.fileName.endsWith(".sh");
|
|
15
|
+
if (file.executable !== undefined) {
|
|
16
|
+
// Explicit setting takes precedence
|
|
17
|
+
return file.executable;
|
|
18
|
+
}
|
|
19
|
+
// Default: .sh files are executable, others are not
|
|
20
|
+
return isShellScript;
|
|
21
|
+
}
|
|
8
22
|
export class RepositoryProcessor {
|
|
9
23
|
gitOps = null;
|
|
10
24
|
gitOpsFactory;
|
|
@@ -64,6 +78,18 @@ export class RepositoryProcessor {
|
|
|
64
78
|
this.gitOps.writeFile(file.fileName, fileContent);
|
|
65
79
|
}
|
|
66
80
|
}
|
|
81
|
+
// Step 5b: Set executable permission for files that need it
|
|
82
|
+
const skippedFileNames = new Set(changedFiles.filter((f) => f.action === "skip").map((f) => f.fileName));
|
|
83
|
+
for (const file of repoConfig.files) {
|
|
84
|
+
// Skip files that were excluded (createOnly + exists)
|
|
85
|
+
if (skippedFileNames.has(file.fileName)) {
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
if (shouldBeExecutable(file)) {
|
|
89
|
+
this.log.info(`Setting executable: ${file.fileName}`);
|
|
90
|
+
await this.gitOps.setExecutable(file.fileName);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
67
93
|
// Step 6: Check for changes (exclude skipped files)
|
|
68
94
|
let hasChanges;
|
|
69
95
|
if (dryRun) {
|
package/package.json
CHANGED