Skip to content

Commit 79f1cab

Browse files
committed
Add release workflows
Change-Id: Ib0d4aa36a776339f0e97c6c25ea6101396b23b81 Co-developed-by: Cursor <noreply@cursor.com>
1 parent 1e3f0c0 commit 79f1cab

File tree

3 files changed

+262
-4
lines changed

3 files changed

+262
-4
lines changed
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
# LoongSuite Release - Build tar.gz and publish to GitHub Release (Scheme 1)
2+
#
3+
# This workflow builds the loongsuite Python Agent package with:
4+
# - util built as loongsuite-util-genai (avoids conflict with upstream opentelemetry-util-genai)
5+
# - Instrumentation packages (genai, loongsuite) depending on loongsuite-util-genai
6+
#
7+
# The tar.gz is published to GitHub Release for loongsuite-bootstrap -a install.
8+
name: LoongSuite Release
9+
10+
run-name: "LoongSuite Release ${{ github.event.inputs.version || github.ref_name }}"
11+
12+
on:
13+
workflow_dispatch:
14+
inputs:
15+
version:
16+
description: 'Release version (e.g. 1.0.0)'
17+
required: true
18+
release_notes:
19+
description: 'Release notes (optional, uses CHANGELOG-loongsuite.md Unreleased if empty)'
20+
required: false
21+
push:
22+
tags:
23+
- 'v*'
24+
25+
permissions:
26+
contents: read
27+
28+
jobs:
29+
release:
30+
permissions:
31+
contents: write # required for creating releases
32+
runs-on: ubuntu-latest
33+
steps:
34+
- uses: actions/checkout@v4
35+
36+
- name: Set version from tag or input
37+
id: version
38+
run: |
39+
if [[ "${{ github.event_name }}" == "push" && "${{ github.ref_type }}" == "tag" ]]; then
40+
# Remove 'refs/tags/' prefix, then optional 'v' (e.g. v1.0.0 -> 1.0.0)
41+
tag="${GITHUB_REF#refs/tags/}"
42+
version="${tag#v}"
43+
else
44+
version="${{ github.event.inputs.version }}"
45+
fi
46+
if [[ -z "$version" ]]; then
47+
echo "Version is required"
48+
exit 1
49+
fi
50+
echo "version=$version" >> $GITHUB_OUTPUT
51+
echo "VERSION=$version" >> $GITHUB_ENV
52+
53+
- uses: actions/setup-python@v5
54+
with:
55+
python-version: '3.11'
56+
57+
- name: Install build dependencies
58+
run: |
59+
python -m pip install --upgrade pip build
60+
61+
- name: Build loongsuite package (Scheme 1)
62+
run: |
63+
python scripts/build_loongsuite_package.py \
64+
--loongsuite-release \
65+
--version ${{ steps.version.outputs.version }}
66+
67+
- name: Generate release notes
68+
id: release_notes
69+
run: |
70+
if [[ -n "${{ github.event.inputs.release_notes }}" ]]; then
71+
echo "${{ github.event.inputs.release_notes }}" > /tmp/release-notes.txt
72+
elif [[ -f CHANGELOG-loongsuite.md ]]; then
73+
# Extract Unreleased section from CHANGELOG-loongsuite.md
74+
sed -n '/^## Unreleased$/,/^## /p' CHANGELOG-loongsuite.md | sed '/^## /d' > /tmp/release-notes.txt
75+
if [[ ! -s /tmp/release-notes.txt ]]; then
76+
echo "LoongSuite Python Agent ${{ steps.version.outputs.version }}" > /tmp/release-notes.txt
77+
echo "" >> /tmp/release-notes.txt
78+
echo "See CHANGELOG-loongsuite.md for details." >> /tmp/release-notes.txt
79+
fi
80+
else
81+
echo "LoongSuite Python Agent ${{ steps.version.outputs.version }}" > /tmp/release-notes.txt
82+
fi
83+
84+
- name: Create GitHub release
85+
env:
86+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
87+
run: |
88+
gh release create "v${{ steps.version.outputs.version }}" \
89+
--title "LoongSuite Python Agent v${{ steps.version.outputs.version }}" \
90+
--notes-file /tmp/release-notes.txt \
91+
dist/loongsuite-python-agent-${{ steps.version.outputs.version }}.tar.gz

loongsuite-distro/src/loongsuite/distro/bootstrap.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,11 +42,13 @@
4242
logger = logging.getLogger(__name__)
4343

4444
# Base dependency packages (must be installed)
45+
# Use loongsuite-util-genai for Scheme 1 release (tar.gz from GitHub) to avoid
46+
# conflict with upstream opentelemetry-util-genai on user's system
4547
BASE_DEPENDENCIES = {
4648
"opentelemetry-api",
4749
"opentelemetry-sdk",
4850
"opentelemetry-instrumentation",
49-
"opentelemetry-util-genai",
51+
"loongsuite-util-genai",
5052
"opentelemetry-semantic-conventions",
5153
}
5254

scripts/build_loongsuite_package.py

Lines changed: 168 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,21 +11,39 @@
1111
6. Skip duplicate packages according to config file
1212
7. Package all whl files into tar.gz
1313
14+
When --loongsuite-release is used (Scheme 1):
15+
- util is built FIRST as loongsuite-util-genai
16+
- Instrumentation packages depending on opentelemetry-util-genai get dependency
17+
replaced to loongsuite-util-genai
18+
- Avoids conflict with upstream opentelemetry-util-genai on user's system
19+
1420
Note: loongsuite-distro is not included as it is published separately to PyPI.
1521
"""
1622

1723
import argparse
1824
import json
1925
import logging
26+
import os
2027
import subprocess
2128
import sys
2229
import tarfile
30+
from contextlib import contextmanager
2331
from pathlib import Path
2432
from typing import List, Set
2533

2634
logging.basicConfig(level=logging.INFO, format="%(levelname)s: %(message)s")
2735
logger = logging.getLogger(__name__)
2836

37+
# Instrumentation packages that depend on opentelemetry-util-genai (need replacement in loongsuite-release)
38+
LOONGSUITE_UTIL_GENAI_DEPENDENTS = [
39+
"instrumentation-genai/opentelemetry-instrumentation-google-genai",
40+
"instrumentation-genai/opentelemetry-instrumentation-vertexai",
41+
"instrumentation-genai/opentelemetry-instrumentation-openai-agents-v2",
42+
"instrumentation-loongsuite/loongsuite-instrumentation-mem0",
43+
"instrumentation-loongsuite/loongsuite-instrumentation-dashscope",
44+
"instrumentation-loongsuite/loongsuite-instrumentation-agentscope",
45+
]
46+
2947

3048
def load_skip_config(config_path: Path) -> Set[str]:
3149
"""Load package names to skip from config file"""
@@ -73,8 +91,25 @@ def get_package_name_from_whl(whl_path: Path) -> str:
7391
return name
7492

7593

94+
@contextmanager
95+
def _patch_pyproject(pyproject_path: Path, replacements: List[tuple]):
96+
"""Temporarily patch pyproject.toml, restore on exit."""
97+
content = pyproject_path.read_text(encoding="utf-8")
98+
try:
99+
patched = content
100+
for old, new in replacements:
101+
patched = patched.replace(old, new)
102+
pyproject_path.write_text(patched, encoding="utf-8")
103+
yield
104+
finally:
105+
pyproject_path.write_text(content, encoding="utf-8")
106+
107+
76108
def build_package(
77-
package_dir: Path, dist_dir: Path, existing_whl_files: Set[Path]
109+
package_dir: Path,
110+
dist_dir: Path,
111+
existing_whl_files: Set[Path],
112+
env_extra: dict | None = None,
78113
) -> List[Path]:
79114
"""Build whl file for a single package"""
80115
pyproject_toml = package_dir / "pyproject.toml"
@@ -87,6 +122,10 @@ def build_package(
87122
# Record whl files before build
88123
before_whl_files = set(dist_dir.glob("*.whl"))
89124

125+
build_env = dict(os.environ)
126+
if env_extra:
127+
build_env.update(env_extra)
128+
90129
result = subprocess.run(
91130
[
92131
sys.executable,
@@ -100,6 +139,7 @@ def build_package(
100139
check=True,
101140
capture_output=True,
102141
text=True,
142+
env=build_env,
103143
)
104144

105145
# Find newly generated whl files (exist after build but not before)
@@ -207,7 +247,121 @@ def collect_packages(
207247
all_whl_files.extend(whl_files)
208248
existing_whl_files.update(whl_files)
209249

210-
# 6. Filter out packages that need to be skipped
250+
return _filter_and_dedupe_whl_files(all_whl_files, skip_packages)
251+
252+
253+
def collect_packages_loongsuite_release(
254+
base_dir: Path,
255+
dist_dir: Path,
256+
skip_packages: Set[str],
257+
) -> List[Path]:
258+
"""
259+
Build for Scheme 1 (loongsuite release): util first as loongsuite-util-genai,
260+
then instrumentation with dependency replacement.
261+
"""
262+
all_whl_files = []
263+
existing_whl_files = set(dist_dir.glob("*.whl"))
264+
265+
util_genai_dir = base_dir / "util" / "opentelemetry-util-genai"
266+
if not (
267+
util_genai_dir.exists()
268+
and (util_genai_dir / "pyproject.toml").exists()
269+
):
270+
raise FileNotFoundError(
271+
f"util/opentelemetry-util-genai not found at {util_genai_dir}"
272+
)
273+
274+
# 1. Build util FIRST as loongsuite-util-genai
275+
logger.info("Building util as loongsuite-util-genai (Scheme 1)...")
276+
with _patch_pyproject(
277+
util_genai_dir / "pyproject.toml",
278+
[('name = "opentelemetry-util-genai"', 'name = "loongsuite-util-genai"')],
279+
):
280+
whl_files = build_package(
281+
util_genai_dir, dist_dir, existing_whl_files
282+
)
283+
all_whl_files.extend(whl_files)
284+
existing_whl_files.update(whl_files)
285+
286+
def _build_with_patch_if_needed(rel_path: str, package_dir: Path) -> List[Path]:
287+
"""Build package, patching opentelemetry-util-genai -> loongsuite-util-genai if needed."""
288+
if rel_path in LOONGSUITE_UTIL_GENAI_DEPENDENTS:
289+
pyproject = package_dir / "pyproject.toml"
290+
with _patch_pyproject(
291+
pyproject,
292+
[("opentelemetry-util-genai", "loongsuite-util-genai")],
293+
):
294+
return build_package(
295+
package_dir, dist_dir, existing_whl_files
296+
)
297+
return build_package(package_dir, dist_dir, existing_whl_files)
298+
299+
# 2. Build instrumentation/ (no util dependency)
300+
instrumentation_dir = base_dir / "instrumentation"
301+
if instrumentation_dir.exists():
302+
logger.info("Building packages under instrumentation/...")
303+
for package_dir in sorted(instrumentation_dir.iterdir()):
304+
if (
305+
package_dir.is_dir()
306+
and (package_dir / "pyproject.toml").exists()
307+
):
308+
whl_files = build_package(
309+
package_dir, dist_dir, existing_whl_files
310+
)
311+
all_whl_files.extend(whl_files)
312+
existing_whl_files.update(whl_files)
313+
314+
# 3. Build instrumentation-genai/ (with dependency patch)
315+
instrumentation_genai_dir = base_dir / "instrumentation-genai"
316+
if instrumentation_genai_dir.exists():
317+
logger.info("Building packages under instrumentation-genai/...")
318+
for package_dir in sorted(instrumentation_genai_dir.iterdir()):
319+
if (
320+
package_dir.is_dir()
321+
and (package_dir / "pyproject.toml").exists()
322+
):
323+
rel_path = str(package_dir.relative_to(base_dir))
324+
whl_files = _build_with_patch_if_needed(rel_path, package_dir)
325+
all_whl_files.extend(whl_files)
326+
existing_whl_files.update(whl_files)
327+
328+
# 4. Build instrumentation-loongsuite/ (with dependency patch)
329+
instrumentation_loongsuite_dir = base_dir / "instrumentation-loongsuite"
330+
if instrumentation_loongsuite_dir.exists():
331+
logger.info("Building packages under instrumentation-loongsuite/...")
332+
for package_dir in sorted(instrumentation_loongsuite_dir.iterdir()):
333+
if (
334+
package_dir.is_dir()
335+
and (package_dir / "pyproject.toml").exists()
336+
):
337+
rel_path = str(package_dir.relative_to(base_dir))
338+
whl_files = _build_with_patch_if_needed(rel_path, package_dir)
339+
all_whl_files.extend(whl_files)
340+
existing_whl_files.update(whl_files)
341+
342+
# 5. Build processor/loongsuite-processor-baggage/
343+
processor_baggage_dir = (
344+
base_dir / "processor" / "loongsuite-processor-baggage"
345+
)
346+
if (
347+
processor_baggage_dir.exists()
348+
and (processor_baggage_dir / "pyproject.toml").exists()
349+
):
350+
logger.info("Building processor/loongsuite-processor-baggage/...")
351+
whl_files = build_package(
352+
processor_baggage_dir, dist_dir, existing_whl_files
353+
)
354+
all_whl_files.extend(whl_files)
355+
existing_whl_files.update(whl_files)
356+
357+
return _filter_and_dedupe_whl_files(all_whl_files, skip_packages)
358+
359+
360+
def _filter_and_dedupe_whl_files(
361+
all_whl_files: List[Path],
362+
skip_packages: Set[str],
363+
) -> List[Path]:
364+
"""Filter skip list and deduplicate whl files."""
211365
filtered_whl_files = []
212366
skipped_count = 0
213367
seen_packages = {} # Used to detect duplicate packages
@@ -302,6 +456,12 @@ def main():
302456
default="dev",
303457
help="Version number (for output filename)",
304458
)
459+
parser.add_argument(
460+
"--loongsuite-release",
461+
action="store_true",
462+
help="Build for Scheme 1: util as loongsuite-util-genai, "
463+
"instrumentation deps replaced (for GitHub Release tar.gz)",
464+
)
305465

306466
args = parser.parse_args()
307467

@@ -318,7 +478,12 @@ def main():
318478
skip_packages = load_skip_config(args.config)
319479

320480
# Collect and build all packages
321-
whl_files = collect_packages(base_dir, dist_dir, skip_packages)
481+
if args.loongsuite_release:
482+
whl_files = collect_packages_loongsuite_release(
483+
base_dir, dist_dir, skip_packages
484+
)
485+
else:
486+
whl_files = collect_packages(base_dir, dist_dir, skip_packages)
322487

323488
if not whl_files:
324489
logger.error("No whl files found, build failed")

0 commit comments

Comments
 (0)