From f75f664f847c763ae6007ebeb28418ed33d81454 Mon Sep 17 00:00:00 2001 From: Justus Magin Date: Sat, 20 Dec 2025 11:17:09 +0100 Subject: [PATCH 01/15] talk only about the package in the readme --- README.md | 103 ++++++++++++++++++++++++++++++++++++++---------------- 1 file changed, 72 insertions(+), 31 deletions(-) diff --git a/README.md b/README.md index 62b3fba..64cffeb 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,18 @@ -# minimum-dependency-versions +# xarray-minimum-dependency-versions -Check that the minimum dependency versions follow `xarray`'s policy. +Check that the minimum dependency versions follow `xarray`'s policy rules. -## Usage +> [!NOTE] +> Be aware that at the moment there is no public python API, so there are no +> stability guarantees. + +## Policy files + +Before we can validate environments we need to create a policy file (named e.g. `policy.yaml`). This allows us to specify the conda channels, the platforms we want to check on, the actual support windows, any overrides, packages that are not supposed to be checked, and allowed violations. -To use the `minimum-dependency-versions` action in workflows, create a policy file (`policy.yaml`): +These windows are checked according to `xarray`'s policy, that is, _a version can be required as soon as it is at least N months old_. As opposed to a fixed support window this means we can never end up requiring a one-month old dependency (or being able to drop all existing versions) for packages with infrequent releases. + +For example: ```yaml channels: @@ -32,8 +40,8 @@ policy: - package4 ``` -If there are no packages with `overrides`, `exclude`, or `ignored_violations`, you can set -them to an empty mapping or sequence, respectively: +If there are no packages with `overrides`, `exclude`, or `ignored_violations`, +you can set them to an empty mapping or sequence, respectively: ```yaml ... @@ -42,33 +50,66 @@ them to an empty mapping or sequence, respectively: ignored_violations: [] ``` -Then add a new step to CI: +### channels -```yaml -jobs: - my-job: - ... - steps: - ... - - uses: xarray-contrib/minimum-dependency-versions@version - with: - policy: policy.yaml - environment-paths: path/to/env.yaml +The names of the conda channels to check. Usually, `conda-forge`. + +### platforms + +The names of the platforms to check. Usually, `noarch` and `linux-64` are sufficient. + +### policy + +The main policy definition. `packages` is a mapping of package names to the number of months, with `default` specifying the default for packages that are not in `packages`. + +Any package listed in `ignored_violations` will show a warning if the policy is violated, but will not count as an error, and it is possible to force a specific version using `overrides`. + +## Usage + +With the policy file, we can check environment files. There are currently two kinds supported: `conda` environment definitions as yaml files, and `pixi` environments. + +Check also `minimum-versions --help` and `minimum-versions validate --help`. + +### conda + +To validate a `conda` environment file, run: + +```sh +minimum-versions validate --policy ./policy.yaml path/to/env1.yaml ``` -To analyze multiple environments at the same time, pass a multi-line string: +or with an explicit prefix -```yaml -jobs: - my-job: - ... - steps: - ... - - - uses: xarray-contrib/minimum-dependency-versions@version - with: - environment-paths: | - path/to/env1.yaml - path/to/env2.yaml - path/to/env3.yaml +```sh +minimum-versions validate --policy ./policy.yaml conda:path/to/env1.yaml +``` + +We can also validate multiple files at the same time: + +```sh +minimum-versions validate --policy .../policy.yaml .../env1.yaml .../env2.yaml +``` + +### pixi + +To validate `pixi` environments, we need to specify the `manifest-path` (usually either `pyproject.toml` or `pixi.toml`): + +```sh +minimum-versions validate --policy ./policy.yaml --manifest-path pixi.toml pixi:test-env +``` + +where `name` in `pixi:` is the name of the environment. + +Again, we can validate multiple environments at once: + +```sh +minimum-versions validate --policy ./policy.yaml --manifest-path pixi.toml pixi:test-env1 pixi:test-env2 +``` + +### time travel support + +To check how validation would look at a certain point in time, use the `--today` option: + +```sh +minimum-versions validate --policy ./policy.yaml ./env1.yaml --today 2025-10-01 ``` From b7c7fec468b49cd5d107e3ed431e18afef07f190 Mon Sep 17 00:00:00 2001 From: Justus Magin Date: Sat, 20 Dec 2025 11:20:29 +0100 Subject: [PATCH 02/15] delete the action --- action.yaml | 53 ----------------------------------------------------- 1 file changed, 53 deletions(-) delete mode 100644 action.yaml diff --git a/action.yaml b/action.yaml deleted file mode 100644 index cf3e5bf..0000000 --- a/action.yaml +++ /dev/null @@ -1,53 +0,0 @@ -name: "minimum-dependency-versions" -description: >- - Check that the minimum dependency versions follow `xarray`'s policy. -inputs: - policy: - description: >- - The path to the policy to follow - required: true - type: string - today: - description: >- - Time machine for testing - required: false - type: string - environments: - description: >- - The names or paths of the environments. Pixi environment names must be - prefixed with `pixi:`. Conda environment paths may be prefixed with - `conda:`. If there is no prefix, it is assumed to be a conda env path. - required: true - type: list - manifest-path: - description: >- - Path to the manifest file of `pixi`. Required for `pixi` environments. - required: false - type: string -outputs: {} -runs: - using: "composite" - - steps: - - name: install dependencies - shell: bash -l {0} - run: | - echo "::group::Install dependencies" - python -m pip install -r ${{ github.action_path }}/requirements.txt - python -m pip install ${{ github.action_path }} - echo "::endgroup::" - - name: analyze environments - shell: bash -l {0} - env: - COLUMNS: 120 - FORCE_COLOR: 3 - POLICY_PATH: ${{ inputs.policy }} - ENVIRONMENTS: ${{ inputs.environments }} - TODAY: ${{ inputs.today }} - MANIFEST_PATH: ${{ inputs.manifest-path }} - run: | - python -m minimum_versions validate \ - --today="$TODAY" \ - --policy="$POLICY_PATH" \ - --manifest-path="$MANIFEST_PATH" \ - $ENVIRONMENTS From 0b9a485a2a8f7b8d13aa94a7a05e85dc29afe65e Mon Sep 17 00:00:00 2001 From: Justus Magin Date: Sat, 20 Dec 2025 11:26:11 +0100 Subject: [PATCH 03/15] lower-pin the versions --- pyproject.toml | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 0e241db..10acc34 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,13 +7,13 @@ license = "Apache-2.0" description = "Validate minimum dependency environments according to xarray's policy scheme" requires-python = ">=3.11" dependencies = [ - "rich", - "rich-click", - "pyyaml", - "cytoolz", - "py-rattler", - "python-dateutil", - "jsonschema", + "rich>=14.2.0", + "rich-click>=1.9.0", + "pyyaml>=6.0.0", + "cytoolz>=1.1.0", + "py-rattler>=0.20.0", + "python-dateutil>=2.9.0", + "jsonschema>=4.25.0", ] dynamic = ["version"] From 55b9f25b8a19841de95540b9d24ea36b2e1b9679 Mon Sep 17 00:00:00 2001 From: Justus Magin Date: Sat, 20 Dec 2025 11:27:28 +0100 Subject: [PATCH 04/15] lint for python 3.11 --- minimum_versions/environments/pixi.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/minimum_versions/environments/pixi.py b/minimum_versions/environments/pixi.py index 60e634a..9508413 100644 --- a/minimum_versions/environments/pixi.py +++ b/minimum_versions/environments/pixi.py @@ -1,7 +1,7 @@ import pathlib import re - import tomllib + from rattler import Version from tlz.dicttoolz import get_in, merge diff --git a/pyproject.toml b/pyproject.toml index 10acc34..e875caf 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -36,7 +36,7 @@ only-include = ["minimum_versions"] only-include = ["minimum_versions"] [tool.ruff] -target-version = "py39" +target-version = "py311" builtins = ["ellipsis"] exclude = [ ".git", From 2bba9df1f3bfc2aa81ceeb5869abcbfd48297aeb Mon Sep 17 00:00:00 2001 From: Justus Magin Date: Sat, 20 Dec 2025 11:30:57 +0100 Subject: [PATCH 05/15] register as a script --- pyproject.toml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index e875caf..99d15df 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -17,6 +17,9 @@ dependencies = [ ] dynamic = ["version"] +[project.scripts] +minimum-versions = "minimum_versions.main:main" + [project.urls] Repository = "https://github.com/xarray-contrib/minimum-dependency-versions" From 1dd69ccb55492c4a14da023259f2e382f87d66f8 Mon Sep 17 00:00:00 2001 From: Justus Magin Date: Sat, 20 Dec 2025 11:44:27 +0100 Subject: [PATCH 06/15] rename the project --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 99d15df..8595a88 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,5 +1,5 @@ [project] -name = "minimum-dependency-versions" +name = "xarray-minimum-dependency-policy" authors = [ { name = "Justus Magin" }, ] From 2f30e0ef0f70d60a556f7e43e5099ccddafa231d Mon Sep 17 00:00:00 2001 From: Justus Magin Date: Sat, 20 Dec 2025 11:44:44 +0100 Subject: [PATCH 07/15] add zizmor --- .pre-commit-config.yaml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index e9276e8..6073ab1 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -29,6 +29,10 @@ repos: rev: v0.24.1 hooks: - id: validate-pyproject + - repo: https://github.com/zizmorcore/zizmor-pre-commit + rev: v1.19.0 + hooks: + - id: zizmor ci: autofix_prs: true From 49e31df3dc9a85e8fe027663394ee6fcb782d8f9 Mon Sep 17 00:00:00 2001 From: Justus Magin Date: Sat, 20 Dec 2025 11:44:52 +0100 Subject: [PATCH 08/15] autoupdate hooks --- .pre-commit-config.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 6073ab1..2f391b0 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -5,16 +5,16 @@ repos: - id: trailing-whitespace - id: end-of-file-fixer - repo: https://github.com/psf/black-pre-commit-mirror - rev: 25.11.0 + rev: 25.12.0 hooks: - id: black - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.14.7 + rev: v0.14.10 hooks: - id: ruff args: ["--fix"] - repo: https://github.com/rbubley/mirrors-prettier - rev: v3.7.3 + rev: v3.7.4 hooks: - id: prettier args: ["--cache-location=.prettier_cache"] From d94ab211157783242aa8d1343c439892501d8b8c Mon Sep 17 00:00:00 2001 From: Justus Magin Date: Sat, 20 Dec 2025 11:47:25 +0100 Subject: [PATCH 09/15] add a cooldown to dependabot --- .github/dependabot.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 65a43de..da7bafb 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -5,3 +5,5 @@ updates: schedule: # Check for updates once a week interval: "weekly" + cooldown: + default-days: 7 From 7dc8c28582e55e7cf9261e745fec772645c3ee59 Mon Sep 17 00:00:00 2001 From: Justus Magin Date: Sat, 20 Dec 2025 11:51:36 +0100 Subject: [PATCH 10/15] follow `zizmor`'s recommendations --- .github/workflows/ci.yml | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 42a5162..38c76ec 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -9,6 +9,8 @@ concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true +permissions: {} + jobs: ci: name: tests @@ -21,6 +23,8 @@ jobs: steps: - name: clone the repository uses: actions/checkout@v6 + with: + persist-credentials: false - name: setup python uses: actions/setup-python@v6 with: @@ -89,6 +93,8 @@ jobs: steps: - name: clone the repository uses: actions/checkout@v6 + with: + persist-credentials: false - name: run action uses: ./ id: action-run @@ -101,20 +107,22 @@ jobs: - name: detect outcome if: always() shell: bash -l {0} + env: + OUTCOME: "${{ steps.action-run.outcome }}" run: | - if [[ "${{ steps.action-run.outcome }}" == "success" && ${{ matrix.expected-failure }} == "true" ]]; then + if [[ "$OUTCOME" == "success" && ${{ matrix.expected-failure }} == "true" ]]; then # unexpected pass echo "workflow xpassed" export STATUS=1 - elif [[ "${{ steps.action-run.outcome }}" == "failure" && ${{ matrix.expected-failure }} == "false" ]]; then + elif [[ "$OUTCOME" == "failure" && ${{ matrix.expected-failure }} == "false" ]]; then # unexpected failure echo "workflow failed" export STATUS=2 - elif [[ "${{ steps.action-run.outcome }}" == "success" && ${{ matrix.expected-failure }} == "false" ]]; then + elif [[ "$OUTCOME" == "success" && ${{ matrix.expected-failure }} == "false" ]]; then # normal pass echo "workflow passed" export STATUS=0 - elif [[ "${{ steps.action-run.outcome }}" == "failure" && ${{ matrix.expected-failure }} == "true" ]]; then + elif [[ "$OUTCOME" == "failure" && ${{ matrix.expected-failure }} == "true" ]]; then # expected failure echo "workflow xfailed" export STATUS=0 From 2a03e507e3b72b5d8a98183428a77bc7677c4195 Mon Sep 17 00:00:00 2001 From: Justus Magin Date: Sat, 20 Dec 2025 11:52:28 +0100 Subject: [PATCH 11/15] remove the e2e tests --- .github/workflows/ci.yml | 92 ---------------------------------------- 1 file changed, 92 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 38c76ec..460702f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -40,95 +40,3 @@ jobs: - name: run tests run: | python -m pytest -rf - - e2e: - name: end-to-end - runs-on: [ubuntu-latest] - - strategy: - fail-fast: false - matrix: - envs: - - "envs/env1.yaml" - - "envs/env2.yaml" - - | - envs/env1.yaml - envs/env2.yaml - expected-failure: ["false"] - policy-file: ["policy.yaml"] - include: - - envs: | - envs/failing-env1.yaml - policy-file: "policy.yaml" - expected-failure: "true" - - envs: | - envs/env1.yaml - envs/failing-env1.yaml - policy-file: "policy.yaml" - expected-failure: "true" - - envs: "envs/env1.yaml" - policy-file: "policy_no_extra_options.yaml" - expected-failure: "false" - - envs: "pixi:env1" - manifest-path: "envs/pixi.toml" - policy-file: "policy.yaml" - expected-failure: "false" - - envs: | - pixi:env1 - pixi:env2 - manifest-path: "envs/pixi.toml" - policy-file: "policy.yaml" - expected-failure: "false" - - envs: | - pixi:env1 - conda:envs/env2.yaml - manifest-path: "envs/pixi.toml" - policy-file: "policy.yaml" - expected-failure: "false" - - envs: "pixi:failing-env" - manifest-path: "envs/pixi.toml" - policy-file: "policy.yaml" - expected-failure: "true" - - steps: - - name: clone the repository - uses: actions/checkout@v6 - with: - persist-credentials: false - - name: run action - uses: ./ - id: action-run - continue-on-error: true - with: - policy: ${{ matrix.policy-file }} - environments: ${{ matrix.envs }} - today: 2024-12-20 - manifest-path: ${{ matrix.manifest-path }} - - name: detect outcome - if: always() - shell: bash -l {0} - env: - OUTCOME: "${{ steps.action-run.outcome }}" - run: | - if [[ "$OUTCOME" == "success" && ${{ matrix.expected-failure }} == "true" ]]; then - # unexpected pass - echo "workflow xpassed" - export STATUS=1 - elif [[ "$OUTCOME" == "failure" && ${{ matrix.expected-failure }} == "false" ]]; then - # unexpected failure - echo "workflow failed" - export STATUS=2 - elif [[ "$OUTCOME" == "success" && ${{ matrix.expected-failure }} == "false" ]]; then - # normal pass - echo "workflow passed" - export STATUS=0 - elif [[ "$OUTCOME" == "failure" && ${{ matrix.expected-failure }} == "true" ]]; then - # expected failure - echo "workflow xfailed" - export STATUS=0 - else - # cancelled - echo "workflow cancelled" - export STATUS=3 - fi - exit $STATUS From bd810fa0319270d85f6e98f6677aa118920da660 Mon Sep 17 00:00:00 2001 From: Justus Magin Date: Sat, 20 Dec 2025 11:58:07 +0100 Subject: [PATCH 12/15] replace `setup-python` + pip with `setup-uv` --- .github/workflows/ci.yml | 14 +++----------- pyproject.toml | 6 ++++++ 2 files changed, 9 insertions(+), 11 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 460702f..1a62ccf 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -25,18 +25,10 @@ jobs: uses: actions/checkout@v6 with: persist-credentials: false - - name: setup python - uses: actions/setup-python@v6 + - name: setup environment + uses: astral-sh/setup-uv@681c641aba71e4a1c380be3ab5e12ad51f415867 # 7.1.6 with: python-version: "${{ matrix.python-version }}" - - name: upgrade pip - run: | - python -m pip install --upgrade pip - - name: install dependencies - run: | - python -m pip install -r requirements.txt - python -m pip install . - python -m pip install pytest - name: run tests run: | - python -m pytest -rf + uv run -m pytest -rf --cov=minimum_versions diff --git a/pyproject.toml b/pyproject.toml index 8595a88..6c2a572 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -83,3 +83,9 @@ branch = true [tool.coverage.report] show_missing = true exclude_lines = ["pragma: no cover", "if TYPE_CHECKING"] + +[dependency-groups] +dev = [ + "pytest>=9.0.2", + "pytest-cov>=7.0.0", +] From 9b2895871c186770680dcfcdef998d39f3cd76c7 Mon Sep 17 00:00:00 2001 From: Justus Magin Date: Sat, 20 Dec 2025 11:59:54 +0100 Subject: [PATCH 13/15] workflow to publish to pypi --- .github/workflows/pypi.yml | 65 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 65 insertions(+) create mode 100644 .github/workflows/pypi.yml diff --git a/.github/workflows/pypi.yml b/.github/workflows/pypi.yml new file mode 100644 index 0000000..05d1c1f --- /dev/null +++ b/.github/workflows/pypi.yml @@ -0,0 +1,65 @@ +name: Build and upload to PyPI + +on: + release: + types: [published] + +permissions: {} + +jobs: + build: + name: Build packages + runs-on: ubuntu-latest + if: github.owner == 'xarray-contrib' + steps: + - name: Checkout + uses: actions/checkout@v5 + with: + fetch-depth: 0 + persist-credentials: false + - name: Set up Python + uses: actions/setup-python@v6 + with: + python-version: "3.x" + - name: Install dependencies + run: | + python -m pip install --upgrade pip + python -m pip install build twine + - name: Build + run: | + python -m build --outdir dist/ . + - name: Check the built archives + run: | + twine check dist/* + pip install dist/*.whl + minimum-versions --help + - name: Upload build artifacts + uses: actions/upload-artifact@v5 + with: + name: packages + path: dist/* + + publish: + name: Upload to PyPI + runs-on: ubuntu-latest + needs: build + if: github.event_name == 'release' + + environment: + name: pypi + url: https://pypi.org/p/xarray-minimum-dependency-policy + permissions: + id-token: write + + steps: + - name: Download build artifacts + uses: actions/download-artifact@v6 + with: + name: packages + path: dist/ + + - name: Publish to PyPI + uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e + with: + verify_metadata: true + verbose: true From d033e99615c3f5be23687939c295e13db60e962a Mon Sep 17 00:00:00 2001 From: Justus Magin Date: Sat, 20 Dec 2025 12:02:02 +0100 Subject: [PATCH 14/15] ignore the lock file --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index 644c670..4990b75 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,5 @@ __pycache__/ .coverage + +uv.lock From f81889f8e33d81a34bab6695a849db67a6cad561 Mon Sep 17 00:00:00 2001 From: Justus Magin Date: Sat, 20 Dec 2025 12:03:48 +0100 Subject: [PATCH 15/15] add a readme --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index 6c2a572..01c4dfe 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,6 +3,7 @@ name = "xarray-minimum-dependency-policy" authors = [ { name = "Justus Magin" }, ] +readme = "README.md" license = "Apache-2.0" description = "Validate minimum dependency environments according to xarray's policy scheme" requires-python = ">=3.11"