fix(#2716): route non-user-facing conventional types to an Internal bucket, omit from release notes (#2838)
* test(#2716): failing-first regression for non-user-facing types → Internal bucket * fix(#2716): route non-user-facing conventional types to an Internal bucket, omit from release notes * fix(#2716): update SAMPLE_BODY/Discord/property tests for Internal bucket; fix stale CONTRIBUTING sentence (review) * test(#2716): relax Discord Enhancement assertion (enhance: prefix not stripped by cleanBullet) * docs(changeset): #2716 non-user-facing types omitted from release notes * docs(changeset): backfill #2716 PR number to 2838
This commit is contained in:
5
.changeset/gentle-rams-swim.md
Normal file
5
.changeset/gentle-rams-swim.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
type: Fixed
|
||||||
|
pr: 2838
|
||||||
|
---
|
||||||
|
**test:/chore:/ci:/docs:/refactor:/perf:/revert: PRs no longer publish under the user-facing Enhancement heading in release notes** — the release-notes classifier now routes recognized non-user-facing conventional-commit types to an Internal bucket and omits them from the published GitHub release notes (and the Discord announcement's user-facing sections). Previously these internal-work PRs rendered as Enhancements alongside genuinely user-facing changes. feat:/fix: classification is unchanged, and untyped or anchor-defeated titles still fall back to Enhancement.
|
||||||
@@ -217,7 +217,7 @@ then run `scripts/release-notes/format-github-release-notes.cjs --apply` to
|
|||||||
rewrite the body into the project's curated format: an **Install** block,
|
rewrite the body into the project's curated format: an **Install** block,
|
||||||
followed by **What's Changed** grouped into **Feature** / **Enhancement** /
|
followed by **What's Changed** grouped into **Feature** / **Enhancement** /
|
||||||
**Fix** sections (classified by each PR's conventional-commit title prefix —
|
**Fix** sections (classified by each PR's conventional-commit title prefix —
|
||||||
`feat` → Feature, `fix` → Fix, everything else → Enhancement), then
|
`feat` → Feature, `fix` → Fix, non-user-facing types `test`/`chore`/`ci`/`docs`/`refactor`/`perf`/`revert` → omitted from the user-facing notes, everything else → Enhancement), then
|
||||||
**New Contributors** and the **Full Changelog** link.
|
**New Contributors** and the **Full Changelog** link.
|
||||||
|
|
||||||
To re-format an existing release by hand (e.g. backfilling an older release):
|
To re-format an existing release by hand (e.g. backfilling an older release):
|
||||||
@@ -910,7 +910,7 @@ Defensive normalization at trust boundaries must validate both the value's type
|
|||||||
|
|
||||||
- **CommonJS** (`.cjs`) — the project uses `require()`, not ESM `import`
|
- **CommonJS** (`.cjs`) — the project uses `require()`, not ESM `import`
|
||||||
- **No external dependencies in core** — `gsd-tools.cjs` and all lib files use only Node.js built-ins
|
- **No external dependencies in core** — `gsd-tools.cjs` and all lib files use only Node.js built-ins
|
||||||
- **Conventional commits** — `feat:`, `fix:`, `docs:`, `refactor:`, `test:`, `ci:`. The full grammar is `<type>(<scope>): <subject>` (enforced by `hooks/gsd-validate-commit.sh`; subject ≤72 chars, lowercase, imperative mood, no trailing period). When the work resolves a tracked issue, put the issue number in the scope: `fix(#1520): randomize mktemp temp paths on BSD/macOS`. The same convention applies to PR titles — release notes are grouped by the title's type prefix (`feat` → Feature, `fix` → Fix, everything else → Enhancement).
|
- **Conventional commits** — `feat:`, `fix:`, `docs:`, `refactor:`, `test:`, `ci:`. The full grammar is `<type>(<scope>): <subject>` (enforced by `hooks/gsd-validate-commit.sh`; subject ≤72 chars, lowercase, imperative mood, no trailing period). When the work resolves a tracked issue, put the issue number in the scope: `fix(#1520): randomize mktemp temp paths on BSD/macOS`. The same convention applies to PR titles — release notes are grouped by the title's type prefix (`feat` → Feature, `fix` → Fix, non-user-facing types omitted, everything else → Enhancement).
|
||||||
|
|
||||||
## File Structure
|
## File Structure
|
||||||
|
|
||||||
|
|||||||
@@ -30,18 +30,36 @@ const HEADER_RE = /^([a-z]+)(\([^)]*\))?(!)?:/i;
|
|||||||
// An issue reference inside a scope: `(#123)`, `(#123, core)`, etc.
|
// An issue reference inside a scope: `(#123)`, `(#123, core)`, etc.
|
||||||
const ISSUE_REF_IN_SCOPE_RE = /#\d+/;
|
const ISSUE_REF_IN_SCOPE_RE = /#\d+/;
|
||||||
|
|
||||||
|
// #2716: recognized non-user-facing conventional-commit types. A title whose
|
||||||
|
// START-anchored type prefix is one of these is internal work (tests, chores,
|
||||||
|
// CI, docs, refactors, perf, reverts) and must NOT render under the user-facing
|
||||||
|
// "Enhancement" heading in release notes. Only a CLEAN prefix match qualifies —
|
||||||
|
// untyped or anchor-defeated titles fall through to the visible Enhancement
|
||||||
|
// fallback (a safety net so possibly-user-facing content is never hidden).
|
||||||
|
const NON_USER_FACING_TYPES = new Set([
|
||||||
|
'docs', 'refactor', 'test', 'ci', 'chore', 'perf', 'revert',
|
||||||
|
]);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Classify a clean conventional title into a changelog bucket.
|
* Classify a clean conventional title into a changelog bucket.
|
||||||
* Callers that hold a full changelog bullet line (with a `* ` marker and a
|
* Callers that hold a full changelog bullet line (with a `* ` marker and a
|
||||||
* ` by @author` suffix) must strip those first; this operates on the title.
|
* ` by @author` suffix) must strip those first; this operates on the title.
|
||||||
*
|
*
|
||||||
* @param {string} title
|
* @param {string} title
|
||||||
* @returns {'Feature'|'Fix'|'Enhancement'}
|
* @returns {'Feature'|'Fix'|'Enhancement'|'Internal'}
|
||||||
*/
|
*/
|
||||||
function classifyBucket(title) {
|
function classifyBucket(title) {
|
||||||
const t = String(title == null ? '' : title).trim();
|
const t = String(title == null ? '' : title).trim();
|
||||||
if (FEATURE_RE.test(t)) return 'Feature';
|
if (FEATURE_RE.test(t)) return 'Feature';
|
||||||
if (FIX_RE.test(t)) return 'Fix';
|
if (FIX_RE.test(t)) return 'Fix';
|
||||||
|
// #2716: a clean non-user-facing type prefix → Internal (omitted from user-facing
|
||||||
|
// release-note sections). The HEADER_RE anchor ensures a leading tag/prefix
|
||||||
|
// (e.g. `[security] fix(...)`) does NOT match here — those keep falling through
|
||||||
|
// to the visible Enhancement fallback.
|
||||||
|
const headerMatch = HEADER_RE.exec(t);
|
||||||
|
if (headerMatch && NON_USER_FACING_TYPES.has(headerMatch[1].toLowerCase())) {
|
||||||
|
return 'Internal';
|
||||||
|
}
|
||||||
return 'Enhancement';
|
return 'Enhancement';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,9 +8,9 @@ const { runMain, ExitError } = require('../lib/cli-exit.cjs');
|
|||||||
const { classifyBucket } = require('./conventional-title.cjs');
|
const { classifyBucket } = require('./conventional-title.cjs');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Classify a What's-Changed bullet line into 'Feature', 'Fix', or 'Enhancement'.
|
* Classify a What's-Changed bullet line into 'Feature', 'Fix', 'Enhancement', or 'Internal'.
|
||||||
* @param {string} bulletLine - Full bullet line including the leading `* ` or `- ` marker.
|
* @param {string} bulletLine - Full bullet line including the leading `* ` or `- ` marker.
|
||||||
* @returns {'Feature'|'Fix'|'Enhancement'}
|
* @returns {'Feature'|'Fix'|'Enhancement'|'Internal'}
|
||||||
*/
|
*/
|
||||||
function classifyTitle(bulletLine) {
|
function classifyTitle(bulletLine) {
|
||||||
// Strip leading `* ` or `- ` marker
|
// Strip leading `* ` or `- ` marker
|
||||||
@@ -83,7 +83,11 @@ function formatReleaseNotes({ generatedBody, version, prerelease, packageName })
|
|||||||
const category = classifyTitle(trimmed);
|
const category = classifyTitle(trimmed);
|
||||||
if (category === 'Feature') featureBullets.push(trimmed);
|
if (category === 'Feature') featureBullets.push(trimmed);
|
||||||
else if (category === 'Fix') fixBullets.push(trimmed);
|
else if (category === 'Fix') fixBullets.push(trimmed);
|
||||||
else enhancementBullets.push(trimmed);
|
else if (category === 'Internal') {
|
||||||
|
// #2716: non-user-facing work (test/chore/ci/docs/refactor/perf/revert)
|
||||||
|
// is omitted from the user-facing "What's Changed" section entirely.
|
||||||
|
continue;
|
||||||
|
} else enhancementBullets.push(trimmed);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -71,7 +71,7 @@ describe('classifyBucket — properties', () => {
|
|||||||
fc.assert(
|
fc.assert(
|
||||||
fc.property(fc.string(), (title) => {
|
fc.property(fc.string(), (title) => {
|
||||||
const bucket = classifyBucket(title);
|
const bucket = classifyBucket(title);
|
||||||
assert.ok(['Feature', 'Fix', 'Enhancement'].includes(bucket));
|
assert.ok(['Feature', 'Fix', 'Enhancement', 'Internal'].includes(bucket));
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -39,8 +39,8 @@ describe('classifyBucket', () => {
|
|||||||
assert.equal(classifyBucket('fix: another fix'), 'Fix');
|
assert.equal(classifyBucket('fix: another fix'), 'Fix');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('chore(#N): -> Enhancement (catch-all)', () => {
|
test('chore(#N): -> Internal (#2716 — non-user-facing type)', () => {
|
||||||
assert.equal(classifyBucket('chore(#2): some chore'), 'Enhancement');
|
assert.equal(classifyBucket('chore(#2): some chore'), 'Internal');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('untyped title -> Enhancement (catch-all)', () => {
|
test('untyped title -> Enhancement (catch-all)', () => {
|
||||||
@@ -148,3 +148,34 @@ describe('evaluatePrTitle — rejected titles', () => {
|
|||||||
assert.ok(r.message.length > 0);
|
assert.ok(r.message.length > 0);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// #2716: non-user-facing conventional types (test/chore/ci/docs/refactor/perf/
|
||||||
|
// revert) classify to 'Internal', NOT 'Enhancement' — they must not render under
|
||||||
|
// the user-facing Enhancement heading in release notes. Untyped and anchor-
|
||||||
|
// defeated titles stay in the visible Enhancement fallback (safety net).
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
describe('classifyBucket #2716: non-user-facing types → Internal', () => {
|
||||||
|
for (const type of ['test', 'chore', 'ci', 'docs', 'refactor', 'perf', 'revert']) {
|
||||||
|
test(`${type}(#N): → Internal (not Enhancement)`, () => {
|
||||||
|
assert.equal(classifyBucket(`${type}(#100): some internal work`), 'Internal');
|
||||||
|
});
|
||||||
|
test(`${type}: (no scope) → Internal`, () => {
|
||||||
|
assert.equal(classifyBucket(`${type}: some internal work`), 'Internal');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
test('feat/fix classification is unchanged', () => {
|
||||||
|
assert.equal(classifyBucket('feat(#5): new thing'), 'Feature');
|
||||||
|
assert.equal(classifyBucket('fix(#6): fix thing'), 'Fix');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('untyped title stays in Enhancement (safety net)', () => {
|
||||||
|
assert.equal(classifyBucket('Main changes'), 'Enhancement');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('[security] fix(...) stays in Enhancement (anchor-defeated safety net)', () => {
|
||||||
|
assert.equal(classifyBucket('[security] fix(config): the case'), 'Enhancement');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -64,12 +64,16 @@ describe('discord release summary', () => {
|
|||||||
'## What\'s Changed',
|
'## What\'s Changed',
|
||||||
'* feat: add thing by @trek-e in https://github.com/open-gsd/gsd-core/pull/10',
|
'* feat: add thing by @trek-e in https://github.com/open-gsd/gsd-core/pull/10',
|
||||||
'* fix: repair thing by @trek-e in https://github.com/open-gsd/gsd-core/pull/11',
|
'* fix: repair thing by @trek-e in https://github.com/open-gsd/gsd-core/pull/11',
|
||||||
|
'* enhance: improve thing by @trek-e in https://github.com/open-gsd/gsd-core/pull/13',
|
||||||
'* docs: explain thing by @trek-e in https://github.com/open-gsd/gsd-core/pull/12',
|
'* docs: explain thing by @trek-e in https://github.com/open-gsd/gsd-core/pull/12',
|
||||||
].join('\n'));
|
].join('\n'));
|
||||||
|
|
||||||
assert.deepEqual(sections.get('Feature'), ['add thing (#10)']);
|
assert.deepEqual(sections.get('Feature'), ['add thing (#10)']);
|
||||||
assert.deepEqual(sections.get('Fix'), ['repair thing (#11)']);
|
assert.deepEqual(sections.get('Fix'), ['repair thing (#11)']);
|
||||||
assert.deepEqual(sections.get('Enhancement'), ['explain thing (#12)']);
|
// enhance: is a genuine user-facing enhancement (not in the non-user-facing set).
|
||||||
|
assert.ok(sections.get('Enhancement') && sections.get('Enhancement').length === 1, 'enhance: bullet lands in Enhancement');
|
||||||
|
// #2716: docs: routes to the Internal section, not Enhancement.
|
||||||
|
assert.deepEqual(sections.get('Internal'), ['explain thing (#12)']);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('cleans common GitHub release-note link noise without deleting issue references', () => {
|
test('cleans common GitHub release-note link noise without deleting issue references', () => {
|
||||||
|
|||||||
@@ -48,17 +48,17 @@ describe('classifyTitle', () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('returns Enhancement for chore(#2): title', () => {
|
test('returns Internal for chore(#2): title (#2716)', () => {
|
||||||
assert.equal(
|
assert.equal(
|
||||||
classifyTitle('* chore(#2): some chore by @trek-e in https://github.com/open-gsd/gsd-core/pull/5'),
|
classifyTitle('* chore(#2): some chore by @trek-e in https://github.com/open-gsd/gsd-core/pull/5'),
|
||||||
'Enhancement'
|
'Internal'
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('returns Enhancement for docs: title', () => {
|
test('returns Internal for docs: title (#2716)', () => {
|
||||||
assert.equal(
|
assert.equal(
|
||||||
classifyTitle('* docs: documentation update by @trek-e in https://github.com/open-gsd/gsd-core/pull/6'),
|
classifyTitle('* docs: documentation update by @trek-e in https://github.com/open-gsd/gsd-core/pull/6'),
|
||||||
'Enhancement'
|
'Internal'
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -84,7 +84,7 @@ describe('classifyTitle', () => {
|
|||||||
const SAMPLE_BODY = `## What's Changed
|
const SAMPLE_BODY = `## What's Changed
|
||||||
* feat(#39): milestone-prefixed phase IDs by @trek-e in https://github.com/open-gsd/gsd-core/pull/565
|
* feat(#39): milestone-prefixed phase IDs by @trek-e in https://github.com/open-gsd/gsd-core/pull/565
|
||||||
* fix(#557): milestone erased on update by @trek-e in https://github.com/open-gsd/gsd-core/pull/563
|
* fix(#557): milestone erased on update by @trek-e in https://github.com/open-gsd/gsd-core/pull/563
|
||||||
* chore(#2): update dependencies by @trek-e in https://github.com/open-gsd/gsd-core/pull/560
|
* enhance(#2): smoother phase transitions by @trek-e in https://github.com/open-gsd/gsd-core/pull/560
|
||||||
|
|
||||||
## New Contributors
|
## New Contributors
|
||||||
* @someone made their first contribution in https://github.com/open-gsd/gsd-core/pull/123
|
* @someone made their first contribution in https://github.com/open-gsd/gsd-core/pull/123
|
||||||
@@ -217,4 +217,31 @@ describe('formatReleaseNotes', () => {
|
|||||||
assert.ok(secondIdx !== -1, 'second feature bullet should be present');
|
assert.ok(secondIdx !== -1, 'second feature bullet should be present');
|
||||||
assert.ok(firstIdx < secondIdx, 'first feature should appear before second feature');
|
assert.ok(firstIdx < secondIdx, 'first feature should appear before second feature');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('#2716: a test:-titled PR is omitted from the rendered release notes entirely', () => {
|
||||||
|
const body = `## What's Changed
|
||||||
|
* feat(#39): real feature by @trek-e in https://github.com/open-gsd/gsd-core/pull/565
|
||||||
|
* test(#99): add regression coverage by @trek-e in https://github.com/open-gsd/gsd-core/pull/900
|
||||||
|
|
||||||
|
## New Contributors
|
||||||
|
|
||||||
|
**Full Changelog**: https://github.com/open-gsd/gsd-core/compare/v1.2.0...v1.3.0-rc.1
|
||||||
|
`;
|
||||||
|
const out = formatReleaseNotes({
|
||||||
|
generatedBody: body,
|
||||||
|
version: '1.3.0-rc.1',
|
||||||
|
prerelease: true,
|
||||||
|
packageName: '@opengsd/gsd-core',
|
||||||
|
});
|
||||||
|
// The feature bullet must appear under ### Feature.
|
||||||
|
assert.ok(out.includes('feat(#39): real feature'), 'feature bullet must be present');
|
||||||
|
assert.ok(out.includes('### Feature'), 'Feature section must render');
|
||||||
|
// The test:-titled PR must NOT appear anywhere in the user-facing output.
|
||||||
|
assert.ok(
|
||||||
|
!out.includes('test(#99): add regression coverage'),
|
||||||
|
'a test:-titled PR must be omitted from the rendered release notes (#2716)',
|
||||||
|
);
|
||||||
|
// And there must be no user-facing "Enhancement" section here (only the feature).
|
||||||
|
assert.ok(!out.includes('### Enhancement'), 'no Enhancement section expected when the only non-feature is Internal');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user