Skip to content
Open
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
158 changes: 42 additions & 116 deletions .github/workflows/wheels.yml → .github/workflows/build-wheels.yml
Original file line number Diff line number Diff line change
@@ -1,37 +1,39 @@
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
# Manual "Run workflow" on this repo, usually after the sklearn release PR is merged.
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 }}
group: build-wheels-${{ 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:
Expand Down Expand Up @@ -165,13 +167,17 @@ jobs:
- name: Checkout scikit-learn-release
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
# This workflow may be called from scikit-learn ([cd build]). Make sure to
# checkout the repository and commit this workflow file comes from.
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

Expand All @@ -196,9 +202,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
Expand All @@ -214,7 +220,9 @@ jobs:
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: cibw-wheels-cp${{ matrix.python }}-${{ matrix.platform_id }}
path: ./dist/*.whl
# `**/` 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:
name: Source distribution
Expand All @@ -224,18 +232,22 @@ jobs:
- name: Checkout scikit-learn-release
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
# This workflow may be called from scikit-learn ([cd build]). Make sure to
# checkout the repository and commit this workflow file comes from.
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"

Expand All @@ -247,100 +259,14 @@ jobs:
env:
SKLEARN_SKIP_NETWORK_TESTS: 1

# 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

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/*

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
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
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
path:
dist
sklearn-sha.txt
63 changes: 63 additions & 0 deletions .github/workflows/nightly.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
name: Nightly wheels

on:
schedule:
# 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
with:
ref: main

nightly_upload:
name: Nightly upload
runs-on: ubuntu-latest
environment: upload_anaconda
needs: [build]

steps:
- name: Download sdist and wheels
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
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}
env:
# 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/*

# 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: 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 }}
# issue_repo: scikit-learn/scikit-learn
# secrets:
# BOT_GITHUB_TOKEN: ${{ secrets.BOT_GITHUB_TOKEN }}
90 changes: 90 additions & 0 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
name: Publish to TestPyPI or 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:
# 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
Comment on lines +30 to +55

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's probably the most controversial part of this rework. To be able in publish to reuse the artifacts from build-wheels, it looks at the 10 last build-wheels runs for the last one with wheels for the requested sklearn version.

It mimics the current scheme in scikit-learn: we build wheels and upload them the anaconda staging area. Then the publish_pypi workflow looks in the staging area for the wheels matching the requested sklearn version.

There's a very simple alternative if we don't want that: require a run_id as input for the publish workflow. That way you provide the run_id of the exact build-wheels workflow you want and you know exactly the wheels you'll get.

It didn't use run_id so far because I felt more convenient to not have to look for the run_id, and have it been done automatically. But I can live without if we find this script to brittle :)


- 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.
- name: Check tag matches the built commit
if: inputs.target == 'pypi'
env:
SKLEARN_VERSION: ${{ inputs.version }}
run: |
set -euo pipefail
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 "tag $SKLEARN_VERSION: $tagged"
if [[ "$built" != "$tagged" ]]; then
echo "tag ${SKLEARN_VERSION} must point at the commit that produced these artifacts."
exit 1
fi
Comment on lines +67 to +82

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added a check to verify that the wheels we're about to upload have been built from the same commit that was tagged. I find it's a nice safeguard.

I wonder if it's too strict. For instance if we merge the release PR, build the wheels, merge a small backport - of something not impacting the wheels like for the website - in the maintenance branch, then tag it would break the check.

Maybe we can start with it and relax if it proves too painful.


- 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
Loading