@aspruyt/xfg 3.8.1 → 3.8.2
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.js +2 -2
- package/dist/sync/file-sync-strategy.d.ts +12 -0
- package/dist/sync/file-sync-strategy.js +30 -0
- package/dist/sync/index.d.ts +4 -1
- package/dist/sync/index.js +4 -0
- package/dist/sync/manifest-strategy.d.ts +20 -0
- package/dist/sync/manifest-strategy.js +47 -0
- package/dist/sync/repository-processor.d.ts +10 -12
- package/dist/sync/repository-processor.js +50 -271
- package/dist/sync/sync-workflow.d.ts +18 -0
- package/dist/sync/sync-workflow.js +134 -0
- package/dist/sync/types.d.ts +35 -0
- package/package.json +2 -2
package/README.md
CHANGED
package/dist/cli/program.js
CHANGED
|
@@ -56,9 +56,9 @@ const syncCommand = new Command("sync")
|
|
|
56
56
|
});
|
|
57
57
|
addSharedOptions(syncCommand);
|
|
58
58
|
program.addCommand(syncCommand);
|
|
59
|
-
// Settings command (
|
|
59
|
+
// Settings command (repository settings and rulesets)
|
|
60
60
|
const settingsCommand = new Command("settings")
|
|
61
|
-
.description("Manage GitHub
|
|
61
|
+
.description("Manage GitHub repository settings and rulesets")
|
|
62
62
|
.action((opts) => {
|
|
63
63
|
runSettings(opts).catch((error) => {
|
|
64
64
|
console.error("Fatal error:", error);
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { RepoConfig } from "../config/types.js";
|
|
2
|
+
import type { RepoInfo } from "../shared/repo-detector.js";
|
|
3
|
+
import type { IWorkStrategy, WorkResult, SessionContext, ProcessorOptions, IFileSyncOrchestrator } from "./types.js";
|
|
4
|
+
/**
|
|
5
|
+
* Strategy that performs full file synchronization.
|
|
6
|
+
* Wraps FileSyncOrchestrator to fit the IWorkStrategy interface.
|
|
7
|
+
*/
|
|
8
|
+
export declare class FileSyncStrategy implements IWorkStrategy {
|
|
9
|
+
private readonly fileSyncOrchestrator;
|
|
10
|
+
constructor(fileSyncOrchestrator: IFileSyncOrchestrator);
|
|
11
|
+
execute(repoConfig: RepoConfig, repoInfo: RepoInfo, session: SessionContext, options: ProcessorOptions): Promise<WorkResult | null>;
|
|
12
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { formatCommitMessage } from "./commit-message.js";
|
|
2
|
+
/**
|
|
3
|
+
* Strategy that performs full file synchronization.
|
|
4
|
+
* Wraps FileSyncOrchestrator to fit the IWorkStrategy interface.
|
|
5
|
+
*/
|
|
6
|
+
export class FileSyncStrategy {
|
|
7
|
+
fileSyncOrchestrator;
|
|
8
|
+
constructor(fileSyncOrchestrator) {
|
|
9
|
+
this.fileSyncOrchestrator = fileSyncOrchestrator;
|
|
10
|
+
}
|
|
11
|
+
async execute(repoConfig, repoInfo, session, options) {
|
|
12
|
+
const { fileChanges, diffStats, changedFiles, hasChanges } = await this.fileSyncOrchestrator.sync(repoConfig, repoInfo, session, options);
|
|
13
|
+
if (!hasChanges) {
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
const fileChangeDetails = changedFiles
|
|
17
|
+
.filter((f) => f.action !== "skip")
|
|
18
|
+
.map((f) => ({
|
|
19
|
+
path: f.fileName,
|
|
20
|
+
action: f.action,
|
|
21
|
+
}));
|
|
22
|
+
return {
|
|
23
|
+
fileChanges,
|
|
24
|
+
changedFiles,
|
|
25
|
+
diffStats,
|
|
26
|
+
commitMessage: formatCommitMessage(changedFiles),
|
|
27
|
+
fileChangeDetails,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
}
|
package/dist/sync/index.d.ts
CHANGED
|
@@ -7,7 +7,10 @@ export { CommitPushManager } from "./commit-push-manager.js";
|
|
|
7
7
|
export { formatCommitMessage } from "./commit-message.js";
|
|
8
8
|
export { FileSyncOrchestrator } from "./file-sync-orchestrator.js";
|
|
9
9
|
export { PRMergeHandler } from "./pr-merge-handler.js";
|
|
10
|
-
export
|
|
10
|
+
export { FileSyncStrategy } from "./file-sync-strategy.js";
|
|
11
|
+
export { ManifestStrategy, type ManifestUpdateParams, } from "./manifest-strategy.js";
|
|
12
|
+
export { SyncWorkflow } from "./sync-workflow.js";
|
|
13
|
+
export type { IFileWriter, FileWriteContext, FileWriterDeps, FileWriteAllResult, FileWriteResult, IManifestManager, OrphanProcessResult, OrphanDeleteOptions, OrphanDeleteDeps, IBranchManager, BranchSetupOptions, IAuthOptionsBuilder, AuthResult, IRepositorySession, SessionOptions, SessionContext, ICommitPushManager, CommitPushOptions, CommitPushResult, GitOpsFactory, IRepositoryProcessor, ProcessorOptions, ProcessorResult, FileChangeDetail, IFileSyncOrchestrator, FileSyncResult, IPRMergeHandler, PRHandlerOptions, WorkResult, IWorkStrategy, ISyncWorkflow, } from "./types.js";
|
|
11
14
|
export { RepositoryProcessor } from "./repository-processor.js";
|
|
12
15
|
export { createEmptyManifest, loadManifest, saveManifest, getManagedFiles, getManagedRulesets, updateManifest, updateManifestRulesets, MANIFEST_FILENAME, type XfgManifest, type XfgManifestConfigEntry, } from "./manifest.js";
|
|
13
16
|
export { getFileStatus, formatStatusBadge, formatDiffLine, generateDiff, createDiffStats, incrementDiffStats, type FileStatus, type DiffStats, } from "./diff-utils.js";
|
package/dist/sync/index.js
CHANGED
|
@@ -7,6 +7,10 @@ export { CommitPushManager } from "./commit-push-manager.js";
|
|
|
7
7
|
export { formatCommitMessage } from "./commit-message.js";
|
|
8
8
|
export { FileSyncOrchestrator } from "./file-sync-orchestrator.js";
|
|
9
9
|
export { PRMergeHandler } from "./pr-merge-handler.js";
|
|
10
|
+
// Strategy pattern components
|
|
11
|
+
export { FileSyncStrategy } from "./file-sync-strategy.js";
|
|
12
|
+
export { ManifestStrategy, } from "./manifest-strategy.js";
|
|
13
|
+
export { SyncWorkflow } from "./sync-workflow.js";
|
|
10
14
|
// Repository processor
|
|
11
15
|
export { RepositoryProcessor } from "./repository-processor.js";
|
|
12
16
|
// Manifest handling
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { RepoConfig } from "../config/types.js";
|
|
2
|
+
import type { RepoInfo } from "../shared/repo-detector.js";
|
|
3
|
+
import type { ILogger } from "../shared/logger.js";
|
|
4
|
+
import type { IWorkStrategy, WorkResult, SessionContext, ProcessorOptions } from "./types.js";
|
|
5
|
+
/**
|
|
6
|
+
* Parameters for manifest-only updates
|
|
7
|
+
*/
|
|
8
|
+
export interface ManifestUpdateParams {
|
|
9
|
+
rulesets: string[];
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Strategy that only updates the manifest with ruleset tracking.
|
|
13
|
+
* Used by updateManifestOnly() for settings command ruleset sync.
|
|
14
|
+
*/
|
|
15
|
+
export declare class ManifestStrategy implements IWorkStrategy {
|
|
16
|
+
private readonly params;
|
|
17
|
+
private readonly log;
|
|
18
|
+
constructor(params: ManifestUpdateParams, log: ILogger);
|
|
19
|
+
execute(_repoConfig: RepoConfig, _repoInfo: RepoInfo, _session: SessionContext, options: ProcessorOptions): Promise<WorkResult | null>;
|
|
20
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { loadManifest, saveManifest, updateManifestRulesets, MANIFEST_FILENAME, } from "./manifest.js";
|
|
2
|
+
/**
|
|
3
|
+
* Strategy that only updates the manifest with ruleset tracking.
|
|
4
|
+
* Used by updateManifestOnly() for settings command ruleset sync.
|
|
5
|
+
*/
|
|
6
|
+
export class ManifestStrategy {
|
|
7
|
+
params;
|
|
8
|
+
log;
|
|
9
|
+
constructor(params, log) {
|
|
10
|
+
this.params = params;
|
|
11
|
+
this.log = log;
|
|
12
|
+
}
|
|
13
|
+
async execute(_repoConfig, _repoInfo, _session, options) {
|
|
14
|
+
const { workDir, dryRun, configId } = options;
|
|
15
|
+
// Load and update manifest
|
|
16
|
+
const existingManifest = loadManifest(workDir);
|
|
17
|
+
const rulesetsWithDeleteOrphaned = new Map(this.params.rulesets.map((name) => [name, true]));
|
|
18
|
+
const { manifest: newManifest } = updateManifestRulesets(existingManifest, configId, rulesetsWithDeleteOrphaned);
|
|
19
|
+
// Check if changed
|
|
20
|
+
const existingConfigs = existingManifest?.configs ?? {};
|
|
21
|
+
if (JSON.stringify(existingConfigs) === JSON.stringify(newManifest.configs)) {
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
if (dryRun) {
|
|
25
|
+
this.log.info(`Would update ${MANIFEST_FILENAME} with rulesets`);
|
|
26
|
+
}
|
|
27
|
+
saveManifest(workDir, newManifest);
|
|
28
|
+
const fileChanges = new Map([
|
|
29
|
+
[
|
|
30
|
+
MANIFEST_FILENAME,
|
|
31
|
+
{
|
|
32
|
+
fileName: MANIFEST_FILENAME,
|
|
33
|
+
content: JSON.stringify(newManifest, null, 2) + "\n",
|
|
34
|
+
action: "update",
|
|
35
|
+
},
|
|
36
|
+
],
|
|
37
|
+
]);
|
|
38
|
+
return {
|
|
39
|
+
fileChanges,
|
|
40
|
+
changedFiles: [
|
|
41
|
+
{ fileName: MANIFEST_FILENAME, action: "update" },
|
|
42
|
+
],
|
|
43
|
+
commitMessage: "chore: update manifest with ruleset tracking",
|
|
44
|
+
fileChangeDetails: [{ path: MANIFEST_FILENAME, action: "update" }],
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -1,18 +1,15 @@
|
|
|
1
|
-
import { RepoConfig } from "../config/index.js";
|
|
2
|
-
import { RepoInfo } from "../shared/repo-detector.js";
|
|
1
|
+
import type { RepoConfig } from "../config/index.js";
|
|
2
|
+
import type { RepoInfo } from "../shared/repo-detector.js";
|
|
3
3
|
import { ILogger } from "../shared/logger.js";
|
|
4
|
-
import { type IFileWriter, type IManifestManager, type IBranchManager, type IAuthOptionsBuilder, type IRepositorySession, type ICommitPushManager, type IFileSyncOrchestrator, type IPRMergeHandler, type IRepositoryProcessor, type GitOpsFactory, type ProcessorOptions, type ProcessorResult } from "./index.js";
|
|
4
|
+
import { type IFileWriter, type IManifestManager, type IBranchManager, type IAuthOptionsBuilder, type IRepositorySession, type ICommitPushManager, type IFileSyncOrchestrator, type IPRMergeHandler, type ISyncWorkflow, type IRepositoryProcessor, type GitOpsFactory, type ProcessorOptions, type ProcessorResult } from "./index.js";
|
|
5
|
+
/**
|
|
6
|
+
* Thin facade that delegates to SyncWorkflow with appropriate strategy.
|
|
7
|
+
* process() uses FileSyncStrategy, updateManifestOnly() uses ManifestStrategy.
|
|
8
|
+
*/
|
|
5
9
|
export declare class RepositoryProcessor implements IRepositoryProcessor {
|
|
6
|
-
private readonly
|
|
7
|
-
private readonly log;
|
|
8
|
-
private readonly authOptionsBuilder;
|
|
9
|
-
private readonly repositorySession;
|
|
10
|
-
private readonly commitPushManager;
|
|
11
|
-
private readonly fileWriter;
|
|
12
|
-
private readonly manifestManager;
|
|
13
|
-
private readonly branchManager;
|
|
10
|
+
private readonly syncWorkflow;
|
|
14
11
|
private readonly fileSyncOrchestrator;
|
|
15
|
-
private readonly
|
|
12
|
+
private readonly log;
|
|
16
13
|
constructor(gitOpsFactory?: GitOpsFactory, log?: ILogger, components?: {
|
|
17
14
|
fileWriter?: IFileWriter;
|
|
18
15
|
manifestManager?: IManifestManager;
|
|
@@ -22,6 +19,7 @@ export declare class RepositoryProcessor implements IRepositoryProcessor {
|
|
|
22
19
|
commitPushManager?: ICommitPushManager;
|
|
23
20
|
fileSyncOrchestrator?: IFileSyncOrchestrator;
|
|
24
21
|
prMergeHandler?: IPRMergeHandler;
|
|
22
|
+
syncWorkflow?: ISyncWorkflow;
|
|
25
23
|
});
|
|
26
24
|
process(repoConfig: RepoConfig, repoInfo: RepoInfo, options: ProcessorOptions): Promise<ProcessorResult>;
|
|
27
25
|
updateManifestOnly(repoInfo: RepoInfo, repoConfig: RepoConfig, options: ProcessorOptions, manifestUpdate: {
|
|
@@ -1,298 +1,77 @@
|
|
|
1
|
-
import { getRepoDisplayName } from "../shared/repo-detector.js";
|
|
2
1
|
import { GitOps } from "../vcs/git-ops.js";
|
|
3
2
|
import { AuthenticatedGitOps } from "../vcs/authenticated-git-ops.js";
|
|
4
3
|
import { logger } from "../shared/logger.js";
|
|
5
4
|
import { hasGitHubAppCredentials } from "../vcs/index.js";
|
|
6
|
-
import { defaultExecutor } from "../shared/command-executor.js";
|
|
7
|
-
import { loadManifest, saveManifest, updateManifestRulesets, MANIFEST_FILENAME, } from "./manifest.js";
|
|
8
5
|
import { GitHubAppTokenManager } from "../vcs/github-app-token-manager.js";
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
path: f.fileName,
|
|
16
|
-
action: f.action,
|
|
17
|
-
}));
|
|
18
|
-
}
|
|
6
|
+
import { FileWriter, ManifestManager, BranchManager, AuthOptionsBuilder, RepositorySession, CommitPushManager, FileSyncOrchestrator, PRMergeHandler, FileSyncStrategy, ManifestStrategy, SyncWorkflow, loadManifest, updateManifestRulesets, MANIFEST_FILENAME, } from "./index.js";
|
|
7
|
+
import { getRepoDisplayName } from "../shared/repo-detector.js";
|
|
8
|
+
/**
|
|
9
|
+
* Thin facade that delegates to SyncWorkflow with appropriate strategy.
|
|
10
|
+
* process() uses FileSyncStrategy, updateManifestOnly() uses ManifestStrategy.
|
|
11
|
+
*/
|
|
19
12
|
export class RepositoryProcessor {
|
|
20
|
-
|
|
21
|
-
log;
|
|
22
|
-
authOptionsBuilder;
|
|
23
|
-
repositorySession;
|
|
24
|
-
commitPushManager;
|
|
25
|
-
fileWriter;
|
|
26
|
-
manifestManager;
|
|
27
|
-
branchManager;
|
|
13
|
+
syncWorkflow;
|
|
28
14
|
fileSyncOrchestrator;
|
|
29
|
-
|
|
15
|
+
log;
|
|
30
16
|
constructor(gitOpsFactory, log, components) {
|
|
31
17
|
const factory = gitOpsFactory ??
|
|
32
18
|
((opts, auth) => new AuthenticatedGitOps(new GitOps(opts), auth));
|
|
33
19
|
const logInstance = log ?? logger;
|
|
34
|
-
this.gitOpsFactory = factory;
|
|
35
20
|
this.log = logInstance;
|
|
36
|
-
this.fileWriter = components?.fileWriter ?? new FileWriter();
|
|
37
|
-
this.manifestManager = components?.manifestManager ?? new ManifestManager();
|
|
38
|
-
this.branchManager = components?.branchManager ?? new BranchManager();
|
|
39
21
|
// Initialize token manager for auth builder
|
|
40
22
|
const tokenManager = hasGitHubAppCredentials()
|
|
41
23
|
? new GitHubAppTokenManager(process.env.XFG_GITHUB_APP_ID, process.env.XFG_GITHUB_APP_PRIVATE_KEY)
|
|
42
24
|
: null;
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
25
|
+
const fileWriter = components?.fileWriter ?? new FileWriter();
|
|
26
|
+
const manifestManager = components?.manifestManager ?? new ManifestManager();
|
|
27
|
+
const branchManager = components?.branchManager ?? new BranchManager();
|
|
28
|
+
const authOptionsBuilder = components?.authOptionsBuilder ??
|
|
29
|
+
new AuthOptionsBuilder(tokenManager, logInstance);
|
|
30
|
+
const repositorySession = components?.repositorySession ??
|
|
31
|
+
new RepositorySession(factory, logInstance);
|
|
32
|
+
const commitPushManager = components?.commitPushManager ?? new CommitPushManager(logInstance);
|
|
33
|
+
const prMergeHandler = components?.prMergeHandler ?? new PRMergeHandler(logInstance);
|
|
51
34
|
this.fileSyncOrchestrator =
|
|
52
35
|
components?.fileSyncOrchestrator ??
|
|
53
|
-
new FileSyncOrchestrator(
|
|
54
|
-
this.
|
|
55
|
-
components?.
|
|
36
|
+
new FileSyncOrchestrator(fileWriter, manifestManager, logInstance);
|
|
37
|
+
this.syncWorkflow =
|
|
38
|
+
components?.syncWorkflow ??
|
|
39
|
+
new SyncWorkflow(authOptionsBuilder, repositorySession, branchManager, commitPushManager, prMergeHandler, logInstance);
|
|
56
40
|
}
|
|
57
41
|
async process(repoConfig, repoInfo, options) {
|
|
58
|
-
const
|
|
59
|
-
|
|
60
|
-
const retries = options.retries ?? 3;
|
|
61
|
-
const executor = options.executor ?? defaultExecutor;
|
|
62
|
-
// Resolve auth
|
|
63
|
-
const authResult = await this.authOptionsBuilder.resolve(repoInfo, repoName);
|
|
64
|
-
if (authResult.skipResult) {
|
|
65
|
-
return authResult.skipResult;
|
|
66
|
-
}
|
|
67
|
-
// Determine merge mode
|
|
68
|
-
const mergeMode = repoConfig.prOptions?.merge ?? "auto";
|
|
69
|
-
const isDirectMode = mergeMode === "direct";
|
|
70
|
-
if (isDirectMode && repoConfig.prOptions?.mergeStrategy) {
|
|
71
|
-
this.log.info(`Warning: mergeStrategy '${repoConfig.prOptions.mergeStrategy}' is ignored in direct mode`);
|
|
72
|
-
}
|
|
73
|
-
let session = null;
|
|
74
|
-
try {
|
|
75
|
-
// Setup workspace
|
|
76
|
-
session = await this.repositorySession.setup(repoInfo, {
|
|
77
|
-
workDir,
|
|
78
|
-
dryRun: dryRun ?? false,
|
|
79
|
-
retries,
|
|
80
|
-
authOptions: authResult.authOptions,
|
|
81
|
-
});
|
|
82
|
-
// Setup branch
|
|
83
|
-
await this.branchManager.setupBranch({
|
|
84
|
-
repoInfo,
|
|
85
|
-
branchName,
|
|
86
|
-
baseBranch: session.baseBranch,
|
|
87
|
-
workDir,
|
|
88
|
-
isDirectMode,
|
|
89
|
-
dryRun: dryRun ?? false,
|
|
90
|
-
retries,
|
|
91
|
-
token: authResult.token,
|
|
92
|
-
gitOps: session.gitOps,
|
|
93
|
-
log: this.log,
|
|
94
|
-
executor,
|
|
95
|
-
});
|
|
96
|
-
// Process files and manifest
|
|
97
|
-
const { fileChanges, diffStats, changedFiles, hasChanges } = await this.fileSyncOrchestrator.sync(repoConfig, repoInfo, session, options);
|
|
98
|
-
// Map file changes for reporting
|
|
99
|
-
const fileChangeDetails = mapToFileChangeDetails(changedFiles);
|
|
100
|
-
if (!hasChanges) {
|
|
101
|
-
return {
|
|
102
|
-
success: true,
|
|
103
|
-
repoName,
|
|
104
|
-
message: "No changes detected",
|
|
105
|
-
skipped: true,
|
|
106
|
-
diffStats,
|
|
107
|
-
fileChanges: fileChangeDetails,
|
|
108
|
-
};
|
|
109
|
-
}
|
|
110
|
-
// Commit and push
|
|
111
|
-
const commitMessage = formatCommitMessage(changedFiles);
|
|
112
|
-
const pushBranch = isDirectMode ? session.baseBranch : branchName;
|
|
113
|
-
const commitResult = await this.commitPushManager.commitAndPush({
|
|
114
|
-
repoInfo,
|
|
115
|
-
gitOps: session.gitOps,
|
|
116
|
-
workDir,
|
|
117
|
-
fileChanges,
|
|
118
|
-
commitMessage,
|
|
119
|
-
pushBranch,
|
|
120
|
-
isDirectMode,
|
|
121
|
-
dryRun: dryRun ?? false,
|
|
122
|
-
retries,
|
|
123
|
-
token: authResult.token,
|
|
124
|
-
executor,
|
|
125
|
-
}, repoName);
|
|
126
|
-
if (!commitResult.success && commitResult.errorResult) {
|
|
127
|
-
return commitResult.errorResult;
|
|
128
|
-
}
|
|
129
|
-
if (commitResult.skipped) {
|
|
130
|
-
return {
|
|
131
|
-
success: true,
|
|
132
|
-
repoName,
|
|
133
|
-
message: "No changes detected after staging",
|
|
134
|
-
skipped: true,
|
|
135
|
-
diffStats,
|
|
136
|
-
fileChanges: fileChangeDetails,
|
|
137
|
-
};
|
|
138
|
-
}
|
|
139
|
-
// Direct mode: no PR
|
|
140
|
-
if (isDirectMode) {
|
|
141
|
-
this.log.info(`Changes pushed directly to ${session.baseBranch}`);
|
|
142
|
-
return {
|
|
143
|
-
success: true,
|
|
144
|
-
repoName,
|
|
145
|
-
message: `Pushed directly to ${session.baseBranch}`,
|
|
146
|
-
diffStats,
|
|
147
|
-
fileChanges: fileChangeDetails,
|
|
148
|
-
};
|
|
149
|
-
}
|
|
150
|
-
// Create and merge PR
|
|
151
|
-
return await this.prMergeHandler.createAndMerge(repoInfo, repoConfig, {
|
|
152
|
-
branchName,
|
|
153
|
-
baseBranch: session.baseBranch,
|
|
154
|
-
workDir,
|
|
155
|
-
dryRun: dryRun ?? false,
|
|
156
|
-
retries,
|
|
157
|
-
prTemplate,
|
|
158
|
-
token: authResult.token,
|
|
159
|
-
executor,
|
|
160
|
-
}, changedFiles, repoName, diffStats, fileChangeDetails);
|
|
161
|
-
}
|
|
162
|
-
finally {
|
|
163
|
-
try {
|
|
164
|
-
session?.cleanup();
|
|
165
|
-
}
|
|
166
|
-
catch {
|
|
167
|
-
// Ignore cleanup errors - best effort
|
|
168
|
-
}
|
|
169
|
-
}
|
|
42
|
+
const strategy = new FileSyncStrategy(this.fileSyncOrchestrator);
|
|
43
|
+
return this.syncWorkflow.execute(repoConfig, repoInfo, options, strategy);
|
|
170
44
|
}
|
|
171
45
|
async updateManifestOnly(repoInfo, repoConfig, options, manifestUpdate) {
|
|
172
46
|
const repoName = getRepoDisplayName(repoInfo);
|
|
173
|
-
const {
|
|
174
|
-
|
|
175
|
-
const
|
|
176
|
-
|
|
177
|
-
const
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
session = await this.repositorySession.setup(repoInfo, {
|
|
187
|
-
workDir,
|
|
188
|
-
dryRun: dryRun ?? false,
|
|
189
|
-
retries,
|
|
190
|
-
authOptions: authResult.authOptions,
|
|
191
|
-
});
|
|
192
|
-
// Load and update manifest
|
|
193
|
-
const existingManifest = loadManifest(workDir);
|
|
194
|
-
const rulesetsWithDeleteOrphaned = new Map(manifestUpdate.rulesets.map((name) => [name, true]));
|
|
195
|
-
const { manifest: newManifest } = updateManifestRulesets(existingManifest, options.configId, rulesetsWithDeleteOrphaned);
|
|
196
|
-
// Check if changed
|
|
197
|
-
const existingConfigs = existingManifest?.configs ?? {};
|
|
198
|
-
if (JSON.stringify(existingConfigs) === JSON.stringify(newManifest.configs)) {
|
|
199
|
-
return {
|
|
200
|
-
success: true,
|
|
201
|
-
repoName,
|
|
202
|
-
message: "No manifest changes detected",
|
|
203
|
-
skipped: true,
|
|
204
|
-
};
|
|
205
|
-
}
|
|
206
|
-
// Prepare manifest file change details for reporting
|
|
207
|
-
const manifestFileChange = [
|
|
208
|
-
{ path: MANIFEST_FILENAME, action: "update" },
|
|
209
|
-
];
|
|
210
|
-
if (dryRun) {
|
|
211
|
-
this.log.info(`Would update ${MANIFEST_FILENAME} with rulesets`);
|
|
212
|
-
return {
|
|
213
|
-
success: true,
|
|
214
|
-
repoName,
|
|
215
|
-
message: "Would update manifest (dry-run)",
|
|
216
|
-
fileChanges: manifestFileChange,
|
|
217
|
-
};
|
|
218
|
-
}
|
|
219
|
-
// Setup branch and commit
|
|
220
|
-
await this.branchManager.setupBranch({
|
|
221
|
-
repoInfo,
|
|
222
|
-
branchName,
|
|
223
|
-
baseBranch: session.baseBranch,
|
|
224
|
-
workDir,
|
|
225
|
-
isDirectMode,
|
|
226
|
-
dryRun: false,
|
|
227
|
-
retries,
|
|
228
|
-
token: authResult.token,
|
|
229
|
-
gitOps: session.gitOps,
|
|
230
|
-
log: this.log,
|
|
231
|
-
executor,
|
|
232
|
-
});
|
|
233
|
-
saveManifest(workDir, newManifest);
|
|
234
|
-
const fileChanges = new Map([
|
|
235
|
-
[
|
|
236
|
-
MANIFEST_FILENAME,
|
|
237
|
-
{
|
|
238
|
-
fileName: MANIFEST_FILENAME,
|
|
239
|
-
content: JSON.stringify(newManifest, null, 2) + "\n",
|
|
240
|
-
action: "update",
|
|
241
|
-
},
|
|
242
|
-
],
|
|
243
|
-
]);
|
|
244
|
-
const pushBranch = isDirectMode ? session.baseBranch : branchName;
|
|
245
|
-
const commitResult = await this.commitPushManager.commitAndPush({
|
|
246
|
-
repoInfo,
|
|
247
|
-
gitOps: session.gitOps,
|
|
248
|
-
workDir,
|
|
249
|
-
fileChanges,
|
|
250
|
-
commitMessage: "chore: update manifest with ruleset tracking",
|
|
251
|
-
pushBranch,
|
|
252
|
-
isDirectMode,
|
|
253
|
-
dryRun: false,
|
|
254
|
-
retries,
|
|
255
|
-
token: authResult.token,
|
|
256
|
-
executor,
|
|
257
|
-
}, repoName);
|
|
258
|
-
if (!commitResult.success && commitResult.errorResult) {
|
|
259
|
-
return commitResult.errorResult;
|
|
260
|
-
}
|
|
261
|
-
if (commitResult.skipped) {
|
|
262
|
-
return {
|
|
263
|
-
success: true,
|
|
264
|
-
repoName,
|
|
265
|
-
message: "No changes detected after staging",
|
|
266
|
-
skipped: true,
|
|
267
|
-
fileChanges: manifestFileChange,
|
|
268
|
-
};
|
|
269
|
-
}
|
|
270
|
-
if (isDirectMode) {
|
|
271
|
-
return {
|
|
272
|
-
success: true,
|
|
273
|
-
repoName,
|
|
274
|
-
message: `Manifest updated directly on ${session.baseBranch}`,
|
|
275
|
-
fileChanges: manifestFileChange,
|
|
276
|
-
};
|
|
277
|
-
}
|
|
278
|
-
// Create and merge PR
|
|
279
|
-
return await this.prMergeHandler.createAndMerge(repoInfo, repoConfig, {
|
|
280
|
-
branchName,
|
|
281
|
-
baseBranch: session.baseBranch,
|
|
282
|
-
workDir,
|
|
283
|
-
dryRun: false,
|
|
284
|
-
retries,
|
|
285
|
-
token: authResult.token,
|
|
286
|
-
executor,
|
|
287
|
-
}, [{ fileName: MANIFEST_FILENAME, action: "update" }], repoName, undefined, manifestFileChange);
|
|
47
|
+
const { workDir, dryRun } = options;
|
|
48
|
+
// Pre-check manifest changes (preserves original early-return behavior)
|
|
49
|
+
const existingManifest = loadManifest(workDir);
|
|
50
|
+
const rulesetsWithDeleteOrphaned = new Map(manifestUpdate.rulesets.map((name) => [name, true]));
|
|
51
|
+
const { manifest: newManifest } = updateManifestRulesets(existingManifest, options.configId, rulesetsWithDeleteOrphaned);
|
|
52
|
+
const existingConfigs = existingManifest?.configs ?? {};
|
|
53
|
+
if (JSON.stringify(existingConfigs) === JSON.stringify(newManifest.configs)) {
|
|
54
|
+
return {
|
|
55
|
+
success: true,
|
|
56
|
+
repoName,
|
|
57
|
+
message: "No manifest changes detected",
|
|
58
|
+
skipped: true,
|
|
59
|
+
};
|
|
288
60
|
}
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
61
|
+
const manifestFileChange = [
|
|
62
|
+
{ path: MANIFEST_FILENAME, action: "update" },
|
|
63
|
+
];
|
|
64
|
+
if (dryRun) {
|
|
65
|
+
this.log.info(`Would update ${MANIFEST_FILENAME} with rulesets`);
|
|
66
|
+
return {
|
|
67
|
+
success: true,
|
|
68
|
+
repoName,
|
|
69
|
+
message: "Would update manifest (dry-run)",
|
|
70
|
+
fileChanges: manifestFileChange,
|
|
71
|
+
};
|
|
296
72
|
}
|
|
73
|
+
// Delegate to workflow for actual commit/push/PR
|
|
74
|
+
const strategy = new ManifestStrategy(manifestUpdate, this.log);
|
|
75
|
+
return this.syncWorkflow.execute(repoConfig, repoInfo, options, strategy);
|
|
297
76
|
}
|
|
298
77
|
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { RepoConfig } from "../config/types.js";
|
|
2
|
+
import { RepoInfo } from "../shared/repo-detector.js";
|
|
3
|
+
import type { ILogger } from "../shared/logger.js";
|
|
4
|
+
import type { ISyncWorkflow, IWorkStrategy, IAuthOptionsBuilder, IRepositorySession, IBranchManager, ICommitPushManager, IPRMergeHandler, ProcessorOptions, ProcessorResult } from "./types.js";
|
|
5
|
+
/**
|
|
6
|
+
* Orchestrates the common sync workflow steps.
|
|
7
|
+
* Used by RepositoryProcessor with different strategies for file sync vs manifest.
|
|
8
|
+
*/
|
|
9
|
+
export declare class SyncWorkflow implements ISyncWorkflow {
|
|
10
|
+
private readonly authOptionsBuilder;
|
|
11
|
+
private readonly repositorySession;
|
|
12
|
+
private readonly branchManager;
|
|
13
|
+
private readonly commitPushManager;
|
|
14
|
+
private readonly prMergeHandler;
|
|
15
|
+
private readonly log;
|
|
16
|
+
constructor(authOptionsBuilder: IAuthOptionsBuilder, repositorySession: IRepositorySession, branchManager: IBranchManager, commitPushManager: ICommitPushManager, prMergeHandler: IPRMergeHandler, log: ILogger);
|
|
17
|
+
execute(repoConfig: RepoConfig, repoInfo: RepoInfo, options: ProcessorOptions, workStrategy: IWorkStrategy): Promise<ProcessorResult>;
|
|
18
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { getRepoDisplayName } from "../shared/repo-detector.js";
|
|
2
|
+
import { defaultExecutor } from "../shared/command-executor.js";
|
|
3
|
+
/**
|
|
4
|
+
* Orchestrates the common sync workflow steps.
|
|
5
|
+
* Used by RepositoryProcessor with different strategies for file sync vs manifest.
|
|
6
|
+
*/
|
|
7
|
+
export class SyncWorkflow {
|
|
8
|
+
authOptionsBuilder;
|
|
9
|
+
repositorySession;
|
|
10
|
+
branchManager;
|
|
11
|
+
commitPushManager;
|
|
12
|
+
prMergeHandler;
|
|
13
|
+
log;
|
|
14
|
+
constructor(authOptionsBuilder, repositorySession, branchManager, commitPushManager, prMergeHandler, log) {
|
|
15
|
+
this.authOptionsBuilder = authOptionsBuilder;
|
|
16
|
+
this.repositorySession = repositorySession;
|
|
17
|
+
this.branchManager = branchManager;
|
|
18
|
+
this.commitPushManager = commitPushManager;
|
|
19
|
+
this.prMergeHandler = prMergeHandler;
|
|
20
|
+
this.log = log;
|
|
21
|
+
}
|
|
22
|
+
async execute(repoConfig, repoInfo, options, workStrategy) {
|
|
23
|
+
const repoName = getRepoDisplayName(repoInfo);
|
|
24
|
+
const { branchName, workDir, dryRun } = options;
|
|
25
|
+
const retries = options.retries ?? 3;
|
|
26
|
+
const executor = options.executor ?? defaultExecutor;
|
|
27
|
+
// Step 1: Resolve auth
|
|
28
|
+
const authResult = await this.authOptionsBuilder.resolve(repoInfo, repoName);
|
|
29
|
+
if (authResult.skipResult) {
|
|
30
|
+
return authResult.skipResult;
|
|
31
|
+
}
|
|
32
|
+
// Step 2: Determine merge mode
|
|
33
|
+
const mergeMode = repoConfig.prOptions?.merge ?? "auto";
|
|
34
|
+
const isDirectMode = mergeMode === "direct";
|
|
35
|
+
// Warn if mergeStrategy is set but ignored in direct mode
|
|
36
|
+
if (isDirectMode && repoConfig.prOptions?.mergeStrategy) {
|
|
37
|
+
this.log.info(`Warning: mergeStrategy '${repoConfig.prOptions.mergeStrategy}' is ignored in direct mode`);
|
|
38
|
+
}
|
|
39
|
+
let session = null;
|
|
40
|
+
try {
|
|
41
|
+
// Step 3: Setup session
|
|
42
|
+
session = await this.repositorySession.setup(repoInfo, {
|
|
43
|
+
workDir,
|
|
44
|
+
dryRun: dryRun ?? false,
|
|
45
|
+
retries,
|
|
46
|
+
authOptions: authResult.authOptions,
|
|
47
|
+
});
|
|
48
|
+
// Step 4: Setup branch
|
|
49
|
+
await this.branchManager.setupBranch({
|
|
50
|
+
repoInfo,
|
|
51
|
+
branchName,
|
|
52
|
+
baseBranch: session.baseBranch,
|
|
53
|
+
workDir,
|
|
54
|
+
isDirectMode,
|
|
55
|
+
dryRun: dryRun ?? false,
|
|
56
|
+
retries,
|
|
57
|
+
token: authResult.token,
|
|
58
|
+
gitOps: session.gitOps,
|
|
59
|
+
log: this.log,
|
|
60
|
+
executor,
|
|
61
|
+
});
|
|
62
|
+
// Step 5: Execute work strategy
|
|
63
|
+
const workResult = await workStrategy.execute(repoConfig, repoInfo, session, options);
|
|
64
|
+
// Step 6: No changes - skip
|
|
65
|
+
if (!workResult) {
|
|
66
|
+
return {
|
|
67
|
+
success: true,
|
|
68
|
+
repoName,
|
|
69
|
+
message: "No changes detected",
|
|
70
|
+
skipped: true,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
// Step 7: Commit and push
|
|
74
|
+
const pushBranch = isDirectMode ? session.baseBranch : branchName;
|
|
75
|
+
const commitResult = await this.commitPushManager.commitAndPush({
|
|
76
|
+
repoInfo,
|
|
77
|
+
gitOps: session.gitOps,
|
|
78
|
+
workDir,
|
|
79
|
+
fileChanges: workResult.fileChanges,
|
|
80
|
+
commitMessage: workResult.commitMessage,
|
|
81
|
+
pushBranch,
|
|
82
|
+
isDirectMode,
|
|
83
|
+
dryRun: dryRun ?? false,
|
|
84
|
+
retries,
|
|
85
|
+
token: authResult.token,
|
|
86
|
+
executor,
|
|
87
|
+
}, repoName);
|
|
88
|
+
// Step 8: Handle commit errors
|
|
89
|
+
if (!commitResult.success && commitResult.errorResult) {
|
|
90
|
+
return commitResult.errorResult;
|
|
91
|
+
}
|
|
92
|
+
if (commitResult.skipped) {
|
|
93
|
+
return {
|
|
94
|
+
success: true,
|
|
95
|
+
repoName,
|
|
96
|
+
message: "No changes detected after staging",
|
|
97
|
+
skipped: true,
|
|
98
|
+
diffStats: workResult.diffStats,
|
|
99
|
+
fileChanges: workResult.fileChangeDetails,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
// Step 9: Direct mode - done
|
|
103
|
+
if (isDirectMode) {
|
|
104
|
+
this.log.info(`Changes pushed directly to ${session.baseBranch}`);
|
|
105
|
+
return {
|
|
106
|
+
success: true,
|
|
107
|
+
repoName,
|
|
108
|
+
message: `Pushed directly to ${session.baseBranch}`,
|
|
109
|
+
diffStats: workResult.diffStats,
|
|
110
|
+
fileChanges: workResult.fileChangeDetails,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
// Step 10: Create and merge PR
|
|
114
|
+
return await this.prMergeHandler.createAndMerge(repoInfo, repoConfig, {
|
|
115
|
+
branchName,
|
|
116
|
+
baseBranch: session.baseBranch,
|
|
117
|
+
workDir,
|
|
118
|
+
dryRun: dryRun ?? false,
|
|
119
|
+
retries,
|
|
120
|
+
prTemplate: options.prTemplate,
|
|
121
|
+
token: authResult.token,
|
|
122
|
+
executor,
|
|
123
|
+
}, workResult.changedFiles, repoName, workResult.diffStats, workResult.fileChangeDetails);
|
|
124
|
+
}
|
|
125
|
+
finally {
|
|
126
|
+
try {
|
|
127
|
+
session?.cleanup();
|
|
128
|
+
}
|
|
129
|
+
catch {
|
|
130
|
+
// Ignore cleanup errors - best effort
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
package/dist/sync/types.d.ts
CHANGED
|
@@ -302,3 +302,38 @@ export interface IPRMergeHandler {
|
|
|
302
302
|
*/
|
|
303
303
|
createAndMerge(repoInfo: RepoInfo, repoConfig: RepoConfig, options: PRHandlerOptions, changedFiles: FileAction[], repoName: string, diffStats?: DiffStats, fileChanges?: FileChangeDetail[]): Promise<ProcessorResult>;
|
|
304
304
|
}
|
|
305
|
+
/**
|
|
306
|
+
* Result of executing work within a sync workflow
|
|
307
|
+
*/
|
|
308
|
+
export interface WorkResult {
|
|
309
|
+
/** File changes to commit */
|
|
310
|
+
fileChanges: Map<string, FileWriteResult>;
|
|
311
|
+
/** Changed files for PR body */
|
|
312
|
+
changedFiles: FileAction[];
|
|
313
|
+
/** Diff statistics for reporting */
|
|
314
|
+
diffStats?: DiffStats;
|
|
315
|
+
/** Human-readable commit message */
|
|
316
|
+
commitMessage: string;
|
|
317
|
+
/** File change details for result reporting */
|
|
318
|
+
fileChangeDetails: FileChangeDetail[];
|
|
319
|
+
}
|
|
320
|
+
/**
|
|
321
|
+
* Strategy for executing work within the sync workflow.
|
|
322
|
+
* Implementations define what changes to make (files vs manifest).
|
|
323
|
+
*/
|
|
324
|
+
export interface IWorkStrategy {
|
|
325
|
+
/**
|
|
326
|
+
* Execute work and return changes to commit.
|
|
327
|
+
* Return null if no changes detected (workflow will skip).
|
|
328
|
+
*/
|
|
329
|
+
execute(repoConfig: RepoConfig, repoInfo: RepoInfo, session: SessionContext, options: ProcessorOptions): Promise<WorkResult | null>;
|
|
330
|
+
}
|
|
331
|
+
/**
|
|
332
|
+
* Orchestrates the common sync workflow steps.
|
|
333
|
+
*/
|
|
334
|
+
export interface ISyncWorkflow {
|
|
335
|
+
/**
|
|
336
|
+
* Execute workflow: auth → session → branch → work → commit → PR
|
|
337
|
+
*/
|
|
338
|
+
execute(repoConfig: RepoConfig, repoInfo: RepoInfo, options: ProcessorOptions, workStrategy: IWorkStrategy): Promise<ProcessorResult>;
|
|
339
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aspruyt/xfg",
|
|
3
|
-
"version": "3.8.
|
|
4
|
-
"description": "CLI tool for repository-as-code",
|
|
3
|
+
"version": "3.8.2",
|
|
4
|
+
"description": "CLI tool for repository-as-code: sync files and manage settings across GitHub, Azure DevOps, and GitLab",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
7
7
|
"bin": {
|