@aspruyt/xfg 3.9.12 → 3.9.13
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/dist/cli/index.d.ts +1 -1
- package/dist/cli/index.js +1 -1
- package/dist/cli/settings/lifecycle-checks.d.ts +11 -0
- package/dist/cli/settings/lifecycle-checks.js +64 -0
- package/dist/cli/settings/process-labels.d.ts +9 -0
- package/dist/cli/settings/process-labels.js +125 -0
- package/dist/cli/settings/process-repo-settings.d.ts +9 -0
- package/dist/cli/settings/process-repo-settings.js +80 -0
- package/dist/cli/settings/process-rulesets.d.ts +9 -0
- package/dist/cli/settings/process-rulesets.js +118 -0
- package/dist/cli/settings/results-collector.d.ts +11 -0
- package/dist/cli/settings/results-collector.js +28 -0
- package/dist/cli/settings-command.d.ts +3 -3
- package/dist/cli/settings-command.js +28 -268
- package/dist/cli/settings-report-builder.d.ts +6 -0
- package/dist/cli/settings-report-builder.js +23 -0
- package/dist/cli/types.d.ts +12 -2
- package/dist/cli/types.js +5 -0
- package/dist/config/index.d.ts +1 -1
- package/dist/config/normalizer.d.ts +0 -4
- package/dist/config/normalizer.js +56 -0
- package/dist/config/types.d.ts +17 -0
- package/dist/config/validator.d.ts +2 -3
- package/dist/config/validator.js +62 -7
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/output/github-summary.d.ts +6 -0
- package/dist/output/github-summary.js +39 -0
- package/dist/output/settings-report.d.ts +18 -1
- package/dist/output/settings-report.js +84 -0
- package/dist/output/unified-summary.js +40 -1
- package/dist/settings/index.d.ts +1 -0
- package/dist/settings/index.js +2 -0
- package/dist/settings/labels/converter.d.ts +15 -0
- package/dist/settings/labels/converter.js +22 -0
- package/dist/settings/labels/diff.d.ts +33 -0
- package/dist/settings/labels/diff.js +156 -0
- package/dist/settings/labels/formatter.d.ts +25 -0
- package/dist/settings/labels/formatter.js +92 -0
- package/dist/settings/labels/github-labels-strategy.d.ts +51 -0
- package/dist/settings/labels/github-labels-strategy.js +102 -0
- package/dist/settings/labels/index.d.ts +6 -0
- package/dist/settings/labels/index.js +10 -0
- package/dist/settings/labels/processor.d.ts +57 -0
- package/dist/settings/labels/processor.js +189 -0
- package/dist/settings/labels/types.d.ts +33 -0
- package/dist/settings/labels/types.js +1 -0
- package/dist/sync/index.d.ts +1 -1
- package/dist/sync/index.js +1 -1
- package/dist/sync/manifest-strategy.d.ts +2 -1
- package/dist/sync/manifest-strategy.js +23 -5
- package/dist/sync/manifest.d.ts +24 -0
- package/dist/sync/manifest.js +98 -6
- package/dist/sync/repository-processor.d.ts +2 -1
- package/dist/sync/repository-processor.js +21 -5
- package/dist/sync/types.d.ts +2 -1
- package/package.json +4 -3
|
@@ -1,276 +1,23 @@
|
|
|
1
|
-
import { resolve
|
|
1
|
+
import { resolve } from "node:path";
|
|
2
2
|
import { existsSync } from "node:fs";
|
|
3
|
-
import chalk from "chalk";
|
|
4
3
|
import { loadRawConfig, normalizeConfig } from "../config/index.js";
|
|
5
4
|
import { validateForSettings } from "../config/validator.js";
|
|
6
|
-
import { parseGitUrl, getRepoDisplayName, isGitHubRepo, } from "../shared/repo-detector.js";
|
|
7
5
|
import { hasGitHubAppCredentials, GitHubAppTokenManager, } from "../vcs/index.js";
|
|
8
6
|
import { logger } from "../shared/logger.js";
|
|
9
|
-
import { generateWorkspaceName } from "../shared/workspace-utils.js";
|
|
10
|
-
import { buildErrorResult } from "../output/summary-utils.js";
|
|
11
|
-
import { getManagedRulesets } from "../sync/manifest.js";
|
|
12
7
|
import { formatSettingsReportCLI } from "../output/settings-report.js";
|
|
13
8
|
import { writeUnifiedSummary } from "../output/unified-summary.js";
|
|
14
|
-
import { buildSettingsReport
|
|
15
|
-
import { defaultProcessorFactory, defaultRulesetProcessorFactory, defaultRepoSettingsProcessorFactory, } from "./types.js";
|
|
16
|
-
import { RepoLifecycleManager,
|
|
9
|
+
import { buildSettingsReport } from "./settings-report-builder.js";
|
|
10
|
+
import { defaultProcessorFactory, defaultRulesetProcessorFactory, defaultRepoSettingsProcessorFactory, defaultLabelsProcessorFactory, } from "./types.js";
|
|
11
|
+
import { RepoLifecycleManager, } from "../lifecycle/index.js";
|
|
12
|
+
import { ResultsCollector } from "./settings/results-collector.js";
|
|
13
|
+
import { runLifecycleChecks } from "./settings/lifecycle-checks.js";
|
|
14
|
+
import { processRulesets } from "./settings/process-rulesets.js";
|
|
15
|
+
import { processRepoSettings } from "./settings/process-repo-settings.js";
|
|
16
|
+
import { processLabels } from "./settings/process-labels.js";
|
|
17
17
|
/**
|
|
18
|
-
*
|
|
19
|
-
* Provides a centralized way to track results across rulesets and repo settings.
|
|
18
|
+
* Run the settings command - manages GitHub Rulesets, repo settings, and labels.
|
|
20
19
|
*/
|
|
21
|
-
|
|
22
|
-
results = [];
|
|
23
|
-
getOrCreate(repoName) {
|
|
24
|
-
let result = this.results.find((r) => r.repoName === repoName);
|
|
25
|
-
if (!result) {
|
|
26
|
-
result = { repoName };
|
|
27
|
-
this.results.push(result);
|
|
28
|
-
}
|
|
29
|
-
return result;
|
|
30
|
-
}
|
|
31
|
-
appendError(repoName, error) {
|
|
32
|
-
const existing = this.getOrCreate(repoName);
|
|
33
|
-
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
34
|
-
if (existing.error) {
|
|
35
|
-
existing.error += `; ${errorMsg}`;
|
|
36
|
-
}
|
|
37
|
-
else {
|
|
38
|
-
existing.error = errorMsg;
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
getAll() {
|
|
42
|
-
return this.results;
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
/**
|
|
46
|
-
* Run lifecycle checks for all unique repos before processing.
|
|
47
|
-
* Returns a Set of git URLs to skip (lifecycle errors or repos that would be created in dry-run).
|
|
48
|
-
*/
|
|
49
|
-
async function runLifecycleChecks(allRepos, config, options, lifecycleManager, results, collector, tokenManager) {
|
|
50
|
-
const checked = new Set();
|
|
51
|
-
const skippedRepos = new Set();
|
|
52
|
-
for (let i = 0; i < allRepos.length; i++) {
|
|
53
|
-
const repoConfig = allRepos[i];
|
|
54
|
-
if (checked.has(repoConfig.git)) {
|
|
55
|
-
continue;
|
|
56
|
-
}
|
|
57
|
-
checked.add(repoConfig.git);
|
|
58
|
-
let repoInfo;
|
|
59
|
-
try {
|
|
60
|
-
repoInfo = parseGitUrl(repoConfig.git, {
|
|
61
|
-
githubHosts: config.githubHosts,
|
|
62
|
-
});
|
|
63
|
-
}
|
|
64
|
-
catch {
|
|
65
|
-
// URL parsing errors are handled in individual processors
|
|
66
|
-
continue;
|
|
67
|
-
}
|
|
68
|
-
const repoName = getRepoDisplayName(repoInfo);
|
|
69
|
-
// Resolve auth token for lifecycle gh commands
|
|
70
|
-
let lifecycleToken;
|
|
71
|
-
if (isGitHubRepo(repoInfo)) {
|
|
72
|
-
try {
|
|
73
|
-
lifecycleToken =
|
|
74
|
-
(await tokenManager?.getTokenForRepo(repoInfo)) ??
|
|
75
|
-
process.env.GH_TOKEN;
|
|
76
|
-
}
|
|
77
|
-
catch {
|
|
78
|
-
lifecycleToken = process.env.GH_TOKEN;
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
try {
|
|
82
|
-
const { outputLines, lifecycleResult } = await runLifecycleCheck(repoConfig, repoInfo, i, {
|
|
83
|
-
dryRun: options.dryRun ?? false,
|
|
84
|
-
workDir: options.workDir,
|
|
85
|
-
githubHosts: config.githubHosts,
|
|
86
|
-
token: lifecycleToken,
|
|
87
|
-
}, lifecycleManager, config.settings?.repo);
|
|
88
|
-
for (const line of outputLines) {
|
|
89
|
-
logger.info(line);
|
|
90
|
-
}
|
|
91
|
-
// In dry-run, skip processing repos that don't exist yet
|
|
92
|
-
if (options.dryRun && lifecycleResult.action !== "existed") {
|
|
93
|
-
skippedRepos.add(repoConfig.git);
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
catch (error) {
|
|
97
|
-
logger.error(i + 1, repoName, `Lifecycle error: ${error instanceof Error ? error.message : String(error)}`);
|
|
98
|
-
results.push(buildErrorResult(repoName, error));
|
|
99
|
-
collector.appendError(repoName, error);
|
|
100
|
-
skippedRepos.add(repoConfig.git);
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
return skippedRepos;
|
|
104
|
-
}
|
|
105
|
-
/**
|
|
106
|
-
* Process rulesets for all configured repositories.
|
|
107
|
-
*/
|
|
108
|
-
async function processRulesets(repos, config, options, processor, repoProcessor, results, collector, lifecycleSkipped) {
|
|
109
|
-
for (let i = 0; i < repos.length; i++) {
|
|
110
|
-
const repoConfig = repos[i];
|
|
111
|
-
if (lifecycleSkipped.has(repoConfig.git)) {
|
|
112
|
-
continue;
|
|
113
|
-
}
|
|
114
|
-
let repoInfo;
|
|
115
|
-
try {
|
|
116
|
-
repoInfo = parseGitUrl(repoConfig.git, {
|
|
117
|
-
githubHosts: config.githubHosts,
|
|
118
|
-
});
|
|
119
|
-
}
|
|
120
|
-
catch (error) {
|
|
121
|
-
logger.error(i + 1, repoConfig.git, String(error));
|
|
122
|
-
results.push(buildErrorResult(repoConfig.git, error));
|
|
123
|
-
collector.appendError(repoConfig.git, error);
|
|
124
|
-
continue;
|
|
125
|
-
}
|
|
126
|
-
const repoName = getRepoDisplayName(repoInfo);
|
|
127
|
-
if (!isGitHubRepo(repoInfo)) {
|
|
128
|
-
logger.skip(i + 1, repoName, "GitHub Rulesets only supported for GitHub repos");
|
|
129
|
-
continue;
|
|
130
|
-
}
|
|
131
|
-
const managedRulesets = getManagedRulesets(null, config.id);
|
|
132
|
-
try {
|
|
133
|
-
logger.progress(i + 1, repoName, "Processing rulesets...");
|
|
134
|
-
const result = await processor.process(repoConfig, repoInfo, {
|
|
135
|
-
configId: config.id,
|
|
136
|
-
dryRun: options.dryRun,
|
|
137
|
-
managedRulesets,
|
|
138
|
-
noDelete: options.noDelete,
|
|
139
|
-
});
|
|
140
|
-
if (result.planOutput && result.planOutput.lines.length > 0) {
|
|
141
|
-
logger.info("");
|
|
142
|
-
logger.info(chalk.bold(`${repoName} - Rulesets:`));
|
|
143
|
-
for (const line of result.planOutput.lines) {
|
|
144
|
-
logger.info(line);
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
if (result.skipped) {
|
|
148
|
-
logger.skip(i + 1, repoName, result.message);
|
|
149
|
-
}
|
|
150
|
-
else if (result.success) {
|
|
151
|
-
logger.success(i + 1, repoName, result.message);
|
|
152
|
-
if (result.manifestUpdate &&
|
|
153
|
-
result.manifestUpdate.rulesets.length > 0) {
|
|
154
|
-
const workDir = resolve(join(options.workDir ?? "./tmp", generateWorkspaceName(i)));
|
|
155
|
-
logger.progress(i + 1, repoName, "Updating manifest...");
|
|
156
|
-
const manifestResult = await repoProcessor.updateManifestOnly(repoInfo, repoConfig, {
|
|
157
|
-
branchName: "chore/sync-rulesets",
|
|
158
|
-
workDir,
|
|
159
|
-
configId: config.id,
|
|
160
|
-
dryRun: options.dryRun,
|
|
161
|
-
retries: options.retries,
|
|
162
|
-
}, result.manifestUpdate);
|
|
163
|
-
if (!manifestResult.success && !manifestResult.skipped) {
|
|
164
|
-
logger.info(`Warning: Failed to update manifest for ${repoName}: ${manifestResult.message}`);
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
else {
|
|
169
|
-
logger.error(i + 1, repoName, result.message);
|
|
170
|
-
collector.appendError(repoName, result.message);
|
|
171
|
-
}
|
|
172
|
-
results.push({
|
|
173
|
-
repoName,
|
|
174
|
-
status: result.skipped
|
|
175
|
-
? "skipped"
|
|
176
|
-
: result.success
|
|
177
|
-
? "succeeded"
|
|
178
|
-
: "failed",
|
|
179
|
-
message: result.message,
|
|
180
|
-
rulesetPlanDetails: result.planOutput?.entries,
|
|
181
|
-
});
|
|
182
|
-
if (!result.skipped) {
|
|
183
|
-
collector.getOrCreate(repoName).rulesetResult = result;
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
catch (error) {
|
|
187
|
-
logger.error(i + 1, repoName, String(error));
|
|
188
|
-
results.push(buildErrorResult(repoName, error));
|
|
189
|
-
collector.appendError(repoName, error);
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
/**
|
|
194
|
-
* Process repo settings for all configured repositories.
|
|
195
|
-
*/
|
|
196
|
-
async function processRepoSettings(repos, config, options, processorFactory, results, collector, lifecycleSkipped, indexOffset) {
|
|
197
|
-
if (repos.length === 0) {
|
|
198
|
-
return;
|
|
199
|
-
}
|
|
200
|
-
const processor = processorFactory();
|
|
201
|
-
console.log(`\nProcessing repo settings for ${repos.length} repositories\n`);
|
|
202
|
-
for (let i = 0; i < repos.length; i++) {
|
|
203
|
-
const repoConfig = repos[i];
|
|
204
|
-
const current = indexOffset + i + 1;
|
|
205
|
-
if (lifecycleSkipped.has(repoConfig.git)) {
|
|
206
|
-
continue;
|
|
207
|
-
}
|
|
208
|
-
let repoInfo;
|
|
209
|
-
try {
|
|
210
|
-
repoInfo = parseGitUrl(repoConfig.git, {
|
|
211
|
-
githubHosts: config.githubHosts,
|
|
212
|
-
});
|
|
213
|
-
}
|
|
214
|
-
catch (error) {
|
|
215
|
-
logger.error(current, repoConfig.git, String(error));
|
|
216
|
-
collector.appendError(repoConfig.git, error);
|
|
217
|
-
continue;
|
|
218
|
-
}
|
|
219
|
-
const repoName = getRepoDisplayName(repoInfo);
|
|
220
|
-
try {
|
|
221
|
-
const result = await processor.process(repoConfig, repoInfo, {
|
|
222
|
-
dryRun: options.dryRun,
|
|
223
|
-
});
|
|
224
|
-
if (result.planOutput && result.planOutput.lines.length > 0) {
|
|
225
|
-
logger.info("");
|
|
226
|
-
logger.info(chalk.bold(`${repoName} - Repo Settings:`));
|
|
227
|
-
for (const line of result.planOutput.lines) {
|
|
228
|
-
logger.info(line);
|
|
229
|
-
}
|
|
230
|
-
if (result.warnings && result.warnings.length > 0) {
|
|
231
|
-
for (const warning of result.warnings) {
|
|
232
|
-
logger.info(chalk.yellow(`Warning: ${warning}`));
|
|
233
|
-
}
|
|
234
|
-
}
|
|
235
|
-
}
|
|
236
|
-
if (result.skipped) {
|
|
237
|
-
// Silent skip
|
|
238
|
-
}
|
|
239
|
-
else if (result.success) {
|
|
240
|
-
logger.success(current, repoName, result.message);
|
|
241
|
-
}
|
|
242
|
-
else {
|
|
243
|
-
logger.error(current, repoName, result.message);
|
|
244
|
-
collector.appendError(repoName, result.message);
|
|
245
|
-
}
|
|
246
|
-
if (!result.skipped) {
|
|
247
|
-
const existing = results.find((r) => r.repoName === repoName);
|
|
248
|
-
if (existing) {
|
|
249
|
-
existing.repoSettingsPlanDetails = result.planOutput?.entries;
|
|
250
|
-
}
|
|
251
|
-
else {
|
|
252
|
-
results.push({
|
|
253
|
-
repoName,
|
|
254
|
-
status: result.success ? "succeeded" : "failed",
|
|
255
|
-
message: result.message,
|
|
256
|
-
repoSettingsPlanDetails: result.planOutput?.entries,
|
|
257
|
-
});
|
|
258
|
-
}
|
|
259
|
-
}
|
|
260
|
-
if (!result.skipped) {
|
|
261
|
-
collector.getOrCreate(repoName).settingsResult = result;
|
|
262
|
-
}
|
|
263
|
-
}
|
|
264
|
-
catch (error) {
|
|
265
|
-
logger.error(current, repoName, String(error));
|
|
266
|
-
collector.appendError(repoName, error);
|
|
267
|
-
}
|
|
268
|
-
}
|
|
269
|
-
}
|
|
270
|
-
/**
|
|
271
|
-
* Run the settings command - manages GitHub Rulesets and repo settings.
|
|
272
|
-
*/
|
|
273
|
-
export async function runSettings(options, processorFactory = defaultRulesetProcessorFactory, repoProcessorFactory = defaultProcessorFactory, repoSettingsProcessorFactory = defaultRepoSettingsProcessorFactory, lifecycleManager) {
|
|
20
|
+
export async function runSettings(options, processorFactory = defaultRulesetProcessorFactory, repoProcessorFactory = defaultProcessorFactory, repoSettingsProcessorFactory = defaultRepoSettingsProcessorFactory, lifecycleManager, labelsProcessorFactory = defaultLabelsProcessorFactory) {
|
|
274
21
|
const configPath = resolve(options.config);
|
|
275
22
|
if (!existsSync(configPath)) {
|
|
276
23
|
console.error(`Config file not found: ${configPath}`);
|
|
@@ -291,8 +38,11 @@ export async function runSettings(options, processorFactory = defaultRulesetProc
|
|
|
291
38
|
const config = normalizeConfig(rawConfig);
|
|
292
39
|
const reposWithRulesets = config.repos.filter((r) => r.settings?.rulesets && Object.keys(r.settings.rulesets).length > 0);
|
|
293
40
|
const reposWithRepoSettings = config.repos.filter((r) => r.settings?.repo && Object.keys(r.settings.repo).length > 0);
|
|
294
|
-
|
|
295
|
-
|
|
41
|
+
const reposWithLabels = config.repos.filter((r) => r.settings?.labels && Object.keys(r.settings.labels).length > 0);
|
|
42
|
+
if (reposWithRulesets.length === 0 &&
|
|
43
|
+
reposWithRepoSettings.length === 0 &&
|
|
44
|
+
reposWithLabels.length === 0) {
|
|
45
|
+
console.log("No settings configured. Add settings.rulesets, settings.repo, or settings.labels to your config.");
|
|
296
46
|
return;
|
|
297
47
|
}
|
|
298
48
|
if (reposWithRulesets.length > 0) {
|
|
@@ -301,8 +51,13 @@ export async function runSettings(options, processorFactory = defaultRulesetProc
|
|
|
301
51
|
if (reposWithRepoSettings.length > 0) {
|
|
302
52
|
console.log(`Found ${reposWithRepoSettings.length} repositories with repo settings`);
|
|
303
53
|
}
|
|
54
|
+
if (reposWithLabels.length > 0) {
|
|
55
|
+
console.log(`Found ${reposWithLabels.length} repositories with labels`);
|
|
56
|
+
}
|
|
304
57
|
console.log("");
|
|
305
|
-
logger.setTotal(reposWithRulesets.length +
|
|
58
|
+
logger.setTotal(reposWithRulesets.length +
|
|
59
|
+
reposWithRepoSettings.length +
|
|
60
|
+
reposWithLabels.length);
|
|
306
61
|
const processor = processorFactory();
|
|
307
62
|
const repoProcessor = repoProcessorFactory();
|
|
308
63
|
const lm = lifecycleManager ?? new RepoLifecycleManager(undefined, options.retries);
|
|
@@ -312,10 +67,15 @@ export async function runSettings(options, processorFactory = defaultRulesetProc
|
|
|
312
67
|
const results = [];
|
|
313
68
|
const collector = new ResultsCollector();
|
|
314
69
|
// Pre-check lifecycle for all unique repos before processing
|
|
315
|
-
const allRepos = [
|
|
70
|
+
const allRepos = [
|
|
71
|
+
...reposWithRulesets,
|
|
72
|
+
...reposWithRepoSettings,
|
|
73
|
+
...reposWithLabels,
|
|
74
|
+
];
|
|
316
75
|
const lifecycleSkipped = await runLifecycleChecks(allRepos, config, options, lm, results, collector, tokenManager);
|
|
317
76
|
await processRulesets(reposWithRulesets, config, options, processor, repoProcessor, results, collector, lifecycleSkipped);
|
|
318
77
|
await processRepoSettings(reposWithRepoSettings, config, options, repoSettingsProcessorFactory, results, collector, lifecycleSkipped, reposWithRulesets.length);
|
|
78
|
+
await processLabels(reposWithLabels, config, options, labelsProcessorFactory(), repoProcessor, results, collector, lifecycleSkipped, reposWithRulesets.length + reposWithRepoSettings.length);
|
|
319
79
|
console.log("");
|
|
320
80
|
const report = buildSettingsReport(collector.getAll());
|
|
321
81
|
const lines = formatSettingsReportCLI(report);
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { SettingsReport } from "../output/settings-report.js";
|
|
2
2
|
import type { RepoSettingsPlanEntry } from "../settings/repo-settings/formatter.js";
|
|
3
3
|
import type { RulesetPlanEntry } from "../settings/rulesets/formatter.js";
|
|
4
|
+
import type { LabelsPlanEntry } from "../settings/labels/formatter.js";
|
|
4
5
|
/**
|
|
5
6
|
* Result from processing a repository's settings and rulesets.
|
|
6
7
|
* Used to collect results during settings command execution.
|
|
@@ -17,6 +18,11 @@ export interface ProcessorResults {
|
|
|
17
18
|
entries?: RulesetPlanEntry[];
|
|
18
19
|
};
|
|
19
20
|
};
|
|
21
|
+
labelsResult?: {
|
|
22
|
+
planOutput?: {
|
|
23
|
+
entries?: LabelsPlanEntry[];
|
|
24
|
+
};
|
|
25
|
+
};
|
|
20
26
|
error?: string;
|
|
21
27
|
}
|
|
22
28
|
export declare function buildSettingsReport(results: ProcessorResults[]): SettingsReport;
|
|
@@ -3,12 +3,14 @@ export function buildSettingsReport(results) {
|
|
|
3
3
|
const totals = {
|
|
4
4
|
settings: { add: 0, change: 0 },
|
|
5
5
|
rulesets: { create: 0, update: 0, delete: 0 },
|
|
6
|
+
labels: { create: 0, update: 0, delete: 0 },
|
|
6
7
|
};
|
|
7
8
|
for (const result of results) {
|
|
8
9
|
const repoChanges = {
|
|
9
10
|
repoName: result.repoName,
|
|
10
11
|
settings: [],
|
|
11
12
|
rulesets: [],
|
|
13
|
+
labels: [],
|
|
12
14
|
};
|
|
13
15
|
// Convert settings processor output
|
|
14
16
|
if (result.settingsResult?.planOutput?.entries) {
|
|
@@ -55,6 +57,27 @@ export function buildSettingsReport(results) {
|
|
|
55
57
|
}
|
|
56
58
|
}
|
|
57
59
|
}
|
|
60
|
+
// Convert labels processor output
|
|
61
|
+
if (result.labelsResult?.planOutput?.entries) {
|
|
62
|
+
for (const entry of result.labelsResult.planOutput.entries) {
|
|
63
|
+
if (entry.action === "unchanged")
|
|
64
|
+
continue;
|
|
65
|
+
const labelChange = {
|
|
66
|
+
name: entry.name,
|
|
67
|
+
action: entry.action,
|
|
68
|
+
newName: entry.newName,
|
|
69
|
+
propertyChanges: entry.propertyChanges,
|
|
70
|
+
config: entry.config,
|
|
71
|
+
};
|
|
72
|
+
repoChanges.labels.push(labelChange);
|
|
73
|
+
if (entry.action === "create")
|
|
74
|
+
totals.labels.create++;
|
|
75
|
+
else if (entry.action === "update")
|
|
76
|
+
totals.labels.update++;
|
|
77
|
+
else if (entry.action === "delete")
|
|
78
|
+
totals.labels.delete++;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
58
81
|
if (result.error) {
|
|
59
82
|
repoChanges.error = result.error;
|
|
60
83
|
}
|
package/dist/cli/types.d.ts
CHANGED
|
@@ -3,13 +3,15 @@ import { RepoInfo } from "../shared/repo-detector.js";
|
|
|
3
3
|
import { type ProcessorResult, type ProcessorOptions } from "../sync/index.js";
|
|
4
4
|
import { RulesetProcessorOptions, RulesetProcessorResult } from "../settings/rulesets/processor.js";
|
|
5
5
|
import { type IRepoSettingsProcessor } from "../settings/repo-settings/processor.js";
|
|
6
|
+
import { type ILabelsProcessor } from "../settings/labels/processor.js";
|
|
6
7
|
/**
|
|
7
8
|
* Processor interface for dependency injection in tests.
|
|
8
9
|
*/
|
|
9
10
|
export interface IRepositoryProcessor {
|
|
10
11
|
process(repoConfig: RepoConfig, repoInfo: RepoInfo, options: ProcessorOptions): Promise<ProcessorResult>;
|
|
11
12
|
updateManifestOnly(repoInfo: RepoInfo, repoConfig: RepoConfig, options: ProcessorOptions, manifestUpdate: {
|
|
12
|
-
rulesets
|
|
13
|
+
rulesets?: string[];
|
|
14
|
+
labels?: string[];
|
|
13
15
|
}): Promise<ProcessorResult>;
|
|
14
16
|
}
|
|
15
17
|
/**
|
|
@@ -42,4 +44,12 @@ export type RepoSettingsProcessorFactory = () => IRepoSettingsProcessor;
|
|
|
42
44
|
* Default factory that creates a real RepoSettingsProcessor.
|
|
43
45
|
*/
|
|
44
46
|
export declare const defaultRepoSettingsProcessorFactory: RepoSettingsProcessorFactory;
|
|
45
|
-
|
|
47
|
+
/**
|
|
48
|
+
* Labels processor interface for dependency injection in tests.
|
|
49
|
+
*/
|
|
50
|
+
export type LabelsProcessorFactory = () => ILabelsProcessor;
|
|
51
|
+
/**
|
|
52
|
+
* Default factory that creates a real LabelsProcessor.
|
|
53
|
+
*/
|
|
54
|
+
export declare const defaultLabelsProcessorFactory: LabelsProcessorFactory;
|
|
55
|
+
export type { IRepoSettingsProcessor, ILabelsProcessor };
|
package/dist/cli/types.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { RepositoryProcessor, } from "../sync/index.js";
|
|
2
2
|
import { RulesetProcessor, } from "../settings/rulesets/processor.js";
|
|
3
3
|
import { RepoSettingsProcessor, } from "../settings/repo-settings/processor.js";
|
|
4
|
+
import { LabelsProcessor, } from "../settings/labels/processor.js";
|
|
4
5
|
/**
|
|
5
6
|
* Default factory that creates a real RepositoryProcessor.
|
|
6
7
|
*/
|
|
@@ -13,3 +14,7 @@ export const defaultRulesetProcessorFactory = () => new RulesetProcessor();
|
|
|
13
14
|
* Default factory that creates a real RepoSettingsProcessor.
|
|
14
15
|
*/
|
|
15
16
|
export const defaultRepoSettingsProcessorFactory = () => new RepoSettingsProcessor();
|
|
17
|
+
/**
|
|
18
|
+
* Default factory that creates a real LabelsProcessor.
|
|
19
|
+
*/
|
|
20
|
+
export const defaultLabelsProcessorFactory = () => new LabelsProcessor();
|
package/dist/config/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export type { MergeMode, MergeStrategy, PRMergeOptions, RulesetTarget, RulesetEnforcement, BypassActorType, BypassMode, PatternOperator, MergeMethod, AlertsThreshold, SecurityAlertsThreshold, BypassActor, RefNameCondition, RulesetConditions, StatusCheckConfig, RequiredReviewer, CodeScanningTool, WorkflowConfig, PullRequestRuleParameters, RequiredStatusChecksParameters, UpdateRuleParameters, RequiredDeploymentsParameters, CodeScanningParameters, CodeQualityParameters, WorkflowsParameters, PatternRuleParameters, FilePathRestrictionParameters, FileExtensionRestrictionParameters, MaxFilePathLengthParameters, MaxFileSizeParameters, PullRequestRule, RequiredStatusChecksRule, RequiredSignaturesRule, RequiredLinearHistoryRule, NonFastForwardRule, CreationRule, UpdateRule, DeletionRule, RequiredDeploymentsRule, CodeScanningRule, CodeQualityRule, WorkflowsRule, CommitAuthorEmailPatternRule, CommitMessagePatternRule, CommitterEmailPatternRule, BranchNamePatternRule, TagNamePatternRule, FilePathRestrictionRule, FileExtensionRestrictionRule, MaxFilePathLengthRule, MaxFileSizeRule, RulesetRule, Ruleset, SquashMergeCommitTitle, SquashMergeCommitMessage, MergeCommitTitle, MergeCommitMessage, RepoVisibility, GitHubRepoSettings, RepoSettings, ContentValue, RawFileConfig, RawRepoFileOverride, RawRepoSettings, RawRepoConfig, RawConfig, FileContent, RepoConfig, Config, } from "./types.js";
|
|
1
|
+
export type { MergeMode, MergeStrategy, PRMergeOptions, RulesetTarget, RulesetEnforcement, BypassActorType, BypassMode, PatternOperator, MergeMethod, AlertsThreshold, SecurityAlertsThreshold, BypassActor, RefNameCondition, RulesetConditions, StatusCheckConfig, RequiredReviewer, CodeScanningTool, WorkflowConfig, PullRequestRuleParameters, RequiredStatusChecksParameters, UpdateRuleParameters, RequiredDeploymentsParameters, CodeScanningParameters, CodeQualityParameters, WorkflowsParameters, PatternRuleParameters, FilePathRestrictionParameters, FileExtensionRestrictionParameters, MaxFilePathLengthParameters, MaxFileSizeParameters, PullRequestRule, RequiredStatusChecksRule, RequiredSignaturesRule, RequiredLinearHistoryRule, NonFastForwardRule, CreationRule, UpdateRule, DeletionRule, RequiredDeploymentsRule, CodeScanningRule, CodeQualityRule, WorkflowsRule, CommitAuthorEmailPatternRule, CommitMessagePatternRule, CommitterEmailPatternRule, BranchNamePatternRule, TagNamePatternRule, FilePathRestrictionRule, FileExtensionRestrictionRule, MaxFilePathLengthRule, MaxFileSizeRule, RulesetRule, Ruleset, SquashMergeCommitTitle, SquashMergeCommitMessage, MergeCommitTitle, MergeCommitMessage, RepoVisibility, GitHubRepoSettings, Label, RepoSettings, ContentValue, RawFileConfig, RawRepoFileOverride, RawRepoSettings, RawRepoConfig, RawConfig, FileContent, RepoConfig, Config, } from "./types.js";
|
|
2
2
|
export { RULESET_FIELD_MAP, RULESET_COMPARABLE_FIELDS } from "./types.js";
|
|
3
3
|
export { loadRawConfig, loadConfig, normalizeConfig } from "./loader.js";
|
|
4
4
|
export { convertContentToString, detectOutputFormat, type OutputFormat, type ConvertOptions, } from "./formatter.js";
|
|
@@ -1,8 +1,4 @@
|
|
|
1
1
|
import type { RawConfig, Config, RepoSettings, RawRepoSettings } from "./types.js";
|
|
2
|
-
/**
|
|
3
|
-
* Merges settings: per-repo settings deep merge with root settings.
|
|
4
|
-
* Returns undefined if no settings are defined.
|
|
5
|
-
*/
|
|
6
2
|
export declare function mergeSettings(root: RawRepoSettings | undefined, perRepo: RawRepoSettings | undefined): RepoSettings | undefined;
|
|
7
3
|
/**
|
|
8
4
|
* Normalizes raw config into expanded, merged config.
|
|
@@ -53,6 +53,42 @@ function mergeRuleset(root, perRepo) {
|
|
|
53
53
|
* Merges settings: per-repo settings deep merge with root settings.
|
|
54
54
|
* Returns undefined if no settings are defined.
|
|
55
55
|
*/
|
|
56
|
+
/**
|
|
57
|
+
* Merges root and per-repo label configs.
|
|
58
|
+
* Per-repo labels override root labels by name.
|
|
59
|
+
* inherit: false skips all root labels.
|
|
60
|
+
* label: false opts out of a specific root label.
|
|
61
|
+
*/
|
|
62
|
+
function mergeLabels(rootLabels, repoLabels) {
|
|
63
|
+
if (!rootLabels && !repoLabels)
|
|
64
|
+
return undefined;
|
|
65
|
+
const root = rootLabels ?? {};
|
|
66
|
+
const repo = repoLabels ?? {};
|
|
67
|
+
const inheritLabels = repo?.inherit !== false;
|
|
68
|
+
const allLabelNames = new Set([
|
|
69
|
+
...Object.keys(root).filter((name) => name !== "inherit"),
|
|
70
|
+
...Object.keys(repo).filter((name) => name !== "inherit"),
|
|
71
|
+
]);
|
|
72
|
+
if (allLabelNames.size === 0)
|
|
73
|
+
return undefined;
|
|
74
|
+
const result = {};
|
|
75
|
+
for (const name of allLabelNames) {
|
|
76
|
+
const rootLabel = root[name];
|
|
77
|
+
const repoLabel = repo[name];
|
|
78
|
+
if (repoLabel === false)
|
|
79
|
+
continue;
|
|
80
|
+
if (!inheritLabels && !repoLabel && rootLabel)
|
|
81
|
+
continue;
|
|
82
|
+
const merged = {
|
|
83
|
+
...(rootLabel && rootLabel !== false ? rootLabel : {}),
|
|
84
|
+
...(repoLabel && repoLabel !== false ? repoLabel : {}),
|
|
85
|
+
};
|
|
86
|
+
// Strip # from color and lowercase
|
|
87
|
+
merged.color = merged.color.replace(/^#/, "").toLowerCase();
|
|
88
|
+
result[name] = merged;
|
|
89
|
+
}
|
|
90
|
+
return Object.keys(result).length > 0 ? result : undefined;
|
|
91
|
+
}
|
|
56
92
|
export function mergeSettings(root, perRepo) {
|
|
57
93
|
if (!root && !perRepo)
|
|
58
94
|
return undefined;
|
|
@@ -106,6 +142,11 @@ export function mergeSettings(root, perRepo) {
|
|
|
106
142
|
};
|
|
107
143
|
}
|
|
108
144
|
}
|
|
145
|
+
// Merge labels by name
|
|
146
|
+
const mergedLabels = mergeLabels(root?.labels, perRepo?.labels);
|
|
147
|
+
if (mergedLabels) {
|
|
148
|
+
result.labels = mergedLabels;
|
|
149
|
+
}
|
|
109
150
|
return Object.keys(result).length > 0 ? result : undefined;
|
|
110
151
|
}
|
|
111
152
|
/**
|
|
@@ -251,6 +292,21 @@ export function normalizeConfig(raw) {
|
|
|
251
292
|
if (raw.settings.repo) {
|
|
252
293
|
normalizedRootSettings.repo = raw.settings.repo;
|
|
253
294
|
}
|
|
295
|
+
if (raw.settings.labels) {
|
|
296
|
+
const filteredLabels = {};
|
|
297
|
+
for (const [name, label] of Object.entries(raw.settings.labels)) {
|
|
298
|
+
if (name === "inherit" || label === false)
|
|
299
|
+
continue;
|
|
300
|
+
const l = label;
|
|
301
|
+
filteredLabels[name] = {
|
|
302
|
+
...l,
|
|
303
|
+
color: l.color.replace(/^#/, "").toLowerCase(),
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
if (Object.keys(filteredLabels).length > 0) {
|
|
307
|
+
normalizedRootSettings.labels = filteredLabels;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
254
310
|
if (raw.settings.deleteOrphaned !== undefined) {
|
|
255
311
|
normalizedRootSettings.deleteOrphaned = raw.settings.deleteOrphaned;
|
|
256
312
|
}
|
package/dist/config/types.d.ts
CHANGED
|
@@ -260,11 +260,25 @@ export interface GitHubRepoSettings {
|
|
|
260
260
|
secretScanningPushProtection?: boolean;
|
|
261
261
|
privateVulnerabilityReporting?: boolean;
|
|
262
262
|
}
|
|
263
|
+
/**
|
|
264
|
+
* GitHub label configuration.
|
|
265
|
+
* @see https://docs.github.com/en/rest/issues/labels
|
|
266
|
+
*/
|
|
267
|
+
export interface Label {
|
|
268
|
+
/** Hex color code (with or without #). Stripped on normalization. */
|
|
269
|
+
color: string;
|
|
270
|
+
/** Label description (max 100 characters) */
|
|
271
|
+
description?: string;
|
|
272
|
+
/** Rename target. Maps to GitHub API's new_name field. */
|
|
273
|
+
new_name?: string;
|
|
274
|
+
}
|
|
263
275
|
export interface RepoSettings {
|
|
264
276
|
/** GitHub rulesets keyed by name */
|
|
265
277
|
rulesets?: Record<string, Ruleset>;
|
|
266
278
|
/** GitHub repository settings */
|
|
267
279
|
repo?: GitHubRepoSettings;
|
|
280
|
+
/** GitHub labels keyed by name */
|
|
281
|
+
labels?: Record<string, Label>;
|
|
268
282
|
deleteOrphaned?: boolean;
|
|
269
283
|
}
|
|
270
284
|
export type ContentValue = Record<string, unknown> | string | string[];
|
|
@@ -295,6 +309,9 @@ export interface RawRepoSettings {
|
|
|
295
309
|
inherit?: boolean;
|
|
296
310
|
};
|
|
297
311
|
repo?: GitHubRepoSettings | false;
|
|
312
|
+
labels?: Record<string, Label | false> & {
|
|
313
|
+
inherit?: boolean;
|
|
314
|
+
};
|
|
298
315
|
deleteOrphaned?: boolean;
|
|
299
316
|
}
|
|
300
317
|
export interface RawRepoConfig {
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import type { RawConfig, RawRepoSettings } from "./types.js";
|
|
2
2
|
/**
|
|
3
|
-
* Validates settings object containing rulesets.
|
|
3
|
+
* Validates settings object containing rulesets, labels, and repo settings.
|
|
4
4
|
*/
|
|
5
|
-
export declare function validateSettings(settings: unknown, context: string, rootRulesetNames?: string[], hasRootRepoSettings?: boolean): void;
|
|
5
|
+
export declare function validateSettings(settings: unknown, context: string, rootRulesetNames?: string[], hasRootRepoSettings?: boolean, rootLabelNames?: string[]): void;
|
|
6
6
|
/**
|
|
7
7
|
* Validates raw config structure before normalization.
|
|
8
8
|
* @throws Error if validation fails
|
|
@@ -15,7 +15,6 @@ export declare function validateRawConfig(config: RawConfig): void;
|
|
|
15
15
|
export declare function validateForSync(config: RawConfig): void;
|
|
16
16
|
/**
|
|
17
17
|
* Checks if settings contain actionable configuration.
|
|
18
|
-
* Currently only rulesets, but extensible for future settings features.
|
|
19
18
|
*/
|
|
20
19
|
export declare function hasActionableSettings(settings: RawRepoSettings | undefined): boolean;
|
|
21
20
|
/**
|