From 859a4d3868843e746a4d50ae827928530f93fd66 Mon Sep 17 00:00:00 2001 From: Nico Loesch Date: Thu, 23 Jul 2026 00:43:33 +0000 Subject: [PATCH 1/6] feat(ci): add lowest-direct resolution input + Dependabot template Adds an optional `resolution` input (default `highest`) to build-test.yml and build-test-postgres.yml, forwarded to `uv sync --resolution`, so consumers can add a second CI job verifying that a declared dependency floor is actually sufficient (not just the latest resolvable version). Adds a canonical templates/dependabot.yml scoped to the CAVA sibling packages, pre-labelled `dependencies` so opened PRs flow through the existing label-gate/release-drafter pipeline. Not yet merged to main. Being piloted on a branch against cava-ci-test first per the CAVA dependency-range-bump plan. --- .github/workflows/build-test-postgres.yml | 9 ++++++++- .github/workflows/build-test.yml | 9 ++++++++- docs/workflows/build-test.md | 18 ++++++++++++++++++ templates/dependabot.yml | 17 +++++++++++++++++ 4 files changed, 51 insertions(+), 2 deletions(-) create mode 100644 templates/dependabot.yml diff --git a/.github/workflows/build-test-postgres.yml b/.github/workflows/build-test-postgres.yml index 0316b09..92901d7 100644 --- a/.github/workflows/build-test-postgres.yml +++ b/.github/workflows/build-test-postgres.yml @@ -47,6 +47,13 @@ on: type: string default: 'src/' required: false + resolution: + description: > + uv resolution strategy (highest, lowest, lowest-direct). Use + lowest-direct to verify declared floor versions are sufficient. + type: string + default: 'highest' + required: false jobs: test: @@ -75,7 +82,7 @@ jobs: python-version: ${{ inputs.python-version }} - name: Install dependencies - run: uv sync --all-extras --dev + run: uv sync --resolution ${{ inputs.resolution }} --all-extras --dev - name: Run setup commands if: inputs.setup-commands != '' diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml index 122e440..38526a6 100644 --- a/.github/workflows/build-test.yml +++ b/.github/workflows/build-test.yml @@ -24,6 +24,13 @@ on: type: string default: 'src/' required: false + resolution: + description: > + uv resolution strategy (highest, lowest, lowest-direct). Use + lowest-direct to verify declared floor versions are sufficient. + type: string + default: 'highest' + required: false jobs: test: @@ -38,7 +45,7 @@ jobs: python-version: ${{ inputs.python-version }} - name: Install dependencies - run: uv sync --all-extras --dev + run: uv sync --resolution ${{ inputs.resolution }} --all-extras --dev - name: Run setup commands if: inputs.setup-commands != '' diff --git a/docs/workflows/build-test.md b/docs/workflows/build-test.md index 89c8d15..125306b 100644 --- a/docs/workflows/build-test.md +++ b/docs/workflows/build-test.md @@ -18,6 +18,7 @@ The check name is `{calling-job-name} / test`. If you name the calling job diffe | `ruff` | boolean | `true` | Whether to run `ruff check .` | | `setup-commands` | string | `''` | Shell commands to run before tests (YAML block scalar, no script file needed) | | `ty-src` | string | `src/` | Path passed to `ty check` for type checking | +| `resolution` | string | `highest` | uv resolution strategy: `highest`, `lowest`, or `lowest-direct` | ## Usage @@ -40,3 +41,20 @@ jobs: --test-host localhost \ --test-port 5432 ``` + +## Minimum-version testing + +Add a second job with `resolution: lowest-direct` to verify that the floor of +every declared dependency range is actually sufficient, not just the latest +version normally resolved in CI: + +```yaml +jobs: + build-test: + uses: AustralianCancerDataNetwork/cava-devops/.github/workflows/build-test.yml@main + + build-test-lowest-direct: + uses: AustralianCancerDataNetwork/cava-devops/.github/workflows/build-test.yml@main + with: + resolution: lowest-direct +``` diff --git a/templates/dependabot.yml b/templates/dependabot.yml new file mode 100644 index 0000000..cf7ceba --- /dev/null +++ b/templates/dependabot.yml @@ -0,0 +1,17 @@ +# Canonical Dependabot config for CAVA package repos. +# Copy to .github/dependabot.yml in the target repo. The `allow` list should +# be trimmed to whichever sibling packages that repo actually depends on. +version: 2 +updates: + - package-ecosystem: "uv" + directory: "/" + schedule: + interval: "weekly" + labels: + - "dependencies" + allow: + - dependency-name: "oa-configurator" + - dependency-name: "orm-loader" + - dependency-name: "omop-alchemy" + - dependency-name: "omop-emb" + - dependency-name: "omop-graph" From 0e07b86bc966e1d5193c74fff10b9e7693dd4df4 Mon Sep 17 00:00:00 2001 From: Nico Loesch Date: Thu, 23 Jul 2026 01:26:35 +0000 Subject: [PATCH 2/6] fix(templates): set versioning-strategy lockfile-only for dependabot.yml Without this, Dependabot's default versioning strategy rewrites the manifest's lower-bound constraint to match whatever it just bumped to, not just uv.lock -- silently collapsing every declared range down to "whatever was last merged" and making lowest-direct floor-testing meaningless. Confirmed via the cava-ci-test pilot (round 1 exhibited this exact bug; round 2 verifies the fix). --- templates/dependabot.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/templates/dependabot.yml b/templates/dependabot.yml index cf7ceba..10360ff 100644 --- a/templates/dependabot.yml +++ b/templates/dependabot.yml @@ -7,6 +7,7 @@ updates: directory: "/" schedule: interval: "weekly" + versioning-strategy: "lockfile-only" labels: - "dependencies" allow: From 7a7378a2274e8ffaffe35a8345508125f7f0e60c Mon Sep 17 00:00:00 2001 From: Nico Loesch Date: Thu, 23 Jul 2026 01:59:58 +0000 Subject: [PATCH 3/6] docs: document resolution input, dependabot.yml, and onboarding steps - build-test-postgres.md was missing the resolution input added earlier to build-test-postgres.yml itself -- brings it in line with build-test.md. - docs/index.md component table now lists templates/dependabot.yml. - setup-new-repo.md gets a new step 4 covering when/how to widen a sibling dependency to a range, add Dependabot (with the required versioning-strategy: lockfile-only), and add the lowest-direct CI job. Subsequent steps renumbered 5-7. --- docs/guides/setup-new-repo.md | 63 +++++++++++++++++++++++++-- docs/index.md | 1 + docs/workflows/build-test-postgres.md | 21 +++++++++ 3 files changed, 82 insertions(+), 3 deletions(-) diff --git a/docs/guides/setup-new-repo.md b/docs/guides/setup-new-repo.md index a78fa6d..f2122c1 100644 --- a/docs/guides/setup-new-repo.md +++ b/docs/guides/setup-new-repo.md @@ -147,7 +147,64 @@ Copy `templates/release-drafter.yml` from this repo to `.github/release-drafter. --- -## 4. Delete old files +## 4. Dependency ranges on sibling packages (if applicable) + +Skip this step if the repo has no dependency on another CAVA package (e.g. `oa-configurator`, which is a base/leaf package with no siblings of its own). + +If this repo depends on one or more CAVA sibling packages, prefer a semver range over an exact pin once the sibling has itself migrated to this centralised CI/CD (its releases are label-gated, so MAJOR only happens on a `breaking`-labelled PR): + +```toml +dependencies = [ + "oa-configurator>=0.1.2,<1.0.0", # not =="0.1.2" +] +``` + +### Add Dependabot + +Copy `templates/dependabot.yml` to `.github/dependabot.yml`, trimming the `allow` list down to only the sibling packages this repo actually depends on: + +```yaml +version: 2 +updates: + - package-ecosystem: "uv" + directory: "/" + schedule: + interval: "weekly" + versioning-strategy: "lockfile-only" + labels: + - "dependencies" + allow: + - dependency-name: "oa-configurator" +``` + +!!! warning "versioning-strategy is required" + Without `versioning-strategy: lockfile-only`, Dependabot's default behaviour rewrites the dependency's lower-bound constraint to match whatever it just bumped to, not just `uv.lock`. That silently collapses the declared range down to "whatever was last merged" and makes the `lowest-direct` job below pointless. Confirmed by direct testing during the rollout of this feature, not assumed from documentation. + +!!! warning "Must be on the default branch" + Dependabot only reads `.github/dependabot.yml` from the repository's default branch. An open, unmerged PR containing the file has no effect — it has to actually land on `main` before Dependabot does anything. + +### Add a minimum-version CI job + +Add a second `ci.yml` job with `resolution: lowest-direct`, so the declared floor of every range is verified on every PR, not just assumed: + +```yaml +jobs: + build-test: + uses: AustralianCancerDataNetwork/cava-devops/.github/workflows/build-test.yml@main + + build-test-lowest-direct: + uses: AustralianCancerDataNetwork/cava-devops/.github/workflows/build-test.yml@main + with: + resolution: lowest-direct +``` + +See [build-test.yml](../workflows/build-test.md#minimum-version-testing) (or [build-test-postgres.yml](../workflows/build-test-postgres.md#minimum-version-testing) for Postgres repos). + +This gives continuous compatibility coverage at both ends of the range: the floor is re-verified on every PR regardless of what's in the lock, and the "latest available" end moves forward each time a Dependabot PR is reviewed and merged. Neither proves every version in between works — that rests on the upstream package's own `breaking`-label discipline, not on anything tested here. + +--- + +## 5. Delete old files Remove these files if present in the repo: @@ -161,7 +218,7 @@ Remove these files if present in the repo: --- -## 5. GitHub configuration +## 6. GitHub configuration ### Merge settings @@ -225,7 +282,7 @@ This creates five labels: `breaking`, `feature`, `fix`, `dependencies` (bump lab --- -## 6. First release after cutover +## 7. First release after cutover Merge all migration changes via a PR labelled `chore`. `merge.yml` triggers and release-drafter creates a draft. What to do next depends on whether the repo has existing tags. diff --git a/docs/index.md b/docs/index.md index 9367a73..ce38de8 100644 --- a/docs/index.md +++ b/docs/index.md @@ -17,6 +17,7 @@ Centralised CI/CD infrastructure for the [AustralianCancerDataNetwork](https://g | Label bootstrap script | `scripts/bootstrap-labels.sh` | One-command label setup via `gh` CLI | | Issue and PR templates | `templates/` | Canonical source; copied manually when onboarding | | Release-drafter config template | `templates/release-drafter.yml` | Canonical changelog config | +| Dependabot config template | `templates/dependabot.yml` | For repos depending on other CAVA sibling packages; see [Onboard a new repo](guides/setup-new-repo.md) | ## Design principles diff --git a/docs/workflows/build-test-postgres.md b/docs/workflows/build-test-postgres.md index d919830..f9f09d0 100644 --- a/docs/workflows/build-test-postgres.md +++ b/docs/workflows/build-test-postgres.md @@ -21,6 +21,7 @@ As with `build-test.yml`, the prefix is the calling job name. | `postgres-db` | string | `test` | Database name to create | | `postgres-user` | string | `test` | Postgres user | | `postgres-password` | string | `test` | Postgres password | +| `resolution` | string | `highest` | uv resolution strategy: `highest`, `lowest`, or `lowest-direct` | ## Usage @@ -85,3 +86,23 @@ jobs: ``` The required status check names become `build-test-sqlite / test` and `build-test-postgres / test`. Add both to the branch protection ruleset. + +## Minimum-version testing + +As with `build-test.yml`, add a second job with `resolution: lowest-direct` to +verify that the floor of every declared dependency range is actually +sufficient: + +```yaml +jobs: + build-test: + uses: AustralianCancerDataNetwork/cava-devops/.github/workflows/build-test-postgres.yml@main + with: + postgres-db: my_test_db + + build-test-lowest-direct: + uses: AustralianCancerDataNetwork/cava-devops/.github/workflows/build-test-postgres.yml@main + with: + postgres-db: my_test_db + resolution: lowest-direct +``` From f1659e274c44d3a7da57d7063396561600fcde3d Mon Sep 17 00:00:00 2001 From: Nico Loesch Date: Fri, 24 Jul 2026 04:13:39 +0000 Subject: [PATCH 4/6] fix(ci): pass --resolution to every uv run step, not just uv sync uv sync --resolution tags the lock with that resolution mode, but a plain `uv run ` with no --resolution flag defaults to expecting `highest` -- it treats a lowest-direct-tagged lock as stale and silently re-resolves everything back to highest before running the command. Confirmed directly: after `uv sync --resolution lowest-direct` installed ruff==0.4.0, a subsequent `uv run ruff --version` logged "Ignoring existing lockfile due to change in resolution mode: lowest-direct vs. highest" and ended up on a completely different ruff version. This meant the lowest-direct job never actually tested the floor -- every step after the initial sync silently upgraded back to highest, making it a redundant, no-op duplicate of the regular highest job. Fixes it by passing --resolution explicitly to the ty/ruff/pytest uv run steps too. --- .github/workflows/build-test-postgres.yml | 6 +++--- .github/workflows/build-test.yml | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/build-test-postgres.yml b/.github/workflows/build-test-postgres.yml index 92901d7..d542371 100644 --- a/.github/workflows/build-test-postgres.yml +++ b/.github/workflows/build-test-postgres.yml @@ -89,11 +89,11 @@ jobs: run: ${{ inputs.setup-commands }} - name: Type check - run: uv run ty check ${{ inputs.ty-src }} + run: uv run --resolution ${{ inputs.resolution }} ty check ${{ inputs.ty-src }} - name: Lint if: inputs.ruff - run: uv run ruff check . + run: uv run --resolution ${{ inputs.resolution }} ruff check . - name: Test - run: uv run pytest -q + run: uv run --resolution ${{ inputs.resolution }} pytest -q diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml index 38526a6..cfb55ac 100644 --- a/.github/workflows/build-test.yml +++ b/.github/workflows/build-test.yml @@ -52,11 +52,11 @@ jobs: run: ${{ inputs.setup-commands }} - name: Type check - run: uv run ty check ${{ inputs.ty-src }} + run: uv run --resolution ${{ inputs.resolution }} ty check ${{ inputs.ty-src }} - name: Lint if: inputs.ruff - run: uv run ruff check . + run: uv run --resolution ${{ inputs.resolution }} ruff check . - name: Test - run: uv run pytest -q + run: uv run --resolution ${{ inputs.resolution }} pytest -q From 37607edc074b110197630ef4f83f74a465756276 Mon Sep 17 00:00:00 2001 From: Nico Loesch Date: Fri, 24 Jul 2026 05:18:39 +0000 Subject: [PATCH 5/6] simplify: drop lowest-direct/Dependabot in favour of --frozen + pre-commit hook Reassessed the compatibility-verification approach: the lowest-direct CI job never caught a real cross-package incompatibility (only bugs in its own scaffolding -- the versioning-strategy and --resolution propagation issues fixed earlier), and duplicates a signal already available for free from any regular Dependabot bump PR's normal CI run. Floor-testing matters far more for widely-distributed public libraries than a closed internal chain with full release control. - build-test.yml / build-test-postgres.yml: drop the resolution input and all --resolution flags; install with `uv sync --frozen` instead, so CI tests exactly what's committed in uv.lock, no implicit re-resolution. - Remove templates/dependabot.yml. CVE coverage now comes from the Dependabot security-updates repo setting alone (no dependabot.yml needed -- confirmed the `allow` list required for scoped version updates silently blocks security-update PRs for anything not listed, a known GitHub gotcha with no clean workaround). - Add templates/githooks/pre-commit: keeps uv.lock in sync with pyproject.toml locally, before a commit is ever made. Diffs staged pyproject.toml for the specific dependency line(s) that changed and runs `uv lock --upgrade-package ` for just those -- confirmed directly this touches only the named package, never an unrelated one. - docs: setup-new-repo.md step 4 trimmed to just the range-widening guidance; new step 5 documents installing the pre-commit hook; build-test.md/build-test-postgres.md/index.md references removed. --- .github/workflows/build-test-postgres.yml | 15 ++----- .github/workflows/build-test.yml | 15 ++----- docs/guides/setup-new-repo.md | 54 +++++++---------------- docs/index.md | 1 - docs/workflows/build-test-postgres.md | 21 --------- docs/workflows/build-test.md | 18 -------- templates/dependabot.yml | 18 -------- templates/githooks/pre-commit | 37 ++++++++++++++++ 8 files changed, 62 insertions(+), 117 deletions(-) delete mode 100644 templates/dependabot.yml create mode 100755 templates/githooks/pre-commit diff --git a/.github/workflows/build-test-postgres.yml b/.github/workflows/build-test-postgres.yml index d542371..ae2a1f6 100644 --- a/.github/workflows/build-test-postgres.yml +++ b/.github/workflows/build-test-postgres.yml @@ -47,13 +47,6 @@ on: type: string default: 'src/' required: false - resolution: - description: > - uv resolution strategy (highest, lowest, lowest-direct). Use - lowest-direct to verify declared floor versions are sufficient. - type: string - default: 'highest' - required: false jobs: test: @@ -82,18 +75,18 @@ jobs: python-version: ${{ inputs.python-version }} - name: Install dependencies - run: uv sync --resolution ${{ inputs.resolution }} --all-extras --dev + run: uv sync --frozen --all-extras --dev - name: Run setup commands if: inputs.setup-commands != '' run: ${{ inputs.setup-commands }} - name: Type check - run: uv run --resolution ${{ inputs.resolution }} ty check ${{ inputs.ty-src }} + run: uv run ty check ${{ inputs.ty-src }} - name: Lint if: inputs.ruff - run: uv run --resolution ${{ inputs.resolution }} ruff check . + run: uv run ruff check . - name: Test - run: uv run --resolution ${{ inputs.resolution }} pytest -q + run: uv run pytest -q diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml index cfb55ac..22b2b6b 100644 --- a/.github/workflows/build-test.yml +++ b/.github/workflows/build-test.yml @@ -24,13 +24,6 @@ on: type: string default: 'src/' required: false - resolution: - description: > - uv resolution strategy (highest, lowest, lowest-direct). Use - lowest-direct to verify declared floor versions are sufficient. - type: string - default: 'highest' - required: false jobs: test: @@ -45,18 +38,18 @@ jobs: python-version: ${{ inputs.python-version }} - name: Install dependencies - run: uv sync --resolution ${{ inputs.resolution }} --all-extras --dev + run: uv sync --frozen --all-extras --dev - name: Run setup commands if: inputs.setup-commands != '' run: ${{ inputs.setup-commands }} - name: Type check - run: uv run --resolution ${{ inputs.resolution }} ty check ${{ inputs.ty-src }} + run: uv run ty check ${{ inputs.ty-src }} - name: Lint if: inputs.ruff - run: uv run --resolution ${{ inputs.resolution }} ruff check . + run: uv run ruff check . - name: Test - run: uv run --resolution ${{ inputs.resolution }} pytest -q + run: uv run pytest -q diff --git a/docs/guides/setup-new-repo.md b/docs/guides/setup-new-repo.md index f2122c1..1b762c3 100644 --- a/docs/guides/setup-new-repo.md +++ b/docs/guides/setup-new-repo.md @@ -159,52 +159,32 @@ dependencies = [ ] ``` -### Add Dependabot +CI installs with `uv sync --frozen`, so it always tests exactly what's committed in `uv.lock` — see [Keeping `uv.lock` in sync](#keeping-uvlock-in-sync) below for how that stays consistent with `pyproject.toml`. Enable **Dependabot security updates** in the repo's settings (Advanced Security) for CVE coverage; no `dependabot.yml` file is needed for that. -Copy `templates/dependabot.yml` to `.github/dependabot.yml`, trimming the `allow` list down to only the sibling packages this repo actually depends on: - -```yaml -version: 2 -updates: - - package-ecosystem: "uv" - directory: "/" - schedule: - interval: "weekly" - versioning-strategy: "lockfile-only" - labels: - - "dependencies" - allow: - - dependency-name: "oa-configurator" -``` - -!!! warning "versioning-strategy is required" - Without `versioning-strategy: lockfile-only`, Dependabot's default behaviour rewrites the dependency's lower-bound constraint to match whatever it just bumped to, not just `uv.lock`. That silently collapses the declared range down to "whatever was last merged" and makes the `lowest-direct` job below pointless. Confirmed by direct testing during the rollout of this feature, not assumed from documentation. - -!!! warning "Must be on the default branch" - Dependabot only reads `.github/dependabot.yml` from the repository's default branch. An open, unmerged PR containing the file has no effect — it has to actually land on `main` before Dependabot does anything. +--- -### Add a minimum-version CI job +## 5. Keeping `uv.lock` in sync -Add a second `ci.yml` job with `resolution: lowest-direct`, so the declared floor of every range is verified on every PR, not just assumed: +`ci.yml` installs with `uv sync --frozen`, meaning CI tests *exactly* what's committed in `uv.lock` — no implicit re-resolution, no drift-catching safety net. That makes it important that `uv.lock` actually reflects `pyproject.toml` at all times, without relying on someone remembering to run `uv lock` after every dependency edit. -```yaml -jobs: - build-test: - uses: AustralianCancerDataNetwork/cava-devops/.github/workflows/build-test.yml@main +Install the canonical pre-commit hook: - build-test-lowest-direct: - uses: AustralianCancerDataNetwork/cava-devops/.github/workflows/build-test.yml@main - with: - resolution: lowest-direct +```bash +mkdir -p .githooks +cp /templates/githooks/pre-commit .githooks/pre-commit +chmod +x .githooks/pre-commit +git config core.hooksPath .githooks ``` -See [build-test.yml](../workflows/build-test.md#minimum-version-testing) (or [build-test-postgres.yml](../workflows/build-test-postgres.md#minimum-version-testing) for Postgres repos). +`git config core.hooksPath` is a one-time, per-clone setting (not committed), so each contributor needs to run it once after cloning. + +What it does: when `pyproject.toml` is part of a commit, the hook diffs it to find exactly which dependency line(s) changed, then runs `uv lock --upgrade-package ` for just those -- not a blanket `uv lock`. This touches only the package(s) you actually edited (plus whatever *that* package's new version transitively requires), never an unrelated dependency that merely happens to have a newer release available. Confirmed directly: `uv lock --upgrade-package certifi` moved only `certifi`, leaving every other locked package untouched. -This gives continuous compatibility coverage at both ends of the range: the floor is re-verified on every PR regardless of what's in the lock, and the "latest available" end moves forward each time a Dependabot PR is reviewed and merged. Neither proves every version in between works — that rests on the upstream package's own `breaking`-label discipline, not on anything tested here. +This runs locally, before the commit is created -- by the time a PR is opened, `uv.lock` is already correct, and CI's `--frozen` install just confirms it. --- -## 5. Delete old files +## 6. Delete old files Remove these files if present in the repo: @@ -218,7 +198,7 @@ Remove these files if present in the repo: --- -## 6. GitHub configuration +## 7. GitHub configuration ### Merge settings @@ -282,7 +262,7 @@ This creates five labels: `breaking`, `feature`, `fix`, `dependencies` (bump lab --- -## 7. First release after cutover +## 8. First release after cutover Merge all migration changes via a PR labelled `chore`. `merge.yml` triggers and release-drafter creates a draft. What to do next depends on whether the repo has existing tags. diff --git a/docs/index.md b/docs/index.md index ce38de8..9367a73 100644 --- a/docs/index.md +++ b/docs/index.md @@ -17,7 +17,6 @@ Centralised CI/CD infrastructure for the [AustralianCancerDataNetwork](https://g | Label bootstrap script | `scripts/bootstrap-labels.sh` | One-command label setup via `gh` CLI | | Issue and PR templates | `templates/` | Canonical source; copied manually when onboarding | | Release-drafter config template | `templates/release-drafter.yml` | Canonical changelog config | -| Dependabot config template | `templates/dependabot.yml` | For repos depending on other CAVA sibling packages; see [Onboard a new repo](guides/setup-new-repo.md) | ## Design principles diff --git a/docs/workflows/build-test-postgres.md b/docs/workflows/build-test-postgres.md index f9f09d0..d919830 100644 --- a/docs/workflows/build-test-postgres.md +++ b/docs/workflows/build-test-postgres.md @@ -21,7 +21,6 @@ As with `build-test.yml`, the prefix is the calling job name. | `postgres-db` | string | `test` | Database name to create | | `postgres-user` | string | `test` | Postgres user | | `postgres-password` | string | `test` | Postgres password | -| `resolution` | string | `highest` | uv resolution strategy: `highest`, `lowest`, or `lowest-direct` | ## Usage @@ -86,23 +85,3 @@ jobs: ``` The required status check names become `build-test-sqlite / test` and `build-test-postgres / test`. Add both to the branch protection ruleset. - -## Minimum-version testing - -As with `build-test.yml`, add a second job with `resolution: lowest-direct` to -verify that the floor of every declared dependency range is actually -sufficient: - -```yaml -jobs: - build-test: - uses: AustralianCancerDataNetwork/cava-devops/.github/workflows/build-test-postgres.yml@main - with: - postgres-db: my_test_db - - build-test-lowest-direct: - uses: AustralianCancerDataNetwork/cava-devops/.github/workflows/build-test-postgres.yml@main - with: - postgres-db: my_test_db - resolution: lowest-direct -``` diff --git a/docs/workflows/build-test.md b/docs/workflows/build-test.md index 125306b..89c8d15 100644 --- a/docs/workflows/build-test.md +++ b/docs/workflows/build-test.md @@ -18,7 +18,6 @@ The check name is `{calling-job-name} / test`. If you name the calling job diffe | `ruff` | boolean | `true` | Whether to run `ruff check .` | | `setup-commands` | string | `''` | Shell commands to run before tests (YAML block scalar, no script file needed) | | `ty-src` | string | `src/` | Path passed to `ty check` for type checking | -| `resolution` | string | `highest` | uv resolution strategy: `highest`, `lowest`, or `lowest-direct` | ## Usage @@ -41,20 +40,3 @@ jobs: --test-host localhost \ --test-port 5432 ``` - -## Minimum-version testing - -Add a second job with `resolution: lowest-direct` to verify that the floor of -every declared dependency range is actually sufficient, not just the latest -version normally resolved in CI: - -```yaml -jobs: - build-test: - uses: AustralianCancerDataNetwork/cava-devops/.github/workflows/build-test.yml@main - - build-test-lowest-direct: - uses: AustralianCancerDataNetwork/cava-devops/.github/workflows/build-test.yml@main - with: - resolution: lowest-direct -``` diff --git a/templates/dependabot.yml b/templates/dependabot.yml deleted file mode 100644 index 10360ff..0000000 --- a/templates/dependabot.yml +++ /dev/null @@ -1,18 +0,0 @@ -# Canonical Dependabot config for CAVA package repos. -# Copy to .github/dependabot.yml in the target repo. The `allow` list should -# be trimmed to whichever sibling packages that repo actually depends on. -version: 2 -updates: - - package-ecosystem: "uv" - directory: "/" - schedule: - interval: "weekly" - versioning-strategy: "lockfile-only" - labels: - - "dependencies" - allow: - - dependency-name: "oa-configurator" - - dependency-name: "orm-loader" - - dependency-name: "omop-alchemy" - - dependency-name: "omop-emb" - - dependency-name: "omop-graph" diff --git a/templates/githooks/pre-commit b/templates/githooks/pre-commit new file mode 100755 index 0000000..398202d --- /dev/null +++ b/templates/githooks/pre-commit @@ -0,0 +1,37 @@ +#!/usr/bin/env bash +# Canonical CAVA pre-commit hook: keeps uv.lock in sync with pyproject.toml. +# +# If pyproject.toml is staged, finds the specific dependency line(s) that +# changed (added, removed, or had their version spec edited) and refreshes +# only those packages in uv.lock via `uv lock --upgrade-package`. Does not +# touch any other package -- this is a targeted sync, not a blanket +# `uv lock`, so it never pulls in an unrelated upstream release. +set -euo pipefail + +if ! git diff --cached --name-only | grep -q '^pyproject\.toml$'; then + exit 0 +fi + +if ! command -v uv >/dev/null 2>&1; then + echo "pre-commit: uv not found on PATH, skipping uv.lock sync" >&2 + exit 0 +fi + +changed_names=$(git diff --cached -- pyproject.toml \ + | grep -E '^[+-]\s*"[A-Za-z0-9][A-Za-z0-9._-]*' \ + | sed -E 's/^[+-]\s*"([A-Za-z0-9][A-Za-z0-9._-]*).*/\1/' \ + | sort -u) + +if [ -z "$changed_names" ]; then + exit 0 +fi + +upgrade_args=() +while IFS= read -r name; do + upgrade_args+=(--upgrade-package "$name") +done <<< "$changed_names" + +echo "pre-commit: syncing uv.lock for changed dependencies:" +echo "$changed_names" | sed 's/^/ - /' +uv lock "${upgrade_args[@]}" +git add uv.lock From aabd40bf1ac047423f1cdfabed153f422afc5f98 Mon Sep 17 00:00:00 2001 From: Nico Loesch Date: Fri, 24 Jul 2026 06:11:30 +0000 Subject: [PATCH 6/6] fix(ci): use plain uv sync instead of --frozen --frozen blindly trusts uv.lock with no validation against pyproject.toml at all. Confirmed directly: raising a dependency floor in pyproject.toml without updating uv.lock still passed --frozen silently (exit 0), meaning a forgotten or bypassed pre-commit hook could leave a floor completely untested indefinitely with no signal anywhere. Plain uv sync is the better fit: it prefers the committed lock when still valid (so no gratuitous bumps just because something newer shipped upstream, confirmed separately), but re-resolves and fails loudly the moment the lock can no longer satisfy pyproject.toml (confirmed with the same raised-floor scenario -- "No solution found", not a silent pass). --locked/uv lock --check were also ruled out: they fail on any staleness at all, not just genuine inconsistency, reintroducing exactly the forced-update-just-because-something-shipped nagging this whole design was trying to avoid. --- .github/workflows/build-test-postgres.yml | 2 +- .github/workflows/build-test.yml | 2 +- docs/guides/setup-new-repo.md | 6 +++--- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/build-test-postgres.yml b/.github/workflows/build-test-postgres.yml index ae2a1f6..0316b09 100644 --- a/.github/workflows/build-test-postgres.yml +++ b/.github/workflows/build-test-postgres.yml @@ -75,7 +75,7 @@ jobs: python-version: ${{ inputs.python-version }} - name: Install dependencies - run: uv sync --frozen --all-extras --dev + run: uv sync --all-extras --dev - name: Run setup commands if: inputs.setup-commands != '' diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml index 22b2b6b..122e440 100644 --- a/.github/workflows/build-test.yml +++ b/.github/workflows/build-test.yml @@ -38,7 +38,7 @@ jobs: python-version: ${{ inputs.python-version }} - name: Install dependencies - run: uv sync --frozen --all-extras --dev + run: uv sync --all-extras --dev - name: Run setup commands if: inputs.setup-commands != '' diff --git a/docs/guides/setup-new-repo.md b/docs/guides/setup-new-repo.md index 1b762c3..a38e469 100644 --- a/docs/guides/setup-new-repo.md +++ b/docs/guides/setup-new-repo.md @@ -159,13 +159,13 @@ dependencies = [ ] ``` -CI installs with `uv sync --frozen`, so it always tests exactly what's committed in `uv.lock` — see [Keeping `uv.lock` in sync](#keeping-uvlock-in-sync) below for how that stays consistent with `pyproject.toml`. Enable **Dependabot security updates** in the repo's settings (Advanced Security) for CVE coverage; no `dependabot.yml` file is needed for that. +CI installs with plain `uv sync`, which prefers what's already committed in `uv.lock` but will fail loudly if it no longer satisfies `pyproject.toml`, see [Keeping `uv.lock` in sync](#keeping-uvlock-in-sync) below for how that stays consistent with `pyproject.toml`. Enable **Dependabot security updates** in the repo's settings (Advanced Security) for CVE coverage; no `dependabot.yml` file is needed for that. --- ## 5. Keeping `uv.lock` in sync -`ci.yml` installs with `uv sync --frozen`, meaning CI tests *exactly* what's committed in `uv.lock` — no implicit re-resolution, no drift-catching safety net. That makes it important that `uv.lock` actually reflects `pyproject.toml` at all times, without relying on someone remembering to run `uv lock` after every dependency edit. +`ci.yml` installs with plain `uv sync`. It prefers whatever's already committed in `uv.lock` (so a dependency that's still valid never gets bumped just because something newer shipped upstream), but it re-resolves and fails loudly the moment the lock can no longer satisfy `pyproject.toml`. Confirmed directly against `--frozen` (which blindly trusts the lock with no validation at all, silently passing even when the two have diverged) and `--locked`/`--check` (which fails on any staleness at all, not just genuine inconsistency) before settling on plain `uv sync` as the one mode that does both correctly. That makes it important that `uv.lock` actually reflects `pyproject.toml`, without relying on someone remembering to run `uv lock` after every dependency edit. Install the canonical pre-commit hook: @@ -180,7 +180,7 @@ git config core.hooksPath .githooks What it does: when `pyproject.toml` is part of a commit, the hook diffs it to find exactly which dependency line(s) changed, then runs `uv lock --upgrade-package ` for just those -- not a blanket `uv lock`. This touches only the package(s) you actually edited (plus whatever *that* package's new version transitively requires), never an unrelated dependency that merely happens to have a newer release available. Confirmed directly: `uv lock --upgrade-package certifi` moved only `certifi`, leaving every other locked package untouched. -This runs locally, before the commit is created -- by the time a PR is opened, `uv.lock` is already correct, and CI's `--frozen` install just confirms it. +This runs locally, before the commit is created. By the time a PR is opened, `uv.lock` is already correct, and CI's `uv sync` install just confirms it. ---