Repository navigation
Conversation
towncrier writes a fragment to CHANGELOG.md only when its name carries a configured type; any other file in changelog.d is never read or deleted, and an empty fragment is left out. Add a Changelog fragments check that fails a PR adding or changing such a file, and make bump_version.py count only the fragments towncrier consumes, warning about the rest. Refs #2314 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Each was added by a merged PR whose release is on PyPI (2.86.1 to 2.99.1), but towncrier never consumed it, so none of their notes reached CHANGELOG.md and every versioning run since counted them. Converting them to typed fragments now would file old notes under the next version. The PR lists each file with its PR, merge date, release and note. Refs #2314 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Property and differential tests: bump_version reads the same config and names as towncrier (parse_toml, parse_newfragment_basename, find_fragments) across six configurations; every name the check accepts towncrier consumes with that type; the real towncrier build renders exactly the fragments the check accepts; the check on a real git repository fails exactly the files towncrier would drop or that would stop the build; and the bump depends only on typed fragments. Also catch two fragments that share towncrier's key, which stops towncrier build and the release. Refs #2314 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Refs #2314 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This was referenced Oct 11, 2026
MaxGhenis
added a commit
that referenced
this pull request
Oct 11, 2026
#2324 makes bump_version.py count only the fragments towncrier consumes and exit when there are none. prepare_release.py now uses that script's own classification when it has one (so a commit with only untyped files releases nothing) and counts every file otherwise, as before. The tests pass with main's bump_version.py and with #2324's, so the two pull requests can land in either order. Based on the patch from #2324's session. Also merges #2303 (the stale #1968 xfail), which this pull request's own check rejects as an XPASS until it lands. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…rigger Sol's review of #2324 (~/reviews/uk-changelog-fragment-check-2026-10-11): - the test file tripped the cached-array scanner (.partition on a call result); parse annotations with a regex instead; - say a name has no towncrier type only when towncrier reads none; other forms it consumes (x.fixed.1.md, x.fixed) are named as such; - a non-UTF-8 fragment is written with replacement characters, not dropped; - reject symbolic links, submodule entries and non-UTF-8 file names; - a type called md no longer breaks 'accepted means consumed as shown'; - bump_version refuses towncrier.toml, sections and issue_pattern, follows the project-name package fallback, and stops where towncrier build is strict (ignore configured); its annotations are escaped; - fail a PR that starts the release workflow with no typed fragment left; - tests for each, and for the five mutants that survived. Refs #2314 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #2314. Also fixes the fragment example from #2162 (item 3).
The defect
towncrier writes a
changelog.dfragment toCHANGELOG.mdonly when its name carries a type configured inpyproject.toml(breaking,added,changed,fixed,removed). It never reads, and never deletes, any other file there, and it leaves an empty fragment out. None of this produces a warning. Checked on main at 6d89338:changelog.d/held 93 untyped files such as1005.md. Each came from a different merged PR, between 2026-04-15 and 2026-09-18, and none of their notes is inCHANGELOG.md. None of those PRs also added a typed fragment..github/bump_version.pycounted every file except.gitkeep, so these 93 files made every versioning run release at least a patch. A push that carried only untyped files got aNo significant changes.release;CHANGELOG.mdhas 73 of them.Change
A
Changelog fragmentscheck (.github/workflows/pr_changelog.yamlruns.github/check_changelog_fragments.py). It has no path filter and runs for merge queue groups, so it can be made required. It fails a PR that adds or changes a file inchangelog.d/that:<slug>.<type>.md, with a type read frompyproject.tomlat the PR's head (a PR can add a type and use it). The message says "no towncrier type" only when towncrier reads none. Other forms towncrier does consume, such as2314.fixed.1.md(whichtowncrier createwrites) orx.fixed, are reported as not in this repository's form.(issue, type, counter)key with another fragment, in the PR or already on main, such as7.fixed.mdand007.fixed.md. That stopstowncrier buildwith "multiple files", and with it the release.It also fails a PR that changes
pyproject.tomlorchangelog.d/when its merge would leave no typed fragment. Those are the paths that start the release workflow, andbump_version.pywould then exit 1 with nothing to release.It only warns when:
- -;CHANGELOG.mdhas 131 such lines.Deleting a fragment always passes, and so do the names towncrier ignores (
.gitkeep,README.md). Each problem is a GitHub annotation on the file.towncrier check --compare-with origin/maindoes reject untyped names. I didn't use it because it scans the whole directory rather than the PR's files, requires every PR to add a fragment, lets empty fragments through, and acceptsx.fixedandx.fixed.txt..github/bump_version.pynow counts only the fragments towncrier consumes:[tool.towncrier]the way towncrier does, and parses names with an exact port ofparse_newfragment_basename.::warningfor each untyped file. With no typed fragment it exits 1, which was already its behaviour for an empty directory.towncrier.tomlfile, sections, or anissue_pattern.ignoreis configured,towncrier buildstops at an untyped file, so the script stops there too, before changing the version.Deleted the 93 untyped files (table below). Their code is released; converting them to typed fragments now would file 2.86–2.99 notes under the next version. Every file was checked:
MERGED, and the PR's merge commit is the first-parent commit that added the file;The audit script and its JSON output are in
~/reviews/uk-changelog-fragment-check-2026-10-11/.Docs:
docs/engineering/skills/github-prs.mdexplains the check.-(Follow-ups from #2160: qualifying-age docstring, UC work-related groups property-file speed, changelog fragment example #2162 item 3).<slug>.<type>.mdform.Effect on main
Versioningjob.bump_version.pyexits 1 with "No changelog fragments with a towncrier type". Before, it cut an emptyNo significant changes.release, because the untyped files kept the directory from ever being empty. The new check fails such a PR before it merges. The check is not a required one, so a PR merged past it can still do this. Since July every merge that triggered versioning added a fragment.2314.fixed.md.uc-deductions-floor-reconciliation.mdand will fail this check on its next run.-. They get a warning, not a failure. I counted 93 such fragments in 71 of 126 open PRs on 2026-10-11; the reviewer counted 112 in 86 later that day.Invariants (tested)
Config:
bump_versionreads the same types (in order), directory, orphan prefix and strictness as towncrier's own loader. This holds for this repository and seven other configurations:ignore;md.It refuses a
towncrier.toml, sections andissue_pattern.Names: for any file name,
parse_fragment_nameequals towncrier'sparse_newfragment_basename(Hypothesis, 500 examples, four type sets).Directories (Hypothesis, 60 directories × 8 configurations):
bump_versioncounts are exactly the ones towncrier'sfind_fragmentsconsumes, with the same types;infer_bumpstops exactly whentowncrier buildwould stop or has nothing to write.Check ⊆ towncrier:
md;<slug>.<type>.mdpasses.Rendering, with the real
towncrier build --draft:- -;PR behaviour, on real git repositories:
on.push.pathsinversioning.yaml.pyproject.tomlhas no[tool.towncrier], or atowncrier.tomlexists.Bump:
breakingis major,addedorremovedminor, any other type patch;All 77 tests in
policyengine_uk/tests/code_health/test_changelog_fragments.pypass locally against the locked towncrier 25.8.0, hypothesis 6.168.2 and pytest 8.4.1. They also pass against towncrier 26.9.0, which the unpinned release job installs today. Each of 29 hand-made mutants of the two scripts fails at least one test, including the five the review found surviving. The repo's cached-array scanner reports nothing on the test file.actionlintandruffare clean.Review
GPT-6.1 Sol reviewed head e946276 (
~/reviews/uk-changelog-fragment-check-2026-10-11/review-2324-sol.md): request changes. It confirmed that the parser matches towncrier, all 93 deletions, the figures above, and the #2306 and #2129 notes. Commit d95d020 addresses its findings:.partitionon a call result), failingTest2314.fixed.1.md)mdbroke the invariant; some config was silently ignoredbump_version.py, a raw error for a non-UTF-8 path, a vacuous assertionpyproject.tomlcould pass and turnVersioningredNot changed: the release job installs unpinned towncrier (
versioning.yaml), which #2306 replaces with the version inuv.lock. A delta review of d95d020 is pending.Coordination
#2306 (W2). Either order now works. #2306's head 0879f6c counts fragments with the tip's own
bump_version.pywhen it has the new helpers, and the old way otherwise. On a local merge of that head with this PR's head, all 97 tests intest_prepare_release.pyandtest_changelog_fragments.pypass. Its allowlist already accepts this module's wheel-mode skip (code_health/*, reason\.github).#2129 (keep
uv.lock's own version in step, draft) also editsbump_version.py. Merging the two conflicts on one import line. One of #2129's tests then needs a[tool.towncrier]table in its fixturepyproject.toml; the fix is in this comment. With it, all 86 tests in both files pass on the merge.Not done here
Changelog fragmentsa required check is a repository setting for an admin. Main has no required checks today, so the check is advisory.- -lines inCHANGELOG.md. EditingCHANGELOG.mdby hand is against the repo's rules.axiom: n/a: CI and changelog tooling
The 93 deleted files: PR, merge date, first release, note
1031.mdis_parentformula so datasets provide parent status directly.1032.md1042.md1043.md1044-explicit-leave.md1044.md1046.md1047.md1048.md1055.mdagefolder.1056.md1065.md1015.md634.md651.md665.mdnamemetadata tag from parameter YAML files930.md950.md1005.mdis_about_to_start_paid_workandis_newly_self_employedas Person-level boolean input va…1616.md395.md420.md201.md257.mdAttributeErrorincps.marriage_tax_reformswhen reform was instantiated with `child_edu…396.md451.mdhours_workedvariable label to "Annual hours worked" for clarity.623.md953.mdlegal-child-qyp-rules.md1008.mdscottish_funded_early_learning_childcare_eligibleand the supporting parameters under `go…257-winter-fuel.mdnp.isininstead of OR-chained equality inwinter_fuel_allowance.289.mdwith_housing/without_housing) into a single `wo…326.md45.mdmarginal_tax_rate_wrt_employer_cost, a person-level variable that reports the share of a…580.md649.mdindex.yamlmetadata for the DCMS parameters and variables folders.737.mdexpanded_ma_reformand `marriage_neutral…853.mdlabortolabourin LSR parameter and variable paths, labels, and descriptions to use…414.mdtrading_allowance,property_allowance, anddividend_allowancevariables that were…1666.mdfuel-duty-litre-rate.mdfuel-volume-uprating.md1681.md1688.md307.mdhousehold_equivalisation_bhcandhousehold_equivalisation_ahccovering t…340.md676.md866.md1171.md1437-revert.md1644-childcare-registry.mdchildcare_entitlementsumbrella entry inprograms.yamlinto three rows (universal…1654-revert.md1670.mdhigh_value_council_tax_surchargefromhousehold_taxandgov_taxwhen `gov.contrib…1671.mdcouncil_tax_less_benefitas a public-facing convenience variable used by downstream…373.md587-format.md587.md623-takeup-start-date.md747.md771-cost-of-living-docs.md771-tax-credits-docs.md920.mdpolicyengine-coreas runtim…local-dataset-filepaths.md1755.mduc-childcare-work-condition.md1379.mdyoy_growth.yaml(CPI pi…1400.md1458-pawhp-refs.md1632.mddocs/book/programs/gov/dwp/state-pension.mddescrib…368.mdcurrent_educationlabel as "Current education enrolment" and add a docstring dist…609.mdcost_of_living_support_paymentparameters — £9…721.md771-housing-benefit.md778.md1765.md745.md746.md771-disability-legacy-docs.mdclass4-maximum-steps.mdtv-licence-fee-2026.mdcouncil-tax-reduction-kingston-upon-thames.mdcouncil-tax-reduction-merton.mdcouncil-tax-reduction-national.mdcouncil-tax-reduction-newham.mdcouncil-tax-reduction-oxford.mdcouncil-tax-reduction-westminster.md1780.mdbus_fare_spendingHousehold input variable (COICOP 7.3.2 bus & coach fares), the companio…1785.mdemployment_sector(public/private) andsic_industry_divisionas Person-level input vari…1799.mdperson_bus_fare_spending, allocating household bus and coach fare spending across members…cgt-rate-effective-dates.mdcapital-gains-realisation-response.md1826.mddynamics/participation.py:impute_wages_for_nonworkersdivided earnings…uprate-electricity-and-gas-consumption.mdelectricity_consumptionandgas_consumption. Both variables declare CPI uprating, bu…🤖 Generated with Claude Code