Skip to content
Open
Show file tree
Hide file tree
Changes from 33 commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
ba3ce17
feat: release_schedule workflow
savente93 Aug 5, 2025
30e7fa7
feat: have action.yml update pixi environments
savente93 Aug 5, 2025
0cdba7a
docs: Update README
savente93 Aug 5, 2025
2a2b9c0
fix: remove now defunct test_action.yaml
savente93 Aug 5, 2025
e6912aa
feat: add python module with helper functions and tests
savente93 Aug 11, 2025
7fee19f
feat: add main update script
savente93 Aug 11, 2025
62b17f2
feat: add workflow to run tests of python code
savente93 Aug 11, 2025
7ae79dc
fix: update release_schedule.yaml to use new env setup
savente93 Aug 11, 2025
9cc5d79
fix: update action.yaml to use new python code
savente93 Aug 11, 2025
190e210
doc: Update README.md
savente93 Aug 11, 2025
8319244
fix: update lockfile
savente93 Aug 11, 2025
a5894a9
fix: allow dependency groups in pep dependencies
savente93 Aug 14, 2025
221ece3
fix: use actual input variable
savente93 Aug 14, 2025
f027d82
fix: don't commit schedule.json
savente93 Aug 14, 2025
2b3570f
fix: test pep dependency with extras
savente93 Aug 14, 2025
52acc1b
fix: rename to spec0-action
savente93 Aug 15, 2025
4cf09ab
fix: fix pyproject.toml
savente93 Aug 15, 2025
890e1fa
fix: readme typo
savente93 Aug 15, 2025
fd06db1
Merge branch 'main' into feat/python
savente93 Aug 15, 2025
0e0cad5
fix: rename imports
savente93 Aug 15, 2025
17d74fa
fix: name issue in workflow
savente93 Aug 15, 2025
943e9a0
feat: make action test itself
savente93 Aug 15, 2025
8359b1f
fix: make schedule_path configurable so we can test it
savente93 Aug 15, 2025
533ecca
fix: import
savente93 Aug 15, 2025
4885cc3
fix: pass schedule_path to script
savente93 Aug 15, 2025
a103c2f
fix: fix pr step conditional
savente93 Aug 15, 2025
5764a23
Merge branch 'main' into feat/python
savente93 Aug 18, 2025
11f4297
Apply suggestions from code review
savente93 Aug 20, 2025
1edb4ec
fix: update authors
savente93 Aug 20, 2025
30042b8
fix: release packaging upper pin
savente93 Aug 20, 2025
fa31983
fix: use SP bot as git name
savente93 Aug 20, 2025
cad5872
fix: remove pixi.lock
savente93 Aug 20, 2025
999a1f3
fix: rename udpate script
savente93 Aug 20, 2025
d3f3bb6
fix: revert rename of versions script
savente93 Aug 20, 2025
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
2 changes: 2 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# SCM syntax highlighting & preventing 3-way merges
pixi.lock merge=binary linguist-language=YAML linguist-generated=true
52 changes: 52 additions & 0 deletions .github/workflows/release_schedule.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
name: Generate release schedule artifacts
on:
schedule:
# At 00:00 on day-of-month 1 in every 3rd month. (i.e. every quarter)
- cron: "0 0 1 */3 *"
# On demand
workflow_dispatch:

jobs:
create-artifacts:
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: Checkout
uses: actions/checkout@v4
with:
# We're going to make a tag that we can we release so we'll need the full history for that
fetch-depth: 0
- name: Set up Python
uses: actions/setup-python@v5

- uses: prefix-dev/[email protected]
with:
pixi-version: "v0.49.0"
environments: schedule
- name: Run spec_zero_versions.py
run: |
pixi run -e schedule generate_schedule
- name: setup git
run: |
# git will complain if we don't do this first
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"

- name: determine tag name
id: tag_name
run: |
echo "TAG_NAME=$(date '+%Y-Q%q')" >> "$GITHUB_OUTPUT"

- name: Publish github release
uses: softprops/action-gh-release@v2
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
generate_release_notes: true
tag_name: ${{ steps.tag_name.outputs.TAG_NAME }}
make_latest: true
files: |
schedule.md
chart.md
schedule.json
22 changes: 22 additions & 0 deletions .github/workflows/test.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
name: Run the update test suite
on:
push:
branches: main
pull_request:
branches: main
# On demand
workflow_dispatch:

jobs:
run-tests:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4

- uses: prefix-dev/[email protected]
with:
pixi-version: "v0.49.0"
environments: dev
- run: |
pixi run test
28 changes: 6 additions & 22 deletions .github/workflows/test_action.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,30 +4,14 @@ on: [push, pull_request]
jobs:
generate_data:
runs-on: ubuntu-latest
name: Generate version data
name: Update test project file
steps:
- name: Checkout
uses: actions/checkout@v5
- name: Generate version data using local action
uses: ./
- name: Check file contents
run: |
printf "Contents of chart.md:\n"
cat chart.md
printf "\n\n"
printf "Contents of schedule.json:\n"
cat schedule.json
printf "\n\n"
printf "Contents of schedule.md:\n"
cat schedule.md
printf "\n\n"
- name: Remove generated files
run: |
printf "Removing generated files...\n"
rm -f chart.md schedule.json schedule.md
ls -R
- uses: actions/download-artifact@v5
with:
name: spec-zero-versions
- name: Display structure of downloaded files
run: ls -R
with:
project_file_name: tests/test_data/pyproject.toml
create_pr: false
schedule_path: tests/test_data/test_schedule.json

8 changes: 5 additions & 3 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
.history
chart.md
schedule.md
schedule.json
# pixi environments
.pixi/*
!.pixi/config.toml
pixi.lock
__pycache__
74 changes: 60 additions & 14 deletions action.yaml
Original file line number Diff line number Diff line change
@@ -1,26 +1,72 @@
name: "Generate SPEC-0000 Data"
description: "Based on the current SPEC 0 schedule, generate a tarball with the latest versions of all packages."
author: Scientific Python Developers
inputs:
target_branch:
description: "Target branch for the pull request"
required: true
default: "main"
project_file_name:
description: "The filename to the project file that lists your dependencies, relative to the repository root. Defaults to 'pyproject.toml' Curretnly only pyproject.toml is supported but others may be added."
required: true
default: "pyproject.toml"
create_pr:
description: "Whether the action should open a PR or not. If this is set to false, this action should have no effect. This is mainly useful for testing"
required: true
default: true
schedule_path:
description: "Path to the schedule.json file. If it does not exist yet, it will be downloaded from the spec0-action repository latest release"
default: schedule.json
token:
description: "GitHub token with repo permissions to create pull requests"
required: true

runs:
using: "composite"
steps:
- name: Set up Python
uses: actions/setup-python@v5
- name: Checkout code
uses: actions/checkout@v4
with:
python-version: "3.13"
- name: Install dependencies
fetch-depth: 0

- name: Set up Git
shell: bash
run: |
git config user.name "Scientific Python [bot]"
git config user.email "[email protected]"

- name: Download schedule artifact
shell: bash
env:
SCHEDULE_FILE: ${{ inputs.schedule_path }}
run: |
pip install -r requirements.txt
- name: Run spec_zero_versions.py
if [ ! -f "$SCHEDULE_FILE" ]; then
gh release download -R "scientific-python/spec0-action" --pattern schedule.json -O "$SCHEDULE_FILE"
fi

- uses: prefix-dev/[email protected]
name: Setup pixi
with:
environments: >-
update
activate-environment: update
pixi-version: v0.49.0
manifest-path: ${{github.action_path}}/pyproject.toml

- name: Run Update script
shell: bash
run: |
python spec_zero_versions.py
- name: Upload files as an artifact
uses: actions/upload-artifact@v4
python ${{github.action_path}}/spec0-update.py "${{github.workspace}}/${{inputs.project_file_name}}" "${{inputs.schedule_path}}"
# let's cleanup after ourselves so it doesn't end up in the PR
rm "${{inputs.schedule_path}}"

- name: Create Pull Request
if: ${{ inputs.create_pr == 'true' }}
uses: peter-evans/create-pull-request@v6
with:
name: spec-zero-versions
path: |
schedule.json
schedule.md
chart.md
token: ${{ inputs.token }}
commit-message: "chore: Drop support for unsupported packages conform SPEC 0"
title: "Drop support for unsupported packages conform SPEC 0"
body: "This PR was created automatically"
base: ${{ inputs.target_branch }}
branch: update-spec-0-dependencies-${{ github.run_id }}
File renamed without changes.
38 changes: 38 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
[project]
authors = [{ name = "Scientific Python Developers" }]
name = "spec0-action"
requires-python = ">= 3.11"
version = "0.1.0"
dependencies = ["packaging>=25.0"]

[build-system]
build-backend = "hatchling.build"
requires = ["hatchling"]

[tool.pixi.workspace]
channels = ["conda-forge"]
platforms = ["linux-64"]

[tool.pixi.feature.update.pypi-dependencies]
spec0-action = { path = ".", editable = true }

[tool.pixi.feature.test.tasks]
test = { cmd = ["pytest", "-vvv"] }

[tool.pixi.feature.schedule.tasks]
generate-schedule = { cmd = ["python", "generate_schedule.py"] }

[tool.pixi.feature.test.dependencies]
pytest = "*"

[tool.pixi.feature.update.dependencies]
tomlkit = ">=0.13.3,<0.14"

[tool.pixi.feature.schedule.dependencies]
pandas = "*"
requests = "*"

[tool.pixi.environments]
dev = ["test", "schedule", "update"]
schedule = ["schedule"]
update = ["update"]
80 changes: 34 additions & 46 deletions readme.md
Original file line number Diff line number Diff line change
@@ -1,63 +1,51 @@
# SPEC-0 Versions Action

This repository contains a GitHub Action to generate the files required for the SPEC-0 documentation.
This repository contains a Github Action to update Python dependencies such that they conform to the SPEC 0 support schedule.
It also contains released versions of the schedule in various formats that that action can use to open PRs in your repository.

## Using the action

To use the action you can copy the yaml below, and paste it into `.github/workflows/update-spec-0.yaml`.
The arguments below are filled with their default value, in most cases you won't have to fill them.
All except for `token` are optional.

Whenever the action is triggered it will open a PR in your repository that will update the dependencies of SPEC 0 to the new lower bound.
For this you will have to provide it with a PAT that has write permissions in the `contents` and `pull request` scopes.
Please refer to the GitHub documentation for instructions on how to do this [here](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens).


```yaml
name: Generate spec-zero data
name: Update SPEC 0 dependencies

on:
push:
branches:
- main
workflow_dispatch:
schedule:
# At 00:00 on day-of-month 2 in every 3rd month. (i.e. every quarter)
# Releases should happen on the first day of the month in scientific-python/spec-zero-tools
# so allow one day as a buffer to avoid timing issues
- cron: "0 0 2 */3 *"

permissions:
contents: write
pull-requests: write

jobs:
devstats-query:
update:
runs-on: ubuntu-latest
steps:
- uses: scientific-python/spec0-action@main
- uses: scientific-python/spec0-action@v1
with:
token: ${{ secrets.GH_PAT }}
project_file_name: "pyproject.toml"
target_branch: 'main'
```

The above would produce an artifact named `spec-zero-versions`, the following files: `schedule.yaml`,`schedule.md` and `chart.md`.

To help projects stay compliant with SPEC-0, we provide a `schedule.json` file that can be used by CI systems to determine new version boundaries.
The structure of the file is as follows:
It should update any of the packages listed in the `dependency`, or `tool.pixi.*` tables.

```json
[
{
"start_date": "iso8601_timestamp",
"packages": {
"package_name": "version"
}
}
]
```
## Limitations

All information in the json file is in a string format that should be easy to use.
The date is the first timestamp of the relevant quarter.
Thus a workflow for using this file could be:
1. Since this action simply parses the toml to do the upgrade and leaves any other bounds intact, it is possible that the environment of the PR becomes unsolvable.
For example if you have a numpy dependency like so: `numpy = ">=1.25.0,<2"` this will get updated in the PR to `numpy = "2.0.0,<2"` which is infeasible.
Keeping the resulting environment solvable is outside the scope of this action, so you might have to be adjusted manually.
2. Currently only `pyproject.toml` is supported by this action, though other manifest files could be considered upon request.

1. Fetch `schedule.json`
2. Determine maximum date that is smaller than current date
3. Update packages listed with new minimum versions

You can obtain the new versions you should set by using this `jq` expression:

```sh
jq 'map(select(.start_date |fromdateiso8601 |tonumber < now))| sort_by("start_date") | reverse | .[0].packages ' schedule.json
```

If you use a package manager like pixi you could update the dependencies with a bash script like this (untested):

```sh
curl -Ls -o schedule.json https://raw.githubusercontent.com/scientific-python/specs/main/spec-0000/schedule.json
for line in $(jq 'map(select(.start_date |fromdateiso8601 |tonumber < now))| sort_by("start_date") | reverse | .[0].packages | to_entries | map(.key + ":" + .value)[]' --raw-output schedule.json); do
package=$(echo "$line" | cut -d ':' -f 1)
version=$(echo "$line" | cut -d ':' -f 2)
if pixi list -x "^$package" &>/dev/null| grep "No packages" -q; then
pixi add "$package>=$version";
fi
done
```
3 changes: 0 additions & 3 deletions requirements.txt

This file was deleted.

1 change: 1 addition & 0 deletions schedule.json
Copy link
Member

Choose a reason for hiding this comment

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

Why is this committed to the repo?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Because it has to be part of the release that the action will check against. We could generate the schedule from scratch every time, but then you lose a paper trail of what was actually in it, so I personally think this solution is better.

Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
[{"packages": {"ipython": "8.8.0", "numpy": "1.25.0", "python": "3.11", "scikit-learn": "1.3.0", "xarray": "2023.1.0"}, "start_date": "2024-10-01T00:00:00Z"}, {"packages": {"ipython": "8.13.0", "matplotlib": "3.8.0", "networkx": "3.1", "scikit-image": "0.21.0", "scipy": "1.11.0", "xarray": "2023.4.0", "zarr": "2.15.0"}, "start_date": "2025-01-01T00:00:00Z"}, {"packages": {"ipython": "8.15.0", "networkx": "3.2", "numpy": "1.26.0", "pandas": "2.1.0", "scikit-image": "0.22.0", "scikit-learn": "1.4.0", "scipy": "1.12.0", "xarray": "2023.7.0", "zarr": "2.16.0"}, "start_date": "2025-04-01T00:00:00Z"}, {"packages": {"ipython": "8.17.0", "matplotlib": "3.9.0", "numpy": "2.0.0", "pandas": "2.2.0", "xarray": "2023.10.0", "zarr": "2.17.0"}, "start_date": "2025-07-01T00:00:00Z"}, {"packages": {"ipython": "8.20.0", "networkx": "3.3", "python": "3.12", "scikit-image": "0.23.0", "xarray": "2024.1.0"}, "start_date": "2025-10-01T00:00:00Z"}, {"packages": {"ipython": "8.24.0", "pandas": "2.3.0", "scikit-learn": "1.5.0", "scipy": "1.13.0", "xarray": "2024.5.0", "zarr": "2.18.0"}, "start_date": "2026-01-01T00:00:00Z"}, {"packages": {"ipython": "8.27.0", "matplotlib": "3.10.0", "networkx": "3.4", "numpy": "2.1.0", "scikit-image": "0.25.0", "scikit-learn": "1.6.0", "scipy": "1.15.0", "xarray": "2024.7.0", "zarr": "3.0.0"}, "start_date": "2026-04-01T00:00:00Z"}, {"packages": {"ipython": "8.28.0", "numpy": "2.2.0", "xarray": "2024.10.0"}, "start_date": "2026-07-01T00:00:00Z"}, {"packages": {"ipython": "8.32.0", "networkx": "3.5", "numpy": "2.3.0", "python": "3.13", "scikit-learn": "1.7.0", "xarray": "2025.1.0"}, "start_date": "2026-10-01T00:00:00Z"}, {"packages": {"ipython": "9.1.0", "scipy": "1.16.0", "xarray": "2025.4.0", "zarr": "3.1.0"}, "start_date": "2027-01-01T00:00:00Z"}, {"packages": {"ipython": "9.4.0", "xarray": "2025.7.0"}, "start_date": "2027-04-01T00:00:00Z"}]
28 changes: 28 additions & 0 deletions spec0-update.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
from spec0_action import update_pyproject_toml, read_toml, write_toml, read_schedule
from pathlib import Path
from argparse import ArgumentParser


if __name__ == '__main__':

parser = ArgumentParser(
prog='spec_zero_update',
description='A script to update your project dependencies to be in line with the scientific python SPEC 0 support schedule',
)

parser.add_argument('toml_path', default="pyproject.toml", help="Path to the project file that lists the dependencies. defaults to 'pyproject.toml'.")
parser.add_argument('schedule_path', default="schedule.json", help="Path to the schedule json payload. defaults to 'schedule.json'")

args = parser.parse_args()

toml_path = Path(args.toml_path)
schedule_path = Path(args.schedule_path)

if not toml_path.exists():
raise ValueError(f"{toml_path} was supplied as path to project file but it did not exist")

project_data = read_toml(toml_path)
schedule_data = read_schedule(schedule_path)
update_pyproject_toml(project_data, schedule_data)

write_toml(toml_path, project_data)
Loading