Repository navigation
Conversation
Each test builds a throwaway repository with this repository's towncrier.toml, commits a base and a pull request, and runs .github/scripts/check-changelog-fragment.sh there. The check must accept only what the release reads: at least one non-empty file directly in changelog.d/ named <name>.<type>.md with a configured type, added by the pull request itself, and no other file in changelog.d/. The cases include policyengine-api #4084's changelog.d/added/4083.md, a pull request that adds no fragment while main has pending ones, an unknown type and an empty file. The proof fixtures hold #4084's fragment as it was and moved into place. These tests fail: towncrier.toml and the check do not exist yet. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
towncrier.toml configures Towncrier at the repository root, which has no pyproject.toml, with policyengine-api's five fragment types. Its ignore list makes Towncrier stop at any file in changelog.d/ it can't read, instead of skipping it. .github/scripts/changelog_fragments.py holds the one definition of a valid fragment: a non-empty UTF-8 file directly in changelog.d/ named <name>.<type>.md, with a type towncrier.toml lists. It reads the directory and the types from towncrier.toml. Its check-pull-request command fails unless the pull request adds a fragment and every file in changelog.d/ is one, and its messages say how to fix each file; for policyengine-api #4084's changelog.d/added/4083.md it names changelog.d/4083.added.md. check-changelog-fragment.sh passes it the files the pull request adds relative to its merge base, so fragments already on main don't count. The test helper registers the module in sys.modules before running it, which its dataclasses need. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
changelog_fragments.py bump must raise VERSION by the largest part the pending fragments' types call for, as policyengine-api's bump_version.py does: breaking makes a major release, added or removed a minor one, changed or fixed a patch. It must read fragments by the check's definition and refuse to run while changelog.d/ holds no fragment or any file it can't read, so a fragment like policyengine-api #4084's stops the release with a reason instead of being skipped. A fragment type without a bump must stop both the check and the bump. build-changelog.sh must run the bump, then Towncrier from a locked .github/towncrier project with the new version, and with --draft print the next section without changing a file. make changelog and make changelog-draft run it. These tests fail: the bump, VERSION, the build script and the Towncrier project do not exist yet. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
VERSION holds the repository's release version, 0.1.0, the version every package already declares. changelog_fragments.py bump raises it by the largest part the pending fragments' types call for: breaking a major release, added or removed a minor one, changed or fixed a patch. It reads fragments by the check's definition and refuses to run while changelog.d/ holds no fragment or anything else, naming each file. Its BUMP_BY_TYPE must cover exactly the types towncrier.toml lists, or the check and the bump both refuse to run. build-changelog.sh runs the bump, then Towncrier with the new version from .github/towncrier, a project of its own whose uv.lock pins Towncrier 26.9.0, so the release installs from a lock like every other environment. make update relocks it with the other projects. make changelog builds a release locally and make changelog-draft prints the next one without changing a file. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The 63 entries of the shared changelog_entry.yaml must move, word for word and in order, into CHANGELOG.md as its 0.1.0 section, and the YAML file must go. The test pins the per-type counts (17 added, 28 changed, 18 fixed), a SHA-256 of the entries, and the two entries YAML read as mappings because of an unquoted ": ", so a conversion through a YAML parser can't pass. It also checks that Towncrier's start marker comes before every section, that VERSION names the newest section, and that sections are listed newest first. These tests fail: CHANGELOG.md does not exist and changelog_entry.yaml is still there. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
CHANGELOG.md starts with Towncrier's start marker and one section, 0.1.0, holding the 63 entries of changelog_entry.yaml word for word and in order, under Added, Changed and Fixed. Towncrier rendered the section from one temporary fragment per entry, so it is formatted as every later release will be, and its entries are byte-identical to the YAML file's lines. changelog_entry.yaml is removed. The history is a section rather than 63 pending fragments because those changes already deployed when their pull requests merged: as fragments, the first release built after this one would have listed months of deployed work under its own version and date, and bumped the version for it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The tests evaluate every job condition of simulation-deploy.yml with the repository's GitHub Actions expression reader, following needs. A merge to main, whether a merge commit, a squash, a rebase or a squash titled like the release, must run only the versioning job; the release commit it pushes must run only the deploy jobs, so it starts no release of its own; a manual dispatch must deploy without a release. Following the pushes one merge causes must end after two, with one deploy. The versioning job must push with the GitHub App token from APP_ID and APP_PRIVATE_KEY, which starts the next run where the job's own token would not, and run build-changelog.sh then commit-release.sh. Client publication must follow only the release commit's successful push run. commit-release.sh runs against a real local remote: it pushes one commit with the release message and only VERSION, CHANGELOG.md and the removed fragments; it exits 0 when main moved on to a newer merge, whose run releases both; it fails when main no longer holds the merge, when anything else changed, or when nothing did. pr.yml must run the changelog fragment check on the merged result with the base commit, in every full CI run, as a job CI complete needs. The proof fixture is simulation-deploy.yml as it is on main, which deploys every push. These tests fail: there is no versioning job, commit-release.sh or check-changelog job yet. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
simulation-deploy.yml gets a versioning job, mirroring the one in policyengine-api's push.yml. For a push to main whose head commit is not the release commit, it creates a GitHub App token from APP_ID and APP_PRIVATE_KEY with contents: write, checks out with it, runs build-changelog.sh, and runs commit-release.sh, which commits VERSION, CHANGELOG.md and the removed fragments as "Update PolicyEngine simulation API" and pushes it to main. The App's push starts the workflow again. The beta deploy, and so prod, and the summary now run only for that release commit or a manual dispatch, and the versioning job never runs for either, so each merge deploys once and the release commit starts no release. publish-clients.yml publishes only after the release commit's run. The workflow-level concurrency group is unchanged. When main moves on to a newer merge while a release is built, commit-release.sh exits 0 instead of failing: the newer merge's run waits in the same concurrency group and releases both merges' fragments. pr.yml gets the check-changelog job, which runs check-changelog-fragment.sh on the merged result against github.event.pull_request.base.sha, with the stack condition, and CI complete needs it. The registry gains sim.process.changelog-fragment, enforced by that job and proven with policyengine-api #4084's fragment, and sim.deploy.release-commit-deploys-once, enforced by the workflow tests and proven with simulation-deploy.yml as it was. rules.md and AGENTS.md are re-rendered. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The daily dependency update and the policyengine.py package update open pull requests that CI checks like any other, so each must add a fragment the shared definition accepts: changelog.d/update-dependencies.changed.md with a fixed text, so the dependency update still compares only the update with its remote branch, and changelog.d/update-policyengine-<version>.changed.md naming both versions. Neither writes one when it changes nothing. The dependency-update tests now run the script in an empty checkout folder, since it writes there. The two fragment tests fail: neither script writes a fragment yet. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
dependency-update-commit-and-push.sh writes changelog.d/update-dependencies.changed.md before it stages an update, and update-policyengine-package.sh writes changelog.d/update-policyengine-<version>.changed.md, naming the old and new versions, and commits it with the lock. Without them, the fragment check would fail every automated pull request. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
github-prs.md gains a Changelog Fragments section: how to name and write a fragment, how each type bumps VERSION, what the check rejects and how to run it locally, and how a merge is released and then deployed by its release commit, including what happens when two merges land close together or the release fails. The PR checklist in github-prs.md and AGENTS.md gains the fragment step, and the PR template a Changelog checkbox. AGENTS.md replaces "There is currently no repository-wide changelog fragment requirement" with the requirement and the release flow, citing both rules. ci-workflows.md says only the versioning job pushes to main, dependencies.md lists the .github/towncrier project and its relock, and README.md and CONTRIBUTING.md describe the release commit and make changelog-draft. This pull request's own fragments are changelog.d/978.added.md and changelog.d/978.changed.md. 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 #978
Same-Repository Draft PR Check
PolicyEngine/policyengine-sim-api, not a personal fork.Changelog
changelog.d/<name>.<type>.md, whose type is breaking, added, changed, fixed or removed:changelog.d/978.added.mdandchangelog.d/978.changed.md.Summary
Moves the changelog from the shared
changelog_entry.yamlto Towncrier fragments, requires a valid fragment on every pull request, and buildsCHANGELOG.mdat each deploy, as policyengine-api does, without the two gaps in policyengine-api's check.Fragments and the check (rule
sim.process.changelog-fragment)towncrier.tomlat the root (the root has nopyproject.toml) with policyengine-api's five types: breaking, added, changed, fixed, removed. Itsignorelist makes Towncrier stop at any unreadable file inchangelog.d/instead of skipping it..github/scripts/changelog_fragments.pyholds the one definition of a fragment, which the check and the version bump both use. A fragment is a non-empty UTF-8 file directly inchangelog.d/, named<name>.<type>.md, with a typetowncrier.tomllists. It reads the directory and the types fromtowncrier.toml, and refuses to run if those types differ from the bump table.The new
check-changelogjob inpr.ymlruns.github/scripts/check-changelog-fragment.shon the merged result againstgithub.event.pull_request.base.sha. It has the stack condition and is inCI complete'sneeds, so it becomes a merge requirement through the existing required check. It fails unless the pull request itself adds a fragment and every file inchangelog.d/is one. policyengine-api's check counts any file anywhere underchangelog.d/, so:changelog.d/added/4083.md, which its release couldn't read (this PR's proof fixture);Both now fail here, as do unknown types, other names and empty files. Towncrier itself silently skips subfolder files and unknown types, and drops empty fragments; I checked this with 26.9.0.
Version and history
VERSION(new,0.1.0) is the repository's release version. The bump writes it, and the build passes it to Towncrier's title. 0.1.0 is the version every package already declares, nothing has ever published another, and a 0.x version claims no API stability.bump_version.py: breaking raises the major version, added or removed the minor, changed or fixed the patch. The largest wins.changelog_entry.yaml(17 added, 28 changed, 18 fixed) are now the0.1.0section ofCHANGELOG.md, word for word and in order, and the YAML file is removed.:).Release at each deploy (rule
sim.deploy.release-commit-deploys-once)simulation-deploy.ymlgains aversioningjob, which runs for a push tomainwhose head commit isn't the release commit:APP_ID,APP_PRIVATE_KEY,permission-contents: write) and checks out with it..github/scripts/build-changelog.sh: the bump, then Towncrier from.github/towncrier, which removes the fragments..github/scripts/commit-release.sh, which commits onlyVERSION,CHANGELOG.mdand the fragment removals asUpdate PolicyEngine simulation API, and pushes the commit tomain.beta(and soprod) andsummaryrun only for that release commit or a manual dispatch, andversioningruns for neither. So each merge deploys once, and the release commit starts no further release.publish-clients.ymlpublishes only after the release commit's run. The workflow-level concurrency groupsimulation-deploy-mainis unchanged, andworkflow_dispatch(with its inputs) still deploys, without a release.mainalready moved on,commit-release.shsays so and exits 0 instead of failing..github/towncrier(pyproject.tomlanduv.lock,package = false).make updaterelocks it,check-uv-locks.shchecks it, and it installs withuv run --frozen, so the locked-installs and uv-version rules cover it with no exception.make changelogbuilds a release locally.make changelog-draftprints the next section and changes nothing.Automated pull requests now add their own fragments, since the new check would fail them otherwise:
changelog.d/update-dependencies.changed.md, with fixed text, so its same-files comparison still works;changelog.d/update-policyengine-<version>.changed.md.Docs:
github-prs.mdgains a "Changelog Fragments" section, and its PR checklist a fragment step.AGENTS.md's changelog sentence and PR steps are updated, the PR template gains a Changelog checkbox, andci-workflows.mdnotes that onlyversioningpushes tomain.dependencies.mdlists.github/towncrier, andREADME.mdandCONTRIBUTING.mddescribe the release flow.rules.mdand theAGENTS.mdrules block are re-rendered.Environment variables and secrets
APP_IDversioning: create-github-app-tokenapp-id${{ secrets.APP_ID }}in the step'swith:, in a push-triggered job of this repositoryAPP_PRIVATE_KEYversioning: create-github-app-tokenprivate-key${{ secrets.APP_PRIVATE_KEY }}, same stepversioning: checkouttoken:, thengit pushincommit-release.shactions/create-github-app-token@v3steps.app-token.outputs.tokengoes to checkout, which stores it as git's credential (the same pattern asupdate-dependencies.yml)RELEASE_COMMIT_MESSAGEcommit-release.shenv:; the tests check that it matches every deploy condition andpublish-clients.ymlBASE_SHAcheck-changelog-fragment.shgithub.event.pull_request.base.sha)env:; the checkout hasfetch-depth: 0github.event.head_commit.messageifs insimulation-deploy.ymlgithub.event.workflow_run.head_commit.messagepublish-clients.ymljobifThe existing
beta/prodsecrets (secrets: inherit) are unchanged.build-changelog.shandchangelog_fragments.pyread no environment variables.Ruleset 3680274: its bypass list includes Integration 2165692 with
bypass_mode: always.gh api apps/policyenginereturns id 2165692.update-dependencies.ymlandcheck-policyengine-updates.ymlopen with tokens fromAPP_ID/APP_PRIVATE_KEYare authored byapp/policyengine. That indicates the App behindAPP_IDis 2165692; I didn't read the secret's value.Update PolicyEngine APIcommits on master are pushed bypolicyengine[bot], which starts its push runs.Unverified until the first merge: that this App push to this repository's
mainsucceeds, and that it starts the deploy run. The first proof is this PR's own merge: it should produce a0.2.0release commit, and that commit's run should deploy. If the push is refused,versioningfails and nothing deploys; dispatchingsimulation-deploy.ymlstill deploysmain.Testing
Each behaviour has a failing-test commit followed by the change. I re-ran each pair's tests at both commits:
In the workflow row, the 6 that pass at the red commit are cases the old workflow also satisfies: the release commit and a dispatch deploy, and publication follows a successful push.
projects/policyengine-simulation-entry:uv run --frozen pytest tests/, 840 passed.projects/policyengine-simulation-executor:test_ci_complete_gate.py,test_policyengine_package_update_scripts.pyandtest_modal_scripts.py, 134 passed.ruff format --check projects/*/src libs/*/srcandruff check .pass, and the changed Python files are formatted.python scripts/run_quality_guards.py,python -m scripts.rules guard --base-ref origin/mainand the rules-tool tests (263) pass.python -m scripts.rules renderleaves no diff..github/scripts/check-uv-locks.shpasses for all 9 locks (after./scripts/generate-clients.sh), and shellcheck 0.11.0 is clean on the 5 changed scripts.BASE_SHA=origin/main .github/scripts/check-changelog-fragment.shaccepts this branch's two fragments.make changelog-draftprints the0.2.0section below.github-actions[bot]commit,Update PolicyEngine simulation API, holdingVERSION0.2.0, the newCHANGELOG.mdsection above0.1.0, and the two fragment removals.src, which this PR doesn't touch.Draft of the first release:
Notes for reviewers
versioning, and the release commit deploys.changelog_entry.yamlwill conflict. Each needs to drop that edit and add a fragment.Update PolicyEngine simulation API(GitHub compares case-insensitively) would deploy without a release. Squash merges add(#N)and merge commits start withMerge pull request.🤖 Generated with Claude Code