Skip to content

Commit d7ec114

Browse files
authored
Merge pull request #55 from astrofrog/generalize-core-package
Generalize package: configurable core package and flat column list
2 parents 2eb12fe + 0d53db0 commit d7ec114

8 files changed

Lines changed: 374 additions & 180 deletions

File tree

.github/workflows/integration.yml

Lines changed: 30 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,40 @@ concurrency:
1717
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
1818

1919
jobs:
20+
# Read the `columns` list from packages.yaml and emit it as the job
21+
# matrix, so the (python, variant) combinations have a single source
22+
# of truth and never need hand-syncing into this workflow.
23+
setup:
24+
runs-on: ubuntu-latest
25+
timeout-minutes: 5
26+
outputs:
27+
matrix: ${{ steps.columns.outputs.matrix }}
28+
steps:
29+
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
30+
31+
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
32+
with:
33+
python-version: '3.12'
34+
35+
- name: Build matrix from packages.yaml
36+
id: columns
37+
run: |
38+
pip install pyyaml
39+
python -c '
40+
import json, yaml
41+
cols = yaml.safe_load(open("packages.yaml"))["columns"]
42+
matrix = [{"python": str(c["python"]), "variant": c["variant"]} for c in cols]
43+
print("matrix=" + json.dumps(matrix))
44+
' >> "$GITHUB_OUTPUT"
45+
2046
variant:
47+
needs: setup
2148
strategy:
2249
fail-fast: false
2350
matrix:
24-
# Keep in sync with `python_versions` in packages.yaml.
25-
# (free-threaded builds: use the "t" suffix, e.g. "3.14t".)
26-
python: ["3.12", "3.14"]
27-
variant: [stable, pre, dev]
51+
# Generated by the `setup` job from `columns:` in packages.yaml;
52+
# each entry is one {python, variant} object.
53+
include: ${{ fromJson(needs.setup.outputs.matrix) }}
2854
runs-on: ubuntu-latest
2955
timeout-minutes: 240
3056
steps:

README.md

Lines changed: 75 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -12,43 +12,54 @@ The dashboard is published to
1212
[astropy.github.io/astropy-integration-testing](https://astropy.github.io/astropy-integration-testing/)
1313
after each scheduled run.
1414

15+
The harness itself is not astropy-specific: `core_package` in
16+
`packages.yaml` defines the ecosystem's core package, so the same code
17+
can drive integration testing for another ecosystem (e.g. sunpy) just
18+
by pointing the config at a different core package and package list.
19+
1520
How it works
1621
------------
1722

1823
The `variant` job runs on a schedule (and on `workflow_dispatch`) as a
19-
matrix over astropy variant and Python version. The three variants are:
20-
21-
| Variant | Astropy | Each package |
22-
|----------|-----------------------------------------------------|----------------------------------------|
23-
| `stable` | Latest non-pre-release on PyPI | Latest non-pre-release on PyPI |
24-
| `pre` | Latest including pre-releases (`--prerelease=allow`)| Latest including pre-releases |
25-
| `dev` | Latest dev wheel from the astropy/simple channel | `git+<repo_url>` (HEAD of main branch) |
26-
27-
Within each matrix job, a single shared venv is built and packages are
28-
installed one at a time in a deterministic order (coordinated first,
29-
alphabetical within each tier). If a package can't be installed
30-
alongside the existing venv (e.g., it pins `astropy<7` but we already
31-
installed astropy 8), it's skipped and recorded; the rest of the venv
32-
is untouched. After installs, `pytest --pyargs <module>` runs for each
33-
package that installed successfully.
34-
35-
The `dashboard` job then downloads the per-matrix-job result JSONs and
36-
publishes the dashboard to `gh-pages`.
24+
matrix over the *columns* configured in `packages.yaml`. Each column is
25+
an independent (Python version, variant) pair — the two axes are not a
26+
cross-product, so you can test e.g. `3.12 + stable`, `3.13 + pre` and
27+
`3.14 + dev` if you want. The three variants are:
28+
29+
| Variant | Core package | Each package |
30+
|----------|-------------------------------------------------------|----------------------------------------|
31+
| `stable` | Latest non-pre-release on PyPI | Latest non-pre-release on PyPI |
32+
| `pre` | Latest including pre-releases (`--prerelease=allow`) | Latest including pre-releases |
33+
| `dev` | Latest dev wheel from `core_package.dev_index_urls` (or `git+repo_url` if unset) | `git+<repo_url>` (HEAD of main branch) |
34+
35+
Within each matrix job, a single shared venv is built: the core package
36+
is installed first, then each package one at a time in a deterministic
37+
order (coordinated first, alphabetical within each tier). If a package
38+
can't be installed alongside the existing venv (e.g., it pins
39+
`astropy<7` but we already installed astropy 8), it's skipped and
40+
recorded; the rest of the venv is untouched. After installs, `pytest
41+
--pyargs <module>` runs for each package that installed successfully.
42+
43+
A small `setup` job parses `columns:` from `packages.yaml` and emits it
44+
as the workflow matrix, so the column list has a single source of
45+
truth. The `dashboard` job then downloads the per-matrix-job result
46+
JSONs and publishes the dashboard to `gh-pages`.
3747

3848
What's in the repo
3949
------------------
4050

4151
| File | Purpose |
4252
|---------------------------------------|------------------------------------------------------|
43-
| `packages.yaml` | The list of packages tested + `python_versions` to test against. |
44-
| `astropy_integration/run.py` | Runs one or more (variant, python) combos: resolve specs, install, test, write `results/<variant>__<python>.json`. |
53+
| `packages.yaml` | The config: `core_package`, the `columns` to test, and the `packages` list. |
54+
| `astropy_integration/config.py` | Loads and validates `packages.yaml` (shared by `run` and `dashboard`). |
55+
| `astropy_integration/run.py` | Runs one or more columns: resolve specs, install, test, write `results/<variant>__<python>.json`. |
4556
| `astropy_integration/dashboard.py` | Reads `results/*.json`, renders `site/index.html` (single self-contained page). |
4657
| `astropy_integration/cli.py` | Console entry point that dispatches the `run` and `dashboard` subcommands. |
4758
| `astropy_integration/status.py` | Shared status vocabulary (used by both `run` and `dashboard`). |
4859
| `astropy_integration/templates/` | HTML/CSS for the dashboard (loaded as package data). |
4960
| `pyproject.toml` | Package metadata; declares the `astropy-integration` console script. |
5061
| `conftest.py` | Repo-root pytest plugin that caps each package to the first `PYTEST_LIMIT_N` tests for PR previews. |
51-
| `.github/workflows/integration.yml` | The matrix workflow (variant x python + dashboard). |
62+
| `.github/workflows/integration.yml` | The matrix workflow (`setup` builds the matrix, `variant` runs each column, `dashboard` publishes). |
5263
| `.github/workflows/preview-link.yml` | Companion that posts the "View dashboard preview" status check on PRs. |
5364
| `sunpy_pytest.ini` | Custom pytest config referenced by sunpy's `pytest_args` (sunpy's own config requires plugins we don't install). |
5465

@@ -78,23 +89,49 @@ python -m http.server -d site 8000
7889
Results land in `results/<variant>__<python>.json`; the dashboard in
7990
`site/`. Both directories are gitignored.
8091

81-
Python versions
82-
---------------
92+
Core package
93+
------------
94+
95+
`packages.yaml` has a top-level `core_package` block — the package
96+
installed into the shared venv before everything else:
97+
98+
```yaml
99+
core_package:
100+
pypi_name: astropy
101+
module: astropy
102+
repo_url: https://github.com/astropy/astropy.git
103+
# dev variant: install nightly wheels from these indexes.
104+
# omit to install the dev version from git+repo_url instead.
105+
dev_index_urls:
106+
- https://pypi.anaconda.org/astropy/simple
107+
- https://pypi.anaconda.org/liberfa/simple
108+
```
109+
110+
To retarget the harness at a different ecosystem, point `core_package`
111+
at that ecosystem's core package and replace the `packages` list.
112+
`dashboard_title` (also top-level) sets the dashboard's heading.
113+
114+
Columns
115+
-------
83116

84-
`packages.yaml` has a top-level `python_versions` list (uv notation,
85-
so `"3.14t"` means the free-threaded 3.14 build):
117+
`packages.yaml` has a top-level `columns` list. Each column is one
118+
(Python version, variant) pair and becomes one dashboard column;
119+
Python version and variant are independent, so list whatever
120+
combinations you want (uv notation for Python, so `"3.14t"` is the
121+
free-threaded 3.14 build):
86122

87123
```yaml
88-
python_versions:
89-
- "3.12"
90-
- "3.14t"
124+
columns:
125+
- {python: "3.12", variant: stable}
126+
- {python: "3.13", variant: pre}
127+
- {python: "3.14t", variant: dev}
91128
```
92129

93-
The runner tests every (variant x python_version) combination. The
94-
dashboard renders Python versions as grouped header columns above the
95-
three variants. **Keep the `matrix.python` list in
96-
`.github/workflows/integration.yml` in sync** with this — the CI uses
97-
its own matrix because GitHub Actions can't read it from YAML directly.
130+
The runner tests every column; `--variant` / `--python` narrow that to
131+
a subset. The dashboard groups consecutive columns that share a Python
132+
version under a spanning header, so a classic `python x variant`
133+
layout still renders as grouped columns. The CI matrix is generated
134+
from this list by the `setup` job, so there's nothing to keep in sync.
98135

99136
Adding or disabling a package
100137
-----------------------------
@@ -118,15 +155,15 @@ Triggering a run from GitHub
118155

119156
1. Actions tab -> `integration-matrix` workflow.
120157
2. "Run workflow" dropdown -> green button.
121-
3. The matrix expands to `len(variants) x len(python_versions)`
122-
parallel jobs; the `dashboard` job waits for them and publishes
123-
to `gh-pages`.
158+
3. The `setup` job reads `columns:` from `packages.yaml` and the
159+
matrix expands to one parallel `variant` job per column; the
160+
`dashboard` job waits for them and publishes to `gh-pages`.
124161

125162
PR previews
126163
-----------
127164

128-
`integration-matrix` also runs on pull requests. Same three-variant
129-
matrix as the scheduled run, just with a different final step: the
165+
`integration-matrix` also runs on pull requests. Same column matrix
166+
as the scheduled run, just with a different final step: the
130167
`dashboard` job uploads `site/index.html` as a non-zipped artifact
131168
(`actions/upload-artifact@v7` with `archive: false`) instead of
132169
publishing to gh-pages. The companion `preview-link` workflow

astropy_integration/config.py

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
"""Load and validate the integration-testing config (``packages.yaml``).
2+
3+
The config has three top-level keys:
4+
5+
- ``core_package``: the ecosystem's core package (astropy here, but
6+
swap it to retarget the whole harness at another ecosystem). It is
7+
installed into the shared venv before anything else.
8+
- ``columns``: a flat list of ``{python, variant}`` pairs. Each one
9+
is a single run and a single dashboard column; Python version and
10+
variant are fully decoupled.
11+
- ``packages``: the per-package list (one entry = one dashboard row).
12+
13+
Both ``run`` and ``dashboard`` read this file, so the parsing and
14+
validation live here in one place.
15+
"""
16+
17+
from pathlib import Path
18+
19+
import yaml
20+
21+
from . import status
22+
23+
24+
def _load(path):
25+
return yaml.safe_load(Path(path).read_text()) or {}
26+
27+
28+
def load_packages(path):
29+
return list(_load(path).get("packages", []))
30+
31+
32+
def load_core_package(path):
33+
"""Return the validated ``core_package`` block.
34+
35+
``module`` defaults to ``pypi_name`` when not given.
36+
"""
37+
core = _load(path).get("core_package") or {}
38+
if not core.get("pypi_name"):
39+
raise ValueError(f"{path}: 'core_package.pypi_name' is required")
40+
core.setdefault("module", core["pypi_name"])
41+
return core
42+
43+
44+
def load_columns(path):
45+
"""Return the validated ``columns`` list as ``[{python, variant}, ...]``.
46+
47+
Order is preserved: it drives both the run order and the dashboard
48+
column order.
49+
"""
50+
raw = _load(path).get("columns") or []
51+
columns = []
52+
for i, col in enumerate(raw):
53+
python = col.get("python")
54+
variant = col.get("variant")
55+
if not python or not variant:
56+
raise ValueError(f"{path}: columns[{i}] needs both 'python' and 'variant'")
57+
if variant not in status.VARIANTS:
58+
raise ValueError(
59+
f"{path}: columns[{i}] has variant '{variant}'; "
60+
f"expected one of {', '.join(status.VARIANTS)}"
61+
)
62+
columns.append({"python": str(python), "variant": variant})
63+
if not columns:
64+
raise ValueError(f"{path}: at least one entry under 'columns' is required")
65+
return columns
66+
67+
68+
def load_dashboard_title(path):
69+
return _load(path).get("dashboard_title") or "Ecosystem integration matrix"

0 commit comments

Comments
 (0)