Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions docs/src/content/docs/changelogs.md
Original file line number Diff line number Diff line change
Expand Up @@ -262,6 +262,9 @@ chore: Update dependencies
#skip-changelog
```

Mentions inside Markdown code (inline code spans or fenced blocks) are
ignored, so you can write about the magic word without skipping the change.

### Skip label

PRs with the `skip-changelog` label are automatically excluded.
Expand Down
29 changes: 29 additions & 0 deletions src/utils/__tests__/changelog-generate.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,35 @@ describe('generateChangesetFromGit', () => {
}
});

it('counts a commit that only quotes the skip magic word in code', async () => {
// Squash-merged feature commit that documents the magic word, wrapped
// across lines as git does, next to a real skipped bump commit.
setup(
[
{
hash: 'abcdef1234567890',
title: 'feat(publish): Add development version bump',
body:
'It then commits `meta: Bump new\n' +
'development version` + `#skip-changelog` with `--all`.',
},
{
hash: 'bcdef1234567890a',
title: 'meta: Bump new development version',
body: '#skip-changelog',
},
],
null,
);

const result = await generateChangesetFromGit(dummyGit, '1.0.0');

expect(result.totalCommits).toBe(1);
expect(result.bumpType).toBe('minor');
expect(result.changelog).toContain('Add development version bump');
expect(result.changelog).not.toContain('Bump new development version');
});

// ============================================================================
// Basic output formatting tests - use snapshots
// ============================================================================
Expand Down
47 changes: 47 additions & 0 deletions src/utils/__tests__/changelog-utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,59 @@ import {
getNormalizedReleaseConfig,
clearReleaseConfigCache,
SKIP_CHANGELOG_MAGIC_WORD,
hasSkipChangelogMagicWord,
type CurrentPRInfo,
} from '../changelog';

// Mock the logger to capture warnings
vi.mock('../../logger');

describe('hasSkipChangelogMagicWord', () => {
it.each([
['on its own line', 'chore: Update deps\n\n#skip-changelog'],
['inline in prose', 'Internal cleanup, #skip-changelog please'],
['with CRLF line endings', 'Internal.\r\n\r\n#skip-changelog\r\n'],
['after an unmatched backtick', 'A stray ` here\n#skip-changelog'],
[
'after a closed fenced block',
'```yaml\nlabels: [a]\n```\n#skip-changelog',
],
['in emphasis', 'Internal only. **#skip-changelog**'],
['in a list item', '- Internal refactor\n- #skip-changelog'],
['in an HTML comment', 'Description\n\n<!-- #skip-changelog -->'],
[
'outside a code span on another paragraph',
'Run `craft\n\nprepare` #skip-changelog',
],
])('detects the magic word %s', (_, text) => {
expect(hasSkipChangelogMagicWord(text)).toBe(true);
});

it.each([
['when absent', 'feat: Add a thing'],
['for an empty body', ''],
['for an undefined body', undefined],
['in an inline code span', 'Commits `#skip-changelog` after release'],
['in a double-backtick span', 'Uses `` `#skip-changelog` `` verbatim'],
[
'in a code span wrapped across lines',
'commits `meta: Bump new\ndevelopment version` + `#skip-changelog` with `--all`',
],
[
'in a fenced code block',
'Example:\n\n```\nchore: x\n\n#skip-changelog\n```\n',
],
['in a tilde fenced block', '~~~\n#skip-changelog\n~~~'],
['in a code span in a list item', '- Commits `#skip-changelog` on bump'],
[
'in a code span in a table',
'| Word | Effect |\n| --- | --- |\n| `#skip-changelog` | Skips |',
],
])('ignores the magic word %s', (_, text) => {
expect(hasSkipChangelogMagicWord(text)).toBe(false);
});
});

describe('shouldExcludePR', () => {
// Config must match NormalizedReleaseConfig structure
const baseConfig = {
Expand Down
32 changes: 30 additions & 2 deletions src/utils/changelog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,34 @@ const MAX_LEFTOVERS = 24;
if (SUBSECTION_HEADER_LEVEL <= VERSION_HEADER_LEVEL) {
throw new Error('Subsection headers should nest under version headers!');
}

/**
* Checks whether a commit message or PR body opts out of the changelog with
* the {@link SKIP_CHANGELOG_MAGIC_WORD}.
*
* Mentions inside Markdown code are ignored, so a message that only quotes the
* magic word (e.g. documents it as `` `#skip-changelog` ``) is not skipped.
*/
export function hasSkipChangelogMagicWord(text: string | undefined): boolean {
if (!text?.includes(SKIP_CHANGELOG_MAGIC_WORD)) {
return false;
}
// Check only tokens that hold prose or HTML (e.g. a hidden `<!-- -->`
// comment) directly. Containers like lists and tables repeat their
// children's source in `raw`, and code tokens are never prose.
let found = false;
marked.walkTokens(marked.lexer(text), token => {
if (
(token.type === 'text' || token.type === 'html') &&
!('tokens' in token && token.tokens?.length) &&
token.raw.includes(SKIP_CHANGELOG_MAGIC_WORD)
) {
found = true;
}
});
return found;
}

/**
* A single changeset with name and description
*/
Expand Down Expand Up @@ -1305,7 +1333,7 @@ export function shouldExcludePR(
body?: string,
): boolean {
// Check for magic word in body
if (body?.includes(SKIP_CHANGELOG_MAGIC_WORD)) {
if (hasSkipChangelogMagicWord(body)) {
return true;
}

Expand Down Expand Up @@ -1876,7 +1904,7 @@ async function fetchRawCommitInfo(
): Promise<RawCommitInfo[]> {
// Early filter: skip commits with magic word in commit body (optimization to avoid GitHub API calls)
const gitCommits = (await getChangesSince(git, rev, until)).filter(
({ body }) => !body.includes(SKIP_CHANGELOG_MAGIC_WORD),
({ body }) => !hasSkipChangelogMagicWord(body),
);

const githubCommits = await getPRAndLabelsFromCommit(
Expand Down
Loading