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
5 changes: 3 additions & 2 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@
#
# SPDX-License-Identifier: MIT

# Weekly updates of the GitHub Actions the workflows use. Python dependencies are
# updated deliberately, not here.
# Weekly updates of the GitHub Actions the workflows pin by commit (the trailing comment
# names the release). Python dependencies are updated deliberately through uv.lock, not
# here.
version: 2
updates:
- package-ecosystem: github-actions
Expand Down
22 changes: 15 additions & 7 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,14 @@ on:
branches: [main]
pull_request:

permissions:
contents: read

# A newer push to the same ref replaces a run in progress.
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

jobs:
gate:
name: gate (${{ matrix.os }})
Expand All @@ -18,18 +26,18 @@ jobs:
matrix:
os: [ubuntu-latest, macos-latest]
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0 # hatch-vcs derives the version from tags
- uses: astral-sh/setup-uv@v6
- uses: astral-sh/setup-uv@07a047e608db84b3d8c06cd9a7c7dcc9c355c18e # v10.3.2
with:
enable-cache: true
- run: uv python install 3.14
- run: uv lock --check
- run: uv sync --locked
# Exactly the definition-of-done gate from AGENTS.md.
- run: uv run poe check
- uses: codecov/codecov-action@v5
- uses: codecov/codecov-action@303a32d7a59b442fa8d48b6a1cc6825c09c847a5 # v7.1.1
if: matrix.os == 'ubuntu-latest'
with:
files: build/coverage/coverage.xml
Expand All @@ -43,10 +51,10 @@ jobs:
name: tests (Windows)
runs-on: windows-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0
- uses: astral-sh/setup-uv@v6
- uses: astral-sh/setup-uv@07a047e608db84b3d8c06cd9a7c7dcc9c355c18e # v10.3.2
with:
enable-cache: true
- run: uv python install 3.14
Expand All @@ -60,10 +68,10 @@ jobs:
name: tests (Python 3.12, lowest dependencies)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0
- uses: astral-sh/setup-uv@v6
- uses: astral-sh/setup-uv@07a047e608db84b3d8c06cd9a7c7dcc9c355c18e # v10.3.2
- run: uv venv --python 3.12
- run: uv pip install --resolution lowest-direct -e ".[gas]"
- run: uv pip install pytest pytest-xdist pytest-randomly hypothesis beartype matplotlib sphinx
Expand Down
41 changes: 41 additions & 0 deletions .github/workflows/release-notes.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# SPDX-FileCopyrightText: 2026 John C. Travers and contributors
#
# SPDX-License-Identifier: MIT

name: Release notes

on:
push:
branches: [main]
workflow_dispatch:

permissions:
contents: read

concurrency:
group: release-notes
cancel-in-progress: false

jobs:
collect:
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
timeout-minutes: 5
permissions:
contents: write
pull-requests: write
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: main
fetch-depth: 0
- uses: astral-sh/setup-uv@07a047e608db84b3d8c06cd9a7c7dcc9c355c18e # v10.3.2
- run: uv python install 3.14
- name: Refresh the rolling draft
env:
GH_TOKEN: ${{ github.token }}
GIT_AUTHOR_NAME: github-actions[bot]
GIT_AUTHOR_EMAIL: 41898282+github-actions[bot]@users.noreply.github.com
GIT_COMMITTER_NAME: github-actions[bot]
GIT_COMMITTER_EMAIL: 41898282+github-actions[bot]@users.noreply.github.com
run: uv run --no-project --python 3.14 python tools/update_release_notes.py
4 changes: 2 additions & 2 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,10 @@ jobs:
id-token: write # the OIDC token that PyPI's trusted publishing exchanges
contents: read
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0 # hatch-vcs derives the version from the tag
- uses: astral-sh/setup-uv@v6
- uses: astral-sh/setup-uv@07a047e608db84b3d8c06cd9a7c7dcc9c355c18e # v10.3.2
- run: uv python install 3.14
- name: Build the sdist and the wheel
run: uv build
Expand Down
20 changes: 13 additions & 7 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ concerns physics or the reading of a paper rather than style, ask rather than gu
| Tests with the 100% statement and branch coverage gate | `uv run poe coverage-check` |
| Copyright and licence of every file | `uv run poe reuse` |
| Preview provisional register renumbering against main | `uv run poe register-sync` |
| Check the pending release notes, or print the changelog they would make | `uv run poe changelog-check`, `uv run poe changelog-preview` |
| Rewrite the catalogue index after adding or editing a source record | `uv run python tools/catalogue_index.py` |
| Re-run the extraction against the paper library | `PAPER_LIBRARY=<path> uv run poe test-library` |
| Re-run every stored fit and compare | `uv run poe test-fits` |
Expand Down Expand Up @@ -80,13 +81,15 @@ Environment variables used by the tools only (the package itself needs none):
5. `numpydoc lint`;
6. `codespell`;
7. `reuse lint`: every file states its copyright and licence;
8. `poe coverage-check`: `pytest` (doctests included) on every core
8. `poe changelog-check`: the release-note fragments and the changelog's structure
validate;
9. `poe coverage-check`: `pytest` (doctests included) on every core
(`pytest-xdist`, `-n auto --dist loadfile`) under `pytest-cov`, requiring **100%
statement and branch coverage** across `src/optmat/`, including the workers and
ordinary-Python subprocesses (coverage's subprocess patch);
9. `sphinx-build -W -n --keep-going` (the gallery executes).
10. `sphinx-build -W -n --keep-going` (the gallery executes).

All nine must pass. CI runs exactly this sequence on every push and pull request; if
All ten must pass. CI runs exactly this sequence on every push and pull request; if
you change one, change the other in the same commit. There is no other safety net: CI
runs only what `poe check` runs, so the lanes below are still run by hand. Do not
declare work finished while a step fails, and never suppress a check to make it pass.
Expand Down Expand Up @@ -127,7 +130,9 @@ Also, before merging:
changes.

New functionality is not done until it is **tested**, **documented in the manual** as
well as in docstrings, **recorded in the register** (below) and noted in `CHANGELOG.md`.
well as in docstrings, **recorded in the register** (below) and noted in a `changelog.d/` fragment
([format](changelog.d/README.md)). The rolling release pull request collects the fragments
into `CHANGELOG.md`; other pull requests do not edit that shared file.
Functionality built from an enhancement proposal is also not done until its phase report
is appended to the OEP (below). In your summary, say what you verified against a primary
source and what you could not.
Expand Down Expand Up @@ -302,7 +307,7 @@ The long form is [OEP 0](docs/oeps/oep-0000-process.md); the index is
- each manual page the OEP produced ends with a link to it;
- its open questions move to issues or to `open` register entries;
- its tracking issue closes;
- the `CHANGELOG.md` line cites it.
- its release-note fragment cites it.

A final OEP is history: the manual describes current behaviour.

Expand Down Expand Up @@ -426,7 +431,8 @@ The manual's conventions pages are the long form.
- **Dependencies:** managed with uv (`uv add`; never `pip install` into the environment).
Ask before adding one. The floors in `pyproject.toml` are the oldest versions the code
needs, not the newest available: do not raise one without a reason (an API, a fixed
bug, a value the tests reproduce), recorded in `CHANGELOG.md`. CI tests the floors.
bug, a value the tests reproduce), recorded in a release-note fragment. CI tests the
floors.
- **Text files are UTF-8, said explicitly:** every text-mode `open`, `Path.open`,
`gzip.open`, `read_text` and `write_text` passes `encoding="utf-8"` ⚙
(`tests/test_encoding.py`); Windows otherwise decodes with its code page.
Expand Down Expand Up @@ -484,7 +490,7 @@ The manual's conventions pages are the long form.
- **Commits:** small, logical, with descriptive messages. Stage deliberately
(`git add <paths>`).
- **Versions:** never tag, release or change a version by hand; versions come from git
tags.
tags, and a release follows [*Releasing*](docs/developers/releasing.md).
- **Coding agents:**
- merge, push, tag or release only when asked or authorised, and do not ask again for
an already authorised action;
Expand Down
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
All notable changes are recorded here, in the style of
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/). The review register
(`REGISTER.md`) records the changes to data and models in detail. Versions before
0.8.0 were internal and were not published.
0.8.0 were internal and were not published. A change's note starts as a fragment under
`changelog.d/`, and the rolling release pull request collects them here.

## Unreleased

Expand Down
6 changes: 4 additions & 2 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -206,7 +206,9 @@ motivated it in the register.
standards (for people and coding agents alike).
4. Keep the change to the task. Add tests with the code, document new functionality in
the manual as well as in docstrings, add register entries for data and model
changes, and add a line to `CHANGELOG.md` under `Unreleased`.
changes, and add a release-note fragment under `changelog.d/` for a notable change,
following [its format](changelog.d/README.md). Leave `CHANGELOG.md` to the rolling
release pull request, which collects the fragments after merges.
5. Run `uv run poe check`; with data or fit changes also `poe test-library` (if you
have the library) and `poe test-fits`. A change to the documentation alone, including
a new or edited OEP, runs the shorter checks listed in [`AGENTS.md`](AGENTS.md),
Expand All @@ -231,7 +233,7 @@ A pull request is ready to merge when:
available;
- a changed fit states why, with the numbers, and its robustness record and tested
documentation tables are regenerated;
- the manual, docstrings and changelog line are present;
- the manual, docstrings and release-note fragment are present;
- a pull request that implements a phase of an OEP appends the phase report to the OEP
and updates its header, index entry and history. The one that completes it archives
the OEP and links it from the manual pages it produced;
Expand Down
6 changes: 3 additions & 3 deletions REUSE.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,10 @@ path = "**"
SPDX-FileCopyrightText = "2026 John C. Travers and contributors"
SPDX-License-Identifier = "MIT"

# Prose documentation and figures: the manual, the register, the changelog and the
# repository's guides.
# Prose documentation and figures: the manual, the register, the changelog and its
# pending notes, the repository's guides and the issue templates.
[[annotations]]
path = ["*.md", "docs/**/*.md", "docs/**/*.rst", "docs/**/*.svg", "docs/**/*.png", "docs/_static/**", "docs/_templates/**"]
path = ["*.md", "changelog.d/*.md", ".github/ISSUE_TEMPLATE/*.md", "docs/**/*.md", "docs/**/*.rst", "docs/**/*.svg", "docs/**/*.png", "docs/_static/**", "docs/_templates/**"]
SPDX-FileCopyrightText = "2026 John C. Travers and contributors"
SPDX-License-Identifier = "CC-BY-4.0"

Expand Down
39 changes: 39 additions & 0 deletions changelog.d/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# Pending release notes

Add one Markdown file for each notable change instead of editing `CHANGELOG.md` in an
implementation PR. Name it `<short-slug>.<category>.md`; a PR number can be the slug.
No separate issue is required for a fragment. Categories are `added`, `changed`,
`breaking`, `deprecated`, `removed`, `fixed` and `security`. `breaking` becomes
**Changed (breaking)** in the changelog.

For example, `helium-matched.added.md` might contain:

```text
Helium on matched measures (TC-1092): `gas.ionisation_validation.helium_matched_ratios`.
```

Write one entry without a heading or leading bullet. Describe the observable change,
including migration instructions for a break, and say whether a recommended value moved.
Cite register entries as `TC-n` and OEPs by number; `poe register-sync` renumbers a
fragment's citations with the rest of the branch, and `tests/test_register.py` checks
them. Markdown links are relative to the root `CHANGELOG.md`, where the text will appear.
Use a unique slug to avoid conflicts. A later correction can edit the original fragment
while it is still pending.

```sh
uv run poe changelog-check # validate fragments and the Unreleased structure
uv run poe changelog-preview # print the assembled changelog without changing files
```

After merges to `main`, the **Release notes** workflow creates or updates one draft PR
on `chore/release-after-<last-version>`. It collects every fragment into `CHANGELOG.md`
and removes those fragments on that branch. Existing Unreleased notes and released
history are retained. Edit source fragments in ordinary PRs; the rolling draft is
generated. The workflow stops on manual edits, a closed PR, or a PR marked ready.

Only the rolling release PR normally changes `CHANGELOG.md`. Mark it ready when release
preparation starts, then follow the
[release checklist](../docs/developers/releasing.md). `uv run poe changelog-collect` is
the manual equivalent: it updates the changelog and deletes the collected fragments in
the current checkout. Use it only in the release worktree, after bringing in any final
changes. There are no automatic merges, tags or publications.
4 changes: 4 additions & 0 deletions changelog.d/changelog-fragments.changed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Contributors add a release note as a fragment under `changelog.d/` instead of editing
this file; a rolling draft pull request collects them, and
[*Releasing*](docs/developers/releasing.md) describes a release from it. No value, model
or public name changed.
1 change: 1 addition & 0 deletions docs/developers/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ data
fitting
documentation
register
releasing
../oeps/index
../roadmap
```
Expand Down
86 changes: 86 additions & 0 deletions docs/developers/releasing.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
# Releasing

A release publishes one source distribution and one wheel of `optmat` to PyPI, archives
the version on Zenodo and updates the manual. The version comes from the `v*` git tag.

## Release notes between releases

Pull requests add independent Markdown fragments under `changelog.d/`, named
`<short-slug>.<category>.md`
([format](https://github.com/LupoLab/optmat/blob/main/changelog.d/README.md)), instead of
editing `CHANGELOG.md`. After each push to `main`, the workflow
`.github/workflows/release-notes.yml` creates or refreshes one draft pull request,
**Prepare the next release notes**, on `chore/release-after-<last-version>`. The latest
released version comes from `CITATION.cff`. The draft groups every pending fragment under
`Unreleased` in `CHANGELOG.md` and removes the collected fragments on its branch. Released
sections are copied unchanged.

Edit pending fragments in ordinary pull requests, not in the generated draft. A manual
refresh is available with:

```sh
gh workflow run release-notes.yml --ref main
```

The updater checks that its branch is exactly what it generated before writing another
commit, so manual edits stop it without being overwritten; a closed draft, or one marked
ready, pauses it. It never force-pushes, approves, merges, tags or publishes.

Its pushes come from the workflow's own token, which starts no other workflow, so CI runs
on the draft only once someone pushes a commit to it: the release commit below.

## Making a release

1. Mark the rolling pull request ready, **before editing it**, which freezes the automatic
refreshes. If more changes reach `main` afterwards, merge `main` into the release
branch and run `uv run poe changelog-collect`, which appends only the fragments still
present. With no fragments pending there is no rolling pull request: make a release
branch from `main` instead.
2. Make the release commit on that branch:
- rename `Unreleased` in `CHANGELOG.md` to `## X.Y.Z (YYYY-MM-DD)`, above an empty
`## Unreleased`, and give it a short summary of the release, saying whether any
recommended value or model changed;
- if the curated records changed, state their counts there, with the sentence that
`tests/test_counts.py` keeps current in `README.md`;
- give `CITATION.cff` the same `version` and `date-released`
(`tests/test_release_metadata.py` checks that they agree).

A minor version may break the public surface; every break is listed under
*Changed (breaking)*.
3. Run `uv run poe test-release`, which builds and installs the distributions offline. CI
runs on the commit.
4. Merge it once CI passes.
5. Tag the merge on `main` and push the tag on its own:

```sh
git tag -a vX.Y.Z COMMIT -m "optmat X.Y.Z"
git push origin vX.Y.Z
```

The repository's rules let only administrators create a `v*` tag.
6. Publish a GitHub release from the tag, with the version's changelog section as its
notes:

```sh
gh release create vX.Y.Z --verify-tag --title "optmat X.Y.Z" --notes-file NOTES.md
```

The release starts `.github/workflows/release.yml`, which builds the distributions,
checks that the built version matches the tag and that the wheel installs and imports,
and publishes to PyPI through trusted publishing. Zenodo archives the release with the
metadata of `.zenodo.json`. The concept DOI in `CITATION.cff` and the README names every
version, so it does not change.
7. Tell solprop if it is waiting on the release: comment on the issue or SEP that asked
for the change.

## One-time setup

These are done, and recorded here for a rebuild of the repository's settings:

- On PyPI, a trusted publisher for `LupoLab/optmat`, the workflow `release.yml` and the
environment `pypi`, and a GitHub environment named `pypi`.
- On Zenodo, the repository switched on under *GitHub*.
- Under GitHub's *Settings → Actions → General → Workflow permissions*, *Allow GitHub
Actions to create and approve pull requests* enabled, with the default token permissions
read-only. Only the release-notes job asks for `contents: write` and
`pull-requests: write`. The workflow runs only from `main`.
5 changes: 3 additions & 2 deletions docs/developers/testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ uv run poe check
```

It runs formatting and lint checks, basedpyright, numpydoc, spelling, the copyright and
licence check (`reuse lint`), the tests under the coverage gate, and Sphinx with
licence check (`reuse lint`), the release-note check, the tests under the coverage gate,
and Sphinx with
warnings treated as errors. The gallery executes during the build. CI
(`.github/workflows/ci.yml`) runs the same gate on Linux and macOS for every push and
pull request, the tests on Windows, and the tests on Python 3.12 with the lowest
Expand Down Expand Up @@ -56,7 +57,7 @@ Regenerate both golden files after a reviewed model change:
uv run python tools/golden_models.py
```

Review the numerical and identity changes together, update the changelog and raise
Review the numerical and identity changes together, add a release-note fragment and raise
the relevant `EVALUATION_VERSION` when evaluation changes. A failed digest check
requires diagnosis: it does not by itself establish a scientific change.

Expand Down
Loading
Loading