@aspruyt/xfg 3.13.0 → 4.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 -4
- package/dist/cli/index.d.ts +0 -2
- package/dist/cli/index.js +0 -1
- package/dist/cli/program.js +0 -12
- package/dist/cli/sync-command.d.ts +2 -3
- package/dist/cli/sync-command.js +143 -4
- package/dist/cli/types.d.ts +11 -4
- package/dist/config/index.d.ts +1 -1
- package/dist/config/index.js +1 -1
- package/dist/config/validator.d.ts +0 -5
- package/dist/config/validator.js +13 -30
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/lifecycle/github-lifecycle-provider.d.ts +9 -0
- package/dist/lifecycle/github-lifecycle-provider.js +31 -0
- package/dist/settings/labels/diff.d.ts +2 -2
- package/dist/settings/labels/diff.js +15 -19
- package/dist/settings/labels/processor.d.ts +0 -10
- package/dist/settings/labels/processor.js +3 -16
- package/dist/settings/rulesets/diff.d.ts +2 -2
- package/dist/settings/rulesets/diff.js +6 -8
- package/dist/settings/rulesets/processor.d.ts +0 -10
- package/dist/settings/rulesets/processor.js +3 -18
- package/dist/sync/index.d.ts +1 -2
- package/dist/sync/index.js +1 -2
- package/dist/sync/manifest.d.ts +3 -45
- package/dist/sync/manifest.js +46 -166
- package/dist/sync/repository-processor.d.ts +1 -6
- package/dist/sync/repository-processor.js +2 -52
- package/dist/sync/types.d.ts +0 -4
- package/dist/vcs/graphql-commit-strategy.js +15 -1
- package/package.json +1 -1
- package/dist/cli/settings/lifecycle-checks.d.ts +0 -11
- package/dist/cli/settings/lifecycle-checks.js +0 -64
- package/dist/cli/settings/process-labels.d.ts +0 -9
- package/dist/cli/settings/process-labels.js +0 -125
- package/dist/cli/settings/process-repo-settings.d.ts +0 -9
- package/dist/cli/settings/process-repo-settings.js +0 -80
- package/dist/cli/settings/process-rulesets.d.ts +0 -9
- package/dist/cli/settings/process-rulesets.js +0 -118
- package/dist/cli/settings-command.d.ts +0 -11
- package/dist/cli/settings-command.js +0 -90
- package/dist/sync/manifest-strategy.d.ts +0 -21
- package/dist/sync/manifest-strategy.js +0 -67
|
@@ -201,13 +201,12 @@ function matchByIndex(current, desired) {
|
|
|
201
201
|
*
|
|
202
202
|
* @param current - Current rulesets from GitHub API
|
|
203
203
|
* @param desired - Desired rulesets from config (name → ruleset)
|
|
204
|
-
* @param
|
|
204
|
+
* @param deleteOrphaned - When true, delete ALL current rulesets not in desired (desired-state model)
|
|
205
205
|
* @returns Array of changes to apply
|
|
206
206
|
*/
|
|
207
|
-
export function diffRulesets(current, desired,
|
|
207
|
+
export function diffRulesets(current, desired, deleteOrphaned) {
|
|
208
208
|
const changes = [];
|
|
209
209
|
const currentByName = new Map(current.map((r) => [r.name, r]));
|
|
210
|
-
const managedSet = new Set(managedNames);
|
|
211
210
|
// Check each desired ruleset
|
|
212
211
|
for (const [name, desiredRuleset] of desired) {
|
|
213
212
|
const currentRuleset = currentByName.get(name);
|
|
@@ -244,11 +243,10 @@ export function diffRulesets(current, desired, managedNames) {
|
|
|
244
243
|
}
|
|
245
244
|
}
|
|
246
245
|
}
|
|
247
|
-
//
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
if (currentRuleset) {
|
|
246
|
+
// Desired-state: delete ALL current rulesets not in desired when deleteOrphaned is true
|
|
247
|
+
if (deleteOrphaned) {
|
|
248
|
+
for (const [name, currentRuleset] of currentByName) {
|
|
249
|
+
if (!desired.has(name)) {
|
|
252
250
|
changes.push({
|
|
253
251
|
action: "delete",
|
|
254
252
|
name,
|
|
@@ -6,9 +6,7 @@ export interface IRulesetProcessor {
|
|
|
6
6
|
process(repoConfig: RepoConfig, repoInfo: RepoInfo, options: RulesetProcessorOptions): Promise<RulesetProcessorResult>;
|
|
7
7
|
}
|
|
8
8
|
export interface RulesetProcessorOptions {
|
|
9
|
-
configId: string;
|
|
10
9
|
dryRun?: boolean;
|
|
11
|
-
managedRulesets: string[];
|
|
12
10
|
noDelete?: boolean;
|
|
13
11
|
token?: string;
|
|
14
12
|
}
|
|
@@ -24,9 +22,6 @@ export interface RulesetProcessorResult {
|
|
|
24
22
|
delete: number;
|
|
25
23
|
unchanged: number;
|
|
26
24
|
};
|
|
27
|
-
manifestUpdate?: {
|
|
28
|
-
rulesets: string[];
|
|
29
|
-
};
|
|
30
25
|
planOutput?: RulesetPlanResult;
|
|
31
26
|
}
|
|
32
27
|
/**
|
|
@@ -45,11 +40,6 @@ export declare class RulesetProcessor implements IRulesetProcessor {
|
|
|
45
40
|
* Format change counts into a summary string.
|
|
46
41
|
*/
|
|
47
42
|
private formatChangeSummary;
|
|
48
|
-
/**
|
|
49
|
-
* Compute manifest update based on current config.
|
|
50
|
-
* Only rulesets with deleteOrphaned enabled should be tracked.
|
|
51
|
-
*/
|
|
52
|
-
private computeManifestUpdate;
|
|
53
43
|
/**
|
|
54
44
|
* Resolves a GitHub App installation token for the given repo.
|
|
55
45
|
* Returns undefined if no token manager or token resolution fails.
|
|
@@ -28,7 +28,7 @@ export class RulesetProcessor {
|
|
|
28
28
|
*/
|
|
29
29
|
async process(repoConfig, repoInfo, options) {
|
|
30
30
|
const repoName = getRepoDisplayName(repoInfo);
|
|
31
|
-
const { dryRun,
|
|
31
|
+
const { dryRun, noDelete, token } = options;
|
|
32
32
|
// Check if this is a GitHub repo
|
|
33
33
|
if (!isGitHubRepo(repoInfo)) {
|
|
34
34
|
return {
|
|
@@ -43,8 +43,7 @@ export class RulesetProcessor {
|
|
|
43
43
|
const desiredRulesets = settings?.rulesets ?? {};
|
|
44
44
|
const deleteOrphaned = settings?.deleteOrphaned ?? false;
|
|
45
45
|
// If no rulesets configured, skip
|
|
46
|
-
if (Object.keys(desiredRulesets).length === 0
|
|
47
|
-
managedRulesets.length === 0) {
|
|
46
|
+
if (Object.keys(desiredRulesets).length === 0) {
|
|
48
47
|
return {
|
|
49
48
|
success: true,
|
|
50
49
|
repoName,
|
|
@@ -73,7 +72,7 @@ export class RulesetProcessor {
|
|
|
73
72
|
}
|
|
74
73
|
}
|
|
75
74
|
// Compute diff
|
|
76
|
-
const changes = diffRulesets(fullRulesets, desiredMap,
|
|
75
|
+
const changes = diffRulesets(fullRulesets, desiredMap, deleteOrphaned);
|
|
77
76
|
// Count changes by type
|
|
78
77
|
const changeCounts = {
|
|
79
78
|
create: changes.filter((c) => c.action === "create").length,
|
|
@@ -92,7 +91,6 @@ export class RulesetProcessor {
|
|
|
92
91
|
dryRun: true,
|
|
93
92
|
changes: changeCounts,
|
|
94
93
|
planOutput,
|
|
95
|
-
manifestUpdate: this.computeManifestUpdate(desiredRulesets, deleteOrphaned),
|
|
96
94
|
};
|
|
97
95
|
}
|
|
98
96
|
// Apply changes
|
|
@@ -130,7 +128,6 @@ export class RulesetProcessor {
|
|
|
130
128
|
message: appliedCount > 0 ? `Applied: ${summary}` : "No changes needed",
|
|
131
129
|
changes: changeCounts,
|
|
132
130
|
planOutput,
|
|
133
|
-
manifestUpdate: this.computeManifestUpdate(desiredRulesets, deleteOrphaned),
|
|
134
131
|
};
|
|
135
132
|
}
|
|
136
133
|
catch (error) {
|
|
@@ -157,18 +154,6 @@ export class RulesetProcessor {
|
|
|
157
154
|
parts.push(`${counts.unchanged} unchanged`);
|
|
158
155
|
return parts.length > 0 ? parts.join(", ") : "no changes";
|
|
159
156
|
}
|
|
160
|
-
/**
|
|
161
|
-
* Compute manifest update based on current config.
|
|
162
|
-
* Only rulesets with deleteOrphaned enabled should be tracked.
|
|
163
|
-
*/
|
|
164
|
-
computeManifestUpdate(rulesets, deleteOrphaned) {
|
|
165
|
-
if (!deleteOrphaned) {
|
|
166
|
-
return undefined;
|
|
167
|
-
}
|
|
168
|
-
// Track all ruleset names when deleteOrphaned is enabled
|
|
169
|
-
const rulesetNames = Object.keys(rulesets).sort();
|
|
170
|
-
return { rulesets: rulesetNames };
|
|
171
|
-
}
|
|
172
157
|
/**
|
|
173
158
|
* Resolves a GitHub App installation token for the given repo.
|
|
174
159
|
* Returns undefined if no token manager or token resolution fails.
|
package/dist/sync/index.d.ts
CHANGED
|
@@ -8,10 +8,9 @@ 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
10
|
export { FileSyncStrategy } from "./file-sync-strategy.js";
|
|
11
|
-
export { ManifestStrategy, type ManifestUpdateParams, } from "./manifest-strategy.js";
|
|
12
11
|
export { SyncWorkflow } from "./sync-workflow.js";
|
|
13
12
|
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";
|
|
14
13
|
export { RepositoryProcessor } from "./repository-processor.js";
|
|
15
|
-
export { createEmptyManifest, loadManifest, saveManifest, getManagedFiles,
|
|
14
|
+
export { createEmptyManifest, loadManifest, saveManifest, getManagedFiles, updateManifest, MANIFEST_FILENAME, type XfgManifest, type XfgManifestConfigEntry, } from "./manifest.js";
|
|
16
15
|
export { getFileStatus, formatStatusBadge, formatDiffLine, generateDiff, createDiffStats, incrementDiffStats, type FileStatus, type DiffStats, } from "./diff-utils.js";
|
|
17
16
|
export { interpolateXfgContent, type XfgTemplateContext, type XfgInterpolationOptions, } from "./xfg-template.js";
|
package/dist/sync/index.js
CHANGED
|
@@ -9,12 +9,11 @@ export { FileSyncOrchestrator } from "./file-sync-orchestrator.js";
|
|
|
9
9
|
export { PRMergeHandler } from "./pr-merge-handler.js";
|
|
10
10
|
// Strategy pattern components
|
|
11
11
|
export { FileSyncStrategy } from "./file-sync-strategy.js";
|
|
12
|
-
export { ManifestStrategy, } from "./manifest-strategy.js";
|
|
13
12
|
export { SyncWorkflow } from "./sync-workflow.js";
|
|
14
13
|
// Repository processor
|
|
15
14
|
export { RepositoryProcessor } from "./repository-processor.js";
|
|
16
15
|
// Manifest handling
|
|
17
|
-
export { createEmptyManifest, loadManifest, saveManifest, getManagedFiles,
|
|
16
|
+
export { createEmptyManifest, loadManifest, saveManifest, getManagedFiles, updateManifest, MANIFEST_FILENAME, } from "./manifest.js";
|
|
18
17
|
// Diff utilities
|
|
19
18
|
export { getFileStatus, formatStatusBadge, formatDiffLine, generateDiff, createDiffStats, incrementDiffStats, } from "./diff-utils.js";
|
|
20
19
|
// XFG templating
|
package/dist/sync/manifest.d.ts
CHANGED
|
@@ -1,11 +1,9 @@
|
|
|
1
1
|
export declare const MANIFEST_FILENAME = ".xfg.json";
|
|
2
2
|
export interface XfgManifestConfigEntry {
|
|
3
3
|
files?: string[];
|
|
4
|
-
rulesets?: string[];
|
|
5
|
-
labels?: string[];
|
|
6
4
|
}
|
|
7
5
|
export interface XfgManifest {
|
|
8
|
-
version:
|
|
6
|
+
version: 4;
|
|
9
7
|
configs: Record<string, XfgManifestConfigEntry>;
|
|
10
8
|
}
|
|
11
9
|
/**
|
|
@@ -18,9 +16,9 @@ export declare function createEmptyManifest(): XfgManifest;
|
|
|
18
16
|
*
|
|
19
17
|
* V1 manifests are treated as non-existent because they lack the config ID
|
|
20
18
|
* namespace required for multi-config support. The next run will create
|
|
21
|
-
* a fresh
|
|
19
|
+
* a fresh v4 manifest.
|
|
22
20
|
*
|
|
23
|
-
* V2 manifests are automatically migrated to
|
|
21
|
+
* V2/V3 manifests are automatically migrated to V4 format.
|
|
24
22
|
*
|
|
25
23
|
* @param workDir - The repository working directory
|
|
26
24
|
* @returns The manifest or null if not found or incompatible
|
|
@@ -47,20 +45,6 @@ export declare function saveManifest(workDir: string, manifest: XfgManifest): vo
|
|
|
47
45
|
* @returns Array of managed file names for the given config
|
|
48
46
|
*/
|
|
49
47
|
export declare function getManagedFiles(manifest: XfgManifest | null, configId: string): string[];
|
|
50
|
-
/**
|
|
51
|
-
* Gets the list of managed rulesets for a specific config from a manifest.
|
|
52
|
-
* Returns an empty array if the manifest is null or the config isn't found.
|
|
53
|
-
*
|
|
54
|
-
* @param manifest - The manifest or null
|
|
55
|
-
* @param configId - The config ID to get rulesets for
|
|
56
|
-
* @returns Array of managed ruleset names for the given config
|
|
57
|
-
*/
|
|
58
|
-
export declare function getManagedRulesets(manifest: XfgManifest | null, configId: string): string[];
|
|
59
|
-
/**
|
|
60
|
-
* Gets the list of managed labels for a specific config from a manifest.
|
|
61
|
-
* Returns an empty array if the manifest is null or the config isn't found.
|
|
62
|
-
*/
|
|
63
|
-
export declare function getManagedLabels(manifest: XfgManifest | null, configId: string): string[];
|
|
64
48
|
/**
|
|
65
49
|
* Updates the manifest with the current set of files that have deleteOrphaned enabled
|
|
66
50
|
* for a specific config. Only modifies that config's files namespace - other configs are untouched.
|
|
@@ -78,29 +62,3 @@ export declare function updateManifest(manifest: XfgManifest | null, configId: s
|
|
|
78
62
|
manifest: XfgManifest;
|
|
79
63
|
filesToDelete: string[];
|
|
80
64
|
};
|
|
81
|
-
/**
|
|
82
|
-
* Updates the manifest with the current set of rulesets that have deleteOrphaned enabled
|
|
83
|
-
* for a specific config. Only modifies that config's rulesets namespace - other configs are untouched.
|
|
84
|
-
*
|
|
85
|
-
* @param manifest - The existing manifest (or null for new repos)
|
|
86
|
-
* @param configId - The config ID to update
|
|
87
|
-
* @param rulesetsWithDeleteOrphaned - Map of ruleset name to deleteOrphaned value (true/false/undefined)
|
|
88
|
-
* @returns Updated manifest and list of rulesets to delete
|
|
89
|
-
*/
|
|
90
|
-
export declare function updateManifestRulesets(manifest: XfgManifest | null, configId: string, rulesetsWithDeleteOrphaned: Map<string, boolean | undefined>): {
|
|
91
|
-
manifest: XfgManifest;
|
|
92
|
-
rulesetsToDelete: string[];
|
|
93
|
-
};
|
|
94
|
-
/**
|
|
95
|
-
* Updates the manifest with the current set of labels that have deleteOrphaned enabled
|
|
96
|
-
* for a specific config. Only modifies that config's labels namespace - other configs are untouched.
|
|
97
|
-
*
|
|
98
|
-
* @param manifest - The existing manifest (or null for new repos)
|
|
99
|
-
* @param configId - The config ID to update
|
|
100
|
-
* @param labelsWithDeleteOrphaned - Map of label name to deleteOrphaned value (true/false/undefined)
|
|
101
|
-
* @returns Updated manifest and list of labels to delete
|
|
102
|
-
*/
|
|
103
|
-
export declare function updateManifestLabels(manifest: XfgManifest | null, configId: string, labelsWithDeleteOrphaned: Map<string, boolean | undefined>): {
|
|
104
|
-
manifest: XfgManifest;
|
|
105
|
-
labelsToDelete: string[];
|
|
106
|
-
};
|
package/dist/sync/manifest.js
CHANGED
|
@@ -30,10 +30,20 @@ function isV3Manifest(manifest) {
|
|
|
30
30
|
typeof manifest.configs === "object" &&
|
|
31
31
|
manifest.configs !== null);
|
|
32
32
|
}
|
|
33
|
+
/**
|
|
34
|
+
* Type guard to check if a manifest is v4 format.
|
|
35
|
+
*/
|
|
36
|
+
function isV4Manifest(manifest) {
|
|
37
|
+
return (typeof manifest === "object" &&
|
|
38
|
+
manifest !== null &&
|
|
39
|
+
manifest.version === 4 &&
|
|
40
|
+
typeof manifest.configs === "object" &&
|
|
41
|
+
manifest.configs !== null);
|
|
42
|
+
}
|
|
33
43
|
/**
|
|
34
44
|
* Migrates a V2 manifest to V3 format.
|
|
35
45
|
* V2: configs is Record<string, string[]>
|
|
36
|
-
* V3: configs is Record<string, { files?: string[], rulesets?: string[] }>
|
|
46
|
+
* V3: configs is Record<string, { files?: string[], rulesets?: string[], labels?: string[] }>
|
|
37
47
|
*/
|
|
38
48
|
function migrateV2ToV3(v2) {
|
|
39
49
|
const v3Configs = {};
|
|
@@ -47,12 +57,27 @@ function migrateV2ToV3(v2) {
|
|
|
47
57
|
configs: v3Configs,
|
|
48
58
|
};
|
|
49
59
|
}
|
|
60
|
+
/**
|
|
61
|
+
* Migrates a V3 manifest to V4 format.
|
|
62
|
+
* V3: configs have files, rulesets, labels
|
|
63
|
+
* V4: configs have files only — rulesets and labels are dropped
|
|
64
|
+
*/
|
|
65
|
+
function migrateV3ToV4(v3) {
|
|
66
|
+
const v4Configs = {};
|
|
67
|
+
for (const [configId, entry] of Object.entries(v3.configs)) {
|
|
68
|
+
// Only preserve files — rulesets and labels are dropped
|
|
69
|
+
if (entry.files && entry.files.length > 0) {
|
|
70
|
+
v4Configs[configId] = { files: entry.files };
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return { version: 4, configs: v4Configs };
|
|
74
|
+
}
|
|
50
75
|
/**
|
|
51
76
|
* Creates an empty manifest with the current version.
|
|
52
77
|
*/
|
|
53
78
|
export function createEmptyManifest() {
|
|
54
79
|
return {
|
|
55
|
-
version:
|
|
80
|
+
version: 4,
|
|
56
81
|
configs: {},
|
|
57
82
|
};
|
|
58
83
|
}
|
|
@@ -62,9 +87,9 @@ export function createEmptyManifest() {
|
|
|
62
87
|
*
|
|
63
88
|
* V1 manifests are treated as non-existent because they lack the config ID
|
|
64
89
|
* namespace required for multi-config support. The next run will create
|
|
65
|
-
* a fresh
|
|
90
|
+
* a fresh v4 manifest.
|
|
66
91
|
*
|
|
67
|
-
* V2 manifests are automatically migrated to
|
|
92
|
+
* V2/V3 manifests are automatically migrated to V4 format.
|
|
68
93
|
*
|
|
69
94
|
* @param workDir - The repository working directory
|
|
70
95
|
* @returns The manifest or null if not found or incompatible
|
|
@@ -77,15 +102,19 @@ export function loadManifest(workDir) {
|
|
|
77
102
|
try {
|
|
78
103
|
const content = readFileSync(manifestPath, "utf-8");
|
|
79
104
|
const parsed = JSON.parse(content);
|
|
80
|
-
//
|
|
81
|
-
if (
|
|
105
|
+
// V4 manifest - return as-is
|
|
106
|
+
if (isV4Manifest(parsed)) {
|
|
82
107
|
return parsed;
|
|
83
108
|
}
|
|
84
|
-
//
|
|
109
|
+
// V3 manifest - migrate to V4
|
|
110
|
+
if (isV3Manifest(parsed)) {
|
|
111
|
+
return migrateV3ToV4(parsed);
|
|
112
|
+
}
|
|
113
|
+
// V2 manifest - migrate to V3, then to V4
|
|
85
114
|
if (isV2Manifest(parsed)) {
|
|
86
|
-
return migrateV2ToV3(parsed);
|
|
115
|
+
return migrateV3ToV4(migrateV2ToV3(parsed));
|
|
87
116
|
}
|
|
88
|
-
// V1 manifest - treat as no manifest (will be overwritten with
|
|
117
|
+
// V1 manifest - treat as no manifest (will be overwritten with v4)
|
|
89
118
|
if (isV1Manifest(parsed)) {
|
|
90
119
|
return null;
|
|
91
120
|
}
|
|
@@ -103,11 +132,14 @@ export function loadManifest(workDir) {
|
|
|
103
132
|
export function parseManifestContent(content) {
|
|
104
133
|
try {
|
|
105
134
|
const parsed = JSON.parse(content);
|
|
106
|
-
if (
|
|
135
|
+
if (isV4Manifest(parsed)) {
|
|
107
136
|
return parsed;
|
|
108
137
|
}
|
|
138
|
+
if (isV3Manifest(parsed)) {
|
|
139
|
+
return migrateV3ToV4(parsed);
|
|
140
|
+
}
|
|
109
141
|
if (isV2Manifest(parsed)) {
|
|
110
|
-
return migrateV2ToV3(parsed);
|
|
142
|
+
return migrateV3ToV4(migrateV2ToV3(parsed));
|
|
111
143
|
}
|
|
112
144
|
return null;
|
|
113
145
|
}
|
|
@@ -140,30 +172,6 @@ export function getManagedFiles(manifest, configId) {
|
|
|
140
172
|
}
|
|
141
173
|
return [...(manifest.configs[configId]?.files ?? [])];
|
|
142
174
|
}
|
|
143
|
-
/**
|
|
144
|
-
* Gets the list of managed rulesets for a specific config from a manifest.
|
|
145
|
-
* Returns an empty array if the manifest is null or the config isn't found.
|
|
146
|
-
*
|
|
147
|
-
* @param manifest - The manifest or null
|
|
148
|
-
* @param configId - The config ID to get rulesets for
|
|
149
|
-
* @returns Array of managed ruleset names for the given config
|
|
150
|
-
*/
|
|
151
|
-
export function getManagedRulesets(manifest, configId) {
|
|
152
|
-
if (!manifest) {
|
|
153
|
-
return [];
|
|
154
|
-
}
|
|
155
|
-
return [...(manifest.configs[configId]?.rulesets ?? [])];
|
|
156
|
-
}
|
|
157
|
-
/**
|
|
158
|
-
* Gets the list of managed labels for a specific config from a manifest.
|
|
159
|
-
* Returns an empty array if the manifest is null or the config isn't found.
|
|
160
|
-
*/
|
|
161
|
-
export function getManagedLabels(manifest, configId) {
|
|
162
|
-
if (!manifest) {
|
|
163
|
-
return [];
|
|
164
|
-
}
|
|
165
|
-
return [...(manifest.configs[configId]?.labels ?? [])];
|
|
166
|
-
}
|
|
167
175
|
/**
|
|
168
176
|
* Updates the manifest with the current set of files that have deleteOrphaned enabled
|
|
169
177
|
* for a specific config. Only modifies that config's files namespace - other configs are untouched.
|
|
@@ -202,147 +210,19 @@ export function updateManifest(manifest, configId, filesWithDeleteOrphaned) {
|
|
|
202
210
|
const updatedConfigs = {
|
|
203
211
|
...(manifest?.configs ?? {}),
|
|
204
212
|
};
|
|
205
|
-
// Preserve existing rulesets and labels for this config
|
|
206
|
-
const existingEntry = manifest?.configs[configId];
|
|
207
|
-
const existingRulesets = existingEntry?.rulesets;
|
|
208
|
-
const existingLabels = existingEntry?.labels;
|
|
209
213
|
// Update this config's managed files
|
|
210
214
|
const sortedManaged = Array.from(newManaged).sort();
|
|
211
|
-
if (sortedManaged.length > 0
|
|
212
|
-
|
|
213
|
-
(existingLabels && existingLabels.length > 0)) {
|
|
214
|
-
updatedConfigs[configId] = {
|
|
215
|
-
...(sortedManaged.length > 0 ? { files: sortedManaged } : {}),
|
|
216
|
-
...(existingRulesets && existingRulesets.length > 0
|
|
217
|
-
? { rulesets: existingRulesets }
|
|
218
|
-
: {}),
|
|
219
|
-
...(existingLabels && existingLabels.length > 0
|
|
220
|
-
? { labels: existingLabels }
|
|
221
|
-
: {}),
|
|
222
|
-
};
|
|
215
|
+
if (sortedManaged.length > 0) {
|
|
216
|
+
updatedConfigs[configId] = { files: sortedManaged };
|
|
223
217
|
}
|
|
224
218
|
else {
|
|
225
|
-
// Remove config entry if no managed files, rulesets, or labels
|
|
226
219
|
delete updatedConfigs[configId];
|
|
227
220
|
}
|
|
228
221
|
return {
|
|
229
222
|
manifest: {
|
|
230
|
-
version:
|
|
223
|
+
version: 4,
|
|
231
224
|
configs: updatedConfigs,
|
|
232
225
|
},
|
|
233
226
|
filesToDelete,
|
|
234
227
|
};
|
|
235
228
|
}
|
|
236
|
-
/**
|
|
237
|
-
* Updates the manifest with the current set of rulesets that have deleteOrphaned enabled
|
|
238
|
-
* for a specific config. Only modifies that config's rulesets namespace - other configs are untouched.
|
|
239
|
-
*
|
|
240
|
-
* @param manifest - The existing manifest (or null for new repos)
|
|
241
|
-
* @param configId - The config ID to update
|
|
242
|
-
* @param rulesetsWithDeleteOrphaned - Map of ruleset name to deleteOrphaned value (true/false/undefined)
|
|
243
|
-
* @returns Updated manifest and list of rulesets to delete
|
|
244
|
-
*/
|
|
245
|
-
export function updateManifestRulesets(manifest, configId, rulesetsWithDeleteOrphaned) {
|
|
246
|
-
// Get existing managed rulesets for this config only
|
|
247
|
-
const existingManaged = new Set(getManagedRulesets(manifest, configId));
|
|
248
|
-
const newManaged = new Set();
|
|
249
|
-
const rulesetsToDelete = [];
|
|
250
|
-
// Process current config rulesets
|
|
251
|
-
for (const [rulesetName, deleteOrphaned] of rulesetsWithDeleteOrphaned) {
|
|
252
|
-
if (deleteOrphaned === true) {
|
|
253
|
-
newManaged.add(rulesetName);
|
|
254
|
-
}
|
|
255
|
-
}
|
|
256
|
-
// Find orphaned rulesets: in old manifest for this config but not in current config
|
|
257
|
-
for (const rulesetName of existingManaged) {
|
|
258
|
-
if (!rulesetsWithDeleteOrphaned.has(rulesetName)) {
|
|
259
|
-
rulesetsToDelete.push(rulesetName);
|
|
260
|
-
}
|
|
261
|
-
}
|
|
262
|
-
// Build updated manifest, preserving other configs
|
|
263
|
-
const updatedConfigs = {
|
|
264
|
-
...(manifest?.configs ?? {}),
|
|
265
|
-
};
|
|
266
|
-
// Preserve existing files and labels for this config
|
|
267
|
-
const existingEntry = manifest?.configs[configId];
|
|
268
|
-
const existingFiles = existingEntry?.files;
|
|
269
|
-
const existingLabels = existingEntry?.labels;
|
|
270
|
-
// Update this config's managed rulesets
|
|
271
|
-
const sortedManaged = Array.from(newManaged).sort();
|
|
272
|
-
if (sortedManaged.length > 0 ||
|
|
273
|
-
(existingFiles && existingFiles.length > 0) ||
|
|
274
|
-
(existingLabels && existingLabels.length > 0)) {
|
|
275
|
-
updatedConfigs[configId] = {
|
|
276
|
-
...(existingFiles && existingFiles.length > 0
|
|
277
|
-
? { files: existingFiles }
|
|
278
|
-
: {}),
|
|
279
|
-
...(sortedManaged.length > 0 ? { rulesets: sortedManaged } : {}),
|
|
280
|
-
...(existingLabels && existingLabels.length > 0
|
|
281
|
-
? { labels: existingLabels }
|
|
282
|
-
: {}),
|
|
283
|
-
};
|
|
284
|
-
}
|
|
285
|
-
else {
|
|
286
|
-
// Remove config entry if no managed files, rulesets, or labels
|
|
287
|
-
delete updatedConfigs[configId];
|
|
288
|
-
}
|
|
289
|
-
return {
|
|
290
|
-
manifest: {
|
|
291
|
-
version: 3,
|
|
292
|
-
configs: updatedConfigs,
|
|
293
|
-
},
|
|
294
|
-
rulesetsToDelete,
|
|
295
|
-
};
|
|
296
|
-
}
|
|
297
|
-
/**
|
|
298
|
-
* Updates the manifest with the current set of labels that have deleteOrphaned enabled
|
|
299
|
-
* for a specific config. Only modifies that config's labels namespace - other configs are untouched.
|
|
300
|
-
*
|
|
301
|
-
* @param manifest - The existing manifest (or null for new repos)
|
|
302
|
-
* @param configId - The config ID to update
|
|
303
|
-
* @param labelsWithDeleteOrphaned - Map of label name to deleteOrphaned value (true/false/undefined)
|
|
304
|
-
* @returns Updated manifest and list of labels to delete
|
|
305
|
-
*/
|
|
306
|
-
export function updateManifestLabels(manifest, configId, labelsWithDeleteOrphaned) {
|
|
307
|
-
const existingManaged = new Set(getManagedLabels(manifest, configId));
|
|
308
|
-
const newManaged = new Set();
|
|
309
|
-
const labelsToDelete = [];
|
|
310
|
-
for (const [labelName, deleteOrphaned] of labelsWithDeleteOrphaned) {
|
|
311
|
-
if (deleteOrphaned === true) {
|
|
312
|
-
newManaged.add(labelName);
|
|
313
|
-
}
|
|
314
|
-
}
|
|
315
|
-
for (const labelName of existingManaged) {
|
|
316
|
-
if (!labelsWithDeleteOrphaned.has(labelName)) {
|
|
317
|
-
labelsToDelete.push(labelName);
|
|
318
|
-
}
|
|
319
|
-
}
|
|
320
|
-
const updatedConfigs = {
|
|
321
|
-
...(manifest?.configs ?? {}),
|
|
322
|
-
};
|
|
323
|
-
// Preserve existing files and rulesets for this config
|
|
324
|
-
const existingEntry = manifest?.configs[configId];
|
|
325
|
-
const existingFiles = existingEntry?.files;
|
|
326
|
-
const existingRulesets = existingEntry?.rulesets;
|
|
327
|
-
const sortedManaged = Array.from(newManaged).sort();
|
|
328
|
-
if (sortedManaged.length > 0 ||
|
|
329
|
-
(existingFiles && existingFiles.length > 0) ||
|
|
330
|
-
(existingRulesets && existingRulesets.length > 0)) {
|
|
331
|
-
updatedConfigs[configId] = {
|
|
332
|
-
...(existingFiles && existingFiles.length > 0
|
|
333
|
-
? { files: existingFiles }
|
|
334
|
-
: {}),
|
|
335
|
-
...(existingRulesets && existingRulesets.length > 0
|
|
336
|
-
? { rulesets: existingRulesets }
|
|
337
|
-
: {}),
|
|
338
|
-
...(sortedManaged.length > 0 ? { labels: sortedManaged } : {}),
|
|
339
|
-
};
|
|
340
|
-
}
|
|
341
|
-
else {
|
|
342
|
-
delete updatedConfigs[configId];
|
|
343
|
-
}
|
|
344
|
-
return {
|
|
345
|
-
manifest: { version: 3, configs: updatedConfigs },
|
|
346
|
-
labelsToDelete,
|
|
347
|
-
};
|
|
348
|
-
}
|
|
@@ -3,8 +3,7 @@ import type { RepoInfo } from "../shared/repo-detector.js";
|
|
|
3
3
|
import { ILogger } from "../shared/logger.js";
|
|
4
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
5
|
/**
|
|
6
|
-
* Thin facade that delegates to SyncWorkflow with
|
|
7
|
-
* process() uses FileSyncStrategy, updateManifestOnly() uses ManifestStrategy.
|
|
6
|
+
* Thin facade that delegates to SyncWorkflow with FileSyncStrategy.
|
|
8
7
|
*/
|
|
9
8
|
export declare class RepositoryProcessor implements IRepositoryProcessor {
|
|
10
9
|
private readonly syncWorkflow;
|
|
@@ -22,8 +21,4 @@ export declare class RepositoryProcessor implements IRepositoryProcessor {
|
|
|
22
21
|
syncWorkflow?: ISyncWorkflow;
|
|
23
22
|
});
|
|
24
23
|
process(repoConfig: RepoConfig, repoInfo: RepoInfo, options: ProcessorOptions): Promise<ProcessorResult>;
|
|
25
|
-
updateManifestOnly(repoInfo: RepoInfo, repoConfig: RepoConfig, options: ProcessorOptions, manifestUpdate: {
|
|
26
|
-
rulesets?: string[];
|
|
27
|
-
labels?: string[];
|
|
28
|
-
}): Promise<ProcessorResult>;
|
|
29
24
|
}
|
|
@@ -3,11 +3,9 @@ import { AuthenticatedGitOps } from "../vcs/authenticated-git-ops.js";
|
|
|
3
3
|
import { logger } from "../shared/logger.js";
|
|
4
4
|
import { hasGitHubAppCredentials } from "../vcs/index.js";
|
|
5
5
|
import { GitHubAppTokenManager } from "../vcs/github-app-token-manager.js";
|
|
6
|
-
import { FileWriter, ManifestManager, BranchManager, AuthOptionsBuilder, RepositorySession, CommitPushManager, FileSyncOrchestrator, PRMergeHandler, FileSyncStrategy,
|
|
7
|
-
import { getRepoDisplayName } from "../shared/repo-detector.js";
|
|
6
|
+
import { FileWriter, ManifestManager, BranchManager, AuthOptionsBuilder, RepositorySession, CommitPushManager, FileSyncOrchestrator, PRMergeHandler, FileSyncStrategy, SyncWorkflow, } from "./index.js";
|
|
8
7
|
/**
|
|
9
|
-
* Thin facade that delegates to SyncWorkflow with
|
|
10
|
-
* process() uses FileSyncStrategy, updateManifestOnly() uses ManifestStrategy.
|
|
8
|
+
* Thin facade that delegates to SyncWorkflow with FileSyncStrategy.
|
|
11
9
|
*/
|
|
12
10
|
export class RepositoryProcessor {
|
|
13
11
|
syncWorkflow;
|
|
@@ -42,52 +40,4 @@ export class RepositoryProcessor {
|
|
|
42
40
|
const strategy = new FileSyncStrategy(this.fileSyncOrchestrator);
|
|
43
41
|
return this.syncWorkflow.execute(repoConfig, repoInfo, options, strategy);
|
|
44
42
|
}
|
|
45
|
-
async updateManifestOnly(repoInfo, repoConfig, options, manifestUpdate) {
|
|
46
|
-
const repoName = getRepoDisplayName(repoInfo);
|
|
47
|
-
const { workDir, dryRun } = options;
|
|
48
|
-
// Pre-check manifest changes (preserves original early-return behavior)
|
|
49
|
-
const existingManifest = loadManifest(workDir);
|
|
50
|
-
let simulatedManifest = existingManifest;
|
|
51
|
-
if (manifestUpdate.rulesets) {
|
|
52
|
-
const rulesetsWithDeleteOrphaned = new Map(manifestUpdate.rulesets.map((name) => [name, true]));
|
|
53
|
-
const result = updateManifestRulesets(simulatedManifest, options.configId, rulesetsWithDeleteOrphaned);
|
|
54
|
-
simulatedManifest = result.manifest;
|
|
55
|
-
}
|
|
56
|
-
if (manifestUpdate.labels) {
|
|
57
|
-
const labelsWithDeleteOrphaned = new Map(manifestUpdate.labels.map((name) => [name, true]));
|
|
58
|
-
const result = updateManifestLabels(simulatedManifest, options.configId, labelsWithDeleteOrphaned);
|
|
59
|
-
simulatedManifest = result.manifest;
|
|
60
|
-
}
|
|
61
|
-
const existingConfigs = existingManifest?.configs ?? {};
|
|
62
|
-
if (JSON.stringify(existingConfigs) ===
|
|
63
|
-
JSON.stringify(simulatedManifest.configs)) {
|
|
64
|
-
return {
|
|
65
|
-
success: true,
|
|
66
|
-
repoName,
|
|
67
|
-
message: "No manifest changes detected",
|
|
68
|
-
skipped: true,
|
|
69
|
-
};
|
|
70
|
-
}
|
|
71
|
-
const manifestFileChange = [
|
|
72
|
-
{ path: MANIFEST_FILENAME, action: "update" },
|
|
73
|
-
];
|
|
74
|
-
if (dryRun) {
|
|
75
|
-
const parts = [];
|
|
76
|
-
if (manifestUpdate.rulesets)
|
|
77
|
-
parts.push("ruleset");
|
|
78
|
-
if (manifestUpdate.labels)
|
|
79
|
-
parts.push("labels");
|
|
80
|
-
const trackingType = parts.join("/") || "settings";
|
|
81
|
-
this.log.info(`Would update ${MANIFEST_FILENAME} with ${trackingType} tracking`);
|
|
82
|
-
return {
|
|
83
|
-
success: true,
|
|
84
|
-
repoName,
|
|
85
|
-
message: "Would update manifest (dry-run)",
|
|
86
|
-
fileChanges: manifestFileChange,
|
|
87
|
-
};
|
|
88
|
-
}
|
|
89
|
-
// Delegate to workflow for actual commit/push/PR
|
|
90
|
-
const strategy = new ManifestStrategy(manifestUpdate, this.log);
|
|
91
|
-
return this.syncWorkflow.execute(repoConfig, repoInfo, options, strategy);
|
|
92
|
-
}
|
|
93
43
|
}
|
package/dist/sync/types.d.ts
CHANGED
|
@@ -257,10 +257,6 @@ export interface ProcessorResult {
|
|
|
257
257
|
*/
|
|
258
258
|
export interface IRepositoryProcessor {
|
|
259
259
|
process(repoConfig: RepoConfig, repoInfo: RepoInfo, options: ProcessorOptions): Promise<ProcessorResult>;
|
|
260
|
-
updateManifestOnly(repoInfo: RepoInfo, repoConfig: RepoConfig, options: ProcessorOptions, manifestUpdate: {
|
|
261
|
-
rulesets?: string[];
|
|
262
|
-
labels?: string[];
|
|
263
|
-
}): Promise<ProcessorResult>;
|
|
264
260
|
}
|
|
265
261
|
/**
|
|
266
262
|
* Result of file synchronization
|
|
@@ -65,6 +65,7 @@ export class GraphQLCommitStrategy {
|
|
|
65
65
|
/403\b/,
|
|
66
66
|
/does\s*not\s*exist/i,
|
|
67
67
|
/could\s*not\s*resolve/i,
|
|
68
|
+
/already\s*exists/i,
|
|
68
69
|
];
|
|
69
70
|
executor;
|
|
70
71
|
constructor(executor) {
|
|
@@ -243,8 +244,21 @@ export class GraphQLCommitStrategy {
|
|
|
243
244
|
}
|
|
244
245
|
else if (!refId) {
|
|
245
246
|
// Branch doesn't exist: create from local HEAD
|
|
247
|
+
// Race condition: on newly created forks, queryRemoteRef may return null
|
|
248
|
+
// due to eventual consistency, but the branch may exist by the time we
|
|
249
|
+
// try to create it. Treat "already exists" as success.
|
|
246
250
|
const sha = (await this.executor.exec("git rev-parse HEAD", workDir)).trim();
|
|
247
|
-
|
|
251
|
+
try {
|
|
252
|
+
await this.createRemoteRef(repositoryId, branchName, sha, workDir, repoInfo, token);
|
|
253
|
+
}
|
|
254
|
+
catch (error) {
|
|
255
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
256
|
+
if (/already exists/i.test(msg)) {
|
|
257
|
+
// Branch was created between our query and create — that's fine
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
throw error;
|
|
261
|
+
}
|
|
248
262
|
}
|
|
249
263
|
// refId exists + !force: no-op (branch already exists)
|
|
250
264
|
}
|
package/package.json
CHANGED
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
import type { GitHubAppTokenManager } from "../../vcs/index.js";
|
|
2
|
-
import type { RepoResult } from "../../output/github-summary.js";
|
|
3
|
-
import type { IRepoLifecycleManager } from "../../lifecycle/index.js";
|
|
4
|
-
import type { Config, RepoConfig } from "../../config/types.js";
|
|
5
|
-
import type { ResultsCollector } from "./results-collector.js";
|
|
6
|
-
import type { SettingsOptions } from "../settings-command.js";
|
|
7
|
-
/**
|
|
8
|
-
* Run lifecycle checks for all unique repos before processing.
|
|
9
|
-
* Returns a Set of git URLs to skip (lifecycle errors or repos that would be created in dry-run).
|
|
10
|
-
*/
|
|
11
|
-
export declare function runLifecycleChecks(allRepos: RepoConfig[], config: Config, options: SettingsOptions, lifecycleManager: IRepoLifecycleManager, results: RepoResult[], collector: ResultsCollector, tokenManager: GitHubAppTokenManager | null): Promise<Set<string>>;
|