@aspruyt/xfg 4.0.4 → 5.0.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 +1 -1
- package/dist/cli/program.d.ts +3 -0
- package/dist/cli/program.js +18 -13
- package/dist/cli/sync-command.js +62 -39
- package/dist/cli/sync-report-builder.js +7 -4
- package/dist/config/formatter.js +14 -9
- package/dist/config/merge.d.ts +2 -4
- package/dist/config/merge.js +15 -67
- package/dist/config/validator.js +2 -9
- package/dist/lifecycle/repo-lifecycle-factory.js +0 -4
- package/dist/output/github-summary.d.ts +3 -2
- package/dist/output/github-summary.js +1 -7
- package/dist/output/lifecycle-report.js +7 -14
- package/dist/output/sync-report.d.ts +2 -19
- package/dist/output/sync-report.js +16 -28
- package/dist/output/types.d.ts +19 -0
- package/dist/output/types.js +1 -0
- package/dist/output/unified-summary.d.ts +2 -1
- package/dist/output/unified-summary.js +4 -1
- package/dist/settings/base-processor.d.ts +3 -1
- package/dist/settings/base-processor.js +9 -5
- package/dist/settings/index.d.ts +1 -1
- package/dist/settings/labels/diff.d.ts +2 -1
- package/dist/settings/labels/formatter.js +2 -4
- package/dist/settings/labels/github-labels-strategy.js +2 -1
- package/dist/settings/labels/processor.js +0 -1
- package/dist/settings/repo-settings/github-repo-settings-strategy.js +2 -1
- package/dist/settings/rulesets/diff-algorithm.js +0 -1
- package/dist/settings/rulesets/diff.d.ts +2 -1
- package/dist/settings/rulesets/diff.js +37 -21
- package/dist/settings/rulesets/formatter.js +44 -38
- package/dist/settings/rulesets/github-ruleset-strategy.js +2 -1
- package/dist/settings/rulesets/processor.js +0 -1
- package/dist/shared/gh-api-utils.d.ts +8 -7
- package/dist/shared/gh-api-utils.js +2 -16
- package/dist/shared/interpolation-engine.d.ts +3 -0
- package/dist/shared/interpolation-engine.js +0 -3
- package/dist/shared/json-utils.d.ts +6 -0
- package/dist/shared/json-utils.js +16 -0
- package/dist/shared/repo-detector.js +0 -4
- package/dist/shared/xfg-template.d.ts +3 -0
- package/dist/shared/xfg-template.js +0 -20
- package/dist/sync/auth-options-builder.js +7 -1
- package/dist/sync/branch-manager.d.ts +1 -1
- package/dist/sync/commit-message.d.ts +1 -1
- package/dist/sync/commit-push-manager.d.ts +1 -1
- package/dist/sync/commit-push-manager.js +2 -2
- package/dist/sync/diff-utils.d.ts +15 -2
- package/dist/sync/diff-utils.js +50 -14
- package/dist/sync/file-sync-orchestrator.js +2 -4
- package/dist/sync/file-sync-strategy.js +11 -4
- package/dist/sync/file-writer.js +9 -4
- package/dist/sync/index.d.ts +2 -1
- package/dist/sync/index.js +1 -0
- package/dist/sync/manifest-manager.d.ts +1 -1
- package/dist/sync/manifest-manager.js +20 -6
- package/dist/sync/pr-merge-handler.js +6 -1
- package/dist/sync/repository-processor.js +8 -1
- package/dist/sync/types.d.ts +5 -4
- package/dist/vcs/authenticated-git-ops.d.ts +9 -1
- package/dist/vcs/authenticated-git-ops.js +7 -14
- package/dist/vcs/git-ops.js +29 -12
- package/dist/vcs/github-pr-strategy.js +6 -1
- package/dist/vcs/gitlab-pr-strategy.js +7 -2
- package/dist/vcs/graphql-commit-strategy.js +2 -1
- package/dist/vcs/index.d.ts +1 -0
- package/dist/vcs/index.js +2 -0
- package/dist/vcs/pr-creator.d.ts +5 -1
- package/dist/vcs/pr-creator.js +4 -4
- package/package.json +1 -1
- package/dist/output/index.d.ts +0 -5
- package/dist/output/index.js +0 -10
- package/dist/shared/index.d.ts +0 -15
- package/dist/shared/index.js +0 -30
|
@@ -86,26 +86,6 @@ function buildXfgConfig(ctx, options) {
|
|
|
86
86
|
restoreEscaped: (content) => `\${xfg:${content}}`,
|
|
87
87
|
};
|
|
88
88
|
}
|
|
89
|
-
/**
|
|
90
|
-
* Interpolate xfg template variables in content.
|
|
91
|
-
*
|
|
92
|
-
* Supports these syntaxes:
|
|
93
|
-
* - ${xfg:repo.name} - Repository name
|
|
94
|
-
* - ${xfg:repo.owner} - Repository owner
|
|
95
|
-
* - ${xfg:repo.fullName} - Full repository name (owner/repo)
|
|
96
|
-
* - ${xfg:repo.url} - Git URL
|
|
97
|
-
* - ${xfg:repo.platform} - Platform type (github, azure-devops, gitlab)
|
|
98
|
-
* - ${xfg:repo.host} - Host domain
|
|
99
|
-
* - ${xfg:file.name} - Current file name
|
|
100
|
-
* - ${xfg:date} - Current date (YYYY-MM-DD)
|
|
101
|
-
* - ${xfg:customVar} - Custom variable from vars config
|
|
102
|
-
* - $${xfg:var} - Escape: outputs literal ${xfg:var}
|
|
103
|
-
*
|
|
104
|
-
* @param content - The content to process (object, string, or string[])
|
|
105
|
-
* @param ctx - Template context with repo info and custom vars
|
|
106
|
-
* @param options - Interpolation options (default: strict mode)
|
|
107
|
-
* @returns Content with interpolated values
|
|
108
|
-
*/
|
|
109
89
|
export function interpolateXfgContent(content, ctx, options = DEFAULT_OPTIONS) {
|
|
110
90
|
const config = buildXfgConfig(ctx, options);
|
|
111
91
|
if (typeof content === "string") {
|
|
@@ -19,7 +19,13 @@ export class AuthOptionsBuilder {
|
|
|
19
19
|
return { ok: true, token, authOptions };
|
|
20
20
|
}
|
|
21
21
|
// Otherwise resolve via token manager / env fallback
|
|
22
|
-
const resolved = await resolveGitHubToken(
|
|
22
|
+
const resolved = await resolveGitHubToken({
|
|
23
|
+
repoInfo,
|
|
24
|
+
tokenManager: this.tokenManager,
|
|
25
|
+
context: repoName,
|
|
26
|
+
log: this.log,
|
|
27
|
+
envToken: this.envToken,
|
|
28
|
+
});
|
|
23
29
|
if (resolved.skipped) {
|
|
24
30
|
return {
|
|
25
31
|
ok: false,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { IPRStrategy } from "../vcs/
|
|
1
|
+
import type { IPRStrategy } from "../vcs/index.js";
|
|
2
2
|
import type { RepoInfo } from "../shared/repo-detector.js";
|
|
3
3
|
import type { ICommandExecutor } from "../shared/command-executor.js";
|
|
4
4
|
import type { IBranchManager, BranchSetupOptions } from "./types.js";
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ICommitStrategy } from "../vcs/
|
|
1
|
+
import type { ICommitStrategy } from "../vcs/index.js";
|
|
2
2
|
import type { RepoInfo } from "../shared/repo-detector.js";
|
|
3
3
|
import type { ICommandExecutor } from "../shared/command-executor.js";
|
|
4
4
|
import type { CommitPushOptions, CommitPushResult, ICommitPushManager } from "./types.js";
|
|
@@ -46,7 +46,7 @@ export class CommitPushManager {
|
|
|
46
46
|
return this.handleCommitError(error, isDirectMode, pushBranch, repoInfo);
|
|
47
47
|
}
|
|
48
48
|
}
|
|
49
|
-
handleCommitError(error, isDirectMode,
|
|
49
|
+
handleCommitError(error, isDirectMode, pushBranch, repoInfo) {
|
|
50
50
|
const repoName = getRepoDisplayName(repoInfo);
|
|
51
51
|
const message = toErrorMessage(error);
|
|
52
52
|
if (isDirectMode &&
|
|
@@ -58,7 +58,7 @@ export class CommitPushManager {
|
|
|
58
58
|
errorResult: {
|
|
59
59
|
success: false,
|
|
60
60
|
repoName,
|
|
61
|
-
message: `Push to '${
|
|
61
|
+
message: `Push to '${pushBranch}' was rejected (likely branch protection). ` +
|
|
62
62
|
`To use 'direct' mode, the target branch must allow direct pushes. ` +
|
|
63
63
|
`Use 'merge: force' to create a PR and merge with admin privileges.`,
|
|
64
64
|
},
|
|
@@ -6,11 +6,24 @@ export declare function getFileStatus(exists: boolean, changed: boolean): FileSt
|
|
|
6
6
|
* Format a single diff line with appropriate color.
|
|
7
7
|
*/
|
|
8
8
|
export declare function formatDiffLine(line: string): string;
|
|
9
|
+
/**
|
|
10
|
+
* Check if a file is a structured data file (JSON, JSON5, YAML, YML).
|
|
11
|
+
*/
|
|
12
|
+
export declare function isStructuredDataFile(fileName: string): boolean;
|
|
13
|
+
/**
|
|
14
|
+
* Compute a unified diff between old and new content.
|
|
15
|
+
* Returns raw diff lines (no ANSI formatting).
|
|
16
|
+
*
|
|
17
|
+
* - oldContent === null → new file (all additions)
|
|
18
|
+
* - newContent === null → deleted file (all removals)
|
|
19
|
+
* - both null → empty array
|
|
20
|
+
*/
|
|
21
|
+
export declare function computeUnifiedDiff(oldContent: string | null, newContent: string | null, contextLines?: number): string[];
|
|
9
22
|
/**
|
|
10
23
|
* Generate a unified diff between old and new content.
|
|
11
|
-
* Returns an array of formatted diff lines.
|
|
24
|
+
* Returns an array of formatted (chalk-colored) diff lines.
|
|
12
25
|
*/
|
|
13
|
-
export declare function generateDiff(oldContent: string | null, newContent: string,
|
|
26
|
+
export declare function generateDiff(oldContent: string | null, newContent: string, contextLines?: number): string[];
|
|
14
27
|
export interface DiffStats {
|
|
15
28
|
newCount: number;
|
|
16
29
|
modifiedCount: number;
|
package/dist/sync/diff-utils.js
CHANGED
|
@@ -18,34 +18,70 @@ export function formatDiffLine(line) {
|
|
|
18
18
|
return line;
|
|
19
19
|
}
|
|
20
20
|
/**
|
|
21
|
-
*
|
|
22
|
-
* Returns an array of formatted diff lines.
|
|
21
|
+
* Check if a file is a structured data file (JSON, JSON5, YAML, YML).
|
|
23
22
|
*/
|
|
24
|
-
export function
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
23
|
+
export function isStructuredDataFile(fileName) {
|
|
24
|
+
return /\.(json|json5|ya?ml)$/i.test(fileName);
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Compute a unified diff between old and new content.
|
|
28
|
+
* Returns raw diff lines (no ANSI formatting).
|
|
29
|
+
*
|
|
30
|
+
* - oldContent === null → new file (all additions)
|
|
31
|
+
* - newContent === null → deleted file (all removals)
|
|
32
|
+
* - both null → empty array
|
|
33
|
+
*/
|
|
34
|
+
export function computeUnifiedDiff(oldContent, newContent, contextLines = 3) {
|
|
35
|
+
if (oldContent === null && newContent === null) {
|
|
36
|
+
return [];
|
|
37
|
+
}
|
|
38
|
+
// New file: all additions
|
|
28
39
|
if (oldContent === null) {
|
|
29
|
-
const
|
|
30
|
-
|
|
31
|
-
|
|
40
|
+
const newLines = newContent.split("\n");
|
|
41
|
+
// Filter trailing empty string from split
|
|
42
|
+
const lines = newLines[newLines.length - 1] === "" ? newLines.slice(0, -1) : newLines;
|
|
43
|
+
if (lines.length === 0)
|
|
44
|
+
return [];
|
|
45
|
+
const result = [`@@ -0,0 +1,${lines.length} @@`];
|
|
46
|
+
for (const line of lines) {
|
|
47
|
+
result.push(`+${line}`);
|
|
48
|
+
}
|
|
49
|
+
return result;
|
|
50
|
+
}
|
|
51
|
+
// Deleted file: all removals
|
|
52
|
+
if (newContent === null) {
|
|
53
|
+
const oldLines = oldContent.split("\n");
|
|
54
|
+
const lines = oldLines[oldLines.length - 1] === "" ? oldLines.slice(0, -1) : oldLines;
|
|
55
|
+
if (lines.length === 0)
|
|
56
|
+
return [];
|
|
57
|
+
const result = [`@@ -1,${lines.length} +0,0 @@`];
|
|
58
|
+
for (const line of lines) {
|
|
59
|
+
result.push(`-${line}`);
|
|
32
60
|
}
|
|
33
61
|
return result;
|
|
34
62
|
}
|
|
35
|
-
//
|
|
63
|
+
// Modified file: LCS diff
|
|
64
|
+
const oldLines = oldContent.split("\n");
|
|
65
|
+
const newLines = newContent.split("\n");
|
|
36
66
|
const hunks = computeDiffHunks(oldLines, newLines, contextLines);
|
|
37
|
-
if (hunks.length === 0)
|
|
67
|
+
if (hunks.length === 0)
|
|
38
68
|
return [];
|
|
39
|
-
}
|
|
40
69
|
const result = [];
|
|
41
70
|
for (const hunk of hunks) {
|
|
42
|
-
result.push(
|
|
71
|
+
result.push(`@@ -${hunk.oldStart},${hunk.oldCount} +${hunk.newStart},${hunk.newCount} @@`);
|
|
43
72
|
for (const line of hunk.lines) {
|
|
44
|
-
result.push(
|
|
73
|
+
result.push(line);
|
|
45
74
|
}
|
|
46
75
|
}
|
|
47
76
|
return result;
|
|
48
77
|
}
|
|
78
|
+
/**
|
|
79
|
+
* Generate a unified diff between old and new content.
|
|
80
|
+
* Returns an array of formatted (chalk-colored) diff lines.
|
|
81
|
+
*/
|
|
82
|
+
export function generateDiff(oldContent, newContent, contextLines = 3) {
|
|
83
|
+
return computeUnifiedDiff(oldContent, newContent, contextLines).map(formatDiffLine);
|
|
84
|
+
}
|
|
49
85
|
/**
|
|
50
86
|
* Compute diff hunks using a simple line-by-line comparison.
|
|
51
87
|
* This is a simplified diff that shows changed regions with context.
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { incrementDiffStats } from "./diff-utils.js";
|
|
2
|
-
import { loadManifest } from "./manifest.js";
|
|
3
2
|
export class FileSyncOrchestrator {
|
|
4
3
|
fileWriter;
|
|
5
4
|
manifestManager;
|
|
@@ -22,10 +21,9 @@ export class FileSyncOrchestrator {
|
|
|
22
21
|
configId,
|
|
23
22
|
hasAppCredentials: options.hasAppCredentials,
|
|
24
23
|
}, { gitOps: session.gitOps, log: this.log });
|
|
25
|
-
const existingManifest = loadManifest(workDir, this.log);
|
|
26
24
|
const filesWithDeleteOrphaned = new Map(repoConfig.files.map((f) => [f.fileName, f.deleteOrphaned]));
|
|
27
|
-
const { manifest: newManifest, filesToDelete } = this.manifestManager.processOrphans(workDir, configId, filesWithDeleteOrphaned);
|
|
28
|
-
|
|
25
|
+
const { manifest: newManifest, existingManifest, filesToDelete, } = this.manifestManager.processOrphans(workDir, configId, filesWithDeleteOrphaned);
|
|
26
|
+
this.manifestManager.deleteOrphans(filesToDelete, { dryRun: dryRun, noDelete: noDelete }, { gitOps: session.gitOps, log: this.log, fileChanges });
|
|
29
27
|
// Save manifest (may add to fileChanges)
|
|
30
28
|
this.manifestManager.saveUpdatedManifest(workDir, newManifest, existingManifest, dryRun, fileChanges);
|
|
31
29
|
// Count stats for entries added after writeFiles (orphan deletes + manifest).
|
|
@@ -15,10 +15,17 @@ export class FileSyncStrategy {
|
|
|
15
15
|
}
|
|
16
16
|
const fileChangeDetails = changedFiles
|
|
17
17
|
.filter((f) => f.action !== "skip")
|
|
18
|
-
.map((f) =>
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
18
|
+
.map((f) => {
|
|
19
|
+
const detail = {
|
|
20
|
+
path: f.fileName,
|
|
21
|
+
action: f.action,
|
|
22
|
+
};
|
|
23
|
+
const writeResult = fileChanges.get(f.fileName);
|
|
24
|
+
if (writeResult?.diffLines) {
|
|
25
|
+
detail.diffLines = writeResult.diffLines;
|
|
26
|
+
}
|
|
27
|
+
return detail;
|
|
28
|
+
});
|
|
22
29
|
return {
|
|
23
30
|
fileChanges,
|
|
24
31
|
changedFiles,
|
package/dist/sync/file-writer.js
CHANGED
|
@@ -2,7 +2,7 @@ import { existsSync } from "node:fs";
|
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { convertContentToString } from "../config/formatter.js";
|
|
4
4
|
import { interpolateXfgContent } from "../shared/xfg-template.js";
|
|
5
|
-
import { getFileStatus, generateDiff, createDiffStats, incrementDiffStats, } from "./diff-utils.js";
|
|
5
|
+
import { getFileStatus, generateDiff, createDiffStats, incrementDiffStats, computeUnifiedDiff, isStructuredDataFile, } from "./diff-utils.js";
|
|
6
6
|
/**
|
|
7
7
|
* Determines if a file should be marked as executable.
|
|
8
8
|
* .sh files are auto-executable unless explicit executable: false is set.
|
|
@@ -65,16 +65,21 @@ export class FileWriter {
|
|
|
65
65
|
const existingContent = gitOps.getFileContent(file.fileName);
|
|
66
66
|
const changed = gitOps.wouldChange(file.fileName, fileContent);
|
|
67
67
|
if (changed) {
|
|
68
|
-
|
|
68
|
+
const writeResult = {
|
|
69
69
|
fileName: file.fileName,
|
|
70
70
|
content: fileContent,
|
|
71
71
|
action,
|
|
72
|
-
}
|
|
72
|
+
};
|
|
73
|
+
// Compute raw diff lines for structured data files (all modes)
|
|
74
|
+
if (isStructuredDataFile(file.fileName)) {
|
|
75
|
+
writeResult.diffLines = computeUnifiedDiff(existingContent, fileContent);
|
|
76
|
+
}
|
|
77
|
+
fileChanges.set(file.fileName, writeResult);
|
|
73
78
|
}
|
|
74
79
|
if (dryRun) {
|
|
75
80
|
const status = getFileStatus(existingContent !== null, changed);
|
|
76
81
|
incrementDiffStats(diffStats, status);
|
|
77
|
-
const diffLines = generateDiff(existingContent, fileContent
|
|
82
|
+
const diffLines = generateDiff(existingContent, fileContent);
|
|
78
83
|
log.fileDiff(file.fileName, status, diffLines);
|
|
79
84
|
}
|
|
80
85
|
else if (changed) {
|
package/dist/sync/index.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
1
|
export type { DiffStats } from "./diff-utils.js";
|
|
2
|
-
export
|
|
2
|
+
export { computeUnifiedDiff, isStructuredDataFile, formatDiffLine, } from "./diff-utils.js";
|
|
3
|
+
export type { FileChangeDetail, GitOpsFactory, IAuthOptionsBuilder, IBranchManager, ICommitPushManager, IFileSyncOrchestrator, IPRMergeHandler, IRepositoryProcessor, IRepositorySession, IWorkStrategy, ProcessorResult, SessionContext, WorkResult, } from "./types.js";
|
|
3
4
|
export { RepositoryProcessor } from "./repository-processor.js";
|
package/dist/sync/index.js
CHANGED
|
@@ -10,6 +10,6 @@ export declare class ManifestManager implements IManifestManager {
|
|
|
10
10
|
warn(msg: string): void;
|
|
11
11
|
} | undefined);
|
|
12
12
|
processOrphans(workDir: string, configId: string, filesWithDeleteOrphaned: Map<string, boolean | undefined>): OrphanProcessResult;
|
|
13
|
-
deleteOrphans(filesToDelete: string[], options: OrphanDeleteOptions, deps: OrphanDeleteDeps):
|
|
13
|
+
deleteOrphans(filesToDelete: string[], options: OrphanDeleteOptions, deps: OrphanDeleteDeps): void;
|
|
14
14
|
saveUpdatedManifest(workDir: string, manifest: XfgManifest, existingManifest: XfgManifest | null, dryRun: boolean, fileChanges: Map<string, FileWriteResult>): void;
|
|
15
15
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { existsSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { loadManifest, saveManifest, updateManifest, MANIFEST_FILENAME, } from "./manifest.js";
|
|
4
|
+
import { computeUnifiedDiff, isStructuredDataFile } from "./diff-utils.js";
|
|
4
5
|
/**
|
|
5
6
|
* Handles manifest loading, saving, and orphan detection.
|
|
6
7
|
*/
|
|
@@ -12,9 +13,9 @@ export class ManifestManager {
|
|
|
12
13
|
processOrphans(workDir, configId, filesWithDeleteOrphaned) {
|
|
13
14
|
const existingManifest = loadManifest(workDir, this.log);
|
|
14
15
|
const { manifest, filesToDelete } = updateManifest(existingManifest, configId, filesWithDeleteOrphaned);
|
|
15
|
-
return { manifest, filesToDelete };
|
|
16
|
+
return { manifest, existingManifest, filesToDelete };
|
|
16
17
|
}
|
|
17
|
-
|
|
18
|
+
deleteOrphans(filesToDelete, options, deps) {
|
|
18
19
|
const { dryRun, noDelete } = options;
|
|
19
20
|
const { gitOps, log, fileChanges } = deps;
|
|
20
21
|
if (filesToDelete.length === 0) {
|
|
@@ -29,11 +30,18 @@ export class ManifestManager {
|
|
|
29
30
|
if (!gitOps.fileExists(fileName)) {
|
|
30
31
|
continue;
|
|
31
32
|
}
|
|
32
|
-
|
|
33
|
+
const writeResult = {
|
|
33
34
|
fileName,
|
|
34
35
|
content: null,
|
|
35
36
|
action: "delete",
|
|
36
|
-
}
|
|
37
|
+
};
|
|
38
|
+
if (isStructuredDataFile(fileName)) {
|
|
39
|
+
const existingContent = gitOps.getFileContent(fileName);
|
|
40
|
+
if (existingContent !== null) {
|
|
41
|
+
writeResult.diffLines = computeUnifiedDiff(existingContent, null);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
fileChanges.set(fileName, writeResult);
|
|
37
45
|
if (dryRun) {
|
|
38
46
|
log.fileDiff(fileName, "DELETED", []);
|
|
39
47
|
}
|
|
@@ -56,11 +64,17 @@ export class ManifestManager {
|
|
|
56
64
|
}
|
|
57
65
|
const manifestExisted = existsSync(join(workDir, MANIFEST_FILENAME));
|
|
58
66
|
const manifestContent = JSON.stringify(manifest, null, 2) + "\n";
|
|
59
|
-
|
|
67
|
+
const writeResult = {
|
|
60
68
|
fileName: MANIFEST_FILENAME,
|
|
61
69
|
content: manifestContent,
|
|
62
70
|
action: manifestExisted ? "update" : "create",
|
|
63
|
-
}
|
|
71
|
+
};
|
|
72
|
+
// Compute diff for the manifest (it's a JSON file)
|
|
73
|
+
const oldManifestContent = existingManifest
|
|
74
|
+
? JSON.stringify(existingManifest, null, 2) + "\n"
|
|
75
|
+
: null;
|
|
76
|
+
writeResult.diffLines = computeUnifiedDiff(oldManifestContent, manifestContent);
|
|
77
|
+
fileChanges.set(MANIFEST_FILENAME, writeResult);
|
|
64
78
|
if (!dryRun) {
|
|
65
79
|
saveManifest(workDir, manifest);
|
|
66
80
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { createPR, mergePR } from "../vcs/
|
|
1
|
+
import { createPR, mergePR, getPRStrategy, } from "../vcs/index.js";
|
|
2
2
|
export class PRMergeHandler {
|
|
3
3
|
log;
|
|
4
4
|
constructor(log) {
|
|
@@ -7,6 +7,9 @@ export class PRMergeHandler {
|
|
|
7
7
|
async createAndMerge(input) {
|
|
8
8
|
const { repoInfo, repoConfig, options, changedFiles, repoName, diffStats, fileChanges, } = input;
|
|
9
9
|
this.log.info("Creating pull request...");
|
|
10
|
+
const strategy = options.dryRun
|
|
11
|
+
? undefined
|
|
12
|
+
: getPRStrategy(repoInfo, options.executor, this.log);
|
|
10
13
|
const prResult = await createPR({
|
|
11
14
|
repoInfo,
|
|
12
15
|
branchName: options.branchName,
|
|
@@ -20,6 +23,7 @@ export class PRMergeHandler {
|
|
|
20
23
|
token: options.token,
|
|
21
24
|
labels: repoConfig.prOptions?.labels,
|
|
22
25
|
log: this.log,
|
|
26
|
+
strategy,
|
|
23
27
|
});
|
|
24
28
|
const mergeMode = repoConfig.prOptions?.merge ?? "auto";
|
|
25
29
|
let mergeResult;
|
|
@@ -41,6 +45,7 @@ export class PRMergeHandler {
|
|
|
41
45
|
executor: options.executor,
|
|
42
46
|
token: options.token,
|
|
43
47
|
log: this.log,
|
|
48
|
+
strategy,
|
|
44
49
|
});
|
|
45
50
|
mergeResult = {
|
|
46
51
|
merged: result.merged ?? false,
|
|
@@ -20,7 +20,14 @@ export class RepositoryProcessor {
|
|
|
20
20
|
const factory = gitOpsFactory ??
|
|
21
21
|
((opts, auth, retries) => {
|
|
22
22
|
const gitOps = new GitOps({ ...opts, log: log });
|
|
23
|
-
return new AuthenticatedGitOps(
|
|
23
|
+
return new AuthenticatedGitOps({
|
|
24
|
+
localOps: gitOps,
|
|
25
|
+
executor: opts.executor,
|
|
26
|
+
workDir: opts.workDir,
|
|
27
|
+
retries: retries ?? 3,
|
|
28
|
+
auth,
|
|
29
|
+
log,
|
|
30
|
+
});
|
|
24
31
|
});
|
|
25
32
|
const tokenManager = components?.tokenManager ?? null;
|
|
26
33
|
const fileWriter = components?.fileWriter ?? new FileWriter();
|
package/dist/sync/types.d.ts
CHANGED
|
@@ -1,17 +1,16 @@
|
|
|
1
1
|
import type { FileContent, RepoConfig } from "../config/types.js";
|
|
2
2
|
import type { RepoInfo } from "../shared/repo-detector.js";
|
|
3
|
-
import type { ILocalGitOps, IGitOps, GitAuthOptions } from "../vcs/
|
|
4
|
-
import type { GitOpsOptions } from "../vcs/git-ops.js";
|
|
3
|
+
import type { ILocalGitOps, IGitOps, GitAuthOptions, GitOpsOptions, FileAction } from "../vcs/index.js";
|
|
5
4
|
import type { DiffStats } from "./diff-utils.js";
|
|
6
5
|
import type { ILogger } from "../shared/logger.js";
|
|
7
6
|
import type { XfgManifest } from "./manifest.js";
|
|
8
7
|
import type { ICommandExecutor } from "../shared/command-executor.js";
|
|
9
|
-
import type { FileAction } from "../vcs/types.js";
|
|
10
8
|
export type GitOpsFactory = (options: GitOpsOptions, auth?: GitAuthOptions, retries?: number) => IGitOps;
|
|
11
9
|
export interface FileWriteResult {
|
|
12
10
|
fileName: string;
|
|
13
11
|
content: string | null;
|
|
14
12
|
action: "create" | "update" | "delete" | "skip";
|
|
13
|
+
diffLines?: string[];
|
|
15
14
|
}
|
|
16
15
|
export interface FileWriteContext {
|
|
17
16
|
repoInfo: RepoInfo;
|
|
@@ -36,6 +35,7 @@ export interface IFileWriter {
|
|
|
36
35
|
}
|
|
37
36
|
export interface OrphanProcessResult {
|
|
38
37
|
manifest: XfgManifest;
|
|
38
|
+
existingManifest: XfgManifest | null;
|
|
39
39
|
filesToDelete: string[];
|
|
40
40
|
}
|
|
41
41
|
export interface OrphanDeleteOptions {
|
|
@@ -49,7 +49,7 @@ export interface OrphanDeleteDeps {
|
|
|
49
49
|
}
|
|
50
50
|
export interface IManifestManager {
|
|
51
51
|
processOrphans(workDir: string, configId: string, filesWithDeleteOrphaned: Map<string, boolean | undefined>): OrphanProcessResult;
|
|
52
|
-
deleteOrphans(filesToDelete: string[], options: OrphanDeleteOptions, deps: OrphanDeleteDeps):
|
|
52
|
+
deleteOrphans(filesToDelete: string[], options: OrphanDeleteOptions, deps: OrphanDeleteDeps): void;
|
|
53
53
|
saveUpdatedManifest(workDir: string, manifest: XfgManifest, existingManifest: XfgManifest | null, dryRun: boolean, fileChanges: Map<string, FileWriteResult>): void;
|
|
54
54
|
}
|
|
55
55
|
/** Common runtime context shared across workflow step options bags. */
|
|
@@ -135,6 +135,7 @@ export interface ProcessorOptions {
|
|
|
135
135
|
export interface FileChangeDetail {
|
|
136
136
|
path: string;
|
|
137
137
|
action: "create" | "update" | "delete";
|
|
138
|
+
diffLines?: string[];
|
|
138
139
|
}
|
|
139
140
|
export interface ProcessorResult {
|
|
140
141
|
success: boolean;
|
|
@@ -8,6 +8,14 @@ import type { GitAuthOptions, ILocalGitOps, IGitOps } from "./types.js";
|
|
|
8
8
|
* the remote origin. Subsequent operations (fetch, push, getDefaultBranch)
|
|
9
9
|
* reuse that authenticated remote URL — no extra auth setup per operation.
|
|
10
10
|
*/
|
|
11
|
+
export interface AuthenticatedGitOpsOptions {
|
|
12
|
+
localOps: ILocalGitOps;
|
|
13
|
+
executor: ICommandExecutor;
|
|
14
|
+
workDir: string;
|
|
15
|
+
retries: number;
|
|
16
|
+
auth?: GitAuthOptions;
|
|
17
|
+
log?: DebugLog;
|
|
18
|
+
}
|
|
11
19
|
export declare class AuthenticatedGitOps implements IGitOps {
|
|
12
20
|
private readonly localOps;
|
|
13
21
|
private readonly executor;
|
|
@@ -15,7 +23,7 @@ export declare class AuthenticatedGitOps implements IGitOps {
|
|
|
15
23
|
private readonly retries;
|
|
16
24
|
private readonly auth?;
|
|
17
25
|
private readonly log?;
|
|
18
|
-
constructor(
|
|
26
|
+
constructor(options: AuthenticatedGitOpsOptions);
|
|
19
27
|
private execWithRetry;
|
|
20
28
|
/**
|
|
21
29
|
* Build the authenticated remote URL.
|
|
@@ -2,13 +2,6 @@ import { escapeShellArg } from "../shared/shell-utils.js";
|
|
|
2
2
|
import { withRetry } from "../shared/retry-utils.js";
|
|
3
3
|
import { toErrorMessage } from "../shared/type-guards.js";
|
|
4
4
|
import { SyncError } from "../shared/errors.js";
|
|
5
|
-
/**
|
|
6
|
-
* Adds authentication to network git operations and delegates local ops.
|
|
7
|
-
*
|
|
8
|
-
* When auth options are provided, clone uses an embedded token URL which sets
|
|
9
|
-
* the remote origin. Subsequent operations (fetch, push, getDefaultBranch)
|
|
10
|
-
* reuse that authenticated remote URL — no extra auth setup per operation.
|
|
11
|
-
*/
|
|
12
5
|
export class AuthenticatedGitOps {
|
|
13
6
|
localOps;
|
|
14
7
|
executor;
|
|
@@ -16,13 +9,13 @@ export class AuthenticatedGitOps {
|
|
|
16
9
|
retries;
|
|
17
10
|
auth;
|
|
18
11
|
log;
|
|
19
|
-
constructor(
|
|
20
|
-
this.localOps = localOps;
|
|
21
|
-
this.executor = executor;
|
|
22
|
-
this.workDir = workDir;
|
|
23
|
-
this.retries = retries;
|
|
24
|
-
this.auth = auth;
|
|
25
|
-
this.log = log;
|
|
12
|
+
constructor(options) {
|
|
13
|
+
this.localOps = options.localOps;
|
|
14
|
+
this.executor = options.executor;
|
|
15
|
+
this.workDir = options.workDir;
|
|
16
|
+
this.retries = options.retries;
|
|
17
|
+
this.auth = options.auth;
|
|
18
|
+
this.log = options.log;
|
|
26
19
|
}
|
|
27
20
|
async execWithRetry(command) {
|
|
28
21
|
return withRetry(() => this.executor.exec(command, this.workDir), {
|
package/dist/vcs/git-ops.js
CHANGED
|
@@ -33,10 +33,15 @@ export class GitOps {
|
|
|
33
33
|
return filePath;
|
|
34
34
|
}
|
|
35
35
|
cleanWorkspace() {
|
|
36
|
-
|
|
37
|
-
|
|
36
|
+
try {
|
|
37
|
+
if (existsSync(this._workDir)) {
|
|
38
|
+
rmSync(this._workDir, { recursive: true, force: true });
|
|
39
|
+
}
|
|
40
|
+
mkdirSync(this._workDir, { recursive: true });
|
|
41
|
+
}
|
|
42
|
+
catch (error) {
|
|
43
|
+
throw new SyncError(`Failed to clean workspace '${this._workDir}': ${toErrorMessage(error)}`);
|
|
38
44
|
}
|
|
39
|
-
mkdirSync(this._workDir, { recursive: true });
|
|
40
45
|
}
|
|
41
46
|
/**
|
|
42
47
|
* Create a new branch from the current HEAD.
|
|
@@ -57,11 +62,14 @@ export class GitOps {
|
|
|
57
62
|
return;
|
|
58
63
|
}
|
|
59
64
|
const filePath = this.validatePath(fileName);
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
+
try {
|
|
66
|
+
mkdirSync(dirname(filePath), { recursive: true });
|
|
67
|
+
const normalized = content.endsWith("\n") ? content : content + "\n";
|
|
68
|
+
writeFileSync(filePath, normalized, "utf-8");
|
|
69
|
+
}
|
|
70
|
+
catch (error) {
|
|
71
|
+
throw new SyncError(`Failed to write file '${fileName}': ${toErrorMessage(error)}`);
|
|
72
|
+
}
|
|
65
73
|
}
|
|
66
74
|
/**
|
|
67
75
|
* Marks a file as executable both on the filesystem and in git's index.
|
|
@@ -74,8 +82,12 @@ export class GitOps {
|
|
|
74
82
|
return;
|
|
75
83
|
}
|
|
76
84
|
const filePath = this.validatePath(fileName);
|
|
77
|
-
|
|
78
|
-
|
|
85
|
+
try {
|
|
86
|
+
chmodSync(filePath, 0o755);
|
|
87
|
+
}
|
|
88
|
+
catch (error) {
|
|
89
|
+
throw new SyncError(`Failed to set executable permissions on '${fileName}': ${toErrorMessage(error)}`);
|
|
90
|
+
}
|
|
79
91
|
// Also update git's index so the executable bit is committed
|
|
80
92
|
const relativePath = relative(this._workDir, filePath);
|
|
81
93
|
await this.exec(`git update-index --add --chmod=+x ${escapeShellArg(relativePath)}`, this._workDir);
|
|
@@ -97,7 +109,7 @@ export class GitOps {
|
|
|
97
109
|
if (code === "ENOENT" || code === "EACCES") {
|
|
98
110
|
return null;
|
|
99
111
|
}
|
|
100
|
-
this.log?.debug(`Unexpected error reading ${fileName}: ${error
|
|
112
|
+
this.log?.debug(`Unexpected error reading ${fileName}: ${toErrorMessage(error)}`);
|
|
101
113
|
return null;
|
|
102
114
|
}
|
|
103
115
|
}
|
|
@@ -180,7 +192,12 @@ export class GitOps {
|
|
|
180
192
|
if (!existsSync(filePath)) {
|
|
181
193
|
return;
|
|
182
194
|
}
|
|
183
|
-
|
|
195
|
+
try {
|
|
196
|
+
rmSync(filePath);
|
|
197
|
+
}
|
|
198
|
+
catch (error) {
|
|
199
|
+
throw new SyncError(`Failed to delete file '${fileName}': ${toErrorMessage(error)}`);
|
|
200
|
+
}
|
|
184
201
|
}
|
|
185
202
|
/**
|
|
186
203
|
* Stage all changes and commit with the given message.
|
|
@@ -87,7 +87,12 @@ export class GitHubPRStrategy extends BasePRStrategy {
|
|
|
87
87
|
const { repoInfo, title, body, branchName, baseBranch, workDir, retries = 3, token, labels, } = options;
|
|
88
88
|
assertGitHubRepo(repoInfo, "GitHub PR strategy");
|
|
89
89
|
const bodyFile = join(workDir, this.bodyFilePath);
|
|
90
|
-
|
|
90
|
+
try {
|
|
91
|
+
writeFileSync(bodyFile, body, "utf-8");
|
|
92
|
+
}
|
|
93
|
+
catch (err) {
|
|
94
|
+
throw new SyncError(`Failed to write PR description to ${bodyFile}: ${toErrorMessage(err)}`);
|
|
95
|
+
}
|
|
91
96
|
const tokenEnv = buildTokenEnv(token);
|
|
92
97
|
let command = `gh pr create --title ${escapeShellArg(title)} --body-file ${escapeShellArg(bodyFile)} --base ${escapeShellArg(baseBranch)} --head ${escapeShellArg(branchName)}`;
|
|
93
98
|
// Append label flags
|
|
@@ -5,7 +5,7 @@ import { assertGitLabRepo } from "../shared/repo-detector.js";
|
|
|
5
5
|
import { BasePRStrategy } from "./pr-strategy.js";
|
|
6
6
|
import { withRetry, isPermanentError } from "../shared/retry-utils.js";
|
|
7
7
|
import { getStderr } from "../shared/command-executor.js";
|
|
8
|
-
import { parseApiJson } from "../shared/
|
|
8
|
+
import { parseApiJson } from "../shared/json-utils.js";
|
|
9
9
|
import { sanitizeCredentials } from "../shared/sanitize-utils.js";
|
|
10
10
|
import { toErrorMessage, safeCleanup } from "../shared/type-guards.js";
|
|
11
11
|
import { NO_OP_DEBUG_LOG } from "../shared/logger.js";
|
|
@@ -146,7 +146,12 @@ export class GitLabPRStrategy extends BasePRStrategy {
|
|
|
146
146
|
assertGitLabRepo(repoInfo, "GitLab PR strategy");
|
|
147
147
|
const repoFlag = this.getRepoFlag(repoInfo);
|
|
148
148
|
const descFile = join(workDir, this.bodyFilePath);
|
|
149
|
-
|
|
149
|
+
try {
|
|
150
|
+
writeFileSync(descFile, body, "utf-8");
|
|
151
|
+
}
|
|
152
|
+
catch (err) {
|
|
153
|
+
throw new SyncError(`Failed to write PR description to ${descFile}: ${toErrorMessage(err)}`);
|
|
154
|
+
}
|
|
150
155
|
// glab mr create with description from file
|
|
151
156
|
const command = `glab mr create --source-branch ${escapeShellArg(branchName)} --target-branch ${escapeShellArg(baseBranch)} --title ${escapeShellArg(title)} --description "$(cat ${escapeShellArg(descFile)})" --yes -R ${escapeShellArg(repoFlag)}`;
|
|
152
157
|
try {
|
|
@@ -2,7 +2,8 @@ import { isGitHubRepo } from "../shared/repo-detector.js";
|
|
|
2
2
|
import { escapeShellArg } from "../shared/shell-utils.js";
|
|
3
3
|
import { withRetry, CORE_PERMANENT_ERROR_PATTERNS, DEFAULT_PERMANENT_ERROR_PATTERNS, } from "../shared/retry-utils.js";
|
|
4
4
|
import { toErrorMessage } from "../shared/type-guards.js";
|
|
5
|
-
import { parseApiJson
|
|
5
|
+
import { parseApiJson } from "../shared/json-utils.js";
|
|
6
|
+
import { buildTokenEnv } from "../shared/gh-api-utils.js";
|
|
6
7
|
import { ValidationError, GraphQLApiError } from "../shared/errors.js";
|
|
7
8
|
/**
|
|
8
9
|
* Maximum payload size for GitHub GraphQL API (50MB).
|
package/dist/vcs/index.d.ts
CHANGED
|
@@ -2,3 +2,4 @@ export type { PRMergeConfig, FileChange, FileAction, IGitOps, ILocalGitOps, INet
|
|
|
2
2
|
export type { GitOpsOptions } from "./git-ops.js";
|
|
3
3
|
export { getCommitStrategy, createTokenManager, } from "./commit-strategy-selector.js";
|
|
4
4
|
export { getPRStrategy } from "./pr-strategy-factory.js";
|
|
5
|
+
export { createPR, mergePR } from "./pr-creator.js";
|
package/dist/vcs/index.js
CHANGED