mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-07-26 09:52:11 +03:00
* fix(release): count changelog.d fragment refs in list-uncovered-commits (#6857) Since fragments-first (#6783), a merged PR's changelog entry usually lives in changelog.d/{features,fixes,maintenance}/<PR>-<slug>.md and is only folded into CHANGELOG.md at release time. list-uncovered-commits.mjs scanned only CHANGELOG.md, so every fragment-covered commit was reported as an uncovered gap (some fragments — e.g. 6708, 6709 — carry no #N in the body, only in the filename). Add fragment-aware ref collection: fragmentFilenameRef() reads the leading <N>- of a fragment filename, fragmentRefs() unions filename PR numbers with every #N in the body, and collectChangelogRefs() unions the CHANGELOG scan window with the fragment refs. main() now reads changelog.d via readChangelogFragments() and feeds it into the union. On release/v3.8.47 tip this moves 44 commits from uncovered to covered (215/341 vs the prior 171/341) without changing the covered/uncovered classification logic. * chore(release): add changelog.d fragment for #6878 Housekeeping item requested in review: the PR fixing changelog-fragment coverage tracking (#6857) did not itself have a changelog.d fragment. --------- Co-authored-by: Diego Rodrigues de Sa e Souza <diegosouza.pw@gmail.com> Co-authored-by: Diego Rodrigues de Sa e Souza <8016841+diegosouzapw@users.noreply.github.com>
This commit is contained in:
1
changelog.d/maintenance/6878-list-uncovered-fragments.md
Normal file
1
changelog.d/maintenance/6878-list-uncovered-fragments.md
Normal file
@@ -0,0 +1 @@
|
||||
- **release:** `list-uncovered-commits.mjs` now unions the CHANGELOG scan window with `changelog.d/` fragment refs (filename `<PR>-` prefix + every `#N` in the body), so a commit covered only by a fragment is no longer reported as an uncovered reconciliation gap ([#6857](https://github.com/diegosouzapw/OmniRoute/issues/6857) via [#6878](https://github.com/diegosouzapw/OmniRoute/pull/6878))
|
||||
@@ -55,6 +55,68 @@ export function computeUncovered(commits, changelogRefs) {
|
||||
return { covered, uncovered };
|
||||
}
|
||||
|
||||
// Subdirectories that hold changelog.d fragments (fragments-first, #6783).
|
||||
const FRAGMENT_DIRS = ["features", "fixes", "maintenance"];
|
||||
|
||||
/**
|
||||
* Read the leading `<N>-` PR/issue number from a changelog.d fragment filename.
|
||||
* Some fragments (e.g. 6708, 6709) carry NO `#N` in their body, so the filename is the only
|
||||
* place the PR number appears — it must still count as covering that PR.
|
||||
* @param {string} filename bare name or a path ending in the fragment file
|
||||
* @returns {number|null}
|
||||
*/
|
||||
export function fragmentFilenameRef(filename) {
|
||||
const base = String(filename).replace(/^.*[\\/]/, "");
|
||||
const m = base.match(/^(\d+)-/);
|
||||
return m ? Number(m[1]) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Collect every ref "covered" by changelog.d fragments: the leading `<N>-` of each fragment
|
||||
* filename PLUS every `#N` inside its body.
|
||||
* @param {{name:string, body:string}[]} fragments
|
||||
* @returns {Set<number>}
|
||||
*/
|
||||
export function fragmentRefs(fragments) {
|
||||
const refs = new Set();
|
||||
for (const f of fragments || []) {
|
||||
const fromName = fragmentFilenameRef(f.name);
|
||||
if (fromName != null) refs.add(fromName);
|
||||
for (const m of String(f.body || "").matchAll(/#(\d+)/g)) refs.add(Number(m[1]));
|
||||
}
|
||||
return refs;
|
||||
}
|
||||
|
||||
/**
|
||||
* Union of the CHANGELOG scan window refs and the changelog.d fragment refs. Since fragments-first
|
||||
* (#6783) a merged PR's changelog entry usually lives in a fragment and is only folded into
|
||||
* CHANGELOG.md at release time, so scanning CHANGELOG.md alone reports fragment-covered commits as
|
||||
* uncovered (#6857).
|
||||
* @param {string} changelog
|
||||
* @param {string} version
|
||||
* @param {{name:string, body:string}[]} fragments
|
||||
* @returns {Set<number>}
|
||||
*/
|
||||
export function collectChangelogRefs(changelog, version, fragments) {
|
||||
const refs = changelogRefWindow(changelog, version);
|
||||
for (const r of fragmentRefs(fragments)) refs.add(r);
|
||||
return refs;
|
||||
}
|
||||
|
||||
/** Read all changelog.d fragment files (name + body) from disk under `root`. */
|
||||
export function readChangelogFragments(root) {
|
||||
const out = [];
|
||||
for (const sub of FRAGMENT_DIRS) {
|
||||
const dir = path.join(root, "changelog.d", sub);
|
||||
if (!fs.existsSync(dir)) continue;
|
||||
for (const name of fs.readdirSync(dir)) {
|
||||
if (!name.endsWith(".md") || name === "README.md") continue;
|
||||
out.push({ name, body: fs.readFileSync(path.join(dir, name), "utf8") });
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/** Read every #N in the version's CHANGELOG section + the [Unreleased] section. */
|
||||
export function changelogRefWindow(changelog, version) {
|
||||
const esc = version.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||
@@ -85,7 +147,8 @@ function main(argv) {
|
||||
})
|
||||
: [];
|
||||
const changelog = fs.readFileSync(path.join(ROOT, "CHANGELOG.md"), "utf8");
|
||||
const refs = changelogRefWindow(changelog, version);
|
||||
const fragments = readChangelogFragments(ROOT);
|
||||
const refs = collectChangelogRefs(changelog, version, fragments);
|
||||
const { covered, uncovered } = computeUncovered(commits, refs);
|
||||
|
||||
if (jsonOut) {
|
||||
|
||||
@@ -2,7 +2,15 @@ import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
const mod = await import("../../scripts/release/list-uncovered-commits.mjs");
|
||||
const { refsOf, typeOf, computeUncovered, changelogRefWindow } = mod;
|
||||
const {
|
||||
refsOf,
|
||||
typeOf,
|
||||
computeUncovered,
|
||||
changelogRefWindow,
|
||||
fragmentFilenameRef,
|
||||
fragmentRefs,
|
||||
collectChangelogRefs,
|
||||
} = mod;
|
||||
|
||||
test("refsOf extracts every #N from a subject", () => {
|
||||
assert.deepEqual(refsOf("fix(x): thing (#5842) (#5901)"), [5842, 5901]);
|
||||
@@ -61,3 +69,62 @@ test("changelogRefWindow scans [Unreleased] + the version section but not older
|
||||
assert.ok(refs.has(20), "picks up the target version refs");
|
||||
assert.ok(!refs.has(999), "does NOT bleed into the previous version");
|
||||
});
|
||||
|
||||
// ── changelog.d fragment coverage (#6857) ────────────────────────────────────
|
||||
// Since fragments-first (#6783) a merged PR's changelog entry usually lives in
|
||||
// changelog.d/{features,fixes,maintenance}/<PR>-<slug>.md, NOT in CHANGELOG.md yet.
|
||||
// A commit whose #N only exists in a fragment must count as covered.
|
||||
|
||||
test("fragmentFilenameRef reads the leading <N>- PR number from a fragment filename", () => {
|
||||
assert.equal(fragmentFilenameRef("6708-gemma4-thinkingconfig-guard.md"), 6708);
|
||||
assert.equal(fragmentFilenameRef("features/6072-ws-server.md"), 6072);
|
||||
assert.equal(fragmentFilenameRef("README.md"), null, "non-numeric prefix → null");
|
||||
assert.equal(fragmentFilenameRef(".gitkeep"), null);
|
||||
});
|
||||
|
||||
test("fragmentRefs unions filename PR numbers with every #N in the body", () => {
|
||||
const fragments = [
|
||||
// no #N in the body — the ref lives ONLY in the filename (real case: 6708)
|
||||
{ name: "6708-gemma4-thinkingconfig-guard.md", body: "- **fix(sse):** skip thinkingConfig\n" },
|
||||
// body cites additional refs beyond the filename
|
||||
{ name: "6709-xai-responses.md", body: "- **feat:** xAI (#6710 relates #6711)\n" },
|
||||
];
|
||||
const refs = fragmentRefs(fragments);
|
||||
assert.ok(refs.has(6708), "filename-only PR number is covered");
|
||||
assert.ok(refs.has(6709), "filename PR number of a body-ref fragment");
|
||||
assert.ok(refs.has(6710), "body #N is covered");
|
||||
assert.ok(refs.has(6711), "every body #N is covered");
|
||||
});
|
||||
|
||||
test("collectChangelogRefs unions the CHANGELOG window with changelog.d fragment refs", () => {
|
||||
const cl = `# Changelog
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [3.9.0] — x
|
||||
|
||||
- **fix(a):** landed ([#20](u))
|
||||
|
||||
---
|
||||
`;
|
||||
const fragments = [{ name: "6708-gemma4.md", body: "- **fix(sse):** guard\n" }];
|
||||
const refs = collectChangelogRefs(cl, "3.9.0", fragments);
|
||||
assert.ok(refs.has(20), "still includes CHANGELOG.md refs");
|
||||
assert.ok(refs.has(6708), "ALSO includes fragment-only refs (the #6857 bug)");
|
||||
});
|
||||
|
||||
test("computeUncovered: a fragment-only ref counts as covered (end-to-end #6857)", () => {
|
||||
// Repro: PR 6708 merged with a fragment (no #N in body, no CHANGELOG bullet yet).
|
||||
const cl = "# Changelog\n\n## [Unreleased]\n\n## [3.9.0] — x\n\n---\n";
|
||||
const fragments = [{ name: "6708-gemma4.md", body: "- **fix(sse):** guard\n" }];
|
||||
const commits = [{ hash: "aa", subject: "fix(sse): gemma thinkingConfig guard (#6708)" }];
|
||||
|
||||
// BEFORE the fix, only the CHANGELOG window is scanned → the commit looks uncovered.
|
||||
const windowOnly = computeUncovered(commits, changelogRefWindow(cl, "3.9.0"));
|
||||
assert.equal(windowOnly.covered, 0, "sanity: CHANGELOG alone does not cover the fragment PR");
|
||||
|
||||
// AFTER the fix, the fragment ref set covers it.
|
||||
const withFragments = computeUncovered(commits, collectChangelogRefs(cl, "3.9.0", fragments));
|
||||
assert.equal(withFragments.covered, 1, "fragment ref makes the commit covered");
|
||||
assert.equal(withFragments.uncovered.length, 0);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user