From 768b03121bd05cb4ad40862e43e2247efb2d46e1 Mon Sep 17 00:00:00 2001 From: Dylan Russell Date: Mon, 9 Nov 2026 19:25:32 +0000 Subject: [PATCH] ci: add build provenance attestation and harden release workflows Assisted-by: Claude Opus 4.6 --- .github/workflows/backport.yml | 11 ++-- .../package-prepare-patch-release.yml | 11 ++-- .github/workflows/package-prepare-release.yml | 25 +++++--- .github/workflows/package-release.yml | 27 +++++--- .github/workflows/prepare-patch-release.yml | 20 +++--- .github/workflows/prepare-release-branch.yml | 24 ++++--- .github/workflows/release.yml | 34 ++++------ RELEASING.md | 20 ++++++ scripts/build.sh | 62 ++++++++++++++++--- scripts/build_a_package.sh | 50 ++++++++++++++- 10 files changed, 206 insertions(+), 78 deletions(-) diff --git a/.github/workflows/backport.yml b/.github/workflows/backport.yml index 84bb3d90075..6d41b996114 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 5ee0e13eed2..197a917425e 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 968adfd2d41..22cef36d947 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 f89f03d5ad2..4ecf2700def 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 73ae0f0c779..492b7347bd5 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 e3952bf45da..a9e7180906f 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 53063359936..b27d3dbe4bd 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 45f26681f37..c099684e493 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 867308e59aa..00ad4c8afa3 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 d4c84fb2438..8c056b3d998 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