diff --git a/.github/workflows/backport.yml b/.github/workflows/backport.yml
index 84bb3d9007..6d41b99611 100644
--- a/.github/workflows/backport.yml
+++ b/.github/workflows/backport.yml
@@ -24,8 +24,7 @@ jobs:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
with:
- # zizmor: ignore[artipacked] Credentials are required by the branch push below.
- persist-credentials: true
+ persist-credentials: false
# history is needed to run git cherry-pick below
fetch-depth: 0
@@ -43,8 +42,9 @@ jobs:
id: create_pr
env:
NUMBER: ${{ github.event.inputs.number }}
- # not using secrets.GITHUB_TOKEN since pull requests from that token do not run workflows
- GITHUB_TOKEN: ${{ steps.otelbot-token.outputs.token }}
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ # not using secrets.GITHUB_TOKEN for pr create since pull requests from that token do not run workflows
+ PR_TOKEN: ${{ steps.otelbot-token.outputs.token }}
run: |
commit=$(gh pr view $NUMBER --json mergeCommit --jq .mergeCommit.oid)
title=$(gh pr view $NUMBER --json title --jq .title)
@@ -52,8 +52,9 @@ jobs:
branch="otelbot/backport-${NUMBER}-to-${GITHUB_REF_NAME//\//-}"
git cherry-pick $commit
+ gh auth setup-git
git push origin HEAD:$branch
- pr_url=$(gh pr create --title "[$GITHUB_REF_NAME] $title" \
+ pr_url=$(GH_TOKEN="${PR_TOKEN}" gh pr create --title "[$GITHUB_REF_NAME] $title" \
--body "Clean cherry-pick of #$NUMBER to the \`$GITHUB_REF_NAME\` branch." \
--head $branch \
--base $GITHUB_REF_NAME)
diff --git a/.github/workflows/package-prepare-patch-release.yml b/.github/workflows/package-prepare-patch-release.yml
index 5ee0e13eed..197a917425 100644
--- a/.github/workflows/package-prepare-patch-release.yml
+++ b/.github/workflows/package-prepare-patch-release.yml
@@ -30,8 +30,7 @@ jobs:
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
with:
- # zizmor: ignore[artipacked] Credentials are required by the branch push below.
- persist-credentials: true
+ persist-credentials: false
- name: Verify prerequisites
run: |
@@ -120,15 +119,17 @@ jobs:
- name: Create pull request
env:
- # not using secrets.GITHUB_TOKEN since pull requests from that token do not run workflows
- GITHUB_TOKEN: ${{ steps.otelbot-token.outputs.token }}
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ # not using secrets.GITHUB_TOKEN for pr create since pull requests from that token do not run workflows
+ PR_TOKEN: ${{ steps.otelbot-token.outputs.token }}
run: |
message="Prepare patch release for ${PACKAGE_NAME} v${NEXT_VERSION}"
branch="otelbot/patch-${PACKAGE_NAME}-version-to-v${NEXT_VERSION}"
git commit -a -m "$message"
+ gh auth setup-git
git push origin HEAD:$branch
- gh pr create --title "[$GITHUB_REF_NAME] $message" \
+ GH_TOKEN="${PR_TOKEN}" gh pr create --title "[$GITHUB_REF_NAME] $message" \
--body "$message." \
--head $branch \
--base $GITHUB_REF_NAME
diff --git a/.github/workflows/package-prepare-release.yml b/.github/workflows/package-prepare-release.yml
index 968adfd2d4..22cef36d94 100644
--- a/.github/workflows/package-prepare-release.yml
+++ b/.github/workflows/package-prepare-release.yml
@@ -100,8 +100,7 @@ jobs:
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
with:
- # zizmor: ignore[artipacked] Credentials are required by the branch pushes below.
- persist-credentials: true
+ persist-credentials: false
- name: Set environment variables
env:
@@ -118,7 +117,10 @@ jobs:
echo "PACKAGE_NAME=$PACKAGE_NAME_OUTPUT" >> $GITHUB_ENV
- name: Create package release branch
+ env:
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
+ gh auth setup-git
git push origin HEAD:$RELEASE_BRANCH_NAME
- name: Update package version
@@ -152,15 +154,17 @@ jobs:
- name: Create pull request against the release branch
env:
- # not using secrets.GITHUB_TOKEN since pull requests from that token do not run workflows
- GITHUB_TOKEN: ${{ steps.otelbot-token.outputs.token }}
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ # not using secrets.GITHUB_TOKEN for pr create since pull requests from that token do not run workflows
+ PR_TOKEN: ${{ steps.otelbot-token.outputs.token }}
run: |
message="Prepare release for ${PACKAGE_NAME} v${VERSION}"
branch="otelbot/prepare-${RELEASE_BRANCH_NAME}"
git commit -a -m "$message"
+ gh auth setup-git
git push origin HEAD:$branch
- gh pr create --title "[$RELEASE_BRANCH_NAME] $message" \
+ GH_TOKEN="${PR_TOKEN}" gh pr create --title "[$RELEASE_BRANCH_NAME] $message" \
--body "$message." \
--head $branch \
--base $RELEASE_BRANCH_NAME
@@ -174,8 +178,7 @@ jobs:
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
with:
- # zizmor: ignore[artipacked] Credentials are required by the branch push below.
- persist-credentials: true
+ persist-credentials: false
- name: Set environment variables
env:
@@ -222,16 +225,18 @@ jobs:
- name: Create pull request against main
env:
- # not using secrets.GITHUB_TOKEN since pull requests from that token do not run workflows
- GITHUB_TOKEN: ${{ steps.otelbot-token-main.outputs.token }}
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ # not using secrets.GITHUB_TOKEN for pr create since pull requests from that token do not run workflows
+ PR_TOKEN: ${{ steps.otelbot-token-main.outputs.token }}
run: |
message="Update ${PACKAGE_NAME} version to v${NEXT_VERSION}"
body="Update \`${PACKAGE_NAME}\` version to v\`${NEXT_VERSION}\`."
branch="otelbot/update-${PACKAGE_NAME}-version-to-v${NEXT_VERSION}"
git commit -a -m "$message"
+ gh auth setup-git
git push origin HEAD:$branch
- gh pr create --title "$message" \
+ GH_TOKEN="${PR_TOKEN}" gh pr create --title "$message" \
--body "$body" \
--head $branch \
--base main
diff --git a/.github/workflows/package-release.yml b/.github/workflows/package-release.yml
index f89f03d5ad..4ecf2700de 100644
--- a/.github/workflows/package-release.yml
+++ b/.github/workflows/package-release.yml
@@ -25,6 +25,8 @@ jobs:
permissions:
contents: write # required for creating releases
pull-requests: write # required for creating pull requests
+ id-token: write # required for generating OIDC token for attestation
+ attestations: write # required for persisting build provenance attestations
runs-on: ubuntu-latest
steps:
- run: |
@@ -35,6 +37,7 @@ jobs:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
with:
+ ref: ${{ github.sha }}
persist-credentials: false
- name: Set environment variables
@@ -87,6 +90,7 @@ jobs:
# back to the release branch
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
with:
+ ref: ${{ github.sha }}
persist-credentials: false
# next few steps publish to pypi
@@ -100,6 +104,11 @@ jobs:
- name: Build wheels
run: ./scripts/build_a_package.sh
+ - name: Attest build provenance
+ uses: actions/attest@1e69f48acb82d1966a394da916b4c1698aa569d6 # v4.2.2
+ with:
+ subject-path: "dist/*"
+
- name: Install twine
run: |
uv sync --locked --only-group twine
@@ -135,17 +144,19 @@ jobs:
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
- gh release create --target $GITHUB_REF_NAME \
+ # Always tag the exact immutable commit SHA that was built, never a mutable branch ref
+ release_target=$(git rev-parse HEAD)
+ gh release create --target "$release_target" \
--title "${PACKAGE_NAME} ${VERSION}" \
--notes-file /tmp/release-notes.txt \
--discussion-category announcements \
- $RELEASE_TAG
+ $RELEASE_TAG \
+ dist/*
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
with:
ref: main
- # zizmor: ignore[artipacked] Credentials are required by the branch push below.
- persist-credentials: true
+ persist-credentials: false
- name: Copy change log updates to main
env:
@@ -164,8 +175,9 @@ jobs:
- name: Create pull request against main
env:
- # not using secrets.GITHUB_TOKEN since pull requests from that token do not run workflows
- GITHUB_TOKEN: ${{ steps.otelbot-token.outputs.token }}
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ # not using secrets.GITHUB_TOKEN for pr create since pull requests from that token do not run workflows
+ PR_TOKEN: ${{ steps.otelbot-token.outputs.token }}
run: |
message="Copy changelog updates from $GITHUB_REF_NAME"
body="Copy changelog updates from \`$GITHUB_REF_NAME\`."
@@ -179,8 +191,9 @@ jobs:
fi
git commit -a -m "$message"
+ gh auth setup-git
git push origin HEAD:$branch
- gh pr create --title "$message" \
+ GH_TOKEN="${PR_TOKEN}" gh pr create --title "$message" \
--body "$body" \
--head $branch \
--base main
diff --git a/.github/workflows/prepare-patch-release.yml b/.github/workflows/prepare-patch-release.yml
index 73ae0f0c77..492b7347bd 100644
--- a/.github/workflows/prepare-patch-release.yml
+++ b/.github/workflows/prepare-patch-release.yml
@@ -14,8 +14,7 @@ jobs:
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
with:
- # zizmor: ignore[artipacked] Credentials are required by the branch push below.
- persist-credentials: true
+ persist-credentials: false
- run: |
if [[ ! $GITHUB_REF_NAME =~ ^release/v[0-9]+\.[0-9]+\.x-0\.[0-9]+bx$ ]]; then
@@ -84,15 +83,17 @@ jobs:
- name: Create pull request
id: create_pr
env:
- # not using secrets.GITHUB_TOKEN since pull requests from that token do not run workflows
- GITHUB_TOKEN: ${{ steps.otelbot-token.outputs.token }}
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ # not using secrets.GITHUB_TOKEN for pr create since pull requests from that token do not run workflows
+ PR_TOKEN: ${{ steps.otelbot-token.outputs.token }}
run: |
message="Prepare release ${STABLE_VERSION}/${UNSTABLE_VERSION}"
branch="otelbot/prepare-release-${STABLE_VERSION}-${UNSTABLE_VERSION}"
git commit -a -m "$message"
+ gh auth setup-git
git push origin HEAD:$branch
- pr_url=$(gh pr create --title "[$GITHUB_REF_NAME] $message" \
+ pr_url=$(GH_TOKEN="${PR_TOKEN}" gh pr create --title "[$GITHUB_REF_NAME] $message" \
--body "$message." \
--head $branch \
--base $GITHUB_REF_NAME)
@@ -109,8 +110,7 @@ jobs:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
with:
ref: main
- # zizmor: ignore[artipacked] Credentials are required by the branch push below.
- persist-credentials: true
+ persist-credentials: false
- name: Use CLA approved github bot
run: .github/scripts/use-cla-approved-github-bot.sh
@@ -118,7 +118,8 @@ jobs:
- name: Backport patch release changelog to main
id: backport_pr
env:
- GITHUB_TOKEN: ${{ steps.otelbot-token.outputs.token }}
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ PR_TOKEN: ${{ steps.otelbot-token.outputs.token }}
run: |
release_branch="otelbot/prepare-release-${STABLE_VERSION}-${UNSTABLE_VERSION}"
message="Backport ${STABLE_VERSION}/${UNSTABLE_VERSION} changelog"
@@ -131,8 +132,9 @@ jobs:
git checkout FETCH_HEAD -- CHANGELOG.md
git commit -m "$message"
+ gh auth setup-git
git push origin HEAD:$branch
- pr_url=$(gh pr create --title "$message" \
+ pr_url=$(GH_TOKEN="${PR_TOKEN}" gh pr create --title "$message" \
--body "$body" \
--head $branch \
--base main)
diff --git a/.github/workflows/prepare-release-branch.yml b/.github/workflows/prepare-release-branch.yml
index e3952bf45d..a9e7180906 100644
--- a/.github/workflows/prepare-release-branch.yml
+++ b/.github/workflows/prepare-release-branch.yml
@@ -44,11 +44,11 @@ jobs:
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
with:
- # zizmor: ignore[artipacked] Credentials are required by the branch pushes below.
- persist-credentials: true
+ persist-credentials: false
- name: Create release branch
env:
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
PRERELEASE_VERSION: ${{ github.event.inputs.prerelease_version }}
run: |
if [[ -z $PRERELEASE_VERSION ]]; then
@@ -73,6 +73,7 @@ jobs:
exit 1
fi
+ gh auth setup-git
git push origin HEAD:$release_branch_name
echo "STABLE_VERSION=$stable_version" >> $GITHUB_ENV
@@ -109,15 +110,17 @@ jobs:
- name: Create pull request against the release branch
id: create_release_branch_pr
env:
- # not using secrets.GITHUB_TOKEN since pull requests from that token do not run workflows
- GITHUB_TOKEN: ${{ steps.otelbot-token.outputs.token }}
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ # not using secrets.GITHUB_TOKEN for pr create since pull requests from that token do not run workflows
+ PR_TOKEN: ${{ steps.otelbot-token.outputs.token }}
run: |
message="Prepare release ${STABLE_VERSION}/${UNSTABLE_VERSION}"
branch="otelbot/prepare-release-${STABLE_VERSION}-${UNSTABLE_VERSION}"
git commit -a -m "$message"
+ gh auth setup-git
git push origin HEAD:$branch
- pr_url=$(gh pr create --title "[$RELEASE_BRANCH_NAME] $message" \
+ pr_url=$(GH_TOKEN="${PR_TOKEN}" gh pr create --title "[$RELEASE_BRANCH_NAME] $message" \
--body "$message." \
--head $branch \
--base $RELEASE_BRANCH_NAME)
@@ -140,8 +143,7 @@ jobs:
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
with:
- # zizmor: ignore[artipacked] Credentials are required by the branch push below.
- persist-credentials: true
+ persist-credentials: false
- name: Set environment variables
env:
@@ -216,16 +218,18 @@ jobs:
- name: Create pull request against main
id: create_main_pr
env:
- # not using secrets.GITHUB_TOKEN since pull requests from that token do not run workflows
- GITHUB_TOKEN: ${{ steps.otelbot-token-main.outputs.token }}
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ # not using secrets.GITHUB_TOKEN for pr create since pull requests from that token do not run workflows
+ PR_TOKEN: ${{ steps.otelbot-token-main.outputs.token }}
run: |
message="Update version to ${STABLE_NEXT_VERSION}/${UNSTABLE_NEXT_VERSION}"
body="Update version to \`${STABLE_NEXT_VERSION}/${UNSTABLE_NEXT_VERSION}\`."
branch="otelbot/update-version-to-${STABLE_NEXT_VERSION}-${UNSTABLE_NEXT_VERSION}"
git commit -a -m "$message"
+ gh auth setup-git
git push origin HEAD:$branch
- pr_url=$(gh pr create --title "$message" \
+ pr_url=$(GH_TOKEN="${PR_TOKEN}" gh pr create --title "$message" \
--body "$body" \
--head $branch \
--base main)
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 5306335993..b27d3dbe4b 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -9,6 +9,8 @@ jobs:
release:
permissions:
contents: write # required for creating releases
+ id-token: write # required for generating OIDC token for attestation
+ attestations: write # required for persisting build provenance attestations
runs-on: ubuntu-latest
steps:
- run: |
@@ -19,6 +21,7 @@ jobs:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
with:
+ ref: ${{ github.sha }}
persist-credentials: false
- name: Set environment variables
@@ -61,18 +64,6 @@ jobs:
fi
fi
- # check out main branch to verify there won't be problems with merging the change log
- # at the end of this workflow
- - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
- with:
- ref: main
- persist-credentials: false
-
- # back to the release branch
- - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
- with:
- persist-credentials: false
-
# next few steps publish to pypi
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
with:
@@ -84,6 +75,11 @@ jobs:
- name: Build wheels
run: ./scripts/build.sh
+ - name: Attest build provenance
+ uses: actions/attest@1e69f48acb82d1966a394da916b4c1698aa569d6 # v4.2.2
+ with:
+ subject-path: "dist/*"
+
- name: Install twine
run: |
uv sync --locked --only-group twine
@@ -120,15 +116,11 @@ jobs:
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
- gh release create --target $GITHUB_REF_NAME \
+ # Always tag the exact immutable commit SHA that was built, never a mutable branch ref
+ release_target=$(git rev-parse HEAD)
+ gh release create --target "$release_target" \
--title "Version ${STABLE_VERSION}/${UNSTABLE_VERSION}" \
--notes-file /tmp/release-notes.txt \
--discussion-category announcements \
- v$UNSTABLE_VERSION
-
- - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
- with:
- # the step below is creating a pull request against main
- ref: main
- persist-credentials: false
-
+ v$UNSTABLE_VERSION \
+ dist/*
diff --git a/RELEASING.md b/RELEASING.md
index 45f26681f3..c099684e49 100644
--- a/RELEASING.md
+++ b/RELEASING.md
@@ -152,6 +152,26 @@ The workflow can only be run against long-term release branch such as `package-r
When a contribution introduces a new package, in order to mitigate name-squatting incidents, reserve the package name under the `opentelemetry` organization on PyPI to simply claim the namespace (rather than releasing a dev version). This should be done shortly after the PR that introduced this package has been merged into `main` by navigating to , scrolling to the bottom (**Add project to organization**), and using the form.
+## Verifying Release Provenance
+
+Every release publishes a **GitHub SLSA Build Provenance Attestation** and attaches the exact `.whl` and `.tar.gz` files to the GitHub Release tag (`v` for general releases, or `==` for per-package releases).
+
+Users can cryptographically verify that a downloaded wheel or sdist was built by this repository's official workflow from the tagged commit SHA:
+
+```bash
+# For general releases (tagged v):
+gh attestation verify \
+ --repo open-telemetry/opentelemetry-python-contrib \
+ --signer-workflow open-telemetry/opentelemetry-python-contrib/.github/workflows/release.yml \
+ --source-digest "$(git rev-parse v^{commit})"
+
+# For per-package releases (tagged ==):
+gh attestation verify \
+ --repo open-telemetry/opentelemetry-python-contrib \
+ --signer-workflow open-telemetry/opentelemetry-python-contrib/.github/workflows/package-release.yml \
+ --source-digest "$(git rev-parse '==^{commit}')"
+```
+
## Troubleshooting
### Publish failed
diff --git a/scripts/build.sh b/scripts/build.sh
index 867308e59a..00ad4c8afa 100755
--- a/scripts/build.sh
+++ b/scripts/build.sh
@@ -5,7 +5,7 @@
set -ev
-BASEDIR=$(dirname "$(readlink -f "$(dirname $0)")")
+BASEDIR=$(dirname "$(readlink -f "$(dirname "$0")")")
DISTDIR=dist
# Get the versions of packaging tools recorded in the lockfile
@@ -13,10 +13,22 @@ uv sync --locked --only-group build --project "$BASEDIR"
PATH="$BASEDIR/.venv/bin:$PATH"
export PATH
+# Exclude packages uploaded within the last 3 days across subsequent uv invocations,
+# ensuring build backend dependencies (e.g. hatchling) also inherit the cutoff.
+export UV_EXCLUDE_NEWER=3d
+
(
- cd $BASEDIR
- mkdir -p $DISTDIR
- rm -rf ${DISTDIR:?}/*
+ cd "$BASEDIR"
+
+ # Ensure no untracked or modified files exist in the checkout before building
+ if [ -n "$(git status --porcelain)" ]; then
+ echo "Error! Working tree is dirty before build:"
+ git status --porcelain
+ exit 1
+ fi
+
+ mkdir -p "$DISTDIR"
+ rm -rf "${DISTDIR:?}/"*
# this is called by release workflow so don't list here independently released packages
for d in exporter/*/ opentelemetry-instrumentation/ opentelemetry-contrib-instrumentations/ opentelemetry-distro/ instrumentation/*/ processor/*/ propagator/*/ resource/*/ sdk-extension/*/ util/*/ ; do
@@ -26,30 +38,62 @@ export PATH
# Some ext directories (such as docker tests) are not intended to be
# packaged. Verify the intent by looking for a pyproject.toml.
if [ -f pyproject.toml ]; then
- python3 -m build --outdir "$BASEDIR/dist/"
+ uv build --out-dir "$BASEDIR/$DISTDIR/"
fi
)
done
+ rm -f "$BASEDIR/$DISTDIR/.gitignore"
(
- cd $DISTDIR
+ cd "$DISTDIR"
for x in * ; do
# FIXME: Remove this once opentelemetry-resource-detector-azure package goes 1.X
if echo "$x" | grep -Eq "^opentelemetry_(resource_detector_azure|util_genai).*(\.tar\.gz|\.whl)$"; then
echo "Skipping $x because of manual upload by Azure maintainers."
- rm $x
+ rm "$x"
# opentelemetry-instrumentation-valkey-py is released independently via package-release.yml
elif echo "$x" | grep -Eq "^opentelemetry_instrumentation_valkey_py-.*(\.tar\.gz|\.whl)$"; then
echo "Skipping $x because it is released independently via package-release.yml."
- rm $x
+ rm "$x"
# NOTE: We filter beta vs 1.0 package at this point because we can read the
# version directly from the .tar.gz/whl file
elif echo "$x" | grep -Eq "^opentelemetry_.*-0\..*(\.tar\.gz|\.whl)$"; then
:
else
echo "Skipping $x because it is not in pre-1.0 state and should be released using a tag."
- rm $x
+ rm "$x"
fi
done
)
+
+ # Audit built wheel contents against startup-hook (.pth / sitecustomize.py)
+ # and *.data/ injection attacks (e.g., LiteLLM v1.107.0 supply-chain vector).
+ python3 - "$BASEDIR/$DISTDIR" <<'PYEOF'
+import pathlib
+import sys
+import zipfile
+
+distdir = pathlib.Path(sys.argv[1])
+wheels = sorted(distdir.glob("*.whl"))
+if not wheels:
+ sys.exit(f"Error! No wheels found in {distdir}")
+
+for wheel in wheels:
+ dist_and_version = "-".join(wheel.name.split("-")[:2])
+ expected_dist_info = f"{dist_and_version}.dist-info/"
+ with zipfile.ZipFile(wheel) as zf:
+ for name in zf.namelist():
+ basename = pathlib.PurePosixPath(name).name
+ if name.endswith(".pth") or (
+ basename in {"sitecustomize.py", "usercustomize.py"}
+ and name != "opentelemetry/instrumentation/auto_instrumentation/sitecustomize.py"
+ ):
+ sys.exit(f"Error! Forbidden startup hook file in {wheel.name}: {name}")
+ if not (name.startswith("opentelemetry/") or name.startswith(expected_dist_info)):
+ sys.exit(
+ f"Error! Unexpected top-level path in {wheel.name}: {name} "
+ f"(expected 'opentelemetry/' or '{expected_dist_info}')"
+ )
+ print(f"Verified wheel contents for {wheel.name} ({len(zf.namelist())} files).")
+PYEOF
)
diff --git a/scripts/build_a_package.sh b/scripts/build_a_package.sh
index d4c84fb243..8c056b3d99 100755
--- a/scripts/build_a_package.sh
+++ b/scripts/build_a_package.sh
@@ -40,11 +40,22 @@ uv sync --locked --only-group build --project "$basedir"
PATH="$basedir/.venv/bin:$PATH"
export PATH
+# Exclude packages uploaded within the last 3 days across subsequent uv invocations,
+# ensuring build backend dependencies (e.g. hatchling) also inherit the cutoff.
+export UV_EXCLUDE_NEWER=3d
+
# Validate version against PEP 440 conventions: https://packaging.pypa.io/en/latest/version.html
python3 -c "from packaging.version import Version; Version('${pkg_version}')"
cd $basedir
+# Ensure no untracked or modified files exist in the checkout before building
+if [ -n "$(git status --porcelain)" ]; then
+ echo "Error! Working tree is dirty before build:"
+ git status --porcelain
+ exit 1
+fi
+
distdir=${basedir}/dist
mkdir -p $distdir
rm -rf ${distdir:?}/*
@@ -60,7 +71,8 @@ directory_with_package=$(dirname $pyproject_toml_file_path)
cd $directory_with_package
-python3 -m build --outdir ${distdir}
+uv build --sdist --out-dir ${distdir}
+rm -f "${distdir}/.gitignore"
cd $distdir
@@ -77,4 +89,38 @@ if ! [ -f $pkg_tar_gz_file ]; then
fi
# Build a wheel for the source distribution
-pip wheel --no-deps $pkg_tar_gz_file
+uv build --wheel --out-dir ${distdir} $pkg_tar_gz_file
+rm -f "${distdir}/.gitignore"
+
+# Audit built wheel contents against startup-hook (.pth / sitecustomize.py)
+# and *.data/ injection attacks (e.g., LiteLLM v1.107.0 supply-chain vector).
+python3 - "$distdir" "$pkg_name" "$pkg_version" <<'PYEOF'
+import pathlib
+import sys
+import zipfile
+
+distdir = pathlib.Path(sys.argv[1])
+pkg_name_norm = sys.argv[2].replace("-", "_")
+pkg_version = sys.argv[3]
+expected_dist_info = f"{pkg_name_norm}-{pkg_version}.dist-info/"
+
+wheels = list(distdir.glob(f"{pkg_name_norm}-{pkg_version}-*.whl"))
+if len(wheels) != 1:
+ sys.exit(f"Error! Expected exactly 1 wheel in {distdir}, found: {wheels}")
+
+with zipfile.ZipFile(wheels[0]) as zf:
+ for name in zf.namelist():
+ basename = pathlib.PurePosixPath(name).name
+ if name.endswith(".pth") or (
+ basename in {"sitecustomize.py", "usercustomize.py"}
+ and name != "opentelemetry/instrumentation/auto_instrumentation/sitecustomize.py"
+ ):
+ sys.exit(f"Error! Forbidden startup hook file in wheel: {name}")
+ if not (name.startswith("opentelemetry/") or name.startswith(expected_dist_info)):
+ sys.exit(
+ f"Error! Unexpected top-level path in wheel: {name} "
+ f"(expected 'opentelemetry/' or '{expected_dist_info}')"
+ )
+
+print(f"Verified wheel contents for {wheels[0].name} ({len(zf.namelist())} files).")
+PYEOF