From f44047706526ee8a0e545051ce9a04b29b3937b4 Mon Sep 17 00:00:00 2001 From: Cameron DeCoster Date: Thu, 17 Sep 2026 10:58:39 -0600 Subject: [PATCH 01/10] Remove testpypi publish step --- .github/workflows/publish_testpypi.yml | 29 -------------------------- 1 file changed, 29 deletions(-) diff --git a/.github/workflows/publish_testpypi.yml b/.github/workflows/publish_testpypi.yml index 332035d7..f5779797 100644 --- a/.github/workflows/publish_testpypi.yml +++ b/.github/workflows/publish_testpypi.yml @@ -68,32 +68,3 @@ jobs: CHOREO_ENABLE_DEBUG: 1 run: xvfb-run uv run --no-sync poe debug-test timeout-minutes: 8 - - testpypi-publish: - name: Upload release to TestPyPI - needs: super-test - if: ${{ !cancelled() && - !failure() && - github.event_name == 'push' && - github.run_attempt == 1 }} - runs-on: ubuntu-latest - environment: - name: testpypi - url: https://test.pypi.org/p/choreographer - # Signs this workflow so pypi trusts it - permissions: - id-token: write - steps: - - name: Checkout - uses: actions/checkout@v4 - - uses: astral-sh/setup-uv@v4 - - uses: actions/setup-python@v5 - with: - python-version-file: "pyproject.toml" - - run: git checkout ${{ github.ref_name }} - - run: uv sync --locked --all-extras --no-sources - - run: uv build - - name: Publish package distributions to PyPI - uses: pypa/gh-action-pypi-publish@release/v1 - with: - repository-url: https://test.pypi.org/legacy/. From 3235698a39341c8232e18ea296a50ff98882ed8f Mon Sep 17 00:00:00 2001 From: Cameron DeCoster Date: Thu, 17 Sep 2026 11:02:26 -0600 Subject: [PATCH 02/10] Rename workflow --- .../workflows/{publish_testpypi.yml => test_and_build.yml} | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) rename .github/workflows/{publish_testpypi.yml => test_and_build.yml} (97%) diff --git a/.github/workflows/publish_testpypi.yml b/.github/workflows/test_and_build.yml similarity index 97% rename from .github/workflows/publish_testpypi.yml rename to .github/workflows/test_and_build.yml index f5779797..23d1698a 100644 --- a/.github/workflows/publish_testpypi.yml +++ b/.github/workflows/test_and_build.yml @@ -1,6 +1,6 @@ -# .github/workflows/publish_testpypi.yml +# .github/workflows/test_and_build.yml --- -name: test-n-build +name: test-and-build on: workflow_dispatch: push: From 8511144a9795a7172905961cc5bf4b0143d401a2 Mon Sep 17 00:00:00 2001 From: Cameron DeCoster Date: Thu, 17 Sep 2026 11:42:53 -0600 Subject: [PATCH 03/10] Update name in existing workflows --- .github/workflows/ruff.yml | 2 +- .github/workflows/test.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ruff.yml b/.github/workflows/ruff.yml index b9a0b78c..241885e8 100644 --- a/.github/workflows/ruff.yml +++ b/.github/workflows/ruff.yml @@ -1,5 +1,5 @@ --- -name: ruff-wf +name: ruff on: pull_request jobs: ruff: diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index e7ae95cb..592464bc 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -1,5 +1,5 @@ --- -name: test-wf +name: test on: pull_request: push: From f8fdd1982efd725fb6958780ebf025bbc6f2e389 Mon Sep 17 00:00:00 2001 From: Cameron DeCoster Date: Thu, 17 Sep 2026 12:17:14 -0600 Subject: [PATCH 04/10] Pin ruff version, actions to commit hashes, add matrix --- .github/workflows/ruff.yml | 6 ++++-- .github/workflows/test.yml | 14 +++++++++----- .github/workflows/test_and_build.yml | 4 ++-- .pre-commit-config.yaml | 6 +++--- 4 files changed, 18 insertions(+), 12 deletions(-) diff --git a/.github/workflows/ruff.yml b/.github/workflows/ruff.yml index 241885e8..2d0d350a 100644 --- a/.github/workflows/ruff.yml +++ b/.github/workflows/ruff.yml @@ -5,7 +5,9 @@ jobs: ruff: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - - uses: astral-sh/ruff-action@v3 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - uses: astral-sh/ruff-action@278981a28ce3188b1e39527901f38254bf3aac89 # v4.1.0 with: src: 'src' + # Keep this in step with the rev in .pre-commit-config.yaml + version: 0.16.8 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 592464bc..ac14bec8 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -8,15 +8,19 @@ on: jobs: test-all: strategy: + fail-fast: false matrix: os: [ubuntu-latest, windows-latest, macos-latest] + # The oldest and the newest supported python: version-specific + # breakage lives at the floor, and the tag workflow covers the middle + python_v: ['3.9', '3.14'] runs-on: ${{ matrix.os }} + env: + UV_PYTHON: ${{ matrix.python_v }} steps: - - uses: actions/checkout@v4 - - uses: astral-sh/setup-uv@v5 - - uses: actions/setup-python@v5 - with: - python-version-file: "pyproject.toml" + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - uses: astral-sh/setup-uv@bec219d24cd3e171d82865faccec33120bb574f4 # v10.1.0 + - run: uv python install ${{ matrix.python_v }} - name: Install Dependencies if: ${{ matrix.os == 'ubuntu-latest' }} diff --git a/.github/workflows/test_and_build.yml b/.github/workflows/test_and_build.yml index 23d1698a..028fffdc 100644 --- a/.github/workflows/test_and_build.yml +++ b/.github/workflows/test_and_build.yml @@ -19,10 +19,10 @@ jobs: env: UV_PYTHON: ${{ matrix.python_v }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: fetch-depth: 1 - - uses: astral-sh/setup-uv@v5 + - uses: astral-sh/setup-uv@bec219d24cd3e171d82865faccec33120bb574f4 # v10.1.0 - name: Install Dependencies if: ${{ matrix.os == 'ubuntu-latest' }} run: sudo apt-get update && sudo apt-get install xvfb diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index b87c2005..0f750872 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -22,8 +22,8 @@ repos: hooks: - id: add-trailing-comma - repo: https://github.com/astral-sh/ruff-pre-commit - # Ruff version. - rev: v0.14.4 + # Ruff version. Keep this in step with the version in ruff.yml + rev: v0.16.8 hooks: # Run the linter. - id: ruff @@ -43,7 +43,7 @@ repos: types: [file, yaml] args: [ '-d', - "{ extends: default, rules: { colons: { max-spaces-after: -1 } } }", + "{ extends: default, rules: { colons: { max-spaces-after: -1 }, line-length: { max: 120 } } }", ] - repo: https://github.com/rhysd/actionlint rev: v1.7.8 From 67ce2517b78f9707a4acae484dd3a553d3306572 Mon Sep 17 00:00:00 2001 From: Cameron DeCoster Date: Thu, 17 Sep 2026 12:17:50 -0600 Subject: [PATCH 05/10] Add workflow to publish to PyPI via OIDC --- .github/workflows/publish_pypi.yml | 48 ++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 .github/workflows/publish_pypi.yml diff --git a/.github/workflows/publish_pypi.yml b/.github/workflows/publish_pypi.yml new file mode 100644 index 00000000..cce63246 --- /dev/null +++ b/.github/workflows/publish_pypi.yml @@ -0,0 +1,48 @@ +# .github/workflows/publish_pypi.yml +--- +name: publish-pypi +on: + workflow_dispatch: + inputs: + tag: + description: Tag to publish, for example v1.4.0 + required: true + type: string +jobs: + pypi-publish: + name: Upload release to PyPI + runs-on: ubuntu-latest + environment: + name: pypi + url: https://pypi.org/p/choreographer + # Signs this workflow so pypi trusts it + permissions: + id-token: write + steps: + - name: Checkout + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + ref: ${{ inputs.tag }} + # setuptools-git-versioning reads the tags to set the version + fetch-depth: 0 + - uses: astral-sh/setup-uv@bec219d24cd3e171d82865faccec33120bb574f4 # v10.1.0 + - run: uv sync --locked --all-extras --no-sources + - run: git diff --quiet HEAD || { echo "Working tree dirty"; exit 1; } + - run: uv build + - name: Verify that the build matches the tag + run: | + expected="${{ inputs.tag }}" + expected="${expected#v}" + wheel="dist/choreographer-$expected-py3-none-any.whl" + sdist="dist/choreographer-$expected.tar.gz" + if [ ! -f "$wheel" ] || [ ! -f "$sdist" ]; then + echo "Expected version $expected, but dist/ holds:" + ls -1 dist/ + exit 1 + fi + echo "Built $expected from ${{ inputs.tag }}" + - name: Publish package distributions to PyPI + uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # v1.14.2 + with: + # A re-run of a tag already on PyPI skips instead of failing + skip-existing: true From b7b4ac6d89eae0265dea17f7edc6cb7e6acc15ae Mon Sep 17 00:00:00 2001 From: Cameron DeCoster Date: Thu, 17 Sep 2026 12:24:44 -0600 Subject: [PATCH 06/10] Add release instructions --- RELEASE.md | 125 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 125 insertions(+) create mode 100644 RELEASE.md diff --git a/RELEASE.md b/RELEASE.md new file mode 100644 index 00000000..19847a02 --- /dev/null +++ b/RELEASE.md @@ -0,0 +1,125 @@ +# How to release choreographer + +This document describes how a maintainer publishes a new version of choreographer. The primary steps are a changelog update, a git tag, and an upload to PyPI. + +## Before you start + +You need the following: + +- Write access to +- An account on PyPI with upload permissions for the `choreographer` project +- A PyPI API token, or membership of the `pypi` deployment environment +- `uv` on your machine +- A local clone of the repository with all tags + +Get the tags with this command: + +```bash +git fetch --tags +``` + +## How the version number works + +No file in the repository holds the version number. The build backend uses `setuptools-git-versioning`, so the most recent git tag sets the version. See the `[tool.setuptools-git-versioning]` table in `pyproject.toml`. + +Print the version that the current checkout produces: + +```bash +uv run --no-sync --with setuptools-git-versioning setuptools-git-versioning +``` + +On a tagged commit, the command prints the clean version, for example `1.3.0`. On any other commit, the command prints a development version, for example `1.3.0.post22+git.4a278bc2`. PyPI rejects a development version, because the version carries a local part after the plus sign. Thus only a tagged commit produces a package that you can upload. + +Tag names start with `v` and follow [semantic versioning](https://semver.org). A release candidate adds `rcN` with no separator, for example `v1.4.0rc0`. + +## Step 1: Update the changelog + +1. Open `CHANGELOG.md`. The file follows the [keep a changelog](https://keepachangelog.com) format. +2. Add a new section under `## [Unreleased]`, in the form `## [X.Y.Z] -- YYYY-MM-DD` +3. Move each entry out of the `[Unreleased]` section into the new section. Leave the `[Unreleased]` heading in place with no entries. +4. Keep the subsection order: `Added`, `Changed`, `Removed`, `Fixed` +5. Make sure that each entry links to the pull request or the issue + +Open a pull request with this change and merge the pull request into `main`. The repository uses conventional commits, so title the pull request `chore: Update files for release of vX.Y.Z`. + +## Step 2: Confirm that main is ready + +1. Open the Actions tab and confirm that `test` passed on `main` +2. Make sure that `uv.lock` matches `pyproject.toml`. The release workflow runs `uv sync --locked` and fails on a stale lock file. + + ```bash + uv lock --check + ``` + + The command exits 0 when the two files agree, and exits 1 when they do not. + +3. Make sure that your working tree is clean. The release workflow fails on a dirty tree, because a dirty tree changes the version. + +## Step 3: Tag the release + +Tag the merge commit of your changelog pull request. The project uses lightweight tags. + +> [!NOTE] +> `setuptools-git-versioning` reads a lightweight tag correctly. But plain `git describe` skips a lightweight tag and reports an older one. Pass `--tags` to see the true most recent tag. + +```bash +git checkout main +git pull +git tag v1.4.0 +git push origin v1.4.0 +``` + +> [!WARNING] +> PyPI refuses a second upload of the same version. If you must correct a release that PyPI holds already, release the next version number. A tag that no package index knows is still safe to move. + +## Step 4: Watch the release workflow + +The tag push starts the `test-and-build` workflow in `.github/workflows/test_and_build.yml`. The single `super-test` job builds the package on Linux, Windows, and macOS, across each supported Python version. The job reinstalls the built wheel, downloads chrome, runs `choreo_diagnose`, and runs the test suite. + +The workflow publishes nothing. The workflow proves that the tagged commit builds and passes on every platform. + +If `super-test` fails, read the failure before you continue. A flaky browser test does not block the release, but a build failure does. + +## Step 5: Publish to PyPI + +The `publish-pypi` workflow in `.github/workflows/publish_pypi.yml` uploads the package. The workflow authenticates with OIDC through the `pypi` deployment environment, so no token is necessary. + +1. Open the Actions tab and select `publish-pypi` +2. Select Run workflow +3. Enter the tag, for example `v1.4.0` +4. Start the run +5. Open the run and approve the deployment under Review deployments + +The `pypi` environment needs a review from the `plotly/libraries_admin` team. You can approve your own run. The job waits for that approval before GitHub issues the OIDC token, so an unapproved run never reaches PyPI. + +The workflow checks out the tag, builds the package, and confirms that the built version matches the tag. The upload carries `skip-existing`, so a second run of the same tag skips the files that PyPI holds already. + +PyPI marks a release candidate as a pre-release, so `pip install choreographer` continues to resolve to the most recent final version. + +> [!NOTE] +> To upload from your machine instead, build from a clean clone of the tag and run `uv publish` with a PyPI API token. Clear `dist/` first. `uv publish` sends every file in `dist/`, so an old artifact there fails the upload. + +## Step 6: Create the GitHub release + +`CHANGELOG.md` points readers to the GitHub releases page for more context. + +1. Open +2. Select the tag that you pushed +3. Use the tag name as the title +4. Paste the new `CHANGELOG.md` section as the body +5. For a release candidate, mark the release as a pre-release + +## Step 7: Confirm the release + +Install the package from PyPI in a clean environment: + +```bash +uv run --no-project --with choreographer==1.4.0 choreo_diagnose --no-run +``` + +If the release changes the public API, tell the maintainers of [kaleido](https://pypi.org/project/kaleido/), which depends on choreographer. + +## Current gaps + +- The `testpypi` deployment environment has no purpose now. No workflow uses that environment. +- No workflow builds or deploys the documentation. The `mkdocs` dependency group in `pyproject.toml` is commented out, and `mkdocs.yml` needs the `quimeta`, `quicopy`, and `quiapi` plugins from `mkquixote`, which installs only over SSH from a private repository. A release changes no documentation site. From b75a0baa695b2c0ecc20f48efa724868bd7fe1a5 Mon Sep 17 00:00:00 2001 From: Cameron DeCoster Date: Thu, 17 Sep 2026 12:25:11 -0600 Subject: [PATCH 07/10] Stop using real URL to fix flaky test --- tests/test_devtools_async_helpers.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_devtools_async_helpers.py b/tests/test_devtools_async_helpers.py index 86a83132..a504963f 100644 --- a/tests/test_devtools_async_helpers.py +++ b/tests/test_devtools_async_helpers.py @@ -27,8 +27,8 @@ async def test_create_and_wait(browser): # Count tabs before initial_tab_count = len(browser.tabs) - # Create a simple HTML page as a data URL - data_url = "https://www.example.com" + # Create a simple HTML page as a data URL. Real URLs are flaky, so do this. + data_url = "data:text/html,

hi

" # Test 1: Create tab with data URL - should succeed tab1 = await create_and_wait(browser, url=data_url, timeout=5.0) From 2545c7e52e3b2c4e0f5fd9cb582f5dced5581d56 Mon Sep 17 00:00:00 2001 From: Cameron DeCoster Date: Thu, 17 Sep 2026 12:28:31 -0600 Subject: [PATCH 08/10] Remove obsolete version file --- .python_version | 1 - 1 file changed, 1 deletion(-) delete mode 100644 .python_version diff --git a/.python_version b/.python_version deleted file mode 100644 index bd28b9c5..00000000 --- a/.python_version +++ /dev/null @@ -1 +0,0 @@ -3.9 From ae75ae3dd573f253f201ebd5f0922424c373476e Mon Sep 17 00:00:00 2001 From: Cameron DeCoster Date: Thu, 17 Sep 2026 14:56:39 -0600 Subject: [PATCH 09/10] Revert "Stop using real URL to fix flaky test" This reverts commit b75a0baa695b2c0ecc20f48efa724868bd7fe1a5. --- tests/test_devtools_async_helpers.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_devtools_async_helpers.py b/tests/test_devtools_async_helpers.py index a504963f..86a83132 100644 --- a/tests/test_devtools_async_helpers.py +++ b/tests/test_devtools_async_helpers.py @@ -27,8 +27,8 @@ async def test_create_and_wait(browser): # Count tabs before initial_tab_count = len(browser.tabs) - # Create a simple HTML page as a data URL. Real URLs are flaky, so do this. - data_url = "data:text/html,

hi

" + # Create a simple HTML page as a data URL + data_url = "https://www.example.com" # Test 1: Create tab with data URL - should succeed tab1 = await create_and_wait(browser, url=data_url, timeout=5.0) From f0079d1762fee6bc2b8e9b9b77dc262d3533f5b9 Mon Sep 17 00:00:00 2001 From: Cameron DeCoster Date: Thu, 17 Sep 2026 15:00:32 -0600 Subject: [PATCH 10/10] Add Python 3.11 to testing --- .github/workflows/test_and_build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test_and_build.yml b/.github/workflows/test_and_build.yml index 028fffdc..c6c3b1d1 100644 --- a/.github/workflows/test_and_build.yml +++ b/.github/workflows/test_and_build.yml @@ -12,7 +12,7 @@ jobs: fail-fast: false matrix: os: [ubuntu-latest, windows-latest, macos-latest] - python_v: ['3.9', '3.10', '3.12', '3.13', '3.14', '3.14t'] + python_v: ['3.9', '3.10', '3.11', '3.12', '3.13', '3.14', '3.14t'] # chrome_v: ['-1'] name: Build and Test runs-on: ${{ matrix.os }}