From 4e418cb1d28f293a950e5397ac01335da7fdb4f9 Mon Sep 17 00:00:00 2001 From: David Doty Date: Sun, 12 Jul 2026 11:17:56 -0700 Subject: [PATCH 1/4] update third-party action versions, and add dependabot to keep them fresh actions/checkout was v2, v2.3.4 and v3 across the workflows, and actions/setup-python was v2 and v4. Both now v7 and v6, the current majors. The old majors run on Node runtimes GitHub is retiring, so those workflows would have started failing on GitHub's schedule rather than ours. Add .github/dependabot.yml with the github-actions ecosystem so the versions no longer go stale silently. Grouped into a single monthly PR rather than one PR per action. Left alone deliberately: - marvinpinto/action-automatic-releases, which is being replaced (#323) - pypa/gh-action-pypi-publish@release/v1, which is the rolling tag PyPA tells you to pin to, not a stale version Closes #324 Co-Authored-By: Claude Opus 4.8 --- .github/dependabot.yml | 17 +++++++++++++++++ .github/workflows/check_pypi_packaging.yml | 4 ++-- .github/workflows/docs-check.yml | 4 ++-- .github/workflows/release.yml | 4 ++-- 4 files changed, 23 insertions(+), 6 deletions(-) create mode 100644 .github/dependabot.yml diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 00000000..15a1938f --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,17 @@ +version: 2 + +updates: + # Keep the versions of the third-party actions used in .github/workflows up to date. + # Without this they go stale silently, until the Node runtime they run on is retired and the + # workflows start failing on GitHub's schedule rather than on ours. + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "monthly" + groups: + # one PR for all of them, rather than one PR per action + actions: + patterns: + - "*" + commit-message: + prefix: "CI" diff --git a/.github/workflows/check_pypi_packaging.yml b/.github/workflows/check_pypi_packaging.yml index 4241d23d..2ff0ab0b 100644 --- a/.github/workflows/check_pypi_packaging.yml +++ b/.github/workflows/check_pypi_packaging.yml @@ -8,9 +8,9 @@ jobs: runs-on: "ubuntu-latest" steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v7 - name: Set up Python - uses: actions/setup-python@v2 + uses: actions/setup-python@v6 with: python-version: '3.x' - name: Install dependencies diff --git a/.github/workflows/docs-check.yml b/.github/workflows/docs-check.yml index c865b081..503b741d 100644 --- a/.github/workflows/docs-check.yml +++ b/.github/workflows/docs-check.yml @@ -5,9 +5,9 @@ jobs: docs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2.3.4 + - uses: actions/checkout@v7 - name: Set up Python - uses: actions/setup-python@v2 + uses: actions/setup-python@v6 with: python-version: '3.x' - name: Install Sphinx diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 7108b237..de076c7e 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -29,9 +29,9 @@ jobs: LICENSE.txt # Publish to PyPI using Trusted Publishers - - uses: actions/checkout@v3 + - uses: actions/checkout@v7 - name: Set up Python - uses: actions/setup-python@v4 + uses: actions/setup-python@v6 with: python-version: '3.x' - name: Install dependencies From b0ee674a3d118f731d4f32dee14298b4a20e7eff Mon Sep 17 00:00:00 2001 From: David Doty Date: Sun, 12 Jul 2026 11:44:05 -0700 Subject: [PATCH 2/4] run unit tests on every currently supported Python, computed rather than hardcoded The matrix was a hardcoded list of Python versions, which goes stale in both directions: a new Python is not tested until someone remembers to add it, and an end-of-life Python keeps being tested until someone remembers to remove it. Today the list happens to be right, but nothing keeps it right. Compute it instead, as the intersection of - the Python releases still supported upstream (released, not yet EOL), and - the versions actions/setup-python can actually install so that a new Python joins the matrix on its own -- and the tests fail loudly if scadnano is not ready for it -- and an EOL Python drops off on its own. The second half of the intersection matters: without it, a Python that upstream has released but the runner images do not carry yet would fail the matrix with "version not found", which is infrastructure noise rather than a real signal. Note this is deliberately not python-version: '3.x'. That specifier means "the newest 3.x", a single version, so it would have shrunk the matrix to one entry rather than covering all supported versions. Co-Authored-By: Claude Opus 4.8 --- .github/workflows/run_unit_tests.yml | 61 ++++++++++++++++++++++++++-- 1 file changed, 58 insertions(+), 3 deletions(-) diff --git a/.github/workflows/run_unit_tests.yml b/.github/workflows/run_unit_tests.yml index 9ff98326..ac9a6533 100644 --- a/.github/workflows/run_unit_tests.yml +++ b/.github/workflows/run_unit_tests.yml @@ -3,18 +3,73 @@ name: Run Unit Tests on: pull_request jobs: - build: + python-versions: + # Work out which Python versions to test, rather than hardcoding them, so that the list cannot go + # stale: a newly released Python joins the matrix on its own (and the tests fail loudly if scadnano + # is not ready for it), and a Python that reaches end-of-life drops off on its own. + # + # Note this is NOT what python-version: '3.x' does. That is a single-version specifier meaning + # "the newest 3.x", so using it here would test one version, not all of them. + name: "Determine supported Python versions" + runs-on: ubuntu-latest + outputs: + versions: ${{ steps.compute.outputs.versions }} + steps: + - name: Compute version matrix + id: compute + shell: python + run: | + import datetime + import json + import os + import urllib.request + + today = datetime.date.today().isoformat() + + # Python releases that are supported upstream: already released, and not yet end-of-life. + with urllib.request.urlopen("https://endoflife.date/api/python.json", timeout=30) as response: + cycles = json.load(response) + supported = { + cycle["cycle"] + for cycle in cycles + if cycle.get("eol") + and cycle["eol"] > today + and cycle.get("releaseDate") + and cycle["releaseDate"] <= today + } + # ...intersected with the versions actions/setup-python can actually install. Without this, + # a Python that upstream has released but the runners do not offer yet would fail the matrix + # with "version not found", which is infrastructure noise rather than a real test failure. + url = "https://raw.githubusercontent.com/actions/python-versions/main/versions-manifest.json" + with urllib.request.urlopen(url, timeout=30) as response: + manifest = json.load(response) + installable = { + ".".join(entry["version"].split(".")[:2]) for entry in manifest if entry.get("stable") + } + + versions = sorted(supported & installable, key=lambda v: [int(part) for part in v.split(".")]) + if not versions: + raise SystemExit("computed an empty Python version matrix") + + print(f"testing Python {', '.join(versions)}") + with open(os.environ["GITHUB_OUTPUT"], "a") as out: + out.write(f"versions={json.dumps(versions)}\n") + + build: + needs: python-versions + name: "Python ${{ matrix.python-version }}" runs-on: ubuntu-latest strategy: fail-fast: false matrix: - python-version: [ "3.10", "3.11", "3.12", "3.13", "3.14" ] + python-version: ${{ fromJSON(needs.python-versions.outputs.versions) }} steps: - uses: actions/checkout@v7 # the runner image ships only one Python on PATH; this is what actually selects the version - # named by the matrix (all of 3.10-3.14 are in the runner's tool cache, so it is a fast lookup) + # named by the matrix (the supported versions are all in the runner's tool cache, so it is a + # fast lookup rather than a download) - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v6 with: From 954fc756c753e6752d9928cd34d42c1fc5a23282 Mon Sep 17 00:00:00 2001 From: David Doty Date: Sun, 12 Jul 2026 11:53:02 -0700 Subject: [PATCH 3/4] revert computed matrix; use an explicit list ending in 3.x Back to a plain hardcoded matrix, which is the ordinary way to do this. Ending the list with "3.x" (the newest released Python) means the latest version is still tested even if nobody remembers to extend the list. Co-Authored-By: Claude Opus 4.8 --- .github/workflows/run_unit_tests.yml | 63 +++------------------------- 1 file changed, 5 insertions(+), 58 deletions(-) diff --git a/.github/workflows/run_unit_tests.yml b/.github/workflows/run_unit_tests.yml index ac9a6533..72f6529f 100644 --- a/.github/workflows/run_unit_tests.yml +++ b/.github/workflows/run_unit_tests.yml @@ -3,73 +3,20 @@ name: Run Unit Tests on: pull_request jobs: - python-versions: - # Work out which Python versions to test, rather than hardcoding them, so that the list cannot go - # stale: a newly released Python joins the matrix on its own (and the tests fail loudly if scadnano - # is not ready for it), and a Python that reaches end-of-life drops off on its own. - # - # Note this is NOT what python-version: '3.x' does. That is a single-version specifier meaning - # "the newest 3.x", so using it here would test one version, not all of them. - name: "Determine supported Python versions" - runs-on: ubuntu-latest - outputs: - versions: ${{ steps.compute.outputs.versions }} - steps: - - name: Compute version matrix - id: compute - shell: python - run: | - import datetime - import json - import os - import urllib.request - - today = datetime.date.today().isoformat() - - # Python releases that are supported upstream: already released, and not yet end-of-life. - with urllib.request.urlopen("https://endoflife.date/api/python.json", timeout=30) as response: - cycles = json.load(response) - supported = { - cycle["cycle"] - for cycle in cycles - if cycle.get("eol") - and cycle["eol"] > today - and cycle.get("releaseDate") - and cycle["releaseDate"] <= today - } - - # ...intersected with the versions actions/setup-python can actually install. Without this, - # a Python that upstream has released but the runners do not offer yet would fail the matrix - # with "version not found", which is infrastructure noise rather than a real test failure. - url = "https://raw.githubusercontent.com/actions/python-versions/main/versions-manifest.json" - with urllib.request.urlopen(url, timeout=30) as response: - manifest = json.load(response) - installable = { - ".".join(entry["version"].split(".")[:2]) for entry in manifest if entry.get("stable") - } - - versions = sorted(supported & installable, key=lambda v: [int(part) for part in v.split(".")]) - if not versions: - raise SystemExit("computed an empty Python version matrix") - - print(f"testing Python {', '.join(versions)}") - with open(os.environ["GITHUB_OUTPUT"], "a") as out: - out.write(f"versions={json.dumps(versions)}\n") - build: - needs: python-versions - name: "Python ${{ matrix.python-version }}" + runs-on: ubuntu-latest strategy: fail-fast: false matrix: - python-version: ${{ fromJSON(needs.python-versions.outputs.versions) }} + # "3.x" means the newest released Python, so the latest version is always tested even if this + # list is not kept up to date. setup.py requires >= 3.10. + python-version: [ "3.10", "3.11", "3.12", "3.13", "3.x" ] steps: - uses: actions/checkout@v7 # the runner image ships only one Python on PATH; this is what actually selects the version - # named by the matrix (the supported versions are all in the runner's tool cache, so it is a - # fast lookup rather than a download) + # named by the matrix - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v6 with: From 22f1b207a9e4f32ae1ab5fdcb23656e18df87c37 Mon Sep 17 00:00:00 2001 From: David Doty Date: Sun, 12 Jul 2026 11:55:09 -0700 Subject: [PATCH 4/4] Update run_unit_tests.yml --- .github/workflows/run_unit_tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/run_unit_tests.yml b/.github/workflows/run_unit_tests.yml index 72f6529f..117a19fc 100644 --- a/.github/workflows/run_unit_tests.yml +++ b/.github/workflows/run_unit_tests.yml @@ -11,7 +11,7 @@ jobs: matrix: # "3.x" means the newest released Python, so the latest version is always tested even if this # list is not kept up to date. setup.py requires >= 3.10. - python-version: [ "3.10", "3.11", "3.12", "3.13", "3.x" ] + python-version: [ "3.10", "3.11", "3.12", "3.13", "3.14" ] steps: - uses: actions/checkout@v7