From bb82ee6cead12204914d2c09abd2bec4c196ee76 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9mie=20du=20Boisberranger?= Date: Wed, 16 Sep 2026 14:03:41 +0200 Subject: [PATCH 1/6] rework workflows --- .../{wheels.yml => build-wheels.yml} | 158 +++++------------- .github/workflows/nightly.yml | 69 ++++++++ .github/workflows/publish.yml | 109 ++++++++++++ 3 files changed, 218 insertions(+), 118 deletions(-) rename .github/workflows/{wheels.yml => build-wheels.yml} (65%) create mode 100644 .github/workflows/nightly.yml create mode 100644 .github/workflows/publish.yml diff --git a/.github/workflows/wheels.yml b/.github/workflows/build-wheels.yml similarity index 65% rename from .github/workflows/wheels.yml rename to .github/workflows/build-wheels.yml index 82a9549..80fe387 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/build-wheels.yml @@ -1,37 +1,42 @@ -name: Publish sdist and wheels +name: Build sdist and wheels on: - schedule: - # 01:01 UTC every day - - cron: "1 1 * * *" pull_request: branches: - main - - "[0-9]+.[0-9]+.X" - push: - branches: - - main - workflow_dispatch: + # Reusable entry point. Callers pass `ref` (sklearn SHA/branch/PR merge ref): + # - nightly.yml: sklearn@main, then uploads to Anaconda + # - scikit-learn wheels.yml: PR head when the commit has [cd build] + workflow_call: inputs: ref: - description: The scikit-learn branch, tag, or commit to build + description: The scikit-learn branch, tag, SHA, or refs/pull/N/merge to build required: false - default: main type: string - environment: - description: Which PyPI environment to upload to, if any + default: main + # Manual "Run workflow" on this repo until a release candidate is green. + # Set `ref` to the sklearn release PR (refs/pull/N/merge); after that PR is + # merged, run once more with the maintenance branch (e.g. 1.9.X). Then + # publish.yml uploads those artifacts. + workflow_dispatch: + inputs: + ref: + description: The scikit-learn branch, tag, SHA, or refs/pull/N/merge to build required: true - type: choice - options: ["none", "chain", "testpypi", "pypi"] + type: string concurrency: group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} cancel-in-progress: true +run-name: Build sklearn ${{ inputs.ref || 'main' }} + +permissions: + contents: read + jobs: build_wheels: name: Build wheel for cp${{ matrix.python }}-${{ matrix.platform_id }} - if: github.repository == 'scikit-learn/scikit-learn-release' runs-on: ${{ matrix.os }} defaults: run: @@ -180,16 +185,20 @@ jobs: platform_id: macosx_arm64 steps: + # Caller may be scikit-learn; tooling always comes from this repo at the + # commit of the reusable workflow. - name: Checkout scikit-learn-release uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: + repository: ${{ job.workflow_repository }} + ref: ${{ job.workflow_sha }} persist-credentials: false - name: Checkout scikit-learn uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: repository: scikit-learn/scikit-learn - ref: ${{ github.event.inputs.ref || 'main' }} + ref: ${{ inputs.ref || 'main' }} path: scikit-learn-src persist-credentials: false @@ -214,9 +223,9 @@ jobs: PYTHONHASHSEED: "0" CIBW_ENVIRONMENT_PASS_LINUX: SOURCE_DATE_EPOCH PYTHONHASHSEED RUNNER_OS with: - package-dir: scikit-learn-src - output-dir: dist - config-file: cibuildwheel.toml + package-dir: scikit-learn-src + output-dir: dist + config-file: cibuildwheel.toml - name: Test Windows wheel in a minimal Docker image # Currently Windows ARM64 runners do not have Docker support and there @@ -242,18 +251,20 @@ jobs: - name: Checkout scikit-learn-release uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: + repository: ${{ job.workflow_repository }} + ref: ${{ job.workflow_sha }} persist-credentials: false - name: Checkout scikit-learn uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: repository: scikit-learn/scikit-learn - ref: ${{ github.event.inputs.ref || 'main' }} + ref: ${{ inputs.ref || 'main' }} path: scikit-learn-src persist-credentials: false - name: Setup Python - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # 6.2.0 + uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # 6.2.0 with: python-version: "3.12" @@ -263,106 +274,17 @@ jobs: - name: Test source distribution run: bash tools/test_source.sh + - name: Record sklearn commit + run: git -C scikit-learn-src rev-parse HEAD | tee sklearn-sha.txt + - name: Store artifacts uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: cibw-sdist path: dist/*.tar.gz - testpypi-publish: - name: Publish release to TestPyPI - if: github.event_name == 'workflow_dispatch' && (github.event.inputs.environment == 'testpypi' || github.event.inputs.environment == 'chain') - runs-on: ubuntu-latest - needs: [build_wheels, build_sdist] - environment: - name: testpypi - url: https://test.pypi.org/p/scikit-learn - permissions: - id-token: write # mandatory for trusted publishing - steps: - - name: Download sdist and wheels - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 - with: - path: dist - merge-multiple: true - - - name: View artifacts - run: ls dist - - - name: Publish - uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # v1.14.0 - with: - repository-url: https://test.pypi.org/legacy/ - skip-existing: true - print-hash: true - attestations: true - - nightly_upload: - name: Nightly upload - if: github.repository == 'scikit-learn/scikit-learn-release' && (github.event_name == 'schedule' || (github.event_name == 'workflow_dispatch' && github.event.inputs.environment == 'none')) - runs-on: ubuntu-latest - environment: upload_anaconda - needs: [build_wheels, build_sdist] - - steps: - - name: Download sdist and wheels - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 - with: - path: dist - merge-multiple: true - - - name: Install micromamba - uses: mamba-org/setup-micromamba@d7c9bd84e824b79d2af72a2d4196c7f4300d3476 # v3.0.0 - with: - # For installation of anaconda-client, required for upload to anaconda.org - init-shell: bash - environment-name: upload-env - create-args: >- - anaconda-client - - - name: Upload to anaconda.org - shell: bash -el {0} # required for micromamba - env: - TOKEN: ${{ secrets.SCIKIT_LEARN_NIGHTLY_UPLOAD_TOKEN }} - run: | - anaconda -q -t ${TOKEN} upload --force -u scientific-python-nightly-wheels ./dist/*.whl - - pypi-publish: - name: Publish release to PyPI - if: >- - github.event_name == 'workflow_dispatch' - && ( - github.event.inputs.environment == 'pypi' - || ( - github.event.inputs.environment == 'chain' - && (needs.testpypi-publish.result == 'success' || needs.testpypi-publish.result == 'skipped') - ) - ) - runs-on: ubuntu-latest - # environment: - # name: pypi - # url: https://pypi.org/p/scikit-learn - # permissions: - # id-token: write # mandatory for trusted publishing - needs: [build_wheels, build_sdist, testpypi-publish] - environment: - name: pypi - url: https://pypi.org/p/scikit-learn - permissions: - id-token: write # mandatory for trusted publishing - steps: - - name: Download sdist and wheels - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + - name: Store sklearn commit + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: - path: dist - merge-multiple: true - - - name: View artifacts - run: ls dist - - # - name: Publish - # uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # v1.14.0 - # with: - # print-hash: true - # attestations: true - + name: cibw-sklearn-sha + path: sklearn-sha.txt diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml new file mode 100644 index 0000000..0b8af89 --- /dev/null +++ b/.github/workflows/nightly.yml @@ -0,0 +1,69 @@ +name: Nightly wheels + +# Always builds scikit-learn/scikit-learn@main and uploads to +# scientific-python-nightly-wheels. Not used for releases. +on: + schedule: + # 01:01 UTC every day + - cron: "1 1 * * *" + +concurrency: + group: ${{ github.workflow }}-${{ github.run_id }} + cancel-in-progress: true + +jobs: + build: + name: Build sdist and wheels + if: github.repository == 'scikit-learn/scikit-learn-release' + uses: ./.github/workflows/build-wheels.yml + permissions: + contents: read + with: + ref: main + + nightly_upload: + name: Nightly upload + if: github.repository == 'scikit-learn/scikit-learn-release' + runs-on: ubuntu-latest + environment: upload_anaconda + needs: [build] + + steps: + - name: Download sdist and wheels + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + path: dist + merge-multiple: true + pattern: cibw-* + + - name: Install micromamba + uses: mamba-org/setup-micromamba@d7c9bd84e824b79d2af72a2d4196c7f4300d3476 # v3.0.0 + with: + init-shell: bash + environment-name: upload-env + create-args: >- + anaconda-client + + - name: Upload to anaconda.org + shell: bash -el {0} + env: + TOKEN: ${{ secrets.SCIKIT_LEARN_NIGHTLY_UPLOAD_TOKEN }} + run: | + anaconda -q -t ${TOKEN} upload --force -u scientific-python-nightly-wheels ./dist/*.whl + + # Open/update a tracking issue on scikit-learn when this nightly fails. + # Uncomment when: + # - BOT_GITHUB_TOKEN is set on this repo (issues: write on scikit-learn) + # - scikit-learn/.github/workflows/update_tracking_issue.yml accepts + # issue_repo, always checkouts scikit-learn/scikit-learn for the script, + # and no longer requires github.repository == 'scikit-learn/scikit-learn' + # + # update-tracker: + # needs: [build] + # if: ${{ always() }} + # uses: scikit-learn/scikit-learn/.github/workflows/update_tracking_issue.yml@main + # with: + # job_status: ${{ needs.build.result }} + # issue_repo: scikit-learn/scikit-learn + # secrets: + # BOT_GITHUB_TOKEN: ${{ secrets.BOT_GITHUB_TOKEN }} diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 0000000..166f452 --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,109 @@ +name: Publish to PyPI + +# Uploads artifacts from an existing "Build sdist and wheels" run, so the +# wheels published to PyPI are the ones that were built and tested, without +# rebuilding between testpypi and pypi. +on: + workflow_dispatch: + inputs: + version: + description: scikit-learn version to publish + required: true + type: string + target: + description: Package index to upload to + required: true + type: choice + options: ["testpypi", "pypi"] + +run-name: Publish ${{ inputs.version }} to ${{ inputs.target }} + +jobs: + publish: + name: Upload to ${{ inputs.target }} + if: github.repository == 'scikit-learn/scikit-learn-release' + runs-on: ubuntu-latest + environment: + name: ${{ inputs.target }} + url: ${{ inputs.target == 'pypi' && 'https://pypi.org/p/scikit-learn' || 'https://test.pypi.org/p/scikit-learn' }} + permissions: + actions: read # list runs and download artifacts + id-token: write # mandatory for trusted publishing + steps: + - name: Find build run + id: find + env: + GH_TOKEN: ${{ github.token }} + SKLEARN_VERSION: ${{ inputs.version }} + run: | + set -euo pipefail + sdist="scikit-learn-${SKLEARN_VERSION}.tar.gz" + for id in $(gh run list --workflow build-wheels.yml --status success --limit 20 --json databaseId --jq '.[].databaseId'); do + rm -rf sdist-check + if gh run download "$id" -n cibw-sdist -D sdist-check && find sdist-check -name "$sdist" | grep -q .; then + echo "Using https://github.com/${{ github.repository }}/actions/runs/${id}" + echo "run_id=$id" >> "$GITHUB_OUTPUT" + exit 0 + fi + done + echo "No successful build-wheels.yml run with $sdist in the last 20 runs." + exit 1 + + - name: Download sdist and wheels + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + run-id: ${{ steps.find.outputs.run_id }} + github-token: ${{ github.token }} + path: dist + merge-multiple: true + pattern: cibw-wheels-* + + - name: Download sdist + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + run-id: ${{ steps.find.outputs.run_id }} + github-token: ${{ github.token }} + name: cibw-sdist + path: dist + + - name: View artifacts + run: | + echo "Built by https://github.com/${{ github.repository }}/actions/runs/${{ steps.find.outputs.run_id }}" + ls -l dist + + # Release order: TestPyPI, then tag that sklearn SHA, then PyPI. + - name: Check tag matches the built commit + if: inputs.target == 'pypi' + env: + SKLEARN_VERSION: ${{ inputs.version }} + GH_TOKEN: ${{ github.token }} + run: | + set -euo pipefail + gh run download "${{ steps.find.outputs.run_id }}" -n cibw-sklearn-sha -D build-info + built=$(tr -d '[:space:]' < build-info/sklearn-sha.txt) + git clone --depth 1 --branch "$SKLEARN_VERSION" \ + https://github.com/scikit-learn/scikit-learn.git sklearn-tag + tagged=$(git -C sklearn-tag rev-parse HEAD) + echo "built=$built" + echo "tag ${SKLEARN_VERSION}=$tagged" + if [[ "$built" != "$tagged" ]]; then + echo "PyPI requires tag ${SKLEARN_VERSION} to point at the commit that produced these artifacts." + echo "Build from that tag (or 1.Y.X after merge) and publish those wheels instead." + exit 1 + fi + + - name: Publish to TestPyPI + if: inputs.target == 'testpypi' + uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # v1.14.0 + with: + repository-url: https://test.pypi.org/legacy/ + skip-existing: true + print-hash: true + attestations: true + + - name: Publish to PyPI + if: inputs.target == 'pypi' + uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # v1.14.0 + with: + print-hash: true + attestations: true From adfbadae2ddb393383fcec810437cb1ca57b430f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9mie=20du=20Boisberranger?= Date: Wed, 16 Sep 2026 18:04:28 +0200 Subject: [PATCH 2/6] continue rework workflows --- .github/workflows/build-wheels.yml | 25 +++++++-------- .github/workflows/nightly.yml | 9 +----- .github/workflows/publish.yml | 51 ++++++++---------------------- 3 files changed, 26 insertions(+), 59 deletions(-) diff --git a/.github/workflows/build-wheels.yml b/.github/workflows/build-wheels.yml index 80fe387..a68e401 100644 --- a/.github/workflows/build-wheels.yml +++ b/.github/workflows/build-wheels.yml @@ -185,12 +185,12 @@ jobs: platform_id: macosx_arm64 steps: - # Caller may be scikit-learn; tooling always comes from this repo at the - # commit of the reusable workflow. - name: Checkout scikit-learn-release uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: - repository: ${{ job.workflow_repository }} + # This workflow may be called from scikit-learn ([cd build]). Make sure to + # checkout scikit-learn-release. + repository: scikit-learn/scikit-learn-release ref: ${{ job.workflow_sha }} persist-credentials: false @@ -241,7 +241,7 @@ jobs: uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: cibw-wheels-cp${{ matrix.python }}-${{ matrix.platform_id }} - path: ./dist/*.whl + path: "**/dist/*.whl" build_sdist: name: Source distribution @@ -251,7 +251,9 @@ jobs: - name: Checkout scikit-learn-release uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: - repository: ${{ job.workflow_repository }} + # This workflow may be called from scikit-learn ([cd build]). Make sure to + # checkout scikit-learn-release. + repository: scikit-learn/scikit-learn-release ref: ${{ job.workflow_sha }} persist-credentials: false @@ -274,17 +276,14 @@ jobs: - name: Test source distribution run: bash tools/test_source.sh - - name: Record sklearn commit + # publish.yml checks this against the release tag before uploading to PyPI. + - name: Record scikit-learn commit run: git -C scikit-learn-src rev-parse HEAD | tee sklearn-sha.txt - name: Store artifacts uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: cibw-sdist - path: dist/*.tar.gz - - - name: Store sklearn commit - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 - with: - name: cibw-sklearn-sha - path: sklearn-sha.txt + path: | + dist + sklearn-sha.txt diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index 0b8af89..a924cc3 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -1,16 +1,10 @@ name: Nightly wheels -# Always builds scikit-learn/scikit-learn@main and uploads to -# scientific-python-nightly-wheels. Not used for releases. on: schedule: # 01:01 UTC every day - cron: "1 1 * * *" -concurrency: - group: ${{ github.workflow }}-${{ github.run_id }} - cancel-in-progress: true - jobs: build: name: Build sdist and wheels @@ -32,13 +26,12 @@ jobs: - name: Download sdist and wheels uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: - path: dist merge-multiple: true - pattern: cibw-* - name: Install micromamba uses: mamba-org/setup-micromamba@d7c9bd84e824b79d2af72a2d4196c7f4300d3476 # v3.0.0 with: + # For installation of anaconda-client, required for upload to anaconda.org init-shell: bash environment-name: upload-env create-args: >- diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 166f452..fb9173c 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -1,8 +1,5 @@ -name: Publish to PyPI +name: Publish to TestPyPI or PyPI -# Uploads artifacts from an existing "Build sdist and wheels" run, so the -# wheels published to PyPI are the ones that were built and tested, without -# rebuilding between testpypi and pypi. on: workflow_dispatch: inputs: @@ -30,6 +27,8 @@ jobs: actions: read # list runs and download artifacts id-token: write # mandatory for trusted publishing steps: + # find the last successful build-wheels.yml run for which the sdist name matches + # the requested version. - name: Find build run id: find env: @@ -54,56 +53,32 @@ jobs: with: run-id: ${{ steps.find.outputs.run_id }} github-token: ${{ github.token }} - path: dist merge-multiple: true - pattern: cibw-wheels-* - - - name: Download sdist - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 - with: - run-id: ${{ steps.find.outputs.run_id }} - github-token: ${{ github.token }} - name: cibw-sdist - path: dist - name: View artifacts - run: | - echo "Built by https://github.com/${{ github.repository }}/actions/runs/${{ steps.find.outputs.run_id }}" - ls -l dist + run: ls -l dist - # Release order: TestPyPI, then tag that sklearn SHA, then PyPI. + # Make sure a tag exists and matches the built commit before uploading to PyPI. - name: Check tag matches the built commit if: inputs.target == 'pypi' env: SKLEARN_VERSION: ${{ inputs.version }} - GH_TOKEN: ${{ github.token }} run: | set -euo pipefail - gh run download "${{ steps.find.outputs.run_id }}" -n cibw-sklearn-sha -D build-info - built=$(tr -d '[:space:]' < build-info/sklearn-sha.txt) - git clone --depth 1 --branch "$SKLEARN_VERSION" \ - https://github.com/scikit-learn/scikit-learn.git sklearn-tag + built=$(cat sklearn-sha.txt) + echo "built sklearn commit: $built" + git clone --depth 1 --branch "$SKLEARN_VERSION" https://github.com/scikit-learn/scikit-learn.git sklearn-tag tagged=$(git -C sklearn-tag rev-parse HEAD) - echo "built=$built" - echo "tag ${SKLEARN_VERSION}=$tagged" + echo "tag $SKLEARN_VERSION: $tagged" if [[ "$built" != "$tagged" ]]; then - echo "PyPI requires tag ${SKLEARN_VERSION} to point at the commit that produced these artifacts." - echo "Build from that tag (or 1.Y.X after merge) and publish those wheels instead." + echo "tag ${SKLEARN_VERSION} must point at the commit that produced these artifacts." exit 1 fi - - name: Publish to TestPyPI - if: inputs.target == 'testpypi' - uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # v1.14.0 - with: - repository-url: https://test.pypi.org/legacy/ - skip-existing: true - print-hash: true - attestations: true - - - name: Publish to PyPI - if: inputs.target == 'pypi' + - name: Publish uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # v1.14.0 with: + repository-url: ${{ inputs.target == 'testpypi' && 'https://test.pypi.org/legacy/' || '' }} + skip-existing: ${{ inputs.target == 'testpypi' }} print-hash: true attestations: true From 721e4946eb56a096c0f12c0a4701988425743d3f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9mie=20du=20Boisberranger?= Date: Wed, 16 Sep 2026 20:23:20 +0200 Subject: [PATCH 3/6] continue rework workflows --- .github/workflows/build-wheels.yml | 12 +++++++----- .github/workflows/nightly.yml | 10 ++++++---- .github/workflows/publish.yml | 18 ++++++++++++------ 3 files changed, 25 insertions(+), 15 deletions(-) diff --git a/.github/workflows/build-wheels.yml b/.github/workflows/build-wheels.yml index a68e401..4b4cc08 100644 --- a/.github/workflows/build-wheels.yml +++ b/.github/workflows/build-wheels.yml @@ -26,7 +26,7 @@ on: type: string concurrency: - group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} + group: build-wheels-${{ github.workflow }}-${{ github.head_ref || github.run_id }} cancel-in-progress: true run-name: Build sklearn ${{ inputs.ref || 'main' }} @@ -189,8 +189,8 @@ jobs: uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: # This workflow may be called from scikit-learn ([cd build]). Make sure to - # checkout scikit-learn-release. - repository: scikit-learn/scikit-learn-release + # checkout the repository and commit this workflow file comes from. + repository: ${{ job.workflow_repository }} ref: ${{ job.workflow_sha }} persist-credentials: false @@ -241,6 +241,8 @@ jobs: uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: cibw-wheels-cp${{ matrix.python }}-${{ matrix.platform_id }} + # `**/` is what keeps the dist/ prefix inside the artifact: the artifact root + # is the path prefix before the first wildcard. path: "**/dist/*.whl" build_sdist: @@ -252,8 +254,8 @@ jobs: uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: # This workflow may be called from scikit-learn ([cd build]). Make sure to - # checkout scikit-learn-release. - repository: scikit-learn/scikit-learn-release + # checkout the repository and commit this workflow file comes from. + repository: ${{ job.workflow_repository }} ref: ${{ job.workflow_sha }} persist-credentials: false diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index a924cc3..d1276cf 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -5,13 +5,14 @@ on: # 01:01 UTC every day - cron: "1 1 * * *" +permissions: + contents: read + jobs: build: name: Build sdist and wheels if: github.repository == 'scikit-learn/scikit-learn-release' uses: ./.github/workflows/build-wheels.yml - permissions: - contents: read with: ref: main @@ -40,9 +41,10 @@ jobs: - name: Upload to anaconda.org shell: bash -el {0} env: - TOKEN: ${{ secrets.SCIKIT_LEARN_NIGHTLY_UPLOAD_TOKEN }} + # anaconda-client uses this env var as the default token. + ANACONDA_API_TOKEN: ${{ secrets.SCIKIT_LEARN_NIGHTLY_UPLOAD_TOKEN }} run: | - anaconda -q -t ${TOKEN} upload --force -u scientific-python-nightly-wheels ./dist/*.whl + anaconda -q upload --force -u scientific-python-nightly-wheels ./dist/*.whl # Open/update a tracking issue on scikit-learn when this nightly fails. # Uncomment when: diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index fb9173c..382ee83 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -27,25 +27,31 @@ jobs: actions: read # list runs and download artifacts id-token: write # mandatory for trusted publishing steps: - # find the last successful build-wheels.yml run for which the sdist name matches - # the requested version. + # find the last successful build-wheels.yml runs, manually dispatched from `main`, + # for which the sdist name matches the requested version. - name: Find build run id: find env: GH_TOKEN: ${{ github.token }} + GH_REPO: ${{ github.repository }} SKLEARN_VERSION: ${{ inputs.version }} run: | set -euo pipefail - sdist="scikit-learn-${SKLEARN_VERSION}.tar.gz" - for id in $(gh run list --workflow build-wheels.yml --status success --limit 20 --json databaseId --jq '.[].databaseId'); do + sdist="scikit_learn-${SKLEARN_VERSION}.tar.gz" + run_ids=$( + gh run list --workflow build-wheels.yml --status success --limit 10 \ + --event workflow_dispatch --branch main \ + --json databaseId --jq '.[].databaseId' + ) + for id in $run_ids; do rm -rf sdist-check if gh run download "$id" -n cibw-sdist -D sdist-check && find sdist-check -name "$sdist" | grep -q .; then - echo "Using https://github.com/${{ github.repository }}/actions/runs/${id}" + echo "Using https://github.com/${GH_REPO}/actions/runs/${id}" echo "run_id=$id" >> "$GITHUB_OUTPUT" exit 0 fi done - echo "No successful build-wheels.yml run with $sdist in the last 20 runs." + echo "No successful build-wheels.yml run with $sdist in the last 10 runs." exit 1 - name: Download sdist and wheels From c11cca721ca501056131e758c354f4b0f38110b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9mie=20du=20Boisberranger?= Date: Wed, 16 Sep 2026 20:52:11 +0200 Subject: [PATCH 4/6] continue rework workflows --- .github/workflows/build-wheels.yml | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/.github/workflows/build-wheels.yml b/.github/workflows/build-wheels.yml index 4b4cc08..242284d 100644 --- a/.github/workflows/build-wheels.yml +++ b/.github/workflows/build-wheels.yml @@ -12,12 +12,9 @@ on: ref: description: The scikit-learn branch, tag, SHA, or refs/pull/N/merge to build required: false - type: string default: main - # Manual "Run workflow" on this repo until a release candidate is green. - # Set `ref` to the sklearn release PR (refs/pull/N/merge); after that PR is - # merged, run once more with the maintenance branch (e.g. 1.9.X). Then - # publish.yml uploads those artifacts. + type: string + # Manual "Run workflow" on this repo, usually after the sklearn release PR is merged. workflow_dispatch: inputs: ref: From 7fcce1704568b2313fbdd7d986dd1fb96006d58f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9mie=20du=20Boisberranger?= Date: Thu, 17 Sep 2026 10:20:16 +0200 Subject: [PATCH 5/6] rework workflows alt --- .github/workflows/nightly.yml | 3 +- .github/workflows/publish.yml | 116 +++++++++++++++++++--------------- 2 files changed, 65 insertions(+), 54 deletions(-) diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index d1276cf..8b80d49 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -18,7 +18,6 @@ jobs: nightly_upload: name: Nightly upload - if: github.repository == 'scikit-learn/scikit-learn-release' runs-on: ubuntu-latest environment: upload_anaconda needs: [build] @@ -55,7 +54,7 @@ jobs: # # update-tracker: # needs: [build] - # if: ${{ always() }} + # if: ${{ always() && github.repository == 'scikit-learn/scikit-learn-release' }} # uses: scikit-learn/scikit-learn/.github/workflows/update_tracking_issue.yml@main # with: # job_status: ${{ needs.build.result }} diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 382ee83..3a287ef 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -1,90 +1,102 @@ -name: Publish to TestPyPI or PyPI +name: Publish to TestPyPI and PyPI on: workflow_dispatch: inputs: - version: - description: scikit-learn version to publish + ref: + description: The scikit-learn maintenance branch to release from, e.g. 1.10.X required: true type: string - target: - description: Package index to upload to - required: true - type: choice - options: ["testpypi", "pypi"] -run-name: Publish ${{ inputs.version }} to ${{ inputs.target }} +run-name: Publish scikit-learn from ${{ inputs.ref }} + +permissions: + contents: read jobs: - publish: - name: Upload to ${{ inputs.target }} + check_ref: + name: Check ref is a maintenance branch + runs-on: ubuntu-latest if: github.repository == 'scikit-learn/scikit-learn-release' + steps: + - env: + REF: ${{ inputs.ref }} + run: | + if [[ ! "$REF" =~ ^[0-9]+\.[0-9]+\.X$ ]]; then + echo "ref must be a maintenance branch, e.g. 1.10.X (got '$REF')" + exit 1 + fi + + build: + name: Build sdist and wheels + needs: [check_ref] + uses: ./.github/workflows/build-wheels.yml + with: + ref: ${{ inputs.ref }} + + testpypi-publish: + name: Publish release to TestPyPI runs-on: ubuntu-latest + needs: [build] environment: - name: ${{ inputs.target }} - url: ${{ inputs.target == 'pypi' && 'https://pypi.org/p/scikit-learn' || 'https://test.pypi.org/p/scikit-learn' }} + name: testpypi + url: https://test.pypi.org/p/scikit-learn permissions: - actions: read # list runs and download artifacts id-token: write # mandatory for trusted publishing steps: - # find the last successful build-wheels.yml runs, manually dispatched from `main`, - # for which the sdist name matches the requested version. - - name: Find build run - id: find - env: - GH_TOKEN: ${{ github.token }} - GH_REPO: ${{ github.repository }} - SKLEARN_VERSION: ${{ inputs.version }} - run: | - set -euo pipefail - sdist="scikit_learn-${SKLEARN_VERSION}.tar.gz" - run_ids=$( - gh run list --workflow build-wheels.yml --status success --limit 10 \ - --event workflow_dispatch --branch main \ - --json databaseId --jq '.[].databaseId' - ) - for id in $run_ids; do - rm -rf sdist-check - if gh run download "$id" -n cibw-sdist -D sdist-check && find sdist-check -name "$sdist" | grep -q .; then - echo "Using https://github.com/${GH_REPO}/actions/runs/${id}" - echo "run_id=$id" >> "$GITHUB_OUTPUT" - exit 0 - fi - done - echo "No successful build-wheels.yml run with $sdist in the last 10 runs." - exit 1 + - name: Download sdist and wheels + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + merge-multiple: true + - name: View artifacts + run: ls -l dist + + - name: Publish + uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # v1.14.0 + with: + repository-url: https://test.pypi.org/legacy/ + skip-existing: true + print-hash: true + attestations: true + + pypi-publish: + name: Publish release to PyPI + runs-on: ubuntu-latest + needs: [testpypi-publish] + environment: + name: pypi + url: https://pypi.org/p/scikit-learn + permissions: + id-token: write # mandatory for trusted publishing + steps: - name: Download sdist and wheels uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: - run-id: ${{ steps.find.outputs.run_id }} - github-token: ${{ github.token }} merge-multiple: true - name: View artifacts run: ls -l dist - # Make sure a tag exists and matches the built commit before uploading to PyPI. + # Check that the sdist version is already a tag, pointing at the commit these + # artifacts were built from. - name: Check tag matches the built commit - if: inputs.target == 'pypi' - env: - SKLEARN_VERSION: ${{ inputs.version }} run: | set -euo pipefail + version=$(basename dist/scikit_learn-*.tar.gz .tar.gz) + version=${version#scikit_learn-} built=$(cat sklearn-sha.txt) - echo "built sklearn commit: $built" - git clone --depth 1 --branch "$SKLEARN_VERSION" https://github.com/scikit-learn/scikit-learn.git sklearn-tag + echo "publishing scikit-learn $version, built from $built" + git clone --depth 1 --branch "$version" https://github.com/scikit-learn/scikit-learn.git sklearn-tag tagged=$(git -C sklearn-tag rev-parse HEAD) - echo "tag $SKLEARN_VERSION: $tagged" + echo "tag $version: $tagged" if [[ "$built" != "$tagged" ]]; then - echo "tag ${SKLEARN_VERSION} must point at the commit that produced these artifacts." + echo "tag $version must point at the commit that produced these artifacts." exit 1 fi - name: Publish uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # v1.14.0 with: - repository-url: ${{ inputs.target == 'testpypi' && 'https://test.pypi.org/legacy/' || '' }} - skip-existing: ${{ inputs.target == 'testpypi' }} print-hash: true attestations: true From 56f076d5421fbbf216ac2f2697565ad06388b927 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9mie=20du=20Boisberranger?= Date: Fri, 18 Sep 2026 10:16:19 +0200 Subject: [PATCH 6/6] fix merge --- .github/workflows/nightly.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index 8b80d49..e186171 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -43,7 +43,7 @@ jobs: # anaconda-client uses this env var as the default token. ANACONDA_API_TOKEN: ${{ secrets.SCIKIT_LEARN_NIGHTLY_UPLOAD_TOKEN }} run: | - anaconda -q upload --force -u scientific-python-nightly-wheels ./dist/*.whl + anaconda -q upload --force -u scientific-python-nightly-wheels ./dist/* # Open/update a tracking issue on scikit-learn when this nightly fails. # Uncomment when: