diff --git a/.github/workflows/loongsuite-release.yml b/.github/workflows/loongsuite-release.yml
index 13d4e413c..5d9b801ed 100644
--- a/.github/workflows/loongsuite-release.yml
+++ b/.github/workflows/loongsuite-release.yml
@@ -1,6 +1,8 @@
# LoongSuite Release Workflow
#
-# This workflow handles the complete LoongSuite release process:
+# This workflow handles the complete LoongSuite release process and an optional
+# PyPI dev-bootstrap mode for creating newly added project names before a formal
+# release:
#
# 1. Create release/{version} branch from main
# 2. Build packages (bootstrap_gen.py, PyPI wheels, GitHub Release tar.gz)
@@ -14,11 +16,15 @@
# run locally for the same workflow.
#
# Trigger:
-# - workflow_dispatch: Manual trigger with version inputs
+# - workflow_dispatch: Manual trigger with mode and version inputs
# - push tags: Automatic trigger on v* tags
#
# PyPI / Test PyPI configuration:
-# - Production PyPI: Set PYPI_API_TOKEN secret, or configure OIDC trusted publishing on pypi.org
+# - Production PyPI: Set PYPI_API_TOKEN secret. This workflow currently uses
+# API-token publishing, not PyPI OIDC Trusted Publishing.
+# - If switching to OIDC Trusted Publishing later, add id-token permissions and
+# configure Pending Publishers for new PyPI project names with
+# workflow=loongsuite-release.yml and environment=pypi.
# - Test PyPI: Set TEST_PYPI_TOKEN secret (pypi-xxx from https://test.pypi.org/manage/account/token/)
# - Use publish_target: testpypi to publish to Test PyPI instead of production
#
@@ -27,38 +33,57 @@
#
name: LoongSuite Release
-run-name: "LoongSuite Release ${{ github.event.inputs.loongsuite_version || github.ref_name }}"
+# Keep this workflow filename stable; any future PyPI OIDC Trusted Publishing
+# setup would reference it from Pending Publisher entries.
+run-name: "LoongSuite ${{ github.event.inputs.mode || 'release' }}"
on:
workflow_dispatch:
inputs:
+ mode:
+ description: 'release: full release; dev-bootstrap-new-projects: publish name-reservation dev wheels for missing PyPI projects'
+ type: choice
+ options:
+ - release
+ - dev-bootstrap-new-projects
+ default: release
loongsuite_version:
- description: 'LoongSuite version (e.g., 0.1.0) - for loongsuite-* packages'
+ description: 'LoongSuite release/base version (e.g., 0.6.0). Dev bootstrap derives a unique .dev version.'
required: true
upstream_version:
- description: 'Upstream OTel version (e.g., 0.60b1) - for opentelemetry-* packages in bootstrap_gen.py'
- required: true
+ description: 'Release mode: upstream OTel version. Blank uses DEFAULT_UPSTREAM_VERSION. Ignored when mode=dev-bootstrap-new-projects.'
+ required: false
skip_pypi:
- description: 'Skip PyPI publish (for testing)'
+ description: 'Release mode only: skip PyPI publish (ignored when mode=dev-bootstrap-new-projects)'
type: boolean
default: false
publish_target:
- description: 'Publish target: pypi or testpypi'
+ description: 'Release mode only: publish target (ignored when mode=dev-bootstrap-new-projects)'
type: choice
options:
- pypi
- testpypi
default: pypi
+ dry_run:
+ description: 'Dev bootstrap only: preview missing PyPI projects without publishing (ignored in release mode)'
+ type: boolean
+ default: true
push:
tags:
- 'v*'
+concurrency:
+ group: loongsuite-release-${{ github.event.inputs.mode || 'release' }}
+ cancel-in-progress: false
+
env:
PYTHON_VERSION: '3.11'
+ DEFAULT_UPSTREAM_VERSION: '0.60b1'
jobs:
- # Build all packages, create release branch, archive changelogs
+ # Release-mode build: create release branch, build packages, and archive changelogs.
build:
+ if: ${{ github.event_name != 'workflow_dispatch' || github.event.inputs.mode == 'release' }}
runs-on: ubuntu-latest
permissions:
contents: write
@@ -72,14 +97,17 @@ jobs:
- name: Set versions from tag or input
id: version
+ env:
+ INPUT_LOONGSUITE_VERSION: ${{ github.event.inputs.loongsuite_version }}
+ INPUT_UPSTREAM_VERSION: ${{ github.event.inputs.upstream_version }}
run: |
if [[ "${{ github.event_name }}" == "push" && "${{ github.ref_type }}" == "tag" ]]; then
tag="${GITHUB_REF#refs/tags/}"
loongsuite_version="${tag#v}"
- upstream_version="${UPSTREAM_VERSION:-0.60b1}"
+ upstream_version="${INPUT_UPSTREAM_VERSION:-$DEFAULT_UPSTREAM_VERSION}"
else
- loongsuite_version="${{ github.event.inputs.loongsuite_version }}"
- upstream_version="${{ github.event.inputs.upstream_version }}"
+ loongsuite_version="$INPUT_LOONGSUITE_VERSION"
+ upstream_version="${INPUT_UPSTREAM_VERSION:-$DEFAULT_UPSTREAM_VERSION}"
fi
if [[ -z "$loongsuite_version" ]]; then
@@ -91,8 +119,8 @@ jobs:
exit 1
fi
- echo "loongsuite_version=$loongsuite_version" >> $GITHUB_OUTPUT
- echo "upstream_version=$upstream_version" >> $GITHUB_OUTPUT
+ echo "loongsuite_version=$loongsuite_version" >> "$GITHUB_OUTPUT"
+ echo "upstream_version=$upstream_version" >> "$GITHUB_OUTPUT"
echo "LoongSuite version: $loongsuite_version"
echo "Upstream version: $upstream_version"
@@ -107,10 +135,13 @@ jobs:
git config user.email "github-actions[bot]@users.noreply.github.com"
- name: Run release script
+ env:
+ LOONGSUITE_VERSION: ${{ steps.version.outputs.loongsuite_version }}
+ UPSTREAM_VERSION: ${{ steps.version.outputs.upstream_version }}
run: |
./scripts/loongsuite/loongsuite_release.sh \
- --loongsuite-version ${{ steps.version.outputs.loongsuite_version }} \
- --upstream-version ${{ steps.version.outputs.upstream_version }} \
+ --loongsuite-version "$LOONGSUITE_VERSION" \
+ --upstream-version "$UPSTREAM_VERSION" \
--skip-install \
--skip-github-release \
--skip-post-release-pr
@@ -133,18 +164,95 @@ jobs:
dist/loongsuite-python-agent-*.tar.gz
dist/release-notes.md
+ # Build dev wheels only for LoongSuite distributions whose PyPI projects do not exist yet.
+ dev-bootstrap-build:
+ if: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.mode == 'dev-bootstrap-new-projects' }}
+ runs-on: ubuntu-latest
+ permissions:
+ contents: read
+ outputs:
+ dev_version: ${{ steps.version.outputs.dev_version }}
+ has_new_projects: ${{ steps.select.outputs.has_new_projects }}
+ missing_distributions: ${{ steps.select.outputs.missing_distributions }}
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Validate selected ref
+ run: |
+ if [[ "$GITHUB_REF_NAME" != "main" ]]; then
+ echo "ERROR: dev-bootstrap-new-projects must be run from main after the plugin PR is merged."
+ echo "Selected ref: $GITHUB_REF_NAME"
+ exit 1
+ fi
+
+ - uses: actions/setup-python@v5
+ with:
+ python-version: ${{ env.PYTHON_VERSION }}
+
+ - name: Install build dependencies
+ run: python -m pip install -r pkg-requirements.txt
+
+ - name: Set dev version
+ id: version
+ env:
+ BASE_VERSION: ${{ github.event.inputs.loongsuite_version }}
+ run: |
+ if [[ -z "$BASE_VERSION" ]]; then
+ echo "ERROR: loongsuite_version is required"
+ exit 1
+ fi
+ dev_number=$((GITHUB_RUN_NUMBER * 100 + GITHUB_RUN_ATTEMPT))
+ dev_version="${BASE_VERSION}.dev${dev_number}"
+ echo "dev_version=$dev_version" >> "$GITHUB_OUTPUT"
+ echo "Using dev bootstrap version: $dev_version"
+
+ - name: Build PyPI wheels
+ env:
+ DEV_VERSION: ${{ steps.version.outputs.dev_version }}
+ run: |
+ rm -rf dist dist-pypi
+ mkdir -p dist-pypi
+ # Name-reservation wheels pin LoongSuite packages to this run's dev
+ # version; they are not intended as installable release candidates.
+ python scripts/loongsuite/build_loongsuite_package.py \
+ --build-pypi \
+ --version "$DEV_VERSION" \
+ --util-genai-version "$DEV_VERSION" \
+ --dist-dir dist-pypi
+
+ - name: Keep only missing PyPI projects
+ id: select
+ env:
+ DRY_RUN: ${{ github.event.inputs.dry_run }}
+ run: |
+ args=(--dist-dir dist-pypi)
+ if [[ "$DRY_RUN" == "true" ]]; then
+ args+=(--dry-run)
+ fi
+ python scripts/loongsuite/select_new_pypi_projects.py "${args[@]}"
+
+ - name: Upload dev PyPI artifacts
+ if: ${{ steps.select.outputs.has_new_projects == 'true' && !inputs.dry_run }}
+ uses: actions/upload-artifact@v4
+ with:
+ name: dev-pypi-packages
+ path: dist-pypi/*.whl
+ if-no-files-found: error
+
# Publish to production PyPI
publish-pypi:
needs: build
runs-on: ubuntu-latest
if: |
- (github.event_name != 'workflow_dispatch' || !inputs.skip_pypi) &&
- (github.event_name != 'workflow_dispatch' || github.event.inputs.publish_target != 'testpypi')
+ github.event_name != 'workflow_dispatch' ||
+ (
+ github.event.inputs.mode == 'release' &&
+ !inputs.skip_pypi &&
+ github.event.inputs.publish_target != 'testpypi'
+ )
environment:
name: pypi
url: https://pypi.org/project/loongsuite-distro/
- permissions:
- id-token: write
steps:
- name: Download PyPI artifacts
uses: actions/download-artifact@v4
@@ -155,13 +263,36 @@ jobs:
- name: Publish to PyPI
uses: pypa/gh-action-pypi-publish@release/v1
with:
+ username: __token__
+ password: ${{ secrets.PYPI_API_TOKEN }}
+ skip-existing: true
+
+ # Publish dev wheels that create newly added PyPI project names before a formal release.
+ publish-dev-bootstrap-pypi:
+ needs: dev-bootstrap-build
+ runs-on: ubuntu-latest
+ if: ${{ needs.dev-bootstrap-build.outputs.has_new_projects == 'true' && !inputs.dry_run }}
+ environment:
+ name: pypi
+ steps:
+ - name: Download dev PyPI artifacts
+ uses: actions/download-artifact@v4
+ with:
+ name: dev-pypi-packages
+ path: dist/
+
+ - name: Publish dev bootstrap wheels to PyPI
+ uses: pypa/gh-action-pypi-publish@release/v1
+ with:
+ username: __token__
+ password: ${{ secrets.PYPI_API_TOKEN }}
skip-existing: true
# Publish to Test PyPI (for testing before production)
publish-testpypi:
needs: build
runs-on: ubuntu-latest
- if: ${{ github.event_name == 'workflow_dispatch' && !inputs.skip_pypi && inputs.publish_target == 'testpypi' }}
+ if: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.mode == 'release' && !inputs.skip_pypi && inputs.publish_target == 'testpypi' }}
steps:
- name: Download PyPI artifacts
uses: actions/download-artifact@v4
@@ -181,6 +312,7 @@ jobs:
github-release:
needs: build
runs-on: ubuntu-latest
+ if: ${{ github.event_name != 'workflow_dispatch' || github.event.inputs.mode == 'release' }}
permissions:
contents: write
steps:
@@ -193,8 +325,8 @@ jobs:
- name: Create GitHub release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ VERSION: ${{ needs.build.outputs.loongsuite_version }}
run: |
- VERSION="${{ needs.build.outputs.loongsuite_version }}"
gh release create "v${VERSION}" \
--title "loongsuite-python-agent ${VERSION}" \
--notes-file dist/release-notes.md \
@@ -204,6 +336,7 @@ jobs:
post-release-pr:
needs: build
runs-on: ubuntu-latest
+ if: ${{ github.event_name != 'workflow_dispatch' || github.event.inputs.mode == 'release' }}
permissions:
contents: write
pull-requests: write
@@ -225,8 +358,8 @@ jobs:
- name: Create post-release branch and PR
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ VERSION: ${{ needs.build.outputs.loongsuite_version }}
run: |
- VERSION="${{ needs.build.outputs.loongsuite_version }}"
BRANCH="post-release/${VERSION}"
TODAY=$(date -u +%Y-%m-%d)
diff --git a/docs/loongsuite-release.md b/docs/loongsuite-release.md
index de429ad4e..424c5586e 100644
--- a/docs/loongsuite-release.md
+++ b/docs/loongsuite-release.md
@@ -449,14 +449,43 @@ Dry Run 模式**不会**创建分支、归档 changelog、提交代码或创建
1. 进入 GitHub 仓库 → **Actions** → **LoongSuite Release**
2. 点击 **Run workflow**
3. 填写参数:
+ - `mode`: `release`
- `loongsuite_version`: `0.1.0`
- - `upstream_version`: `0.60b1`
+ - `upstream_version`: 上游 OTel 版本;留空时使用 workflow 中的 `DEFAULT_UPSTREAM_VERSION`
- `skip_pypi`: 测试时可勾选
4. 执行
CI 工作流会调用同一个脚本完成构建和分支管理,然后在独立 job 中发布 PyPI、创建 GitHub Release,并自动创建 post-release PR 到 main。
-#### 方式 3: Tag 触发
+#### 方式 3: 新 PyPI 项目 Dev Bootstrap
+
+当新插件合入 `main` 后,可以先创建正式 PyPI 项目名,避免正式 release 时一次性首次创建过多项目触发 PyPI 限流。
+
+1. 确认新插件目录在 `instrumentation-loongsuite/` 下,且没有被 `loongsuite_pypi_manifest.py` 或 `loongsuite-build-config.json` 跳过。
+2. 确认 GitHub Actions 中的 `PYPI_API_TOKEN` 有权限上传并创建新的 PyPI project。API Token 发布模式不需要为每个新项目提前配置 Pending Publisher;只有改用 OIDC Trusted Publishing 时才需要在 PyPI 为新项目添加 Pending Publisher。
+3. 进入 GitHub 仓库 → **Actions** → **LoongSuite Release** → **Run workflow**,并选择 `main` 分支:
+ - `mode`: `dev-bootstrap-new-projects`
+ - `loongsuite_version`: 下一个正式版本号,例如 `0.6.0`
+ - `upstream_version`: 该模式下不参与发布决策,可留空
+ - `skip_pypi`、`publish_target` 在该模式下不参与发布决策
+ - `dry_run`: 首次建议保持 `true`,只在确认 Actions summary 中的 missing projects 后改为 `false` 再执行上传
+4. 工作流会构建唯一 dev 版本:`.dev`,其中 `N = github.run_number * 100 + github.run_attempt`,因此同一次 workflow rerun 也不会复用版本号。随后查询 `https://pypi.org/pypi//json`,只保留 PyPI 上还不存在的项目对应 wheel;`dry_run=false` 时才上传。
+
+该模式不会创建 release 分支,不会创建 GitHub Release,也不会创建 post-release PR。如果所有 PyPI 项目都已存在,publish job 会自动跳过。这个 dev wheel 只用于预创建正式 PyPI 项目名;因为依赖版本会指向同一个临时 dev 版本,默认不保证 `pip install ==` 可用,正式安装验证仍以后续正式 release 为准。
+
+`publish-dev-bootstrap-pypi` job 仍使用 GitHub Environment `pypi`。如果团队希望上传前有人工确认,请在 GitHub Settings → Environments → `pypi` 中配置 required reviewers;如果不配置 required reviewers,流程上的确认点就是先用默认 `dry_run=true` 查看 Actions summary,再手动重跑并改为 `dry_run=false`。
+
+本地可以先 dry-run 筛选逻辑:
+
+```bash
+rm -rf dist-pypi
+python scripts/loongsuite/build_loongsuite_package.py \
+ --build-pypi --version 0.6.0.dev0 --dist-dir dist-pypi
+python scripts/loongsuite/select_new_pypi_projects.py \
+ --dist-dir dist-pypi --dry-run
+```
+
+#### 方式 4: Tag 触发
```bash
git tag v0.1.0
@@ -472,21 +501,23 @@ git push origin v0.1.0
| 收集/归档 changelog | 脚本内调用 Python 脚本 | 同上 |
| Commit + Push | 脚本内 `git commit` + `git push` | 同上 |
| GitHub Release | 脚本内 `gh release create`(可 skip) | 独立 job |
-| PyPI publish | 不执行(本地不做) | 独立 job 通过 OIDC/Token |
+| PyPI publish | 不执行(本地不做) | 独立 job 通过 API Token(未来可迁移到 OIDC) |
| Post-release PR | 脚本内创建 PR(需 `gh` CLI) | 独立 job,自动创建 |
+| 新 PyPI 项目 dev-bootstrap | 不执行 | `mode=dev-bootstrap-new-projects` 独立执行,仅构建和上传缺失项目名的 dev wheel |
#### PyPI / Test PyPI 发布配置
-**发布到生产 PyPI(二选一):**
+**发布到生产 PyPI:**
-1. **API Token**:在 GitHub 仓库 Settings → Secrets → Actions 中添加:
- - `PYPI_API_TOKEN`:从 [pypi.org/manage/account/token/](https://pypi.org/manage/account/token/) 创建
+1. **API Token(当前 workflow 使用)**:在 GitHub 仓库 Settings → Secrets → Actions 中添加:
+ - `PYPI_API_TOKEN`:从 [pypi.org/manage/account/token/](https://pypi.org/manage/account/token/) 创建。使用 API Token 发布时,首次上传新 distribution 会创建对应 PyPI project,不需要 Pending Publisher。
-2. **OIDC Trusted Publishing**(推荐):
+2. **OIDC Trusted Publishing(可选替代方案)**:
- PyPI 项目设置 → Publishing → Add a new pending publisher
- Owner: `alibaba`,Repository: `loongsuite-python-agent`
- Workflow: `loongsuite-release.yml`,Environment: `pypi`
- 在 GitHub 仓库中创建 Environment `pypi`(Settings → Environments)
+ - 如果用 OIDC 创建新 project,需要为每个新 project 配置 Pending Publisher。
**发布到 Test PyPI(测试用):**
@@ -497,6 +528,7 @@ git push origin v0.1.0
**重要说明:**
- `dist-pypi/` 中的 `loongsuite_util_genai-*.whl`、`loongsuite_distro-*.whl` 以及 `loongsuite_instrumentation_*.whl`(`instrumentation-loongsuite` 中当前参与 PyPI 构建的插件;`agno` / `mcp` / `dify` 暂排除)会上传到 PyPI;每个新插件首次发布前需在 PyPI 上完成项目/Trusted Publisher 配置
+- 新插件可以先通过 `mode=dev-bootstrap-new-projects` 发布唯一 dev 版本来创建正式 PyPI 项目名;正式 release 后续只上传正式版本
- `loongsuite-python-agent-*.tar.gz` 仅用于 GitHub Release,**禁止**上传到 PyPI
### 5.4 Post-Release PR
@@ -659,7 +691,8 @@ pip install loongsuite-util-genai
**问题**: PyPI 发布 403 Forbidden
```bash
-# 解决: 检查 OIDC trusted publishing 配置或 API token
+# 解决: 检查 PYPI_API_TOKEN 是否有效、未过期,并且有目标 project 的上传权限
+# 如果未来改用 OIDC Trusted Publishing,再检查对应的 publisher 配置
```
**问题**: 版本号已存在
diff --git a/scripts/loongsuite/select_new_pypi_projects.py b/scripts/loongsuite/select_new_pypi_projects.py
new file mode 100644
index 000000000..814e7c0ce
--- /dev/null
+++ b/scripts/loongsuite/select_new_pypi_projects.py
@@ -0,0 +1,196 @@
+#!/usr/bin/env python3
+
+# Copyright The OpenTelemetry Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""Keep only wheels whose PyPI projects do not exist yet."""
+
+from __future__ import annotations
+
+import argparse
+import os
+import re
+import sys
+import time
+import urllib.error
+import urllib.parse
+import urllib.request
+from pathlib import Path
+
+from loongsuite_pypi_manifest import list_pypi_distribution_names
+
+
+def normalize_distribution_name(name: str) -> str:
+ return re.sub(r"[-_.]+", "-", name).lower()
+
+
+def distribution_name_from_wheel(wheel_path: Path) -> str:
+ return wheel_path.name.split("-", 1)[0].replace("_", "-")
+
+
+def project_exists_on_pypi(distribution_name: str, *, timeout: float) -> bool:
+ normalized = normalize_distribution_name(distribution_name)
+ url = f"https://pypi.org/pypi/{urllib.parse.quote(normalized)}/json"
+ request = urllib.request.Request(
+ url,
+ headers={"User-Agent": "loongsuite-python-agent-release/1.0"},
+ )
+ for attempt in range(1, 4):
+ try:
+ with urllib.request.urlopen(request, timeout=timeout) as response:
+ return 200 <= response.status < 300
+ except urllib.error.HTTPError as exc:
+ if exc.code in (404, 410):
+ return False
+ retryable = exc.code == 429 or exc.code >= 500
+ if not retryable or attempt == 3:
+ raise RuntimeError(
+ f"PyPI project lookup failed for {distribution_name} "
+ f"at {url}: HTTP {exc.code}"
+ ) from exc
+ except OSError as exc:
+ if attempt == 3:
+ raise RuntimeError(
+ f"PyPI project lookup failed for {distribution_name} "
+ f"at {url}: {exc}"
+ ) from exc
+
+ time.sleep(attempt)
+
+
+def write_github_outputs(output_path: Path, values: dict[str, str]) -> None:
+ with output_path.open("a", encoding="utf-8") as f:
+ for key, value in values.items():
+ f.write(f"{key}={value}\n")
+
+
+def write_step_summary(
+ summary_path: Path,
+ missing_distributions: list[str],
+ kept_wheels: list[Path],
+ removed_wheels: list[Path],
+) -> None:
+ lines = ["## LoongSuite PyPI dev bootstrap", ""]
+ if missing_distributions:
+ lines.append("New PyPI projects to create:")
+ lines.extend(f"- `{name}`" for name in missing_distributions)
+ else:
+ lines.append("No missing PyPI projects were found.")
+ lines.extend(
+ [
+ "",
+ f"Kept wheels: {len(kept_wheels)}",
+ f"Removed wheels: {len(removed_wheels)}",
+ ]
+ )
+ with summary_path.open("a", encoding="utf-8") as summary:
+ summary.write("\n".join(lines) + "\n")
+
+
+def main() -> int:
+ parser = argparse.ArgumentParser(
+ description="Prune built LoongSuite wheels to new PyPI project names."
+ )
+ parser.add_argument(
+ "--base-dir",
+ type=Path,
+ default=Path(__file__).resolve().parent.parent.parent,
+ )
+ parser.add_argument(
+ "--dist-dir",
+ type=Path,
+ default=Path(__file__).resolve().parent.parent.parent / "dist-pypi",
+ )
+ parser.add_argument("--timeout", type=float, default=15.0)
+ parser.add_argument("--dry-run", action="store_true")
+ parser.add_argument(
+ "--github-output",
+ type=Path,
+ default=Path(os.environ["GITHUB_OUTPUT"])
+ if "GITHUB_OUTPUT" in os.environ
+ else None,
+ )
+ parser.add_argument(
+ "--summary",
+ type=Path,
+ default=Path(os.environ["GITHUB_STEP_SUMMARY"])
+ if "GITHUB_STEP_SUMMARY" in os.environ
+ else None,
+ )
+ args = parser.parse_args()
+
+ expected_distributions = list_pypi_distribution_names(args.base_dir)
+ if not expected_distributions:
+ raise RuntimeError(
+ "No PyPI distributions were found in the LoongSuite manifest; "
+ "refusing to prune wheels."
+ )
+ missing_distributions = [
+ name
+ for name in expected_distributions
+ if not project_exists_on_pypi(name, timeout=args.timeout)
+ ]
+ missing_normalized = {
+ normalize_distribution_name(name) for name in missing_distributions
+ }
+
+ kept_wheels: list[Path] = []
+ removed_wheels: list[Path] = []
+ for wheel_path in sorted(args.dist_dir.glob("*.whl")):
+ wheel_distribution = normalize_distribution_name(
+ distribution_name_from_wheel(wheel_path)
+ )
+ if wheel_distribution in missing_normalized:
+ kept_wheels.append(wheel_path)
+ else:
+ removed_wheels.append(wheel_path)
+ if not args.dry_run:
+ wheel_path.unlink()
+
+ kept_normalized = {
+ normalize_distribution_name(distribution_name_from_wheel(wheel))
+ for wheel in kept_wheels
+ }
+ missing_without_wheel = sorted(missing_normalized - kept_normalized)
+ if missing_without_wheel:
+ raise RuntimeError(
+ "PyPI projects are missing, but no matching wheel was built: "
+ + ", ".join(missing_without_wheel)
+ )
+
+ print("Missing PyPI projects:")
+ for name in missing_distributions or ["(none)"]:
+ print(f" - {name}")
+ print(f"Kept wheels: {len(kept_wheels)}")
+ print(f"Removed wheels: {len(removed_wheels)}")
+
+ outputs = {
+ "has_new_projects": "true" if missing_distributions else "false",
+ "missing_distributions": ",".join(missing_distributions),
+ }
+ if args.github_output:
+ write_github_outputs(args.github_output, outputs)
+ if args.summary:
+ write_step_summary(
+ args.summary,
+ missing_distributions,
+ kept_wheels,
+ removed_wheels,
+ )
+
+ return 0
+
+
+if __name__ == "__main__":
+ sys.exit(main())