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
13 changes: 13 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,14 @@ or

`yarn run auto-changelog update --useShortPrLink`

#### Require PR numbers (filter out commits without PR numbers)

- Only include commits that have associated PR numbers in the changelog
- Commits without PR numbers (like direct commits) will be filtered out
- This is useful for projects that want to ensure all changelog entries come from reviewed pull requests

`yarn run auto-changelog update --requirePrNumbers`

#### Update the current release section of the changelog

`yarn run auto-changelog update --rc`
Expand All @@ -50,6 +58,10 @@ or

`yarn run auto-changelog update --autoCategorize --useChangelogEntry --useShortPrLink --rc`

### With requirePrNumbers (for stricter PR-based workflows)

`yarn run auto-changelog update --autoCategorize --useChangelogEntry --useShortPrLink --requirePrNumbers --rc`

#### Update the changelog for a renamed package

This option is designed to be used for packages that live in a monorepo.
Expand Down Expand Up @@ -132,6 +144,7 @@ const updatedChangelog = await updateChangelog({
currentVersion: '1.0.0',
repoUrl: 'https://github.com/ExampleUsernameOrOrganization/ExampleRepository',
isReleaseCandidate: false,
requirePrNumbers: false, // Optional: set to true to filter out commits without PR numbers
});
await fs.writeFile('CHANGELOG.md', updatedChangelog);
```
Expand Down
15 changes: 15 additions & 0 deletions src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,10 @@ type UpdateOptions = {
* Whether to use short PR links in the changelog entries
*/
useShortPrLink: boolean;
/**
* Whether to require PR numbers for all commits. If true, commits without PR numbers are filtered out.
*/
requirePrNumbers: boolean;
};

/**
Expand All @@ -121,6 +125,7 @@ type UpdateOptions = {
* @param options.autoCategorize - Whether to categorize commits automatically based on their messages.
* @param options.useChangelogEntry - Whether to read `CHANGELOG entry:` from the commit body and the no-changelog label.
* @param options.useShortPrLink - Whether to use short PR links in the changelog entries.
* @param options.requirePrNumbers - Whether to require PR numbers for all commits. If true, commits without PR numbers are filtered out.
*/
async function update({
changelogPath,
Expand All @@ -134,6 +139,7 @@ async function update({
autoCategorize,
useChangelogEntry,
useShortPrLink,
requirePrNumbers,
}: UpdateOptions) {
const changelogContent = await readChangelog(changelogPath);

Expand All @@ -149,6 +155,7 @@ async function update({
autoCategorize,
useChangelogEntry,
useShortPrLink,
requirePrNumbers,
});

if (newChangelogContent) {
Expand Down Expand Up @@ -357,6 +364,12 @@ async function main() {
description: 'Use short PR links in the changelog entries',
type: 'boolean',
})
.option('requirePrNumbers', {
default: false,
description:
'Only include commits with PR numbers in the changelog. Commits without PR numbers will be filtered out',
type: 'boolean',
})
.epilog(updateEpilog),
)
.command(
Expand Down Expand Up @@ -416,6 +429,7 @@ async function main() {
prLinks,
useChangelogEntry,
useShortPrLink,
requirePrNumbers,
} = argv;
let { currentVersion } = argv;

Expand Down Expand Up @@ -547,6 +561,7 @@ async function main() {
autoCategorize,
useChangelogEntry: Boolean(useChangelogEntry),
useShortPrLink: Boolean(useShortPrLink),
requirePrNumbers: Boolean(requirePrNumbers),
});
} else if (command === 'validate') {
let packageRename: PackageRename | undefined;
Expand Down
73 changes: 59 additions & 14 deletions src/get-new-changes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export type AddNewCommitsOptions = {
projectRootDirectory?: string;
useChangelogEntry: boolean;
useShortPrLink: boolean;
requirePrNumbers?: boolean;
};

// Get array of all ConventionalCommitType values
Expand Down Expand Up @@ -61,6 +62,22 @@ function removeConventionalCommitPrefixIfPresent(message: string) {
return message.replace(regex, '');
}

/**
* Remove HTML comments from the given message.
* Handles both complete comments (<!-- ... -->) and unclosed comments (<!-- ...).
* This prevents PR template content from breaking the changelog markdown.
*
* @param message - The changelog entry message.
* @returns The message without HTML comments.
*/
function stripHtmlComments(message: string): string {
// Remove complete HTML comments (<!-- ... -->)
let result = message.replace(/<!--[\s\S]*?-->/gu, '');
// Remove unclosed HTML comments (<!-- without closing -->)
result = result.replace(/<!--[\s\S]*$/gu, '');
return result.trim();
}

type Commit = {
prNumber?: string;
subject: string;
Expand Down Expand Up @@ -122,11 +139,12 @@ async function getCommits(
const changelogMatch = body.match(/\nCHANGELOG entry:\s(\S.+?)\n\n/su);

if (changelogMatch) {
const changelogEntry = changelogMatch[1].replace('\n', ' ');
const changelogEntry = changelogMatch[1].replace('\n', ' ').trim();

description = changelogEntry; // This may be string 'null' to indicate no description

if (description !== 'null') {
// Check for 'null' (case-insensitive) to exclude entries marked as no-changelog
if (description.toLowerCase() !== 'null') {
// Remove outer backticks if present. Example: `feat: new feature description` -> feat: new feature description
description = removeOuterBackticksIfPresent(description);

Expand All @@ -141,7 +159,8 @@ async function getCommits(
description = subject.match(/^(.+)\s\(#\d+\)/u)?.[1] ?? '';
}

if (description !== 'null') {
// Filter out entries marked as no-changelog (case-insensitive null check)
if (description.toLowerCase() !== 'null') {
const prLabels = await getPrLabels(repoUrl, prNumber);

if (prLabels.includes('no-changelog')) {
Expand Down Expand Up @@ -233,6 +252,7 @@ function deduplicateCommits(
* current git repository.
* @param options.useChangelogEntry - Whether to use `CHANGELOG entry:` from the commit body and the no-changelog label.
* @param options.useShortPrLink - Whether to use short PR links in the changelog entries.
* @param options.requirePrNumbers - Whether to require PR numbers for all commits. If true, commits without PR numbers are filtered out.
* @returns A list of new change entries to add to the changelog, based on commits made since the last release.
*/
export async function getNewChangeEntries({
Expand All @@ -243,6 +263,7 @@ export async function getNewChangeEntries({
projectRootDirectory,
useChangelogEntry,
useShortPrLink,
requirePrNumbers = false,
}: AddNewCommitsOptions) {
const commitRange =
mostRecentTag === null ? 'HEAD' : `${mostRecentTag}..HEAD`;
Expand All @@ -256,8 +277,12 @@ export async function getNewChangeEntries({
useChangelogEntry,
);

const filteredPrCommits = requirePrNumbers
? commits.filter((commit) => commit.prNumber !== undefined)
: commits;

const newCommits = deduplicateCommits(
commits,
filteredPrCommits,
loggedPrNumbers,
loggedDescriptions,
);
Expand All @@ -266,6 +291,11 @@ export async function getNewChangeEntries({
// Handle edge case where PR description includes multiple CHANGELOG entries
let newDescription = description?.replace(/CHANGELOG entry: /gu, '');

// Strip HTML comments that may come from PR templates to prevent broken markdown
if (newDescription) {
newDescription = stripHtmlComments(newDescription);
}

// For merge commits, use the description for categorization because the subject
// is "Merge pull request #123..." which would be incorrectly excluded
const subjectForCategorization = isMergeCommit ? description : subject;
Expand Down Expand Up @@ -323,16 +353,31 @@ async function getPrLabels(

const { owner, repo } = getOwnerAndRepoFromUrl(repoUrl);

const { data: pullRequest } = await github.rest.pulls.get({
owner,
repo,
// eslint-disable-next-line @typescript-eslint/naming-convention
pull_number: Number(prNumber),
});

if (pullRequest) {
const labels = pullRequest.labels.map((label: any) => label.name);
return labels;
try {
const { data: pullRequest } = await github.rest.pulls.get({
owner,
repo,
// eslint-disable-next-line @typescript-eslint/naming-convention
pull_number: Number(prNumber),
});

if (pullRequest) {
const labels = pullRequest.labels.map((label: any) => label.name);
return labels;
}
} catch (error: unknown) {
// If PR doesn't exist (404), return empty labels instead of throwing
if (
error instanceof Error &&
'status' in error &&
(error as { status: number }).status === 404
) {
console.warn(
`PR #${prNumber} not found in ${owner}/${repo}, skipping label check`,
);
return [];
}
throw error;
}

return [];
Expand Down
7 changes: 7 additions & 0 deletions src/update-changelog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,10 @@ export type UpdateChangelogOptions = {
* Whether to use short PR links in the changelog entries.
*/
useShortPrLink?: boolean;
/**
* Whether to require PR numbers for all commits. If true, commits without PR numbers are filtered out.
*/
requirePrNumbers?: boolean;
};

/**
Expand Down Expand Up @@ -140,6 +144,7 @@ export type UpdateChangelogOptions = {
* based on commit message prefixes.
* @param options.useChangelogEntry - Whether to use `CHANGELOG entry:` from the commit body and the no-changelog label.
* @param options.useShortPrLink - Whether to use short PR links in the changelog.
* @param options.requirePrNumbers - Whether to require PR numbers for all commits. If true, commits without PR numbers are filtered out.
* @returns The updated changelog text.
*/
export async function updateChangelog({
Expand All @@ -154,6 +159,7 @@ export async function updateChangelog({
autoCategorize,
useChangelogEntry = false,
useShortPrLink = false,
requirePrNumbers = false,
}: UpdateChangelogOptions): Promise<string | undefined> {
const changelog = parseChangelog({
changelogContent,
Expand Down Expand Up @@ -204,6 +210,7 @@ export async function updateChangelog({
projectRootDirectory,
useChangelogEntry,
useShortPrLink,
requirePrNumbers,
});

for (const entry of newChangeEntries.reverse()) {
Expand Down
Loading