Skip to content

ci: generate CHANGELOG entries from the issues that merged PRs close - #1252

Open
rodrigobustamante wants to merge 2 commits into
InditexTech:mainfrom
rodrigobustamante:ci/changelog-from-prs
Open

rodrigobustamante wants to merge 2 commits into
InditexTech:mainfrom
rodrigobustamante:ci/changelog-from-prs

Conversation

@rodrigobustamante

Copy link
Copy Markdown
Contributor

Closes #1236

Implements the flow agreed in #1236 as a single PR: script, tests, dry-run, the always-on check and preview, the release wiring and the docs. Please merge it with skip-release.

What changes

  • .github/scripts/changelog-from-prs.mjs, one script with two modes that share the code:
    • --pr N: read-only check and preview of one PR.
    • Range (default): git describe --tags --abbrev=0 --match '[0-9]*' → first-parent commits since that tag → their merged PRs into --branch → each PR's closing issues. No date filter.
    • One line per closed issue, deduped by issue, with the issue title verbatim (only NBSP/whitespace normalised). Section by issue label, first match wins: security → Security, bug → Fixed, enhancement/kind/feature → Added, otherwise Changed with a "section guessed" warning.
    • Writes only [Unreleased], keeps manual lines, never repeats an issue already listed there, and is idempotent. Before writing it checks that nothing outside [Unreleased] changes.
    • API calls retry with backoff. Any error, or a paginated list that would be truncated, exits non-zero, so the release stops before the bump, the commit and the tag.
    • --dry-run --from X --to Y prints the entries and compares them with the hand-written [Y] section.
  • Tests: node --test .github/scripts/changelog-from-prs.test.mjs (24 tests, no dependencies).
  • code-changelog-check.yml (new): runs on every PR to main/main-* on opened, synchronize, ready_for_review, labeled, unlabeled and edited. It writes the result to the job summary, so it works on fork PRs. It is warn-only through CHANGELOG_LINK_ENFORCE: 'false'; the follow-up PR flips it to 'true' after the next release. Dependabot is exempt.
  • code-release_preview.yml:
    • The "CHANGELOG.md has no changes" failure is gone.
    • The preview writes the entries already pending since the last tag plus this PR's, then runs the bump.
    • The comment shows the next version, the generated lines and sections, the warnings and "as of <sha>". On fork PRs it goes to the job summary.
    • Added issues: read.
  • Release job: the script runs right before keep-a-changelog bump, so the signed CHANGELOG commit includes the entries. publish-snapshot is unchanged.
  • Docs:
    • CONTRIBUTING: the PR step, the label list (I also fixed release-type/minor saying "major") and the release-docs step.
    • AGENTS.md.
    • A new .github/pull_request_template.md with the Closes # line.
    • The workflow docs under .github/workflows/docs/.

bump with an empty [Unreleased]: it already happened in the 5.5.0 release (3aff8d2 bumped an empty section), so snapshots and releases with no entries keep working.

One deviation from the agreed rule: skip-release

Running the dry-run showed that skip-release cannot mean "no changelog entry". Most PRs are merged with skip-release and released together by a later PR. In 5.6.0..5.7.0, 8 of the 9 PRs have it, mine included, and their entries are part of 5.7.0.

Range skip-release = no entry skip-release keeps linked issues
5.6.0..5.7.0 1 of 8 hand-written entries all 8, plus #1216 (missing by hand)
5.7.0..5.7.1 1 of 3 all 3, plus #1241

So in this PR, skip-release:

  • skips the release on merge;
  • waives the linked-issue rule;
  • does not drop the entries of the issues the PR does close. Those go into the next release.

A skip-release PR with no linked issue simply gets no entry. The preview comment and CONTRIBUTING state this. If you prefer your original rule, it is a three-line change in buildEntries.

Validation (--dry-run)

Questions for you

  1. Issues out of scope for the changelog. The internal label is described as "out of the scope for changelog files", and Release docs X issues would appear as Changed in every release. Should issues labelled internal (and maybe docs/release) be skipped, listed in the report instead of silently? Your rule says anything else goes to Changed, so I left them in.
  2. Label description. Only a maintainer can change it. Suggested text for skip-release: "Do not publish a release on merge. The PR does not need a linked issue; the issues it closes go into the next release."
  3. Known limits.
    • Bypass via the PR itself. The check runs on pull_request, so it runs the PR's own copy of the script and workflow. Once it enforces, a PR that edits them could pass without a link. CODEOWNERS on .github/ is the usual guard.
    • Titles go out unescaped. Issue titles are published verbatim (your rule), so a title with an @team mention would ping from the bot's preview comment.

Rollout

  • Merge with skip-release.
  • The check only warns until the next release.
  • The next normal release is the first real end-to-end run. If the script fails, the release stops before the tag.
  • After that release, a small PR sets CHANGELOG_LINK_ENFORCE: 'true'.

Add .github/scripts/changelog-from-prs.mjs, which reads the closing
issues of merged pull requests and writes one line per issue into
[Unreleased], with the issue title verbatim and the section picked from
the issue labels. The release job runs it before the bump, the release
preview shows the entries, and a new warn-only check asks every PR to be
linked to an issue. Contributors no longer edit code/CHANGELOG.md.

Refs InditexTech#1236

Signed-off-by: Rodrigo Bustamante <rbustamantejelvez@gmail.com>
@rodrigobustamante
rodrigobustamante requested a review from a team as a code owner October 27, 2026 12:16
@signoff-validator

Copy link
Copy Markdown

Contribution License Agreement - Valid 🟢

All commits contain valid signoff. As a confirmation, remember that you are agreeing to our Contribution License Agreement.

@jesusmpc

Copy link
Copy Markdown
Contributor

Hi @rodrigobustamante, thanks for this. The write-up and the dry-run numbers made it easy to review.

I ran your tests on the PR head with Node 24.14.1 (.tool-versions says 24.11.1): 24/24 pass. The PR's own checks are green, including Changelog / Linked issue. I also checked the claims below against the repo and the GitHub API, not only the diff.

Overall: I'm happy with the design and would merge it with skip-release. One script for the PR check, the preview and the release is the right call. Writing only [Unreleased], checking that nothing else changed, and stopping before the tag on any error is what I'd want. Your data convinces me on the skip-release deviation, so I'd keep your version.

Should settle before enforcing

1. An issue can be listed again in a later release.

2. Tracking issues create noise entries (your question 1).

  • Release PRs here are ordinary PRs with a release-type/* label, and each links its own issue. I checked the last six, so enforcement won't fail them.
  • Docs-release tracking issues like Release docs 5.7.0 #1241 ("Release docs 5.7.0") are linked and land as Changed in every release.
  • Release docs 5.7.0 #1241 has no labels, so a rule that skips docs/release would not catch it. docs/release is a PR label.
  • The internal label is described as "Internal changes out of the scope for changelog files", which fits. I'd skip issues labelled internal or kind/internal, list them in the report so nothing is dropped silently, and label tracking issues internal from now on.

Smaller points

3. Section order. An issue with both bug and enhancement lands in Fixed. #1222, #1227 and #1229 only carry enhancement, so they would move from Fixed to Added. I opened #1253 to decide the labels and the rule order before the next release, so this doesn't need to block the PR.

4. associatedPullRequests(first: 20) fails hard above 20. That's fine by design. Could the error tell whoever runs the release what to do next?

On your "known limits"

Bypass via the PR itself. main is already covered:

  • .github/CODEOWNERS has /.github/ @InditexTech/weavejs-admins.
  • main has require_code_owner_reviews: true.

A PR that edits the script or workflow therefore needs an admin review, so no new CODEOWNERS rule is needed. Gaps for the maintainers, none caused by this PR:

  • enforce_admins is false on main.
  • main has no required status checks. When you flip CHANGELOG_LINK_ENFORCE to 'true', Changelog / Linked issue will turn red but won't block a merge unless it is added as required. The ci-governance-required-checks-main ruleset is in evaluate mode and may be the place for it.
  • The long-lived main-* branches (main-0-x, main-3-x) have no CODEOWNERS file and no classic branch protection. The "protected branches" rulesets that match main* are all disabled, and the only active rule I found there is workflows. They look legacy, so this is low priority, but the check targets main-*.

@team mentions in titles. I'd leave it out of this PR and track it as a follow-up.

  • Only the bot's preview comment can notify anyone. Job summaries and CHANGELOG.md don't.
  • The follow-up idea is to put titles in a code span in the preview comment only, with a test for a title like Fix @InditexTech/weavejs-admins crash, so the changelog text stays verbatim.

Things I checked that are fine

  • sanitize-changelog.mjs import: its main-guard (import.meta.url === file://${process.argv[1]}) doesn't fire when it's imported.
  • Token permissions: the release job has contents: write, issues: write and pull-requests: write. The preview job has contents: read, issues: read and pull-requests: write. Both are enough for the GraphQL reads.
  • Git history: both workflows use fetch-depth: 0, so git describe --tags --match '[0-9]*' finds the last tag.

Not verified

  • The first real end-to-end run is the next release, as you note.
  • I don't know whether GitHub Release notes, built from the changelog, ping @mentions.
  • I haven't measured how often a commit gets near the 20-PR cap.

Maintainer-only

The skip-release label description is still "Do not publish a release for this change." Your suggested text is better, and only a maintainer can change it.

AGENTS.md

This PR changes the rule to "do not edit code/CHANGELOG.md". Until it merges, other branches will keep hand-writing entries. Your alreadyListed logic handles that, so it isn't a blocker.

Thanks again for the thorough work.

…entries

Issues labelled internal or kind/internal, such as release tracking
issues, are reported instead of written, and an issue already listed in
an earlier release is not listed again. A truncated GraphQL page now
says which limit to raise.

Refs InditexTech#1236

Signed-off-by: Rodrigo Bustamante <rbustamantejelvez@gmail.com>
@rodrigobustamante

Copy link
Copy Markdown
Contributor Author

Thanks for the thorough review, and for checking the branch protection side. Points 1, 2 and 4 are addressed in bc11e72.

1. Issues already released. An entry whose issue is already listed in any released section is now skipped, and the report warns about it:

#1147 is already listed in the [5.2.2] release, so it is not listed again.

That line comes from a dry-run of 5.2.2..5.3.0, the #1148/#1160 case you found. In a --dry-run of a range that is already released, only the releases older than --to count, so the comparison with the hand-written section still works. The PR check now reads code/CHANGELOG.md with --dry-run too, so it shows the warning on the PR and never writes the file.

2. Out-of-scope issues. Issues labelled internal or kind/internal still count as the PR's link, but they get no entry. Both the check and the release report list them under Out of scope, so nothing is dropped silently. CONTRIBUTING, AGENTS.md and the workflow docs say so. For the rule to catch the existing tracking issues, they need the label: #1241 and #1250 ("Release docs 5.7.0" and "5.7.1") have none today.

3. Section order. Noted. I'll follow #1253 for my three issues.

4. The 20-PR cap. The error now names the connection and the next step, for example:

Too many pull requests for commit <sha> to read in one page, so the changelog would be incomplete. Nothing was written and, in the release job, nothing was tagged or published. Raise the first: limit of associatedPullRequests in .github/scripts/changelog-from-prs.mjs and run the workflow again.

The same applies to the labels and closing-issue limits.

Tests: 29 pass (node --test .github/scripts/changelog-from-prs.test.mjs), plus new cases for each point. actionlint is clean.

For the @mention follow-up, I'm happy to open an issue for the code-span idea in the preview comment if you'd like.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

skip-release Do not publish a release for this change.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Proposal: generate the CHANGELOG entries in CI from the issues each PR closes

2 participants