@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.
Files changed (57) hide show
  1. package/dist/cli/index.d.ts +1 -1
  2. package/dist/cli/index.js +1 -1
  3. package/dist/cli/settings/lifecycle-checks.d.ts +11 -0
  4. package/dist/cli/settings/lifecycle-checks.js +64 -0
  5. package/dist/cli/settings/process-labels.d.ts +9 -0
  6. package/dist/cli/settings/process-labels.js +125 -0
  7. package/dist/cli/settings/process-repo-settings.d.ts +9 -0
  8. package/dist/cli/settings/process-repo-settings.js +80 -0
  9. package/dist/cli/settings/process-rulesets.d.ts +9 -0
  10. package/dist/cli/settings/process-rulesets.js +118 -0
  11. package/dist/cli/settings/results-collector.d.ts +11 -0
  12. package/dist/cli/settings/results-collector.js +28 -0
  13. package/dist/cli/settings-command.d.ts +3 -3
  14. package/dist/cli/settings-command.js +28 -268
  15. package/dist/cli/settings-report-builder.d.ts +6 -0
  16. package/dist/cli/settings-report-builder.js +23 -0
  17. package/dist/cli/types.d.ts +12 -2
  18. package/dist/cli/types.js +5 -0
  19. package/dist/config/index.d.ts +1 -1
  20. package/dist/config/normalizer.d.ts +0 -4
  21. package/dist/config/normalizer.js +56 -0
  22. package/dist/config/types.d.ts +17 -0
  23. package/dist/config/validator.d.ts +2 -3
  24. package/dist/config/validator.js +62 -7
  25. package/dist/index.d.ts +1 -1
  26. package/dist/index.js +1 -1
  27. package/dist/output/github-summary.d.ts +6 -0
  28. package/dist/output/github-summary.js +39 -0
  29. package/dist/output/settings-report.d.ts +18 -1
  30. package/dist/output/settings-report.js +84 -0
  31. package/dist/output/unified-summary.js +40 -1
  32. package/dist/settings/index.d.ts +1 -0
  33. package/dist/settings/index.js +2 -0
  34. package/dist/settings/labels/converter.d.ts +15 -0
  35. package/dist/settings/labels/converter.js +22 -0
  36. package/dist/settings/labels/diff.d.ts +33 -0
  37. package/dist/settings/labels/diff.js +156 -0
  38. package/dist/settings/labels/formatter.d.ts +25 -0
  39. package/dist/settings/labels/formatter.js +92 -0
  40. package/dist/settings/labels/github-labels-strategy.d.ts +51 -0
  41. package/dist/settings/labels/github-labels-strategy.js +102 -0
  42. package/dist/settings/labels/index.d.ts +6 -0
  43. package/dist/settings/labels/index.js +10 -0
  44. package/dist/settings/labels/processor.d.ts +57 -0
  45. package/dist/settings/labels/processor.js +189 -0
  46. package/dist/settings/labels/types.d.ts +33 -0
  47. package/dist/settings/labels/types.js +1 -0
  48. package/dist/sync/index.d.ts +1 -1
  49. package/dist/sync/index.js +1 -1
  50. package/dist/sync/manifest-strategy.d.ts +2 -1
  51. package/dist/sync/manifest-strategy.js +23 -5
  52. package/dist/sync/manifest.d.ts +24 -0
  53. package/dist/sync/manifest.js +98 -6
  54. package/dist/sync/repository-processor.d.ts +2 -1
  55. package/dist/sync/repository-processor.js +21 -5
  56. package/dist/sync/types.d.ts +2 -1
  57. package/package.json +4 -3
@@ -1,276 +1,23 @@
1
- import { resolve, join } from "node:path";
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, } from "./settings-report-builder.js";
15
- import { defaultProcessorFactory, defaultRulesetProcessorFactory, defaultRepoSettingsProcessorFactory, } from "./types.js";
16
- import { RepoLifecycleManager, runLifecycleCheck, } from "../lifecycle/index.js";
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
- * Collects processing results for the SettingsReport.
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
- class ResultsCollector {
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
- if (reposWithRulesets.length === 0 && reposWithRepoSettings.length === 0) {
295
- console.log("No settings configured. Add settings.rulesets or settings.repo to your config.");
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 + reposWithRepoSettings.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 = [...reposWithRulesets, ...reposWithRepoSettings];
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
  }
@@ -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: string[];
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
- export type { IRepoSettingsProcessor };
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();
@@ -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
  }
@@ -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
  /**