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
13 changes: 9 additions & 4 deletions .github/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,16 +14,21 @@ All build/test workflows cover Python 3.10-3.15 plus free-threaded 3.14t and 3.1
- **openssf-scorecard.yml** — security posture scanning
- **coverity.yml** — weekly Coverity Scan static analysis of the C/Cython extensions (scan.coverity.com)

## Actions
- **actions/conda-python-matrix/** — composite action that reads the `python:` list of a recipe's `conda_build_config.yaml` and outputs the build/test matrix. `action.yml` and `conda_python_matrix.py` (standard library only) refer to nothing outside the directory, so it can move unchanged to a shared repository together with `conda-build-test.yml`. The `variants` job references it as `./.github/actions/conda-python-matrix`, which resolves against the caller's checkout; after the move that line must become `$/.github/actions/conda-python-matrix`, which resolves against the repository of the workflow file itself. It is not `$/` already because actionlint 1.7.12, used by pre-commit, rejects that syntax (rhysd/actionlint#711). Run it locally with `python .github/actions/conda-python-matrix/conda_python_matrix.py conda-recipe-cf/conda_build_config.yaml`

## CI/CD policy
- Keep build matrix (Python versions, platforms) in workflow files only
- Platforms are set in the caller workflows. The conda workflows read their Python versions from the `python:` list in the recipe's `conda_build_config.yaml`: the `variants` job of `conda-build-test.yml` turns it into the build/test matrix with the `.github/actions/conda-python-matrix` composite action. The other workflows keep their Python matrix in the workflow file
- Every entry of a recipe's `python:` list is a full spec on its own line, without selectors (`3.12.* *_cpython`, `3.13.* *_cp313`, `3.14.* *_cp314`, `3.14.* *_cp314t`); the `variants` job rejects bare versions and selectors
- The `variants` job keeps the check names of the former static matrix: `Build (3.12)` for a version with one build, `Build (3.14, 3.14.* *_cp314t, 3.14t)` for a version with GIL and free-threaded builds. The master ruleset requires the 3.14 `Test` checks by these exact names
- Required checks: conda build + test on supported Python versions/platforms in CI
- Python 3.14 is expressed as two `include` rows carrying `python_spec` (`3.14.* *_cp314` and `3.14.* *_cp314t`); a bare `--python 3.14` has been observed to select the free-threaded ABI and must not be relied on
- In workflow-file matrices (`build_pip.yml`), Python 3.14 is expressed as two `include` rows carrying `python_spec` (`3.14.* *_cp314` and `3.14.* *_cp314t`); a bare `--python 3.14` has been observed to select the free-threaded ABI and must not be relied on
- Python 3.15 follows the same pattern as 3.14: two `include` rows with `python_spec` `3.15.* *_cp315` and `3.15.* *_cp315t`
- `include` rows create new matrix combinations and inherit nothing, so `experimental` must be repeated wherever the job reads them
- Artifact naming: `$PACKAGE_NAME $OS Python $VERSION`, where `$VERSION` is `python_tag` (`3.14`, `3.14t`, `3.15`, or `3.15t`) when set, otherwise `python`
- Artifact naming: `$PACKAGE_NAME $OS Python $VERSION`, where `$VERSION` is `python_tag`: the Python version, with a `t` suffix for free-threaded builds (`3.12`, `3.14`, `3.14t`)
- Channels: `conda-forge`, Intel channel
- Build jobs run `conda build --no-test`; test jobs run `conda build --test` on the built package on a fresh runner, so CI runs the test section of the recipe, as the feedstock does
- `conda-package-cf.yml` and `conda-recipe-cf/` follow `conda-forge/mkl-service-feedstock`: when the feedstock is re-rendered with new pins (compiler versions, `mkl_devel`, Python versions), update `conda-recipe-cf/conda_build_config.yaml` and the `python-versions` input to match
- `conda-package-cf.yml` and `conda-recipe-cf/` follow `conda-forge/mkl-service-feedstock`: when the feedstock is re-rendered with new pins (compiler versions, `mkl_devel`, Python versions), update `conda-recipe-cf/conda_build_config.yaml`, including its `python:` list, to match

## Security
- OpenSSF Scorecard runs automatically
Expand Down
24 changes: 24 additions & 0 deletions .github/actions/conda-python-matrix/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
name: Conda Python matrix
description: >-
Build the conda build/test matrix from the python list of a recipe's
conda_build_config.yaml

inputs:
recipe-dir:
description: Path to the conda recipe directory
required: true

outputs:
matrix:
description: Matrix for strategy.matrix, as JSON
value: ${{ steps.read.outputs.matrix }}

runs:
using: composite
steps:
- name: Read Python versions from conda_build_config.yaml
id: read
shell: bash
env:
CBC_PATH: ${{ inputs.recipe-dir }}/conda_build_config.yaml
run: python3 "${GITHUB_ACTION_PATH}/conda_python_matrix.py" "${CBC_PATH}"
117 changes: 117 additions & 0 deletions .github/actions/conda-python-matrix/conda_python_matrix.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
"""Build the conda CI matrix from the python list of a conda_build_config.yaml.

Usage:
python conda_python_matrix.py RECIPE_DIR/conda_build_config.yaml

Prints the matrix entries and, when $GITHUB_OUTPUT is set, writes
matrix=<JSON> to it for strategy.matrix of later jobs. Standard library only,
so it runs on any runner without conda.

The python: key must hold full specs, one per line and without selectors,
such as "3.12.* *_cpython", "3.13.* *_cp313" or "3.14.* *_cp314t", quoted or
not, so that every job gets an explicit ABI. Each entry becomes one matrix
entry with:
- python: the version, e.g. "3.14"
- python_spec: the entry itself, for conda build --python
- python_tag: the version, with a "t" suffix for free-threaded builds, for
artifact names
- label: the check name suffix. A version with a single build shows only the
version ("3.12"); a version with GIL and free-threaded builds shows the
version, spec and tag ("3.14, 3.14.* *_cp314t, 3.14t"). These are the
names of the former static matrix, which branch rulesets may require.
"""

import argparse
import collections
import json
import os
import re
import sys

SPEC_RE = re.compile(r"(\d+\.\d+)\.\* \*_(cpython|cp\d+t?)")


class DeclarationError(Exception):
"""The python: list cannot be turned into a matrix."""


def read_specs(path):
"""Return the entries of the python: key of a conda_build_config.yaml."""
specs, in_list = [], False
with open(path, encoding="utf-8") as f:
for line in f:
if line.startswith("python:"):
if specs or line.strip() != "python:":
raise DeclarationError(
"expected a single 'python:' key without selectors"
)
in_list = True
elif in_list and re.match(r"\s+-", line):
if "#" in line:
raise DeclarationError(
"selectors on python entries are not supported: "
+ line.strip()
)
spec = line.split("-", 1)[1].strip()
if len(spec) > 1 and spec[0] == spec[-1] and spec[0] in "'\"":
spec = spec[1:-1]
specs.append(spec)
elif in_list and line.strip() and not line[0].isspace():
in_list = False
if not specs:
raise DeclarationError("no python entries found")
return specs


def build_matrix(specs):
"""Return the strategy.matrix for the given python entries."""
parsed = []
for spec in specs:
m = SPEC_RE.fullmatch(spec)
if not m:
raise DeclarationError(
f"unsupported python entry '{spec}', "
"expected e.g. '3.14.* *_cp314t'"
)
parsed.append((spec, m.group(1), m.group(2)))

builds = collections.Counter(version for _, version, _ in parsed)
include = []
for spec, version, abi in parsed:
tag = version + ("t" if abi.endswith("t") else "")
if builds[version] == 1:
label = version
else:
label = f"{version}, {spec}, {tag}"
include.append(
{
"python": version,
"python_spec": spec,
"python_tag": tag,
"label": label,
}
)
return {"include": include}


def main(argv=None):
parser = argparse.ArgumentParser(description=__doc__.splitlines()[0])
parser.add_argument("cbc", help="path to conda_build_config.yaml")
args = parser.parse_args(argv)

try:
matrix = build_matrix(read_specs(args.cbc))
except DeclarationError as e:
print(f"{args.cbc}: {e}", file=sys.stderr)
return 1

print(json.dumps(matrix["include"], indent=2))
output = os.environ.get("GITHUB_OUTPUT")
if output:
with open(output, "a", encoding="utf-8") as f:
f.write("matrix=" + json.dumps(matrix) + "\n")
return 0


if __name__ == "__main__":
sys.exit(main())
73 changes: 31 additions & 42 deletions .github/workflows/conda-build-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,6 @@ on:
required: false
default: false
type: boolean
python-versions:
description: JSON list of Python versions to build and test in addition to 3.14 and 3.15, which always get both the GIL and free-threaded builds
required: false
default: '["3.10", "3.11", "3.12", "3.13"]'
type: string

permissions:
contents: read
Expand All @@ -37,26 +32,34 @@ env:
package-name: mkl-service

jobs:
variants:
name: Variants

runs-on: ubuntu-latest
timeout-minutes: 5

outputs:
matrix: ${{ steps.matrix.outputs.matrix }}

steps:
- name: Checkout repo
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false

- name: Read Python versions from conda_build_config.yaml
id: matrix
uses: ./.github/actions/conda-python-matrix
with:
recipe-dir: ${{ inputs.recipe-dir }}

build:
name: Build
name: Build (${{ matrix.label }})
needs: variants

strategy:
fail-fast: false
matrix:
python: ${{ fromJSON(inputs.python-versions) }}
include:
- python: '3.14'
python_spec: '3.14.* *_cp314'
python_tag: '3.14'
- python: '3.14'
python_spec: '3.14.* *_cp314t'
python_tag: '3.14t'
- python: '3.15'
python_spec: '3.15.* *_cp315'
python_tag: '3.15'
- python: '3.15'
python_spec: '3.15.* *_cp315t'
python_tag: '3.15t'
matrix: ${{ fromJSON(needs.variants.outputs.matrix) }}

runs-on: ${{ inputs.runs-on }}
timeout-minutes: 15
Expand Down Expand Up @@ -99,7 +102,7 @@ jobs:
env:
CHANNELS_LIST: ${{ inputs.channels-list }}
RECIPE_DIR: ${{ inputs.recipe-dir }}
PYTHON_SPEC: ${{ matrix.python_spec || matrix.python }}
PYTHON_SPEC: ${{ matrix.python_spec }}
run: |
# CHANNELS_LIST intentionally word-splits into multiple -c args
# shellcheck disable=SC2086
Expand All @@ -108,37 +111,23 @@ jobs:
- name: Upload artifact
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: ${{ env.package-name }} ${{ runner.os }} Python ${{ matrix.python_tag || matrix.python }}
name: ${{ env.package-name }} ${{ runner.os }} Python ${{ matrix.python_tag }}
path: ${{ env.CONDA_BLD }}/${{ env.package-name }}-*.conda

- name: Upload wheels artifact
if: ${{ inputs.build-wheels }}
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: ${{ env.package-name }} ${{ runner.os }} Wheels Python ${{ matrix.python_tag || matrix.python }}
name: ${{ env.package-name }} ${{ runner.os }} Wheels Python ${{ matrix.python_tag }}
path: ${{ env.WHEELS_OUTPUT_FOLDER }}/mkl_service-*.whl

test:
name: Test
needs: build
name: Test (${{ matrix.label }})
needs: [variants, build]

strategy:
fail-fast: false
matrix:
python: ${{ fromJSON(inputs.python-versions) }}
include:
- python: '3.14'
python_spec: '3.14.* *_cp314'
python_tag: '3.14'
- python: '3.14'
python_spec: '3.14.* *_cp314t'
python_tag: '3.14t'
- python: '3.15'
python_spec: '3.15.* *_cp315'
python_tag: '3.15'
- python: '3.15'
python_spec: '3.15.* *_cp315t'
python_tag: '3.15t'
matrix: ${{ fromJSON(needs.variants.outputs.matrix) }}

runs-on: ${{ inputs.runs-on }}
timeout-minutes: 15
Expand All @@ -154,7 +143,7 @@ jobs:
- name: Download artifact
uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
with:
name: ${{ env.package-name }} ${{ runner.os }} Python ${{ matrix.python_tag || matrix.python }}
name: ${{ env.package-name }} ${{ runner.os }} Python ${{ matrix.python_tag }}
path: ${{ env.pkg-dir }}

- name: Setup miniconda
Expand Down
7 changes: 2 additions & 5 deletions .github/workflows/conda-package-cf.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

# Build like conda-forge/mkl-service-feedstock: same Python versions and
# Visual Studio 2022 on Windows
# Build like conda-forge/mkl-service-feedstock, whose Python versions are listed
# in conda-recipe-cf/conda_build_config.yaml, with Visual Studio 2022 on Windows
jobs:
linux:
name: linux
Expand All @@ -24,7 +24,6 @@ jobs:
conda-subdir: linux-64
channels-list: -c conda-forge --override-channels
recipe-dir: conda-recipe-cf
python-versions: '["3.11", "3.12", "3.13"]'

windows:
name: windows
Expand All @@ -34,7 +33,6 @@ jobs:
conda-subdir: win-64
channels-list: -c conda-forge --override-channels
recipe-dir: conda-recipe-cf
python-versions: '["3.11", "3.12", "3.13"]'

osx:
name: osx
Expand All @@ -44,4 +42,3 @@ jobs:
conda-subdir: osx-64
channels-list: -c conda-forge --override-channels
recipe-dir: conda-recipe-cf
python-versions: '["3.11", "3.12", "3.13"]'
1 change: 1 addition & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ python -m pip install --no-deps --no-build-isolation .
- **Python versions:** 3.10, 3.11, 3.12, 3.13, 3.14, 3.15, and free-threaded 3.14t and 3.15t (the conda-forge workflow starts at 3.11, like the feedstock)
- **Python 3.14 ABI selection:** conda jobs must request an explicit ABI, `3.14.* *_cp314` (GIL) or `3.14.* *_cp314t` (free-threaded). Once the recipes stopped constraining `python-gil`, a bare `--python 3.14` was observed to resolve to the free-threaded build, so it must not be relied on.
- **Python 3.15 ABI selection:** same as 3.14; conda jobs request `3.15.* *_cp315` (GIL) or `3.15.* *_cp315t` (free-threaded) explicitly.
- **Where the conda jobs get Python versions:** from the `python:` list in the recipe's `conda_build_config.yaml` (`conda-recipe/` or `conda-recipe-cf/`), one build and one test job per entry
- **Workflows:** `.github/workflows/`
- `conda-package.yml` — main conda build/test pipeline
- `conda-package-cf.yml` — conda build/test using only conda-forge channel, set up like `conda-forge/mkl-service-feedstock`
Expand Down
10 changes: 10 additions & 0 deletions conda-recipe-cf/conda_build_config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,13 @@ c_compiler: # [win]
- vs2022 # [win]
mkl_devel: # [not osx]
- '2026' # [not osx]
# Python versions of conda-forge/mkl-service-feedstock, plus 3.15t; CI builds
# and tests one job per entry, see .github/workflows/conda-build-test.yml
python:
- 3.11.* *_cpython
- 3.12.* *_cpython
- 3.13.* *_cp313
- 3.14.* *_cp314
- 3.14.* *_cp314t
- 3.15.* *_cp315
- 3.15.* *_cp315t
4 changes: 2 additions & 2 deletions conda-recipe/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,11 @@ Conda package build recipe for conda-forge and Intel channel distribution.
- **meta.yaml** — package metadata, dependencies, build requirements
- **build.sh** — Linux/macOS build script
- **bld.bat** — Windows build script
- **conda_build_config.yaml** — compiler / stdlib pinning used by conda-build variants
- **conda_build_config.yaml** — compiler / stdlib pinning used by conda-build variants, and the `python:` list of versions that CI builds and tests (one job per entry)

## Build configuration
- **Channels:** `conda-forge`, Intel channel
- **Python versions in CI build matrix:** 3.10, 3.11, 3.12, 3.13, 3.14, 3.15, and free-threaded 3.14t and 3.15t (3.11+ for `conda-recipe-cf/`)
- **Python versions in CI build matrix:** 3.10, 3.11, 3.12, 3.13, 3.14, 3.15, and free-threaded 3.14t and 3.15t (3.11+ for `conda-recipe-cf/`), from the `python:` list in each recipe's `conda_build_config.yaml`. A plain `conda build conda-recipe` therefore builds every listed version; pass `--python "3.12.* *_cpython"` to build one
- **Compilers:** pinned via `conda_build_config.yaml` (GCC/GXX on Linux, VS2022 on Windows); `conda-recipe-cf/` also pins gcc 15, clang 21 and `mkl_devel` 2026, as the feedstock's conda-forge pinning does
- **Dependencies:** `mkl` (runtime), `mkl-devel` (build), `cython >=3.1.0`

Expand Down
10 changes: 10 additions & 0 deletions conda-recipe/conda_build_config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,13 @@ c_stdlib: # [win]
- vs # [win]
c_compiler: # [win]
- vs2022 # [win]
# CI builds and tests one job per entry, see .github/workflows/conda-build-test.yml
python:
- 3.10.* *_cpython
- 3.11.* *_cpython
- 3.12.* *_cpython
- 3.13.* *_cp313
- 3.14.* *_cp314
- 3.14.* *_cp314t
- 3.15.* *_cp315
- 3.15.* *_cp315t
Loading