Skip to content

build: refuse PyPI packages uploaded less than 3 days ago#485

Merged
gibiw merged 1 commit into
mainfrom
build/pip-min-release-age
May 13, 2026
Merged

build: refuse PyPI packages uploaded less than 3 days ago#485
gibiw merged 1 commit into
mainfrom
build/pip-min-release-age

Conversation

@gibiw

@gibiw gibiw commented May 13, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds a 3-day cooldown on PyPI artifacts in CI as the analog of npm's
min-release-age=3. Defends against the recent wave of PyPI/npm
hijacks where attackers publish a malicious version and have a few
hours before maintainers notice.

What changed

  • .github/workflows/pythonpackage.yml:
    • New workflow-level env: PIP_UPLOADED_PRIOR_TO: "P3D" — propagates
      to every step and subprocess (tox, python -m build, integration
      test venvs).
    • Every explicit pip install --upgrade pip is now pinned to
      pip>=26.1 (the version that learned the relative-duration form
      P3D).
    • Each of the 4 integration-test venvs (pytest, behave, robot,
      tavern) gets a pip install --upgrade 'pip>=26.1' before its
      real install, because python -m venv seeds whatever pip
      ensurepip bundles (older than 26.0 on every Python version in
      the matrix).
  • pip.conf: new file at repo root. Developers can opt in locally
    with export PIP_CONFIG_FILE=$(git rev-parse --show-toplevel)/pip.conf.

Why P3D and not larger / smaller

3 days was the requested value (mirrors npm min-release-age=3).
Roughly: long enough for the community to flag a hijack, short enough
to not block legitimate security releases of upstream deps.

Bootstrap safety

The python -m pip install --upgrade 'pip>=26.1' step runs with the
env var already set under an older pip (the one setup-python seeds).
Older pip (≤25) does not error on unknown PIP_* env vars — see
pypa/pip#8523 — so the
bootstrap upgrade is safe.

Known gap

The test job runs tox, and tox creates venvs via virtualenv
whose seeded pip is older than 26.1. Test dependencies (pytest,
pytest-cov, pytest-bdd) installed inside those venvs are therefore
not protected by the cooldown on this PR. The env var is
inherited but silently ignored.

Closing this gap fully would require either:

  • per-package [testenv] install_command overrides across 7 tox
    configs (brittle shell quoting), or
  • bumping virtualenv's bundled-wheel pin until it ships pip>=26.1, or
  • migrating tox to tox-uv.

Deferred to a follow-up. The high-value paths (build/publish job and
integration-test job) are covered by this PR.

Test plan

  • CI green on the test matrix (Python 3.9–3.13 × 7 packages)
  • CI green on integration-test (4 reporters validated)
  • On the next release tag, build-n-publish produces a wheel
    with no <3-day-old transitive deps
  • Local: PIP_CONFIG_FILE=$(pwd)/pip.conf pip install --dry-run somepkg honors the cooldown

Defense against the recent wave of PyPI hijack attacks where attackers
publish a malicious version and have a few hours before maintainers
notice. Mirrors npm's `min-release-age=3` setting.

- Set PIP_UPLOADED_PRIOR_TO=P3D at workflow level so all pip
  subprocesses (integration test venvs, `python -m build`) refuse
  PyPI artifacts younger than 3 days.
- Pin pip>=26.1 in every explicit upgrade step — the relative-duration
  form `P3D` was added in pip 26.1.
- Wrap every pip self-upgrade in `env -u PIP_UPLOADED_PRIOR_TO`.
  GitHub runners ship pip 26.0, which knows --uploaded-prior-to but
  only accepts ISO datetimes; passing P3D fails validation before the
  upgrade can land.
- In the publish job, split build-deps install into two pip calls so
  setuptools/wheel/build are installed under the cooldown rather than
  the bootstrap.
- Add a repo-level pip.conf so local developers can opt in by
  pointing PIP_CONFIG_FILE at it.

Known gap: the test job's tox-managed venvs install testing deps
(pytest, pytest-cov, pytest-bdd) through pip seeded by virtualenv,
which currently ships an older pip that does not honor the env var.
Closing this fully would require either per-package tox config
changes across 7 packages or a virtualenv bundle bump; deferred.
@gibiw gibiw force-pushed the build/pip-min-release-age branch from 853b0d7 to 7d68f57 Compare May 13, 2026 10:25
@gibiw gibiw merged commit a015644 into main May 13, 2026
37 checks passed
@gibiw gibiw deleted the build/pip-min-release-age branch May 13, 2026 10:29
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.

1 participant