@codluv/versionguard 0.6.0 → 0.8.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.
@@ -19,6 +19,18 @@ export interface ChangelogValidationResult {
19
19
  */
20
20
  hasEntryForVersion: boolean;
21
21
  }
22
+ /**
23
+ * Options for changelog structure enforcement.
24
+ *
25
+ * @public
26
+ * @since 0.7.0
27
+ */
28
+ export interface ChangelogStructureOptions {
29
+ /** Validate section headers against an allowed list. */
30
+ enforceStructure?: boolean;
31
+ /** Allowed section names. Defaults to Keep a Changelog standard sections. */
32
+ sections?: string[];
33
+ }
22
34
  /**
23
35
  * Validates a changelog file for release readiness.
24
36
  *
@@ -28,19 +40,26 @@ export interface ChangelogValidationResult {
28
40
  * The validator checks for a top-level changelog heading, an `[Unreleased]`
29
41
  * section, and optionally a dated entry for the requested version.
30
42
  *
43
+ * When `structure.enforceStructure` is `true`, section headers (`### Name`)
44
+ * are validated against the allowed list and empty sections produce warnings.
45
+ *
31
46
  * @param changelogPath - Path to the changelog file.
32
47
  * @param version - Version that must be present in the changelog.
33
48
  * @param strict - Whether to require compare links and dated release headings.
34
49
  * @param requireEntry - Whether the requested version must already have an entry.
50
+ * @param structure - Optional structure enforcement options.
35
51
  * @returns The result of validating the changelog file.
36
52
  * @example
37
53
  * ```ts
38
54
  * import { validateChangelog } from 'versionguard';
39
55
  *
40
- * const result = validateChangelog('CHANGELOG.md', '1.2.0', true, true);
56
+ * const result = validateChangelog('CHANGELOG.md', '1.2.0', true, true, {
57
+ * enforceStructure: true,
58
+ * sections: ['Added', 'Changed', 'Fixed'],
59
+ * });
41
60
  * ```
42
61
  */
43
- export declare function validateChangelog(changelogPath: string, version: string, strict?: boolean, requireEntry?: boolean): ChangelogValidationResult;
62
+ export declare function validateChangelog(changelogPath: string, version: string, strict?: boolean, requireEntry?: boolean, structure?: ChangelogStructureOptions): ChangelogValidationResult;
44
63
  /**
45
64
  * Gets the most recent released version from a changelog.
46
65
  *
@@ -1 +1 @@
1
- {"version":3,"file":"changelog.d.ts","sourceRoot":"","sources":["../src/changelog.ts"],"names":[],"mappings":"AAEA;;;;;;GAMG;AACH,MAAM,WAAW,yBAAyB;IACxC;;OAEG;IACH,KAAK,EAAE,OAAO,CAAC;IACf;;OAEG;IACH,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB;;OAEG;IACH,kBAAkB,EAAE,OAAO,CAAC;CAC7B;AAID;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,iBAAiB,CAC/B,aAAa,EAAE,MAAM,EACrB,OAAO,EAAE,MAAM,EACf,MAAM,GAAE,OAAc,EACtB,YAAY,GAAE,OAAc,GAC3B,yBAAyB,CAgD3B;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,gBAAgB,CAAC,aAAa,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAQrE;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,eAAe,CAC7B,aAAa,EAAE,MAAM,EACrB,OAAO,EAAE,MAAM,EACf,IAAI,GAAE,MAA8C,GACnD,IAAI,CAmBN;AAED;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAgB,kBAAkB,CAAC,aAAa,EAAE,MAAM,GAAG,OAAO,CAMjE;AASD;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,wBAAgB,oBAAoB,CAClC,aAAa,EAAE,MAAM,EACrB,IAAI,GAAE,MAA8C,GACnD,OAAO,CAoET"}
1
+ {"version":3,"file":"changelog.d.ts","sourceRoot":"","sources":["../src/changelog.ts"],"names":[],"mappings":"AAEA;;;;;;GAMG;AACH,MAAM,WAAW,yBAAyB;IACxC;;OAEG;IACH,KAAK,EAAE,OAAO,CAAC;IACf;;OAEG;IACH,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB;;OAEG;IACH,kBAAkB,EAAE,OAAO,CAAC;CAC7B;AAcD;;;;;GAKG;AACH,MAAM,WAAW,yBAAyB;IACxC,wDAAwD;IACxD,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B,6EAA6E;IAC7E,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;CACrB;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,wBAAgB,iBAAiB,CAC/B,aAAa,EAAE,MAAM,EACrB,OAAO,EAAE,MAAM,EACf,MAAM,GAAE,OAAc,EACtB,YAAY,GAAE,OAAc,EAC5B,SAAS,CAAC,EAAE,yBAAyB,GACpC,yBAAyB,CAuD3B;AAiCD;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,gBAAgB,CAAC,aAAa,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAQrE;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,eAAe,CAC7B,aAAa,EAAE,MAAM,EACrB,OAAO,EAAE,MAAM,EACf,IAAI,GAAE,MAA8C,GACnD,IAAI,CAmBN;AAED;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAgB,kBAAkB,CAAC,aAAa,EAAE,MAAM,GAAG,OAAO,CAMjE;AASD;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,wBAAgB,oBAAoB,CAClC,aAAa,EAAE,MAAM,EACrB,IAAI,GAAE,MAA8C,GACnD,OAAO,CAoET"}
@@ -323,7 +323,15 @@ const calver = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProper
323
323
  validate: validate$2
324
324
  }, Symbol.toStringTag, { value: "Module" }));
325
325
  const CHANGELOG_DATE_REGEX = /^\d{4}-\d{2}-\d{2}$/;
326
- function validateChangelog(changelogPath, version, strict = true, requireEntry = true) {
326
+ const KEEP_A_CHANGELOG_SECTIONS = [
327
+ "Added",
328
+ "Changed",
329
+ "Deprecated",
330
+ "Removed",
331
+ "Fixed",
332
+ "Security"
333
+ ];
334
+ function validateChangelog(changelogPath, version, strict = true, requireEntry = true, structure) {
327
335
  if (!fs.existsSync(changelogPath)) {
328
336
  return {
329
337
  valid: !requireEntry,
@@ -360,12 +368,36 @@ function validateChangelog(changelogPath, version, strict = true, requireEntry =
360
368
  }
361
369
  }
362
370
  }
371
+ if (structure?.enforceStructure) {
372
+ const allowed = structure.sections ?? KEEP_A_CHANGELOG_SECTIONS;
373
+ const sectionErrors = validateSections(content, allowed);
374
+ errors.push(...sectionErrors);
375
+ }
363
376
  return {
364
377
  valid: errors.length === 0,
365
378
  errors,
366
379
  hasEntryForVersion
367
380
  };
368
381
  }
382
+ function validateSections(content, allowed) {
383
+ const errors = [];
384
+ const lines = content.split("\n");
385
+ for (let i = 0; i < lines.length; i++) {
386
+ const sectionMatch = lines[i].match(/^### (.+)/);
387
+ if (!sectionMatch) continue;
388
+ const sectionName = sectionMatch[1].trim();
389
+ if (!allowed.includes(sectionName)) {
390
+ errors.push(
391
+ `Invalid changelog section "### ${sectionName}" (line ${i + 1}). Allowed: ${allowed.join(", ")}`
392
+ );
393
+ }
394
+ const nextContentLine = lines.slice(i + 1).find((l) => l.trim().length > 0);
395
+ if (!nextContentLine || nextContentLine.startsWith("#")) {
396
+ errors.push(`Empty changelog section "### ${sectionName}" (line ${i + 1})`);
397
+ }
398
+ }
399
+ return errors;
400
+ }
369
401
  function addVersionEntry(changelogPath, version, date = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10)) {
370
402
  if (!fs.existsSync(changelogPath)) {
371
403
  throw new Error(`Changelog not found: ${changelogPath}`);
@@ -1560,6 +1592,89 @@ function checkHardcodedVersions(expectedVersion, config, ignorePatterns, cwd = p
1560
1592
  }
1561
1593
  return mismatches;
1562
1594
  }
1595
+ const BINARY_EXTENSIONS = /* @__PURE__ */ new Set([
1596
+ ".png",
1597
+ ".jpg",
1598
+ ".jpeg",
1599
+ ".gif",
1600
+ ".ico",
1601
+ ".svg",
1602
+ ".woff",
1603
+ ".woff2",
1604
+ ".ttf",
1605
+ ".eot",
1606
+ ".otf",
1607
+ ".zip",
1608
+ ".tar",
1609
+ ".gz",
1610
+ ".bz2",
1611
+ ".7z",
1612
+ ".pdf",
1613
+ ".exe",
1614
+ ".dll",
1615
+ ".so",
1616
+ ".dylib",
1617
+ ".wasm",
1618
+ ".mp3",
1619
+ ".mp4",
1620
+ ".webm",
1621
+ ".webp",
1622
+ ".avif"
1623
+ ]);
1624
+ function scanRepoForVersions(expectedVersion, scanConfig, ignorePatterns, cwd = process.cwd()) {
1625
+ const files = [
1626
+ ...new Set(
1627
+ globSync("**/*", {
1628
+ cwd,
1629
+ absolute: true,
1630
+ dot: true,
1631
+ ignore: [
1632
+ ...ignorePatterns,
1633
+ // Always skip changelogs (handled by changelog validation) and lockfiles
1634
+ "CHANGELOG.md",
1635
+ "*.lock",
1636
+ "package-lock.json",
1637
+ "yarn.lock",
1638
+ "pnpm-lock.yaml",
1639
+ // Skip VG's own config
1640
+ ".versionguard.yml",
1641
+ ".versionguard.yaml"
1642
+ ]
1643
+ })
1644
+ )
1645
+ ].sort();
1646
+ const allowedFiles = new Set(
1647
+ scanConfig.allowlist.flatMap((entry) => resolveFiles([entry.file], cwd, []))
1648
+ );
1649
+ const mismatches = [];
1650
+ for (const filePath of files) {
1651
+ if (BINARY_EXTENSIONS.has(path.extname(filePath).toLowerCase())) continue;
1652
+ if (allowedFiles.has(filePath)) continue;
1653
+ let content;
1654
+ try {
1655
+ content = fs.readFileSync(filePath, "utf-8");
1656
+ } catch {
1657
+ continue;
1658
+ }
1659
+ if (content.slice(0, 8192).includes("\0")) continue;
1660
+ for (const patternStr of scanConfig.patterns) {
1661
+ const regex = new RegExp(patternStr, "gm");
1662
+ let match = regex.exec(content);
1663
+ while (match) {
1664
+ const found = match[1] ?? match[0] ?? "";
1665
+ if (found && found !== expectedVersion && found !== "Unreleased") {
1666
+ mismatches.push({
1667
+ file: path.relative(cwd, filePath),
1668
+ line: getLineNumber(content, match.index),
1669
+ found
1670
+ });
1671
+ }
1672
+ match = regex.exec(content);
1673
+ }
1674
+ }
1675
+ }
1676
+ return mismatches;
1677
+ }
1563
1678
  const DEFAULT_SEMVER_CONFIG = {
1564
1679
  allowVPrefix: false,
1565
1680
  allowBuildMetadata: true,
@@ -1775,7 +1890,21 @@ const DEFAULT_CONFIG = {
1775
1890
  enabled: true,
1776
1891
  file: "CHANGELOG.md",
1777
1892
  strict: true,
1778
- requireEntry: true
1893
+ requireEntry: true,
1894
+ enforceStructure: false,
1895
+ sections: ["Added", "Changed", "Deprecated", "Removed", "Fixed", "Security"]
1896
+ },
1897
+ scan: {
1898
+ enabled: false,
1899
+ patterns: [
1900
+ // version = "1.2.3" or version: "1.2.3" in code/config
1901
+ `(?:version\\s*[:=]\\s*["'])([\\d]+\\.[\\d]+\\.[\\d]+(?:-[\\w.]+)?)["']`,
1902
+ // Docker FROM image:1.2.3
1903
+ "(?:FROM\\s+\\S+:)(\\d+\\.\\d+\\.\\d+(?:-[\\w.]+)?)",
1904
+ // GitHub Actions uses: action@v1.2.3
1905
+ "(?:uses:\\s+\\S+@v?)(\\d+\\.\\d+\\.\\d+(?:-[\\w.]+)?)"
1906
+ ],
1907
+ allowlist: []
1779
1908
  },
1780
1909
  git: {
1781
1910
  hooks: {
@@ -2690,7 +2819,11 @@ function getTagPreflightError(config, cwd, expectedVersion, allowAutoFix = false
2690
2819
  path.join(cwd, config.changelog.file),
2691
2820
  version,
2692
2821
  config.changelog.strict,
2693
- config.changelog.requireEntry
2822
+ config.changelog.requireEntry,
2823
+ {
2824
+ enforceStructure: config.changelog.enforceStructure,
2825
+ sections: config.changelog.sections
2826
+ }
2694
2827
  );
2695
2828
  if (!changelogResult.valid) {
2696
2829
  return changelogResult.errors[0] ?? "Changelog validation failed";
@@ -2791,6 +2924,14 @@ function validate(config, cwd = process.cwd()) {
2791
2924
  );
2792
2925
  }
2793
2926
  }
2927
+ if (config.scan?.enabled) {
2928
+ const scanFindings = scanRepoForVersions(version, config.scan, config.ignore, cwd);
2929
+ for (const finding of scanFindings) {
2930
+ errors.push(
2931
+ `Stale version in ${finding.file}:${finding.line} - found "${finding.found}" but expected "${version}"`
2932
+ );
2933
+ }
2934
+ }
2794
2935
  let changelogValid = true;
2795
2936
  if (config.changelog.enabled) {
2796
2937
  const changelogPath = path.join(cwd, config.changelog.file);
@@ -2798,7 +2939,11 @@ function validate(config, cwd = process.cwd()) {
2798
2939
  changelogPath,
2799
2940
  version,
2800
2941
  config.changelog.strict,
2801
- config.changelog.requireEntry
2942
+ config.changelog.requireEntry,
2943
+ {
2944
+ enforceStructure: config.changelog.enforceStructure,
2945
+ sections: config.changelog.sections
2946
+ }
2802
2947
  );
2803
2948
  if (!changelogResult.valid) {
2804
2949
  changelogValid = false;
@@ -2882,7 +3027,7 @@ function isWorktreeClean(cwd) {
2882
3027
  }
2883
3028
  }
2884
3029
  export {
2885
- validateVersion as $,
3030
+ validateTagForPush as $,
2886
3031
  checkHardcodedVersions as A,
2887
3032
  checkHookIntegrity as B,
2888
3033
  checkHooksPathOverride as C,
@@ -2901,16 +3046,17 @@ export {
2901
3046
  initConfig as P,
2902
3047
  resolveVersionSource as Q,
2903
3048
  RegexVersionSource as R,
2904
- semver as S,
3049
+ scanRepoForVersions as S,
2905
3050
  TomlVersionSource as T,
2906
- suggestTagMessage as U,
3051
+ semver as U,
2907
3052
  VersionFileSource as V,
2908
- sync as W,
2909
- syncVersion as X,
3053
+ suggestTagMessage as W,
3054
+ sync as X,
2910
3055
  YamlVersionSource as Y,
2911
- validateChangelog as Z,
2912
- validateTagForPush as _,
3056
+ syncVersion as Z,
3057
+ validateChangelog as _,
2913
3058
  installHooks as a,
3059
+ validateVersion as a0,
2914
3060
  getPackageVersion as b,
2915
3061
  createCkmEngine as c,
2916
3062
  getVersionFeedback as d,
@@ -2937,4 +3083,4 @@ export {
2937
3083
  canBump as y,
2938
3084
  checkEnforceHooksPolicy as z
2939
3085
  };
2940
- //# sourceMappingURL=index-DeZAx4Le.js.map
3086
+ //# sourceMappingURL=index-Cipg9sxE.js.map