Skip to content

Commit 1a43844

Browse files
ci(py-315): prune cp315 wheels at every publication choke point
PR #19880 filtered cp315 out of the PyPI upload only, on the stated premise that the other destination was "internal S3 and the private prerelease index". That is wrong: s3://dd-trace-py-builds is anonymously readable and listable and is registered in DataDog/cloud-inventory as a "Public bucket to host dev builds of dd-trace-py from GitLab to share with customers". The pipeline republished cp315 wheels to main/ on 2026-08-27, so the leak was live, not historical. Three mechanisms publish a ddtrace wheel outside the pipeline: twine to PyPI, aws s3 cp to that bucket, and adms to pypi-private-prereleases. All three now call .gitlab/scripts/prune-unsupported-wheels.sh first, which holds the withheld ABI tags in one place. Putting it inside upload-wheels-to-s3.sh covers every S3 caller and every index suffix, including "upload serverless", which no filter reached before. The PyPI-side shell in release.yml reverts to the pre-filter form plus one prune call, per review feedback: with cp315 gone from the "ddtrace package" artifact, twine check no longer needs a hand-built file list. tests/internal/test_unsupported_wheel_pruning.py replaces the PyPI-specific test. It pins the prune behaviour and asserts that every file under .gitlab/ running a publish command also calls the prune script, so a new upload path cannot silently reintroduce cp315.
1 parent 959c3bc commit 1a43844

6 files changed

Lines changed: 354 additions & 257 deletions

File tree

.gitlab/package.yml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,8 @@ variables:
106106
IMAGE_TAG: *AARCH64_IMAGES
107107
# TODO(py-315): drop the cp315 allow_failure rule once #17849 ports the bytecode
108108
# wrapping path to sys.monitoring (currently raises NotImplementedError on 3.15).
109+
# The cp315 wheels this matrix produces are kept for CI signal only and are deleted
110+
# before every publication step by .gitlab/scripts/prune-unsupported-wheels.sh.
109111
rules:
110112
- if: $PYTHON_TAG == "cp315-cp315"
111113
allow_failure: true
@@ -226,6 +228,9 @@ variables:
226228
echo "Version '${PACKAGE_VERSION:-}' has no local segment (+...) -- nothing to patch, exiting."
227229
exit 0
228230
fi
231+
# The prerelease index exists to hand custom builds to customers and internal services,
232+
# so it gets the same treatment as PyPI and S3.
233+
- .gitlab/scripts/prune-unsupported-wheels.sh pywheels
229234
- python3 .gitlab/scripts/patch-wheel-versions.py pywheels pywheels-patched
230235
- |
231236
set -euo pipefail
@@ -497,6 +502,8 @@ download_dependency_wheels:
497502
- "pywheels/*.whl"
498503
- "pywheels/*.tar.gz"
499504

505+
# This job's artifact is what release_pypi_prod uploads to PyPI and what "upload all"
506+
# publishes as the default commit-scoped S3 index, so pruning here covers both.
500507
"ddtrace package":
501508
extends: .package_base
502509
needs:
@@ -507,6 +514,7 @@ download_dependency_wheels:
507514
- "collect windows wheels"
508515
- "package version"
509516
script:
517+
- .gitlab/scripts/prune-unsupported-wheels.sh pywheels
510518
- .gitlab/validate-ddtrace-package.py pywheels
511519

512520
"ddtrace package serverless":
@@ -515,4 +523,5 @@ download_dependency_wheels:
515523
- "build linux serverless"
516524
- "package version"
517525
script:
526+
- .gitlab/scripts/prune-unsupported-wheels.sh pywheels
518527
- .gitlab/validate-ddtrace-package.py pywheels --mode=serverless

.gitlab/release.yml

Lines changed: 9 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -15,28 +15,15 @@
1515
TWINE_NON_INTERACTIVE: "1"
1616
script:
1717
- export TWINE_PASSWORD=$(aws ssm get-parameter --region us-east-1 --name "ci.${CI_PROJECT_NAME}.${PYPI_REPOSITORY}_token" --with-decryption --query "Parameter.Value" --out text)
18-
# TODO(py-315): cp315 wheels are built for internal S3 and the private prerelease index,
19-
# and are withheld from PyPI while Python 3.15 is unsupported. Drop this filter once
20-
# 3.15 is a supported target. Everything else, sdist included, is still uploaded.
21-
#
22-
# The list is built before "twine check" so both steps see exactly the same
23-
# distributions: a malformed cp315 wheel from an allow_failure build must not fail a
24-
# release whose supported wheels are all fine.
25-
- |
26-
set -eu
27-
set --
28-
for dist in pywheels/ddtrace-*; do
29-
case "${dist}" in *cp315*) continue ;; esac
30-
if [ -f "${dist}" ]; then set -- "$@" "${dist}"; fi
31-
done
32-
if [ "$#" -eq 0 ]; then
33-
echo "[ERROR] no PyPI-eligible distributions found in pywheels/ -- refusing to upload" >&2
34-
exit 1
35-
fi
36-
printf 'Uploading %s distribution(s) to %s:\n' "$#" "${PYPI_REPOSITORY}"
37-
printf ' %s\n' "$@"
38-
uvx --with="twine>=5.0,<7" twine check --strict "$@"
39-
uvx --with="twine>=5.0,<7" twine upload --repository "${PYPI_REPOSITORY}" "$@"
18+
# "ddtrace package" already pruned cp315 out of the artifact this job inherits. Pruning
19+
# again keeps the invariant local and checkable: every publish command under .gitlab/
20+
# is preceded by a prune call, and tests/internal/test_unsupported_wheel_pruning.py
21+
# fails if a new one is not. It also means "twine check" and "twine upload" see the same
22+
# set, so a malformed cp315 wheel from an allow_failure build cannot fail a release
23+
# whose supported wheels are all fine.
24+
- .gitlab/scripts/prune-unsupported-wheels.sh pywheels
25+
- uvx --with="twine>=5.0,<7" twine check --strict pywheels/*
26+
- uvx --with="twine>=5.0,<7" twine upload --repository ${PYPI_REPOSITORY} pywheels/ddtrace-*
4027
artifacts:
4128
paths:
4229
- pywheels/*.whl
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
#!/usr/bin/env bash
2+
# Deletes wheels whose Python ABI tag is built for CI signal but is not fit to publish.
3+
#
4+
# Usage: prune-unsupported-wheels.sh <dir> [<dir> ...]
5+
#
6+
# Every job that hands a ddtrace wheel to something outside this pipeline must call this
7+
# first. Today that is:
8+
# * .gitlab/scripts/upload-wheels-to-s3.sh -> the public dd-trace-py-builds bucket
9+
# * .gitlab/release.yml (.release_pypi) -> PyPI
10+
# * .gitlab/package.yml ("ddtrace package") -> the artifact release_pypi_prod and
11+
# "upload all" consume
12+
# * .gitlab/package.yml (.patch_wheel_versions_base) -> the pypi-private-prereleases index
13+
#
14+
# tests/internal/test_unsupported_wheel_pruning.py asserts that every file under .gitlab/
15+
# that runs a publish command also calls this script, so a new upload path cannot skip it
16+
# without failing CI.
17+
#
18+
# Debug symbol archives (debugwheelhouse/*.zip) are deliberately not pruned: they are not
19+
# installable and carry no ABI risk. Pass only wheel directories.
20+
21+
set -euo pipefail
22+
23+
# Python ABI tags withheld from every publication channel. Single source of truth.
24+
#
25+
# TODO(py-315): cp315 wheels are built by "build linux" and "build linux serverless" under
26+
# allow_failure so that 3.15 keeps producing CI signal, but they are compiled against the
27+
# 3.15.0b1 PyThreadState layout and are ABI-broken. Drop cp315 from this list once #19861
28+
# makes those wheels correct and 3.15 is a supported target.
29+
UNSUPPORTED_TAGS=("cp315")
30+
31+
if [ "$#" -eq 0 ]; then
32+
echo "Usage: $0 <dir> [<dir> ...]" >&2
33+
exit 1
34+
fi
35+
36+
shopt -s nullglob
37+
38+
for dir in "$@"; do
39+
if [ ! -d "${dir}" ]; then
40+
echo "[ERROR] ${dir} is not a directory -- refusing to publish unpruned wheels" >&2
41+
exit 1
42+
fi
43+
for tag in "${UNSUPPORTED_TAGS[@]}"; do
44+
matches=("${dir}"/*"${tag}"*.whl)
45+
if [ ${#matches[@]} -eq 0 ]; then
46+
continue
47+
fi
48+
printf 'Pruning %s unsupported %s wheel(s) from %s/:\n' "${#matches[@]}" "${tag}" "${dir}"
49+
printf ' %s\n' "${matches[@]}"
50+
rm -f -- "${matches[@]}"
51+
done
52+
done

.gitlab/scripts/upload-wheels-to-s3.sh

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,16 @@ if [ -z "$S3_PATH" ]; then
1818
fi
1919

2020
shopt -s nullglob
21+
22+
if [ ! -d pywheels ]; then
23+
echo "No packages found in pywheels/"
24+
exit 0
25+
fi
26+
27+
# s3://dd-trace-py-builds is anonymously readable and listable, so nothing unsupported may
28+
# leave via this script. Every caller and every index suffix goes through here.
29+
.gitlab/scripts/prune-unsupported-wheels.sh pywheels
30+
2131
WHEELS=(pywheels/*.whl pywheels/*.tar.gz)
2232

2333
if [ ${#WHEELS[@]} -eq 0 ]; then

tests/internal/test_release_pypi_filter.py

Lines changed: 0 additions & 235 deletions
This file was deleted.

0 commit comments

Comments
 (0)