feat(release): archive releases on Zenodo and gate CITATION.cff at tag time - #1036
feat(release): archive releases on Zenodo and gate CITATION.cff at tag time#1036Yiminnn wants to merge 1 commit into
Conversation
…g time Zenodo's GitHub integration archives each published Release and mints a DOI, deriving authorship from GitHub contributor statistics unless the repository ships .zenodo.json. Add that file, with version and publication_date left out on purpose: Zenodo takes both from the tag and the Release, so pinning them would go stale on every release. CITATION.cff had drifted to 0.6.9 while five 0.7.x releases shipped, because nothing checked it. It now names 0.7.4, and the tag-driven public release validates it before anything is built or published — a citation file naming a different version stops the release while it is still reversible, unlike a PyPI publish or the Release that triggers archiving. Everyday CI deliberately does not require cff == pyproject: CITATION.cff names the last published release, so sitting behind main's .devN line is correct, not drift. The creators list is a placeholder pending the agreed author list, and tests/test_citation_metadata.py fails while that placeholder is present. A published DOI cannot be withdrawn, so the switch must not be flipped, and this must not merge, until real authorship replaces it.
|
@xdotli — this needs the author list from you before it can go anywhere.
Per author, please drop in a comment:
Two open questions worth a call while you are here:
Once you post the list I will drop it in, CI turns green, and I will mark this |
Summary
Sets up Zenodo archiving for public releases, and fixes the metadata drift that
made our citation file unciteable.
Zenodo's GitHub integration archives every published GitHub Release, snapshots
the tag's source tree, and mints a version DOI (plus a concept DOI that always
resolves to the newest version — that is the one to cite). Without a
.zenodo.jsonin the repo, Zenodo derives authorship from GitHub contributorstatistics, which is not our author list. So this PR adds that file.
.zenodo.jsondeliberately omitsversionandpublication_date: Zenodo takesboth from the tag and the Release, so pinning them here would go stale on every
release — exactly the bug the rest of this PR fixes.
CITATION.cffhad drifted to0.6.9 / 2026-08-15while five 0.7.x releasesshipped, because nothing checked it. It now names
0.7.4 / 2026-08-17, and thetag-driven release validates it:
tools/release_version.py public-releasenow also readsCITATION.cffandfails when its
versiondiffers from the release tag, or whendate-releasedis in the future.and publish. A PyPI publish and the GitHub Release that triggers Zenodo
archiving are irreversible; a failed validation is not.
cff == pyproject.CITATION.cffnames the last published release, so whilemainsits on a.devNversion the citation file is legitimately one release behind. That iscorrect, not drift, and the tag-time gate is what keeps it honest.
docs/release.mdgains a## Zenodo Archivingsection (mechanism, plus theadmin-only one-time setup steps), and the public-release step list now covers
bumping
CITATION.cffand leaving it on the version just released.Blocked on: the author list
.zenodo.json'screatorsis a placeholder, andtests/test_citation_metadata.py::test_zenodo_creators_are_resolvedfails onpurpose while that placeholder is present. CI is red by design until real
authorship lands — a published DOI cannot be withdrawn, so placeholder authors
must not be able to reach
main.Needed per author: name as
Family, Given, the order they should appear in,ORCID, and affiliation.
Ordering constraint for whoever flips the switch
The Zenodo switch requires admin rights on this repo, and it must be flipped
only after a
.zenodo.jsonwith the real author list has merged tomain.Zenodo archives the tagged commit, so with our release cadence the very next tag
would otherwise mint a permanent DOI with contributor-statistics authorship.
Steps are written out in
docs/release.md.Also note Zenodo archives releases published after the switch is flipped, so
the first archived version will be the next release, not v0.7.4.
Test plan
uv run --extra dev python -m pytest tests/— 1184 passed, 6 skipped, andthe 1 intentional failure above.
uv run ruff check .,uv run ruff format --check,uv run ty check src/ tools/.pyprojectbumped to 0.7.5 withCITATION.cffleft at 0.7.4 and tagv0.7.5is rejected withCITATION.cff records version '0.7.4' but this release publishes '0.7.5'; a futuredate-releasedis rejected; tagv0.7.4on this branch passes and emitsversion=0.7.4; andinternal-previewstill runs under--with packaging --with pyyaml.CITATION.cffvalidates against CFF 1.2.0 (cffconvert --validate)..zenodo.jsonvalidates against Zenodo's legacy deposit JSON Schema with0 errors, and
isVariantFormOfwas checked against Zenodo's liverelation-type vocabulary rather than assumed.
record's authors/title/license came from
.zenodo.json.Follow-up (out of scope here)
The concept DOI does not exist until the first archived release, so adding the
DOI badge to
README.mdand the DOI toCITATION.cffhas to be a second PR.Notes on the YAML edge cases
Two things the parser has to survive, both covered by tests: YAML parses an
unquoted
version: 1.0as a float (while0.7.4is a string), so thecomparison goes through PEP 440 rather than string equality; and an unquoted
date-releasedarrives as adatetime.datewhile a quoted one arrives as astring. The future-date check also tolerates one day of skew, because a releaser
east of UTC tagging after local midnight legitimately writes a date that is
still "tomorrow" for the runner.