Skip to content

chore: rolling uv exclude-newer supply-chain quarantine#2616

Open
tonyandrewmeyer wants to merge 6 commits into
canonical:mainfrom
tonyandrewmeyer:ci/uv-hardening
Open

chore: rolling uv exclude-newer supply-chain quarantine#2616
tonyandrewmeyer wants to merge 6 commits into
canonical:mainfrom
tonyandrewmeyer:ci/uv-hardening

Conversation

@tonyandrewmeyer

@tonyandrewmeyer tonyandrewmeyer commented Jul 4, 2026

Copy link
Copy Markdown
Collaborator

Adds a rolling package-level cooldown in [tool.uv]:

exclude-newer = "7 days"

uv refuses to resolve against any package published in the last 7 days, so a compromised release has a window to be caught / yanked upstream before it can enter this project. Complements (does not replace) the Dependabot cooldown, which only protects Dependabot-authored PRs — exclude-newer also covers manual uv add, uv lock regens, uvx bootstraps, and CI re-resolves, which Dependabot cooldown alone doesn't reach.

claude and others added 6 commits July 2, 2026 12:03
Add --locked to the project-scoped `uv run` invocations at the top
level of .github/workflows/ (zizmor.yaml, integration.yaml).

Without --locked, if a PR widens a version range in pyproject.toml
without regenerating uv.lock, uv silently re-resolves in CI and the
transitive-dependency delta never appears in the diff. --locked makes
uv fail instead of re-resolving when uv.lock is out of sync with
pyproject.toml, forcing a fresh uv.lock commit and making the
resolution change reviewable.

Not using --frozen: --frozen skips the freshness check entirely (it
just uses the lock file as-is, even if stale relative to
pyproject.toml), so it doesn't provide this guarantee.

Scope: only bare project-scoped `uv run`/`uv sync` calls against the
root uv.lock. The `uv run --script --no-project` and the
working-directory=examples/* `uv run` invocations in
example-charm-tests.yaml were left untouched -- the latter resolves
against each example charm's own uv.lock (each examples/* directory
has its own pyproject.toml/uv.lock), a separate resolution story from
the root project. `uv tool install`, `uvx`, `uv build`, `uv export`,
`uv add`/`uv remove` invocations are out of scope for --locked (they
are not `uv run`/`uv sync`) and were left untouched, as were all
pip/tox/poetry invocations.
Add [tool.uv].exclude-newer = "7 days" to pyproject.toml. uv will
ignore any package release newer than 7 days old when resolving
dependencies, across every uv resolution path: manual `uv add`,
`uv lock` regenerations, uvx bootstraps, and CI re-resolves.

This complements (does not replace) Dependabot's cooldown, which only
protects Dependabot-authored PRs. exclude-newer is a rolling window --
"7 days" is evaluated relative to whenever the resolution runs, not a
fixed date -- so it continuously quarantines newly published releases
from every other route a bad or compromised release could enter the
dependency tree.

Reference: uv docs (exclude-newer accepts durations like "7 days");
Canonical Security "How-To: Secure a repo" -- Minimum release age.
Add [tool.uv].no-build = true to pyproject.toml. With this set, uv
refuses to build or install any package from a source distribution
(sdist) and will only install from wheels.

Why: installing an sdist (via `pip install <sdist>` or `uv sync`
falling back to one) runs that package's setup.py / PEP 517 build
hooks at install time -- arbitrary code execution controlled by
whatever is in the sdist. Wheels are pre-built and do not run install
scripts, so restricting resolution to wheels closes this vector
fleet-wide.

Fleet verification (2026-07-02): 0 of 571 dependencies across the
fleet's uv.lock files are sdist-only (confirmed 0/73 in this repo's
own root uv.lock), so this is a zero-cost change today -- no package
currently in use requires falling back to an sdist build.

Escape hatch: if a future dependency is genuinely sdist-only and must
be installed anyway, exempt it individually with
[tool.uv].no-build-package = ["specific-pkg"] rather than reverting
this setting.

Reference: Canonical Security "How-To: Secure a repo" -- Install
scripts section.
Replace the `[tool.uv]` block with the canonical fleet comment
(rolling quarantine rationale), drop `no-build = true`, and drop
`--locked` from CI workflow `uv run` / `uv sync` invocations.

See canonical/pytest-jubilant#98 for the exemplar PR and
canonical/charm-tech#22 (references/decisions.md) for the recorded
deferral of `no-build` and `--locked`: uv has no allow-list to
exempt the workspace project from `no-build`, and rolling
`exclude-newer` is fundamentally incompatible with `--locked`
(uv records the resolved-at-lock-time timestamp in `uv.lock`, so
every day CI recomputes a different value and `--locked` errors).
Rolling `exclude-newer` is the surviving pattern.
…exclude-newer

The rolling '[tool.uv].exclude-newer = "7 days"' resolves to now() - 7
days at each invocation and uv records the absolute timestamp in
uv.lock. tox-uv's uv-venv-lock-runner passes --locked to 'uv sync', so
every run the newly-computed floor differs from the stored one, uv
re-resolves, sees a config change, and errors. Rolling exclude-newer
and --locked are fundamentally incompatible; exclude-newer is the
security control with unique coverage (paths Dependabot cooldown
doesn't reach), so --locked has to go.
Reverts the tox.ini runner change from uv-venv-runner back to
uv-venv-lock-runner: uv-venv-runner uses 'uv pip install' with only the
tox-declared dep groups, which skips the workspace install and left
runtime deps (e.g. ops-scenario==8.9.0.dev0) unresolvable.

Instead, add 'uv lock' before every tox invocation across
framework-tests.yaml and example-charm-tests.yaml. That re-locks
against the current '[tool.uv].exclude-newer = "7 days"' floor, so
tox's uv-venv-lock-runner sees a fresh uv.lock and its internal
'uv sync --locked' succeeds. --locked stays as the security control
inside tox; the rolling exclude-newer floor no longer breaks it.
@tonyandrewmeyer tonyandrewmeyer marked this pull request as ready for review July 4, 2026 06:36

@dwilding dwilding left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't understand why the extra locks are needed. Especially the ones in example-charm-tests.yaml, which run in the charm projects where nothing has changed.

@james-garner-canonical

Copy link
Copy Markdown
Contributor

I don't understand why the extra locks are needed. Especially the ones in example-charm-tests.yaml, which run in the charm projects where nothing has changed.

Yeah, I have the same question.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants