diff --git a/.github/workflows/lint-imports.yml b/.github/workflows/lint-imports.yml new file mode 100644 index 0000000..bfb109a --- /dev/null +++ b/.github/workflows/lint-imports.yml @@ -0,0 +1,34 @@ +name: Lint Import Boundaries +on: + workflow_call: + inputs: + python-version: + description: Python version to use + type: string + default: '3.12' + required: false + config-path: + description: Path to the calling repository's import-linter configuration + type: string + default: '.importlinter' + required: false + +jobs: + lint-imports: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v5 + with: + fetch-depth: 0 + + - uses: astral-sh/setup-uv@v5 + with: + python-version: ${{ inputs.python-version }} + + - name: Install dependencies + run: uv sync --all-extras --dev + + - name: Check import contracts + env: + IMPORT_LINTER_CONFIG: ${{ inputs.config-path }} + run: uv run lint-imports --config "$IMPORT_LINTER_CONFIG" diff --git a/docs/guides/setup-new-repo.md b/docs/guides/setup-new-repo.md index a78fa6d..a38e469 100644 --- a/docs/guides/setup-new-repo.md +++ b/docs/guides/setup-new-repo.md @@ -147,7 +147,44 @@ 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" +] +``` + +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 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: + +```bash +mkdir -p .githooks +cp /templates/githooks/pre-commit .githooks/pre-commit +chmod +x .githooks/pre-commit +git config core.hooksPath .githooks +``` + +`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 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. + +--- + +## 6. Delete old files Remove these files if present in the repo: @@ -161,7 +198,7 @@ Remove these files if present in the repo: --- -## 5. GitHub configuration +## 7. GitHub configuration ### Merge settings @@ -225,7 +262,7 @@ This creates five labels: `breaking`, `feature`, `fix`, `dependencies` (bump lab --- -## 6. 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/workflows/index.md b/docs/workflows/index.md index bcc47f6..88d9af7 100644 --- a/docs/workflows/index.md +++ b/docs/workflows/index.md @@ -7,6 +7,7 @@ All workflows are called via `uses: AustralianCancerDataNetwork/cava-devops/.git | [`label-gate.yml`](label-gate.md) | PR open/update | Blocks merge until exactly one bump label is present | | [`build-test.yml`](build-test.md) | PR open/update | Installs, lints, and runs the test suite | | [`build-test-postgres.yml`](build-test-postgres.md) | PR open/update | Same as `build-test`, with a Postgres service container | +| [`lint-imports.yml`](lint-imports.md) | PR open/update | Checks repository-owned import contracts | | [`release-drafter.yml`](release-drafter.md) | PR merged | Updates the standing draft release with the merged PR | | [`publish.yml`](publish.md) | Tag push | Verifies tag is on main, builds wheel and sdist, uploads as artifact | | [`deploy-docs.yml`](deploy-docs.md) | Tag push / `workflow_dispatch` | Installs `dev` extra and deploys MkDocs to GitHub Pages | diff --git a/docs/workflows/lint-imports.md b/docs/workflows/lint-imports.md new file mode 100644 index 0000000..490badb --- /dev/null +++ b/docs/workflows/lint-imports.md @@ -0,0 +1,32 @@ +# lint-imports.yml + +Installs the calling repository's development dependencies and checks its import +contracts. The contract configuration remains in the package repository because package +boundaries are part of that repository's architecture. + +## Required status check name + +```text +lint-imports / lint-imports +``` + +The first segment is the calling job name. A differently named calling job changes that +segment. + +## Inputs + +| Input | Type | Default | Description | +|---|---|---|---| +| `python-version` | string | `3.12` | Python version | +| `config-path` | string | `.importlinter` | Path to the calling repository's import-linter configuration | + +## Usage + +The calling repository must include `import-linter` in an installed dependency group or +extra. + +```yaml +jobs: + lint-imports: + uses: AustralianCancerDataNetwork/cava-devops/.github/workflows/lint-imports.yml@main +``` diff --git a/mkdocs.yml b/mkdocs.yml index 1c781df..9bd8d3f 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -51,6 +51,7 @@ nav: - label-gate: workflows/label-gate.md - build-test: workflows/build-test.md - build-test-postgres: workflows/build-test-postgres.md + - lint-imports: workflows/lint-imports.md - release-drafter: workflows/release-drafter.md - publish: workflows/publish.md - deploy-docs: workflows/deploy-docs.md diff --git a/scripts/bootstrap-labels.sh b/scripts/bootstrap-labels.sh old mode 100644 new mode 100755 diff --git a/templates/githooks/pre-commit b/templates/githooks/pre-commit new file mode 100755 index 0000000..2ad0d36 --- /dev/null +++ b/templates/githooks/pre-commit @@ -0,0 +1,36 @@ +#!/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 \ + | sed -nE 's/^[+-][[:space:]]*"([A-Za-z0-9][A-Za-z0-9._-]*).*/\1/p' \ + | 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