From c3f15e92c3388a50fbcbae89610d2a5191a08c05 Mon Sep 17 00:00:00 2001 From: cvanelteren Date: Thu, 19 Mar 2026 12:44:00 +1000 Subject: [PATCH] Fix release metadata and Zenodo flow --- .github/workflows/publish-pypi.yml | 22 ++++-- .zenodo.json | 4 +- CITATION.cff | 6 +- README.rst | 14 +--- docs/contributing.rst | 39 +++++----- ultraplot/tests/test_release_metadata.py | 93 ++++++++++++++++++++++++ 6 files changed, 135 insertions(+), 43 deletions(-) create mode 100644 ultraplot/tests/test_release_metadata.py diff --git a/.github/workflows/publish-pypi.yml b/.github/workflows/publish-pypi.yml index 6e3a6d0ee..80ff7fd0f 100644 --- a/.github/workflows/publish-pypi.yml +++ b/.github/workflows/publish-pypi.yml @@ -1,8 +1,6 @@ name: Publish to PyPI on: pull_request: - release: - types: [published] push: tags: ["v*"] @@ -88,9 +86,7 @@ jobs: with: repository-url: https://test.pypi.org/legacy/ verbose: true - # releases generate both release and tag events so - # we get a race condition if we don't skip existing - skip-existing: ${{ (github.event_name == 'release' || github.event_name == 'push') && 'true' || 'false' }} + skip-existing: true publish-pypi: name: Publish to PyPI @@ -99,7 +95,7 @@ jobs: name: prod url: https://pypi.org/project/ultraplot/ runs-on: ubuntu-latest - if: github.event_name == 'release' + if: github.event_name == 'push' permissions: id-token: write contents: read @@ -119,3 +115,17 @@ jobs: uses: pypa/gh-action-pypi-publish@release/v1 with: verbose: true + skip-existing: true + + publish-github-release: + name: Publish GitHub release + needs: publish-pypi + runs-on: ubuntu-latest + if: github.event_name == 'push' + permissions: + contents: write + steps: + - name: Create GitHub release + uses: softprops/action-gh-release@v2 + with: + generate_release_notes: true diff --git a/.zenodo.json b/.zenodo.json index 1d1641fed..fb302d4b0 100644 --- a/.zenodo.json +++ b/.zenodo.json @@ -34,6 +34,6 @@ "scheme": "url" } ], - "version": "1.57", - "publication_date": "2025-01-01" // need to fix + "version": "2.1.3", + "publication_date": "2026-03-11" } diff --git a/CITATION.cff b/CITATION.cff index 2939d7feb..076700452 100644 --- a/CITATION.cff +++ b/CITATION.cff @@ -8,9 +8,9 @@ authors: - family-names: "Becker" given-names: "Matthew R." orcid: "https://orcid.org/0000-0001-7774-2246" -date-released: "2025-01-01" -version: "1.57" -doi: "10.5281/zenodo.15733580" +date-released: "2026-03-11" +version: "2.1.3" +doi: "10.5281/zenodo.15733564" repository-code: "https://github.com/Ultraplot/UltraPlot" license: "MIT" keywords: diff --git a/README.rst b/README.rst index 42854d464..de4ee8185 100644 --- a/README.rst +++ b/README.rst @@ -125,16 +125,10 @@ To install a development version of UltraPlot, you can use or clone the repository and run ``pip install -e .`` inside the ``ultraplot`` folder. -If you use UltraPlot in your research, please cite it using the following BibTeX entry:: - - @software{vanElteren2025, - author = {Casper van Elteren and Matthew R. Becker}, - title = {UltraPlot: A succinct wrapper for Matplotlib}, - year = {2025}, - version = {1.57.1}, - publisher = {GitHub}, - url = {https://github.com/Ultraplot/UltraPlot} - } +If you use UltraPlot in your research, please cite the latest release metadata in +``CITATION.cff``. GitHub can export this metadata as BibTeX from the +repository's "Cite this repository" panel, and the Zenodo badge below points to +the project DOI across releases. .. |downloads| image:: https://static.pepy.tech/personalized-badge/UltraPlot?period=total&units=international_system&left_color=black&right_color=orange&left_text=Downloads :target: https://pepy.tech/project/ultraplot diff --git a/docs/contributing.rst b/docs/contributing.rst index 414c2b1a1..93828557d 100644 --- a/docs/contributing.rst +++ b/docs/contributing.rst @@ -256,23 +256,24 @@ be carried out as follows: #. Create a new branch ``release-vX.Y.Z`` with the version for the release. #. Make sure to update ``CHANGELOG.rst`` and that all new changes are reflected - in the documentation: + in the documentation. Before tagging, also sync ``CITATION.cff`` and + ``.zenodo.json`` to the release version and date: .. code-block:: bash - git add CHANGELOG.rst - git commit -m 'Update changelog' + git add CHANGELOG.rst CITATION.cff .zenodo.json + git commit -m 'Prepare release metadata' -#. Open a new pull request for this branch targeting ``master``. +#. Open a new pull request for this branch targeting ``main``. #. After all tests pass and the pull request has been approved, merge into - ``master``. + ``main``. -#. Get the latest version of the master branch: +#. Get the latest version of the ``main`` branch: .. code-block:: bash - git checkout master + git switch main git pull #. Tag the current commit and push to github: @@ -280,20 +281,14 @@ be carried out as follows: .. code-block:: bash git tag -a vX.Y.Z -m "Version X.Y.Z" - git push origin master --tags + git push origin main --tags -#. Build and publish release on PyPI: + Pushing a ``vX.Y.Z`` tag triggers the release workflow, which publishes the + package and creates the corresponding GitHub release. Zenodo archives GitHub + releases, not bare git tags. - .. code-block:: bash - - # Remove previous build products and build the package - rm -r dist build *.egg-info - python setup.py sdist bdist_wheel - # Check the source and upload to the test repository - twine check dist/* - twine upload --repository-url https://test.pypi.org/legacy/ dist/* - # Go to https://test.pypi.org/project/ultraplot/ and make sure everything looks ok - # Then make sure the package is installable - pip install --index-url https://test.pypi.org/simple/ ultraplot - # Register and push to pypi - twine upload dist/* +#. After the workflow completes, confirm that the repository "Cite this + repository" panel reflects ``CITATION.cff``, that the release is available + on TestPyPI and PyPI, and that Zenodo created a new release record. If + Zenodo does not create a new version, reconnect the repository in Zenodo + and re-run the GitHub release workflow. diff --git a/ultraplot/tests/test_release_metadata.py b/ultraplot/tests/test_release_metadata.py new file mode 100644 index 000000000..8e91a63dd --- /dev/null +++ b/ultraplot/tests/test_release_metadata.py @@ -0,0 +1,93 @@ +from __future__ import annotations + +import json +import re +import subprocess +from pathlib import Path + +import pytest + +ROOT = Path(__file__).resolve().parents[2] +CITATION_CFF = ROOT / "CITATION.cff" +ZENODO_JSON = ROOT / ".zenodo.json" +README = ROOT / "README.rst" +PUBLISH_WORKFLOW = ROOT / ".github" / "workflows" / "publish-pypi.yml" + + +def _citation_scalar(key): + """ + Extract a quoted top-level scalar from the repository CFF metadata. + """ + text = CITATION_CFF.read_text(encoding="utf-8") + match = re.search(rf'^{re.escape(key)}:\s*"([^"]+)"\s*$', text, re.MULTILINE) + assert match is not None, f"Missing {key!r} in {CITATION_CFF}" + return match.group(1) + + +def _latest_release_tag(): + """ + Return the latest release tag and tag date from the local git checkout. + """ + try: + tag_result = subprocess.run( + ["git", "tag", "--sort=-v:refname"], + check=True, + cwd=ROOT, + capture_output=True, + text=True, + ) + except (FileNotFoundError, subprocess.CalledProcessError) as exc: + pytest.skip(f"Could not inspect git tags: {exc}") + tags = [tag for tag in tag_result.stdout.splitlines() if tag.startswith("v")] + if not tags: + pytest.skip("No release tags found in this checkout") + tag = tags[0] + date_result = subprocess.run( + [ + "git", + "for-each-ref", + f"refs/tags/{tag}", + "--format=%(creatordate:short)", + ], + check=True, + cwd=ROOT, + capture_output=True, + text=True, + ) + return tag.removeprefix("v"), date_result.stdout.strip() + + +def test_release_metadata_matches_latest_git_tag(): + """ + Citation metadata should track the latest tagged release. + """ + version, release_date = _latest_release_tag() + assert _citation_scalar("version") == version + assert _citation_scalar("date-released") == release_date + + +def test_zenodo_metadata_is_valid_and_synced(): + """ + Zenodo metadata should parse as JSON and match the citation file. + """ + metadata = json.loads(ZENODO_JSON.read_text(encoding="utf-8")) + assert metadata["version"] == _citation_scalar("version") + assert metadata["publication_date"] == _citation_scalar("date-released") + + +def test_readme_citation_section_uses_repository_metadata(): + """ + The README should point readers at the maintained citation metadata. + """ + text = README.read_text(encoding="utf-8") + assert "CITATION.cff" in text + assert "@software{" not in text + + +def test_publish_workflow_creates_github_release_for_tags(): + """ + Release tags should create a GitHub release so Zenodo can archive it. + """ + text = PUBLISH_WORKFLOW.read_text(encoding="utf-8") + assert 'tags: ["v*"]' in text + assert "softprops/action-gh-release@v2" in text