From 0af4e9187b4d96e5f9a499104e82e83793ae9a10 Mon Sep 17 00:00:00 2001 From: Chris Sewell Date: Mon, 9 Nov 2026 18:51:06 +0100 Subject: [PATCH 1/3] =?UTF-8?q?=F0=9F=94=A7=20MAINTAIN:=20Build=20both=20d?= =?UTF-8?q?istributions=20on=20every=20CI=20run?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add a `build` job to `tests.yml` that builds myst-parser, then rewrites the project with `docutils_setup.py` and builds myst-docutils, each with `python -m build` into its own directory. It runs `twine check --strict`, checks each directory holds one wheel and one sdist of the expected project, and on a tag checks the built version matches the tag. Each directory is uploaded as its own artifact. The job is part of the `check` aggregator, so a broken release build fails a pull request rather than a tag push. --- .github/workflows/tests.yml | 69 +++++++++++++++++++++++++++++++++++++ 1 file changed, 69 insertions(+) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index d8ea2697..77165b2d 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -106,6 +106,74 @@ jobs: - name: Run docutils CLI run: echo "test" | myst-docutils-html + build: + + # Builds both distributions on every run, so a broken release build fails a PR, not a tag. + # The publish jobs below upload exactly these files. + name: Build distributions + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v7 + with: + persist-credentials: false + - name: Set up Python 3.13 + uses: actions/setup-python@v6 + with: + python-version: "3.13" + # `python -m build` uses the `[build-system]` pin in pyproject.toml, + # so the release build cannot drift from it (an unpinned flit CLI can). + - name: Install build tools + run: python -m pip install build twine tomlkit + - name: Build myst-parser + run: python -m build --outdir dist-parser + - name: Convert the project to myst-docutils + run: python .github/workflows/docutils_setup.py pyproject.toml README.md + - name: Build myst-docutils + run: python -m build --outdir dist-docutils + - name: Check the distributions + run: python -m twine check --strict dist-parser/* dist-docutils/* + - name: Check the distribution names, and the version against the tag + env: + TAG: ${{ startsWith(github.ref, 'refs/tags/') && github.ref_name || '' }} + run: | + python - <<'EOF' + import os + import pathlib + import sys + + tag = os.environ["TAG"].removeprefix("v") + errors = [] + for outdir, project in (("dist-parser", "myst_parser"), ("dist-docutils", "myst_docutils")): + files = sorted(p.name for p in pathlib.Path(outdir).iterdir()) + wheels = [f for f in files if f.endswith(".whl")] + sdists = [f.removesuffix(".tar.gz") for f in files if f.endswith(".tar.gz")] + if len(files) != 2 or len(wheels) != 1 or len(sdists) != 1: + errors.append(f"{outdir}: expected one wheel and one sdist, got {files}") + continue + names = {wheels[0].split("-")[0], sdists[0].rsplit("-", 1)[0]} + versions = {wheels[0].split("-")[1], sdists[0].rsplit("-", 1)[1]} + if names != {project}: + errors.append(f"{outdir}: expected {project}, got {sorted(names)}") + if tag and versions != {tag}: + errors.append(f"{outdir}: tag v{tag} != built version(s) {sorted(versions)}") + print(f"{outdir}: {files}") + if errors: + sys.exit("\n".join(errors)) + EOF + - uses: actions/upload-artifact@v7 + with: + name: dist-parser + path: dist-parser/ + if-no-files-found: error + retention-days: 7 + - uses: actions/upload-artifact@v7 + with: + name: dist-docutils + path: dist-docutils/ + if-no-files-found: error + retention-days: 7 + # https://github.com/marketplace/actions/alls-green#why check: # This job does nothing and is only used for the branch protection @@ -115,6 +183,7 @@ jobs: - pre-commit - tests - check-myst-docutils + - build runs-on: ubuntu-latest From b83ad7d8d016f27c7120aff1b3c2226f9183e995 Mon Sep 17 00:00:00 2001 From: Chris Sewell Date: Mon, 9 Nov 2026 18:51:35 +0100 Subject: [PATCH 2/3] =?UTF-8?q?=F0=9F=90=9B=20FIX:=20Publish=20the=20CI-bu?= =?UTF-8?q?ilt=20distributions=20instead=20of=20rebuilding=20with=20flit?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Both publish jobs ran `pip install flit~=3.4` then `flit build`. That now resolves flit 3.12 with flit_core 4.2, and `flit build` fails with `TypeError: SdistBuilder.build() got an unexpected keyword argument 'gen_setup_py'`, so a tag push could not publish either distribution. `publish` and `publish-docutils` now download the `dist-parser` and `dist-docutils` artifacts from the `build` job and upload them with trusted publishing, so what is published is what CI built and checked. Neither job checks out the repository or installs anything. `publish-docutils` still runs only after `publish` succeeds. --- .github/workflows/tests.yml | 27 ++++++++++++++------------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 77165b2d..7ac35180 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -198,6 +198,7 @@ jobs: name: Publish myst-parser to PyPI needs: - check + - build if: github.event_name == 'push' && startsWith(github.event.ref, 'refs/tags') runs-on: ubuntu-latest permissions: @@ -206,18 +207,20 @@ jobs: name: pypi url: https://pypi.org/p/myst-parser steps: - - uses: actions/checkout@v7 - - uses: actions/setup-python@v6 + - uses: actions/download-artifact@v8 with: - python-version: "3.11" - - run: pip install flit~=3.4 - - run: flit build + name: dist-parser + path: dist-parser/ - uses: pypa/gh-action-pypi-publish@release/v1 + with: + packages-dir: dist-parser/ publish-docutils: name: Publish myst-docutils to PyPI - needs: [publish] + needs: + - build + - publish if: github.event_name == 'push' && startsWith(github.event.ref, 'refs/tags') runs-on: ubuntu-latest permissions: @@ -226,12 +229,10 @@ jobs: name: pypi url: https://pypi.org/p/myst-docutils steps: - - uses: actions/checkout@v7 - - uses: actions/setup-python@v6 + - uses: actions/download-artifact@v8 with: - python-version: "3.11" - - run: pip install flit~=3.4 tomlkit - - name: Modify setup - run: python .github/workflows/docutils_setup.py pyproject.toml README.md - - run: flit build + name: dist-docutils + path: dist-docutils/ - uses: pypa/gh-action-pypi-publish@release/v1 + with: + packages-dir: dist-docutils/ From fda90cf57a8a5d5b30fb7bc29031e1b7a15a14e4 Mon Sep 17 00:00:00 2001 From: Chris Sewell Date: Mon, 9 Nov 2026 19:05:04 +0100 Subject: [PATCH 3/3] =?UTF-8?q?=F0=9F=94=A7=20MAINTAIN:=20Keep=20the=20bui?= =?UTF-8?q?lt=20distributions=20for=20the=20default=20retention=20period?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The publish jobs download the artifacts the build job uploaded. A 7-day retention made a publish fail if it ran more than 7 days after the tag: an approval on the `pypi` environment that came late, or a re-run of a failed publish job. Dropping `retention-days` uses the repository default instead. --- .github/workflows/tests.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 7ac35180..f11bef42 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -166,13 +166,11 @@ jobs: name: dist-parser path: dist-parser/ if-no-files-found: error - retention-days: 7 - uses: actions/upload-artifact@v7 with: name: dist-docutils path: dist-docutils/ if-no-files-found: error - retention-days: 7 # https://github.com/marketplace/actions/alls-green#why check: # This job does nothing and is only used for the branch protection