@aspruyt/xfg 3.5.2 → 3.5.4

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.
@@ -8,6 +8,12 @@ export interface RulesetChange {
8
8
  current?: GitHubRuleset;
9
9
  desired?: Ruleset;
10
10
  }
11
+ /**
12
+ * Normalizes any ruleset object (GitHub API or config) for comparison.
13
+ * Converts all keys to snake_case, filters to comparable fields only,
14
+ * and recursively normalizes values.
15
+ */
16
+ export declare function normalizeRuleset(obj: GitHubRuleset | Ruleset): Record<string, unknown>;
11
17
  /**
12
18
  * Projects `current` onto the shape of `desired`.
13
19
  * Only keeps keys/structure present in `desired`, filtering out API noise.
@@ -30,15 +30,21 @@ function normalizeValue(value) {
30
30
  return value;
31
31
  }
32
32
  /**
33
- * Normalizes a GitHub ruleset for comparison.
33
+ * Normalizes any ruleset object (GitHub API or config) for comparison.
34
+ * Converts all keys to snake_case, filters to comparable fields only,
35
+ * and recursively normalizes values.
34
36
  */
35
- function normalizeGitHubRuleset(ruleset) {
37
+ export function normalizeRuleset(obj) {
36
38
  const normalized = {};
37
- for (const [key, value] of Object.entries(ruleset)) {
38
- if (!RULESET_COMPARABLE_FIELDS.has(key) || value === undefined) {
39
+ for (const [key, value] of Object.entries(obj)) {
40
+ if (value === undefined) {
41
+ continue;
42
+ }
43
+ const snakeKey = camelToSnake(key);
44
+ if (!RULESET_COMPARABLE_FIELDS.has(snakeKey)) {
39
45
  continue;
40
46
  }
41
- normalized[key] = normalizeValue(value);
47
+ normalized[snakeKey] = normalizeValue(value);
42
48
  }
43
49
  return normalized;
44
50
  }
@@ -51,15 +57,7 @@ function normalizeConfigRuleset(ruleset) {
51
57
  enforcement: ruleset.enforcement ?? "active",
52
58
  ...ruleset,
53
59
  };
54
- const normalized = {};
55
- for (const [key, value] of Object.entries(withDefaults)) {
56
- if (value === undefined) {
57
- continue;
58
- }
59
- const snakeKey = camelToSnake(key);
60
- normalized[snakeKey] = normalizeValue(value);
61
- }
62
- return normalized;
60
+ return normalizeRuleset(withDefaults);
63
61
  }
64
62
  /**
65
63
  * Performs deep equality comparison of two normalized values.
@@ -198,7 +196,7 @@ export function diffRulesets(current, desired, managedNames) {
198
196
  }
199
197
  else {
200
198
  // Existing ruleset - check if changed
201
- const normalizedCurrent = normalizeGitHubRuleset(currentRuleset);
199
+ const normalizedCurrent = normalizeRuleset(currentRuleset);
202
200
  const normalizedDesired = normalizeConfigRuleset(desiredRuleset);
203
201
  const projectedCurrent = projectToDesiredShape(normalizedCurrent, normalizedDesired);
204
202
  if (deepEqual(projectedCurrent, normalizedDesired)) {
@@ -1,7 +1,6 @@
1
1
  // src/ruleset-plan-formatter.ts
2
2
  import chalk from "chalk";
3
- import { projectToDesiredShape, } from "./ruleset-diff.js";
4
- import { RULESET_COMPARABLE_FIELDS } from "./config.js";
3
+ import { projectToDesiredShape, normalizeRuleset, } from "./ruleset-diff.js";
5
4
  // =============================================================================
6
5
  // Property Diff Algorithm
7
6
  // =============================================================================
@@ -352,37 +351,6 @@ export function formatPropertyTree(diffs) {
352
351
  // =============================================================================
353
352
  // Ruleset Plan Formatter
354
353
  // =============================================================================
355
- /**
356
- * Normalize a GitHubRuleset or Ruleset for comparison.
357
- * Converts to snake_case and removes metadata fields.
358
- */
359
- function normalizeForDiff(obj) {
360
- const result = {};
361
- for (const [key, value] of Object.entries(obj)) {
362
- // Convert camelCase to snake_case for consistency
363
- const snakeKey = key.replace(/([A-Z])/g, "_$1").toLowerCase();
364
- if (!RULESET_COMPARABLE_FIELDS.has(snakeKey) || value === undefined)
365
- continue;
366
- result[snakeKey] = normalizeNestedValue(value);
367
- }
368
- return result;
369
- }
370
- function normalizeNestedValue(value) {
371
- if (value === null || value === undefined)
372
- return value;
373
- if (Array.isArray(value))
374
- return value.map(normalizeNestedValue);
375
- if (typeof value === "object") {
376
- const obj = value;
377
- const result = {};
378
- for (const [key, val] of Object.entries(obj)) {
379
- const snakeKey = key.replace(/([A-Z])/g, "_$1").toLowerCase();
380
- result[snakeKey] = normalizeNestedValue(val);
381
- }
382
- return result;
383
- }
384
- return value;
385
- }
386
354
  /**
387
355
  * Format a full ruleset config as tree lines (for create action).
388
356
  */
@@ -467,8 +435,8 @@ export function formatRulesetPlan(changes) {
467
435
  for (const change of updateChanges) {
468
436
  lines.push(chalk.yellow(` ~ ruleset "${change.name}"`));
469
437
  if (change.current && change.desired) {
470
- const currentNorm = normalizeForDiff(change.current);
471
- const desiredNorm = normalizeForDiff(change.desired);
438
+ const currentNorm = normalizeRuleset(change.current);
439
+ const desiredNorm = normalizeRuleset(change.desired);
472
440
  const projectedCurrent = projectToDesiredShape(currentNorm, desiredNorm);
473
441
  const diffs = computePropertyDiffs(projectedCurrent, desiredNorm);
474
442
  const treeLines = formatPropertyTree(diffs);
@@ -1,5 +1,5 @@
1
1
  import { isGitHubRepo, getRepoDisplayName } from "./repo-detector.js";
2
- import { GitHubRulesetStrategy } from "./strategies/github-ruleset-strategy.js";
2
+ import { GitHubRulesetStrategy, } from "./strategies/github-ruleset-strategy.js";
3
3
  import { diffRulesets } from "./ruleset-diff.js";
4
4
  import { formatRulesetPlan, } from "./ruleset-plan-formatter.js";
5
5
  // =============================================================================
@@ -49,8 +49,21 @@ export class RulesetProcessor {
49
49
  const currentRulesets = await this.strategy.list(githubRepo, strategyOptions);
50
50
  // Convert desired rulesets to Map
51
51
  const desiredMap = new Map(Object.entries(desiredRulesets));
52
+ // Hydrate rulesets that match desired names with full details from get()
53
+ // The list endpoint only returns summary fields (id, name, target, enforcement)
54
+ // but not rules, conditions, or bypass_actors needed for accurate diffing
55
+ const fullRulesets = [];
56
+ for (const summary of currentRulesets) {
57
+ if (desiredMap.has(summary.name)) {
58
+ const full = await this.strategy.get(githubRepo, summary.id, strategyOptions);
59
+ fullRulesets.push(full);
60
+ }
61
+ else {
62
+ fullRulesets.push(summary);
63
+ }
64
+ }
52
65
  // Compute diff
53
- const changes = diffRulesets(currentRulesets, desiredMap, managedRulesets);
66
+ const changes = diffRulesets(fullRulesets, desiredMap, managedRulesets);
54
67
  // Count changes by type
55
68
  const changeCounts = {
56
69
  create: changes.filter((c) => c.action === "create").length,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aspruyt/xfg",
3
- "version": "3.5.2",
3
+ "version": "3.5.4",
4
4
  "description": "CLI tool for repository-as-code",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",