@aspruyt/xfg 1.3.1 → 1.4.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 +33 -5
- 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 +1 -1
package/PR.md
CHANGED
package/README.md
CHANGED
|
@@ -182,11 +182,12 @@ repos: # List of repositories
|
|
|
182
182
|
|
|
183
183
|
### Root-Level Fields
|
|
184
184
|
|
|
185
|
-
| Field
|
|
186
|
-
|
|
|
187
|
-
| `files`
|
|
188
|
-
| `repos`
|
|
189
|
-
| `prOptions`
|
|
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
|
+
| `prTemplate` | Custom PR body template (inline or `@path/to/file` reference) | No |
|
|
190
191
|
|
|
191
192
|
### Per-File Fields
|
|
192
193
|
|
|
@@ -216,6 +217,33 @@ repos: # List of repositories
|
|
|
216
217
|
| `deleteBranch` | Delete source branch after merge | `true` |
|
|
217
218
|
| `bypassReason` | Reason for bypassing policies (Azure DevOps only, required for `force`) | - |
|
|
218
219
|
|
|
220
|
+
### PR Template Customization
|
|
221
|
+
|
|
222
|
+
Customize the PR body with a template. Use the `{{FILE_CHANGES}}` placeholder for the list of changed files:
|
|
223
|
+
|
|
224
|
+
```yaml
|
|
225
|
+
# Inline template
|
|
226
|
+
prTemplate: |
|
|
227
|
+
## Configuration Update
|
|
228
|
+
|
|
229
|
+
This PR synchronizes the following files:
|
|
230
|
+
|
|
231
|
+
{{FILE_CHANGES}}
|
|
232
|
+
|
|
233
|
+
Please review and merge.
|
|
234
|
+
|
|
235
|
+
# Or reference an external file
|
|
236
|
+
prTemplate: "@templates/pr-body.md"
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
**Available Placeholders:**
|
|
240
|
+
|
|
241
|
+
| Placeholder | Description | Example Output |
|
|
242
|
+
| ------------------ | ----------------------------------- | -------------------------------------------------------- |
|
|
243
|
+
| `{{FILE_CHANGES}}` | Bulleted list of files with actions | `- Created \`config.json\`\n- Updated \`settings.yaml\`` |
|
|
244
|
+
|
|
245
|
+
**Default Template:** If not specified, uses the built-in template with Summary, Changes, and Source sections.
|
|
246
|
+
|
|
219
247
|
### Per-Repo File Override Fields
|
|
220
248
|
|
|
221
249
|
| Field | Description | Required |
|
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