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
22 changes: 16 additions & 6 deletions .github/workflows/publish-pypi.yml
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
name: Publish to PyPI
on:
pull_request:
release:
types: [published]
push:
tags: ["v*"]

Expand Down Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
4 changes: 2 additions & 2 deletions .zenodo.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
6 changes: 3 additions & 3 deletions CITATION.cff
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
14 changes: 4 additions & 10 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
39 changes: 17 additions & 22 deletions docs/contributing.rst
Original file line number Diff line number Diff line change
Expand Up @@ -256,44 +256,39 @@ 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:

.. 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.
93 changes: 93 additions & 0 deletions ultraplot/tests/test_release_metadata.py
Original file line number Diff line number Diff line change
@@ -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
Loading