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 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 42a5162..1a62ccf 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,106 +23,12 @@ jobs: steps: - name: clone the repository uses: actions/checkout@v6 - - name: setup python - uses: actions/setup-python@v6 + with: + persist-credentials: false + - 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 - - 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 - - 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} - run: | - if [[ "${{ steps.action-run.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 - # unexpected failure - echo "workflow failed" - export STATUS=2 - elif [[ "${{ steps.action-run.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 - # expected failure - echo "workflow xfailed" - export STATUS=0 - else - # cancelled - echo "workflow cancelled" - export STATUS=3 - fi - exit $STATUS + uv run -m pytest -rf --cov=minimum_versions 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 diff --git a/.gitignore b/.gitignore index 644c670..4990b75 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,5 @@ __pycache__/ .coverage + +uv.lock diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index e9276e8..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"] @@ -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 diff --git a/README.md b/README.md index 6c1a61f..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,94 +50,66 @@ them to an empty mapping or sequence, respectively: ignored_violations: [] ``` -Then add a new step to CI. +### channels + +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 analyze conda environments, simply pass the path to the environment file (`env.yaml`) to the `environments` key. +To validate a `conda` environment file, run: + +```sh +minimum-versions validate --policy ./policy.yaml path/to/env1.yaml +``` -The conda environment file _must_ specify exactly the `conda-forge` channel. +or with an explicit prefix -```yaml -jobs: - my-job: - ... - steps: - ... - - uses: xarray-contrib/minimum-dependency-versions@version - with: - policy: policy.yaml - environments: path/to/env.yaml +```sh +minimum-versions validate --policy ./policy.yaml conda:path/to/env1.yaml ``` -To analyze multiple environments at the same time, pass a multi-line string: +We can also validate multiple files at the same time: -```yaml -jobs: - my-job: - ... - steps: - ... - - - uses: xarray-contrib/minimum-dependency-versions@version - with: - environments: | - path/to/env1.yaml - path/to/env2.yaml - conda:path/to/env3.yaml # the conda: prefix is optional +```sh +minimum-versions validate --policy .../policy.yaml .../env1.yaml .../env2.yaml ``` ### pixi -To analyze pixi environments, specify the environment name prefixed with `pixi:` and point to the manifest file using `manifest-path`. +To validate `pixi` environments, we need to specify the `manifest-path` (usually either `pyproject.toml` or `pixi.toml`): -Any environment must pin the dependencies, which must be exact pins (i.e. `x.y.*` or `>=x.y.0,` is the name of the environment. -```yaml -jobs: - my-job: - ... - steps: - ... - - - uses: xarray-contrib/minimum-dependency-versions@version - with: - environments: | - pixi:env1 - pixi:env2 - manifest-path: /path/to/pixi.toml # or pyproject.toml +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 ``` -### Mixing environment types +### time travel support -It is even possible to mix environment types (once again, the `conda:` prefix is optional but recommended): +To check how validation would look at a certain point in time, use the `--today` option: -```yaml -jobs: - my-job: - ... - steps: - ... - - - uses: xarray-contrib/minimum-dependency-versions@version - with: - environments: | - pixi:env1 - conda:path/to/env.yaml - manifest-path: path/to/pixi.toml # or pyproject.toml +```sh +minimum-versions validate --policy ./policy.yaml ./env1.yaml --today 2025-10-01 ``` 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 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 0e241db..01c4dfe 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,22 +1,26 @@ [project] -name = "minimum-dependency-versions" +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" 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"] +[project.scripts] +minimum-versions = "minimum_versions.main:main" + [project.urls] Repository = "https://github.com/xarray-contrib/minimum-dependency-versions" @@ -36,7 +40,7 @@ only-include = ["minimum_versions"] only-include = ["minimum_versions"] [tool.ruff] -target-version = "py39" +target-version = "py311" builtins = ["ellipsis"] exclude = [ ".git", @@ -80,3 +84,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", +]