Skip to content
Draft
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
6 changes: 3 additions & 3 deletions .github/scripts/migrate_v2_metadata_schema.sh
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,6 @@
set -euo pipefail
set +x

uv run alembic -c alembic-v2.ini upgrade head
uv run alembic -c alembic-v2.ini current --check-heads
uv run alembic -c alembic-v2.ini check
uv run --frozen alembic -c alembic-v2.ini upgrade head
uv run --frozen alembic -c alembic-v2.ini current --check-heads
uv run --frozen alembic -c alembic-v2.ini check
13 changes: 12 additions & 1 deletion .github/workflows/pr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,10 @@ jobs:
# Full CI runs only on the lowest unmerged pull request in a stack.
if: ${{ !github.event.pull_request.stack || github.event.pull_request.stack.base.ref == github.event.pull_request.base.ref }}
runs-on: ubuntu-latest
env:
# uv installs uv.lock on the Cloud Run image's Python, not the 3.14
# that .python-version selects.
UV_PYTHON: "3.12"
steps:
- name: Checkout repo
uses: actions/checkout@v4
Expand All @@ -121,8 +125,15 @@ jobs:
uses: actions/setup-python@v5
with:
python-version: "3.12"
# The same uv version as the Cloud Run image and the release job.
- name: Set up uv
uses: astral-sh/setup-uv@v6
with:
version: "0.13.4"
- name: Check uv.lock matches pyproject.toml
run: uv lock --check
- name: Test the rules registry tool
run: python -m unittest discover -s scripts/rules/tests -t .
run: uv run --frozen --extra dev python -m unittest discover -s scripts/rules/tests -t .
- name: Install shellcheck (for the ci-scripts guard)
run: pip install "shellcheck-py==0.11.0.1"
- name: Run quality guards
Expand Down
12 changes: 6 additions & 6 deletions .github/workflows/seed-v2-database.yml
Original file line number Diff line number Diff line change
Expand Up @@ -39,30 +39,30 @@ jobs:
- name: Verify the database environment is isolated
run: bash .github/scripts/validate_database_environment.sh supabase
- name: Qualify the pending policy migration
run: uv run python scripts/qualify_v2_policy_migration.py
run: uv run --frozen python scripts/qualify_v2_policy_migration.py
env:
V2_MIGRATION_DATABASE_URL: ${{ secrets.V2_MIGRATION_DATABASE_URL }}
- name: Qualify the pending household migration
run: uv run python scripts/qualify_v2_household_migration.py
run: uv run --frozen python scripts/qualify_v2_household_migration.py
env:
V2_MIGRATION_DATABASE_URL: ${{ secrets.V2_MIGRATION_DATABASE_URL }}
- name: Qualify the pending user-association reshape
run: uv run python scripts/qualify_v2_user_association_migration.py
run: uv run --frozen python scripts/qualify_v2_user_association_migration.py
env:
V2_MIGRATION_DATABASE_URL: ${{ secrets.V2_MIGRATION_DATABASE_URL }}
- name: Upgrade and verify the v2 schema
run: bash .github/scripts/migrate_v2_metadata_schema.sh
env:
V2_MIGRATION_DATABASE_URL: ${{ secrets.V2_MIGRATION_DATABASE_URL }}
- name: Grant declared v2 table privileges
run: uv run python scripts/reconcile_v2_privileges.py apply
run: uv run --frozen python scripts/reconcile_v2_privileges.py apply
env:
V2_MIGRATION_DATABASE_URL: ${{ secrets.V2_MIGRATION_DATABASE_URL }}
- name: Verify declared v2 table privileges
run: uv run python scripts/reconcile_v2_privileges.py verify
run: uv run --frozen python scripts/reconcile_v2_privileges.py verify
env:
V2_MIGRATION_DATABASE_URL: ${{ secrets.V2_MIGRATION_DATABASE_URL }}
- name: Seed and validate the v2 metadata catalog
run: uv run python scripts/publish_v2_metadata_catalog.py
run: uv run --frozen python scripts/publish_v2_metadata_catalog.py
env:
V2_DATA_WRITE_DATABASE_URL: ${{ secrets.V2_DATA_WRITE_DATABASE_URL }}
8 changes: 8 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,10 @@ When modifying the `Makefile`, read
When adding or changing a GitHub Actions workflow step or a script under
`.github/scripts/`, read `docs/engineering/skills/ci-workflows.md`.

When adding or changing anything that installs Python packages (a workflow
step, a script, a Makefile target, a Dockerfile, a Modal image,
`pyproject.toml` or `uv.lock`), read `docs/engineering/skills/dependencies.md`.

When adding, changing, or removing a rule, a guard test, a checker
configuration, or a CI job that enforces one, read
`docs/engineering/skills/rules-registry.md`.
Expand Down Expand Up @@ -107,6 +111,10 @@ To add or change a rule, read `docs/engineering/skills/rules-registry.md`.

- Checked: `api.ci.multiline-run-is-script`, `api.ci.shell-scripts-pass-shellcheck`, `api.ci.workflow-scripts-tested`

### Dependencies and uv.lock

- Checked: `api.dependencies.install-from-lock`, `api.dependencies.lock-committed`, `api.dependencies.scheduled-relock`, `api.dependencies.test-from-lock`

### Process and repository hygiene

- Checked: `api.agents.adapters-point-to-agents-md`, `api.process.changelog-fragment`, `api.process.cloud-run-image-builds`, `api.process.docker-image-builds`, `api.process.rules-registry`, `api.process.stacked-prs-full-ci-at-bottom`
Expand Down
1 change: 1 addition & 0 deletions changelog.d/uv-lock-rules.added.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Registered rules, checked by a new quality guard, that every project has a committed `uv.lock` checked in pull request CI, that every install and test command uses `uv.lock` unless it is a registered exception, and that a scheduled workflow relocks `uv.lock`.
2 changes: 2 additions & 0 deletions docs/engineering/skills/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ Current skills:
deployment safeguards, and migration validation.
- `ci-workflows.md`: one line of code per workflow `run:` block, tested
workflow scripts, and shellcheck.
- `dependencies.md`: installing and testing from `uv.lock`, the scheduled
relock, and how to register an install that cannot use the lock.
- `github-prs.md`: PR workflow and migration PR handoff expectations.
- `migration_contracts.md`: API v2 migration route contracts, route-group
metadata, generated migration artifacts, and quality guards.
Expand Down
79 changes: 79 additions & 0 deletions docs/engineering/skills/dependencies.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
# Dependencies and uv.lock

Read this before adding or changing anything that installs Python packages:
a workflow step, a script under `.github/`, a Makefile target, a Dockerfile,
a Modal image, `pyproject.toml` or `uv.lock`.

The Cloud Run image installs exactly the versions in `uv.lock`
(`uv sync --frozen` in `gcp/cloud_run/Dockerfile`). Everything else installs
and tests the same versions, so that CI tests what production runs. Four rules
say how; the `locked-installs` quality guard (`scripts/rules/locked_installs.py`)
checks all four in the pull request `quality-guards` job.

## We use uv.lock

`api.dependencies.lock-committed`: every directory with a `pyproject.toml` has
a committed `uv.lock` beside it, and a pull request CI job runs
`uv lock --check` for it. After changing `pyproject.toml`, run `uv lock` and
commit `uv.lock`.

## We install from uv.lock

`api.dependencies.install-from-lock`: every install of Python packages installs
exactly what `uv.lock` pins:

- `uv sync --frozen` (or `--locked`) to create the environment;
- `uv run --frozen` (or `--locked`) to run a command in it;
- in a Modal image, `Image.uv_sync(..., frozen=True)`.

The guard reports every other installer: `pip install`, `uv pip install`,
`uv sync` or `uv run` without `--frozen`/`--locked`, `uv run --with` or
`--no-project`, `uvx`, `uv tool`, `pipx`, `uv add`, `poetry`, `conda`, and
Modal's `pip_install`, `uv_pip_install` and `pip_install_from_*`. It scans
workflow `run:` blocks, every tracked shell script, Makefile recipes,
Dockerfiles and Python modules outside `tests/` (for Modal image calls and
installer subprocess calls). `add_local_python_source` copies local source
into a Modal image; it installs no packages, so the guard does not report it.

## We test from uv.lock

`api.dependencies.test-from-lock`: every `pytest`, `coverage run`,
`python -m unittest`, `tox` or `nox` command in those files runs through
`uv run --frozen`. CI jobs also set `UV_PYTHON: "3.12"`; see
[testing.md](testing.md#test-environment).

## We relock on a schedule

`api.dependencies.scheduled-relock`: a workflow on a `schedule` trigger relocks
every `uv.lock` with upgrades and opens or updates a pull request, whose CI
tests the new versions. Here it is `.github/workflows/relock-dependencies.yml`.
The guard reads the workflow's configuration and the scripts its jobs run; it
does not run the workflow.

## Exceptions

An install that cannot use `uv.lock` yet is a `[[rule.exception]]` on
`api.dependencies.install-from-lock` (or `api.dependencies.test-from-lock`) in
`rules/registry.toml`. Each one names one site and says why and until when:

```toml
[[rule.exception]]
path = "docker/Dockerfile" # the file
match = 'pip install "policyengine[models]==' # text from the one command it covers
reason = "Why this install cannot use uv.lock."
removal = "The condition under which the exception is removed."
```

- `match` is required: an exception without one is rejected. Choose text
that survives version bumps (leave the version out).
- Give `removal`, or a review date `expires = 2026-12-31` together with a
tracking `issue = <number>`.
- A command-line tool installed outside the project and pinned with `==` (for
example `pip install "shellcheck-py==0.11.0.1"`) may use
`category = "pinned-tool"` instead of a removal condition. The guard checks
that every package in the command is pinned with `==`.
- An exception that matches no unlocked install fails the guard; remove it
when the install moves to `uv.lock`.

The registered exceptions are listed in
[docs/generated/rules.md](../../generated/rules.md#exceptions-and-review-dates).
2 changes: 1 addition & 1 deletion docs/engineering/skills/rules-registry.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ gap is visible, rather than leaving it out.
| `[[rule.ci]]` | `workflow`, `job`, and an `invocation` string that must appear in that job's `run:` text (or its reusable-workflow `uses:`). |
| `[rule.proof]` | Optional; see "Proof". |
| `[rule.baseline]` | Only for `ratcheted` rules: `file` listing tolerated violations, and their `count`. |
| `[[rule.exception]]` | Temporary carve-out: `path`, `reason`, `expires`. |
| `[[rule.exception]]` | Carve-out: `path`, `reason`, and `expires`; or, for rules whose guard reads them, `match` (one site), `removal`, `issue` and `category` ([dependencies.md](dependencies.md#exceptions)). |
| `[rule.downgrade]` | Required `reason` when a rule moves from enforced/ratcheted to advisory/doc-only. |

`blocking` is derived, never declared: a rule blocks merges when its status is
Expand Down
6 changes: 4 additions & 2 deletions docs/engineering/skills/testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,10 @@ versions the Cloud Run image installs (`gcp/cloud_run/Dockerfile`):
- A CI job that runs tests sets `UV_PYTHON: "3.12"`, installs with
`make install` (or `uv sync --frozen --extra dev`), runs every pytest and
coverage command through `uv run --frozen`, and never installs packages with
pip. `tests/unit/test_locked_test_environments.py` checks this for every
workflow job that runs pytest or coverage.
pip. `tests/unit/test_locked_test_environments.py` checks the Python
version and the Make targets; rules `api.dependencies.install-from-lock`
and `api.dependencies.test-from-lock`
([dependencies.md](dependencies.md)) check every install and test command.

## Migration Test Layout

Expand Down
37 changes: 34 additions & 3 deletions docs/generated/rules.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,11 @@ Every rule that governs `policyengine-api` is listed here with how it is enforce

| Tier | enforced | ratcheted | advisory | doc-only | Total |
| --- | ---: | ---: | ---: | ---: | ---: |
| mechanical | 51 | 0 | 1 | 12 | 64 |
| mechanical | 55 | 0 | 1 | 12 | 68 |
| metric | 0 | 0 | 0 | 0 | 0 |
| judgment | 0 | 0 | 0 | 1 | 1 |

**Blocking:** 51 of 65 rules.
**Blocking:** 55 of 69 rules.

## Gaps

Expand Down Expand Up @@ -69,7 +69,14 @@ Every rule that governs `policyengine-api` is listed here with how it is enforce

### Exceptions and review dates

- None.
- `api.dependencies.install-from-lock`: exception for `.github/scripts/install_locked_ruff.sh` (`pip install "ruff==`) allowed as a pinned-tool (Installs the ruff version uv.lock pins, read from uv.lock, without installing the whole project for the lint job.)
- `api.dependencies.install-from-lock`: exception for `.github/scripts/verify_observability_collector.sh` (`uv run --no-project --with grpcio --with opentelemetry-proto`) until the deploy job syncs the project with uv sync --frozen and the check runs through uv run --frozen (The collector check runs in the deploy job without installing the project; grpcio and opentelemetry-proto are locked only as transitive dependencies of the project.)
- `api.dependencies.install-from-lock`: exception for `.github/workflows/migrate-v1-cloud-sql.yml` (`pip install -e ".[dev]"`) until the job installs with uv sync --frozen and runs its scripts through uv run --frozen, after a run against the staging database succeeds (The migration scripts run with the bare python setup-python provides, against the production and staging Cloud SQL databases; moving them to uv run --frozen has not been tested against those databases.)
- `api.dependencies.install-from-lock`: exception for `.github/workflows/pr.yml` (`pip install "shellcheck-py==`) allowed as a pinned-tool (shellcheck is a command-line tool the ci-scripts guard runs; it is not a dependency of the project.)
- `api.dependencies.install-from-lock`: exception for `.github/workflows/process-v1-household-mirror-event.yml` (`pip install -e ".[dev]"`) until the job installs with uv sync --frozen and runs its script through uv run --frozen, after a run against the staging database succeeds (The operator script runs with the bare python setup-python provides, against the production and staging databases; moving it to uv run --frozen has not been tested against those databases.)
- `api.dependencies.install-from-lock`: exception for `.github/workflows/push.yml` (`pip install "shellcheck-py==`) allowed as a pinned-tool (shellcheck is a command-line tool the ci-scripts guard runs; it is not a dependency of the project.)
- `api.dependencies.install-from-lock`: exception for `.github/workflows/update-policyengine-bundle.yaml` (`pip install requests uv`) until the workflow installs uv with astral-sh/setup-uv at a pinned version and runs its Python through uv run --frozen (The bundle updater changes uv.lock, so it runs before a matching lock exists; it installs uv itself and requests for its inline Python with setup-python's interpreter.)
- `api.dependencies.install-from-lock`: exception for `docker/Dockerfile` (`pip install "policyengine[models]==`) until the image installs from uv.lock (uv sync --frozen, or uv export --frozen piped to pip install --no-deps), or is removed (The image packages the PolicyEngine .py bundle for interactive use, not this project; uv.lock locks this project. The bundle updater keeps the pin equal to pyproject.toml's, but ipython and the bundle's own dependencies are not pinned.)

## API v2 layer boundaries

Expand Down Expand Up @@ -394,6 +401,30 @@ flowchart LR
r_api_ci_workflow_scripts_tested --> e_quality_guard_ci_scripts
```

## Dependencies and uv.lock

| Rule | Statement | Tier | Status | Blocking | Enforced by | CI | Proof |
| --- | --- | --- | --- | --- | --- | --- | --- |
| `api.dependencies.install-from-lock` | Every install of Python packages in a workflow, a shell script, a Makefile, a Dockerfile or a Modal image installs exactly what uv.lock pins (uv sync --frozen, uv run --frozen), unless it is a registered exception. | mechanical | enforced | yes | quality guard `locked-installs` | `pr.yml:quality-guards` | yes |
| `api.dependencies.lock-committed` | Every directory with a pyproject.toml has a committed uv.lock beside it, and a pull request CI job runs uv lock --check for it. | mechanical | enforced | yes | quality guard `locked-installs` | `pr.yml:quality-guards`, `pr.yml:quality-guards` | yes |
| `api.dependencies.scheduled-relock` | A workflow on a schedule relocks every uv.lock with upgrades and opens or updates a pull request with the result. | mechanical | enforced | yes | quality guard `locked-installs` | `pr.yml:quality-guards` | yes |
| `api.dependencies.test-from-lock` | Every test command in a workflow, a shell script, a Makefile or a Dockerfile runs inside the uv.lock environment, through uv run --frozen. | mechanical | enforced | yes | quality guard `locked-installs` | `pr.yml:quality-guards` | yes |

```mermaid
flowchart LR
r_api_dependencies_install_from_lock("api.dependencies.install-from-lock (enforced)")
e_quality_guard_locked_installs["quality guard locked-installs"]
j_CI_pr_yml_quality_guards["CI pr.yml:quality-guards"]
r_api_dependencies_lock_committed("api.dependencies.lock-committed (enforced)")
r_api_dependencies_scheduled_relock("api.dependencies.scheduled-relock (enforced)")
r_api_dependencies_test_from_lock("api.dependencies.test-from-lock (enforced)")
r_api_dependencies_install_from_lock --> e_quality_guard_locked_installs
e_quality_guard_locked_installs --> j_CI_pr_yml_quality_guards
r_api_dependencies_lock_committed --> e_quality_guard_locked_installs
r_api_dependencies_scheduled_relock --> e_quality_guard_locked_installs
r_api_dependencies_test_from_lock --> e_quality_guard_locked_installs
```

## Process and repository hygiene

| Rule | Statement | Tier | Status | Blocking | Enforced by | CI | Proof |
Expand Down
Loading
Loading