Skip to content
Merged
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
27 changes: 5 additions & 22 deletions .github/workflows/test_action.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,30 +4,13 @@ on: [push, pull_request]
jobs:
generate_data:
runs-on: ubuntu-latest
name: Generate version data
name: Run action on test file in repo
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@v6
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
21 changes: 21 additions & 0 deletions .github/workflows/test_bench.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
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@v5

- uses: prefix-dev/[email protected]
with:
pixi-version: "v0.49.0"
- run: |
pixi run test
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,6 @@
chart.md
schedule.md
schedule.json
__pycache__
*.pyc
*.lock
104 changes: 88 additions & 16 deletions action.yaml
Original file line number Diff line number Diff line change
@@ -1,26 +1,98 @@
name: "Generate SPEC-0000 Data"
description: "Based on the current SPEC 0 schedule, generate a tarball with the latest versions of all packages."
name: "Update SPEC 0 dependencies"
description: "Update the lower bounds of Python dependencies covered by the Scientific Python SPEC 0 support schedule"
author: Scientific Python Developers
inputs:
target_branch:
description: "Target branch for the pull request"
required: true
default: "main"
project_file_name:
description: "Path to the project file listing dependencies, relative to repository root. Defaults to 'pyproject.toml'. Currently only pyproject.toml is supported."
required: true
default: "pyproject.toml"
create_pr:
description: "Whether the action should open a PR or not. Set to false for dry-run/testing."
required: true
default: true
commit_msg:
description: "Commit message for the commit to update the versions. by default 'Drop support for unsupported packages conform SPEC 0'. has no effect if `create_pr` is set to false"
required: false
default: "chore: Drop support for unsupported packages conform SPEC 0"
pr_title:
description: "The title of the PR that will be opened. by default 'Drop support for unsupported packages conform SPEC 0'. has no effect if `create_pr` is set to false"
required: false
default: "chore: Drop support for unsupported packages conform SPEC 0"
schedule_path:
description: "Path to the schedule.json file relative to the project root. If missing, it will be downloaded from the latest release of savente93/SPEC0-schedule"
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@v6
- name: Checkout code
uses: actions/checkout@v5

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

- uses: prefix-dev/[email protected]
name: Setup Pixi
with:
python-version: "3.13"
- name: Install dependencies
pixi-version: v0.49.0
manifest-path: ${{ github.action_path }}/pyproject.toml

- name: Regenerate schedule file if necessary
shell: bash
env:
SCHEDULE_FILE: ${{ inputs.schedule_path }}
GH_TOKEN: ${{ inputs.token }}
run: |
set -e
if [ ! -f "${{ github.workspace }}/$SCHEDULE_FILE" ]; then
echo "Regenerating schedule.json..."
pixi run generate-schedule --locked
if diff -q schedule.json "${{ github.workspace }}/$SCHEDULE_FILE" >/dev/null; then
echo "Source and destination have identical contents – nothing to move."
else
mv schedule.json "${{ github.workspace }}/$SCHEDULE_FILE"
fi
else
echo "Schedule file already exists at $SCHEDULE_FILE"
fi


- name: Run update script
shell: bash
run: |
pip install -r requirements.txt
- name: Run spec_zero_versions.py
set -e
echo "Updating ${{inputs.project_file_name}} using schedule ${{inputs.schedule_path}}"
pixi run --manifest-path ${{ github.action_path }}/pyproject.toml update-dependencies "${{ github.workspace }}/${{ inputs.project_file_name }}" "${{ github.workspace }}/${{ inputs.schedule_path }}"

- name: Show changes (dry-run)
if: ${{ inputs.create_pr != 'true' }}
shell: bash
run: |
python spec_zero_versions.py
- name: Upload files as an artifact
uses: actions/upload-artifact@v5
echo "Dry run: showing changes that would be committed"
git --no-pager diff ${{ inputs.project_file_name }}

- name: Create Pull Request
if: ${{ inputs.create_pr == 'true' }}
uses: peter-evans/create-pull-request@v7
with:
name: spec-zero-versions
path: |
schedule.json
schedule.md
chart.md
token: ${{ inputs.token }}
commit-message: ${{ inputs.commit_msg }}
path: ${{ inputs.project_file_name }}
title: ${{ inputs.pr_title }}
body: "This PR was created automatically"
base: ${{ inputs.target_branch }}
branch: update-spec0-dependencies-${{ github.run_id }}

branding:
icon: 'check-square'
color: 'blue'
33 changes: 33 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
[project]
authors = [{ name = "Scientific Python Developers" }]
name = "spec0-action"
description = "Python code to update the lower bounds of Scientific Python libraries according to SPEC 0"
requires-python = ">= 3.11"
version = "1.0.0"
dependencies = ["packaging>=25.0", "pandas>=2.3.3", "requests>=2.32.5,<3"]

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

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

[tool.pixi.pypi-dependencies]
spec0-action= { path = ".", editable = true }
tomlkit = ">=0.13.3,<0.14"

[tool.pixi.tasks]
update-dependencies = { cmd = ["python", "run_spec0_update.py"] }
generate-schedule = { cmd = ["python", "spec0_versions.py"] }

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

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

[tool.pixi.environments]
test = ["test"]

85 changes: 42 additions & 43 deletions readme.md
Original file line number Diff line number Diff line change
@@ -1,63 +1,62 @@
# SPEC-0 Versions Action
# 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 in your `pyproject.toml` such that they conform to the SPEC 0 support schedule. You can find this schedule [here](https://scientific-python.org/specs/spec-0000/)

## Using the action


### Example workflow

To use the action you can copy the yaml below, and paste it into `.github/workflows/update-spec0.yaml`. 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
schedule:
# At 00:00 on day-of-month 3 in every 3rd month. (i.e. every quarter)
# Releases should happen on the second day of the quarter in savente93/SPEC0-schedule to
# avoid fence post errors, so allow one day as a buffer to avoid timing issues here as well.
- cron: "0 0 3 */3 *"
# On demand:
workflow_dispatch:

permissions:
contents: write
pull-requests: write

jobs:
devstats-query:
update:
runs-on: ubuntu-latest
steps:
- uses: scientific-python/spec0-action@main
- uses: savente93/[email protected]
with:
token: ${{ secrets.GH_PAT }} # <- GH_PAT you will have to configure in the repo as a secret
```

The above would produce an artifact named `spec-zero-versions`, the following files: `schedule.yaml`,`schedule.md` and `chart.md`.
It should update any of the packages listed in the `dependency`, or `tool.pixi.*` tables. For examples of before and after you can see [./tests/test_data/pyproject.toml](./tests/test_data/pyproject.toml) and [./tests/test_data/pyproject_updated.toml](./tests/test_data/pyproject_updated.toml) respectively. Other tools are not yet supported, but I am open to feature requests.

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:
The newest lower bounds will be downloaded from [https://github.com/savente93/SPEC0-schedule](https://github.com/savente93/SPEC0-schedule) but you should not have to worry about this.

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

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:
### Parameters

1. Fetch `schedule.json`
2. Determine maximum date that is smaller than current date
3. Update packages listed with new minimum versions
| Input | Required | Default | Description |
| ------------------- | -------- | ------------------ | -------------------------------------------------------------------------------|
| token | yes | — | Personal access token with `contents` & `pull-request` scopes |
| project\_file\_name | no | `"pyproject.toml"` | File to update dependencies in |
| schedule\_path | no | `"schedule.json"` | path to schedule json data. only relevant if you have it committed in your repo |
| target\_branch | no | `"main"` | Branch to open PR against |
| create_pr | no | `true` | Open a PR with new versions |
| pr_title | no | `chore: Drop support for unsupported packages conform SPEC 0` | The title of the PR that will be opened |
| commit_msg | no | `chore: Drop support for unsupported packages conform SPEC 0` | Commit message of the commit to update the 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
```
## Limitations

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 adjust them manually.
2. Currently only `pyproject.toml` is supported by this action, though other manifest files could be considered upon request.

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.

29 changes: 29 additions & 0 deletions run_spec0_update.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
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(
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")

if not schedule_path.exists():
raise ValueError(f"{schedule_path} was supplied as path to schedule 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