Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 13 additions & 3 deletions .github/workflows/harness-sidecar-release-gate.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ on:
- 'tests/test_harness_sidecar_release_gate.py'
- 'tests/test_cloud.py'
- 'tests/test_studio_release_server.py'
- 'tests/test_studio_release_cold_start.py'
- 'tests/test_studio_release_workflow.py'
- 'veadk/cli/generated_agent_codegen.py'
- 'veadk/cli/generated_agent_mcp.py'
Expand All @@ -39,6 +40,7 @@ on:
- 'veadk/extensions/harness/**'
- 'veadk/integrations/agentkit/app.py'
- 'pyproject.toml'
- 'uv.lock'
pull_request:
paths:
- '.github/workflows/harness-sidecar-release-gate.yaml'
Expand All @@ -60,6 +62,7 @@ on:
- 'tests/test_harness_sidecar_release_gate.py'
- 'tests/test_cloud.py'
- 'tests/test_studio_release_server.py'
- 'tests/test_studio_release_cold_start.py'
- 'tests/test_studio_release_workflow.py'
- 'veadk/cli/generated_agent_codegen.py'
- 'veadk/cli/generated_agent_mcp.py'
Expand All @@ -76,6 +79,7 @@ on:
- 'veadk/extensions/harness/**'
- 'veadk/integrations/agentkit/app.py'
- 'pyproject.toml'
- 'uv.lock'

permissions:
contents: read
Expand All @@ -91,13 +95,18 @@ jobs:
- uses: actions/setup-python@v5
with:
python-version: '3.12'
cache: pip
cache-dependency-path: |

- uses: astral-sh/setup-uv@v6
with:
enable-cache: true
cache-dependency-glob: |
pyproject.toml
uv.lock

- name: Install Python test dependencies
run: python -m pip install -e '.[dev]'
run: |
uv sync --frozen --extra dev
echo "$GITHUB_WORKSPACE/.venv/bin" >> "$GITHUB_PATH"

- name: Run Python Sidecar checks in parallel
shell: bash
Expand Down Expand Up @@ -149,6 +158,7 @@ jobs:
(
python -m pytest -q \
tests/test_studio_release_server.py::test_release_server_agentkit_cli_pin_matches_veadk \
tests/test_studio_release_cold_start.py \
tests/test_studio_release_server.py::test_publisher_repairs_missing_agentkit_cli_before_manifest \
tests/test_studio_release_server.py::test_release_server_zip_normalizes_runtime_file_permissions \
tests/test_studio_release_server.py::test_stage_deployment_uses_frontend_service_package \
Expand Down
27 changes: 21 additions & 6 deletions .github/workflows/publish-studio-release.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@ on:
- 'veadk/**'
- 'pyproject.toml'
- 'uv.lock'
- 'tests/test_studio_release_cold_start.py'
- 'tests/test_studio_release_workflow.py'
- 'tests/test_studio_release_lock_contract.py'
- 'README.md'
- 'LICENSE'

Expand Down Expand Up @@ -103,6 +106,13 @@ jobs:
- name: Validate frozen Studio runtime lock
run: uv lock --check

- name: Validate cold-start and thin release contracts
run: >-
uv run --frozen --group dev python -m pytest -q
tests/test_studio_release_cold_start.py
tests/test_studio_release_workflow.py
tests/test_studio_release_lock_contract.py

- name: Build and validate Studio bundle
run: |
set -euo pipefail
Expand All @@ -124,7 +134,7 @@ jobs:
STUDIO_DEPENDENCY_SOURCES,
)
from veadk.cli.studio_package import build_frontend_assets
from veadk.cli.studio_release import build_studio_release
from frontend.service.studio_release_server.publisher import build_studio_release


def prepare_dependency(dependency, destination):
Expand Down Expand Up @@ -187,6 +197,9 @@ jobs:
changelog=changelog,
frontend_assets=frontend_assets,
dependency_wheels=dependency_inputs,
env=dict(os.environ),
thin=True,
provider="volcengine",
)
print(f"Bundle built in {time.monotonic() - started:.1f}s", flush=True)
print(
Expand All @@ -201,11 +214,14 @@ jobs:
)
)
PY
bundles=("$output_dir"/studio-bundle-*.zip)
bundles=("$output_dir"/studio-bundle-??????????????.zip)
thin_bundles=("$output_dir"/studio-bundle-??????????????-thin.zip)
manifests=("$output_dir"/manifest-*.json)
test "${#bundles[@]}" -eq 1
test "${#thin_bundles[@]}" -eq 1
test "${#manifests[@]}" -eq 1
unzip -t "${bundles[0]}"
unzip -t "${thin_bundles[0]}"

- name: Simulate customer update and smoke-test Studio
env:
Expand All @@ -220,7 +236,7 @@ jobs:
test "$(python3 -c 'import sys; print(f"{sys.version_info.major}.{sys.version_info.minor}")')" = "3.12"
COLD_START_DEADLINE_SECONDS=60
STUDIO_MAX_BUNDLE_BYTES=$((256 * 1024 * 1024))
bundle=("$RUNNER_TEMP"/studio-release-output/studio-bundle-*.zip)
bundle=("$RUNNER_TEMP"/studio-release-output/studio-bundle-??????????????.zip)
test "${#bundle[@]}" -eq 1
test "$(stat -c %s "${bundle[0]}")" -lt "$STUDIO_MAX_BUNDLE_BYTES"

Expand Down Expand Up @@ -278,7 +294,7 @@ jobs:
runtime_venv="$RUNNER_TEMP/studio-release-runtime-${provider}"
log="$RUNNER_TEMP/studio-${provider}.log"
response="$RUNNER_TEMP/studio-${provider}-ui-config.json"
export package_dir
export package_dir bundle_path="${bundle[0]}"
test ! -e "$package_dir"
test ! -e "$runtime_venv"
uv run --frozen --group dev python - <<'PY'
Expand All @@ -287,8 +303,7 @@ jobs:

from veadk.cli.studio_self_update import extract_studio_bundle

output_dir = Path(os.environ["RUNNER_TEMP"]) / "studio-release-output"
bundle = next(output_dir.glob("studio-bundle-*.zip"))
bundle = Path(os.environ["bundle_path"])
extract_studio_bundle(bundle, Path(os.environ["package_dir"]))
PY
uv venv --python 3.12 "$runtime_venv"
Expand Down
8 changes: 8 additions & 0 deletions frontend/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1037,6 +1037,14 @@ and `latest.json`. Configure only
`STUDIO_RELEASE_SERVER_URL` and `STUDIO_RELEASE_SERVER_API_KEY` as GitHub
Secrets; GitHub receives no TOS credentials.

Release verification uses the Release Server publisher to build both full and
thin bundles. Full bundles retain checked-hash bytecode for cold starts. Thin
bundles keep upstream dependency wheels byte-for-byte identical to `uv.lock`;
only the local VeADK wheel is precompiled. Each bundle has its own hash-pinned
runtime lock. Public provenance and license checks run before any artifact is
published. Changes to this build logic require deploying the Release Server
before starting a new release; merging the source alone does not update it.

The Release Server runtime and deployment assets are isolated from the public
Python package under `frontend/service/studio_release_server`. After changing
the service, deploy it from the repository root:
Expand Down
23 changes: 23 additions & 0 deletions frontend/service/studio_release_server/offline_runtime.py
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@ def build_studio_offline_runtime(
dependency_sources: Sequence[Path],
environment: Mapping[str, str] | None = None,
optimize_cold_start: bool = False,
thin_package_dir: Path | None = None,
) -> str:
"""Bundle every locked Linux dependency and return offline requirements."""
lock_source = source_root / "uv.lock"
Expand Down Expand Up @@ -230,6 +231,28 @@ def build_studio_offline_runtime(
if optimize_cold_start:
if sys.implementation.name != "cpython" or sys.version_info[:2] != (3, 12):
raise ValueError("Studio cold-start optimization requires CPython 3.12.")
if thin_package_dir is not None:
# Public artifacts must retain the exact PyPI bytes recorded in uv.lock.
# Snapshot before full-bundle optimization, with an independent hash lock.
shutil.copytree(wheelhouse, thin_package_dir)
thin_veadk = thin_package_dir / staged_veadk.name
if optimize_cold_start:
_augment_checked_hash_wheel(
thin_veadk, _COLD_START_WHEEL_ROOTS["veadk-python"]
)
thin_lock = thin_package_dir / STUDIO_RUNTIME_LOCK
shutil.copy2(runtime_lock, thin_lock)
_pin_runtime_lock_to_wheelhouse(thin_lock, thin_package_dir, thin_veadk)
thin_requirements = build_studio_offline_requirements(
thin_package_dir, wheel_prefix="./"
)
_verify_offline_resolution(
thin_package_dir,
thin_requirements,
uv=uv,
environment=build_environment,
)
if optimize_cold_start:
_enhance_studio_cold_start_wheels(wheelhouse)
_pin_runtime_lock_to_wheelhouse(runtime_lock, wheelhouse, staged_veadk)
for wheel in sorted(wheelhouse.glob("*.whl")):
Expand Down
16 changes: 12 additions & 4 deletions frontend/service/studio_release_server/publisher.py
Original file line number Diff line number Diff line change
Expand Up @@ -951,6 +951,8 @@ def _build_local_requirements(
frontend_assets: Path,
dependency_wheels: Path,
env: Mapping[str, str],
*,
thin_package_dir: Path | None = None,
) -> str:
wheel_source = package_dir / "wheel-source"
stage_studio_wheel_source(source_root, frontend_assets, wheel_source)
Expand Down Expand Up @@ -984,16 +986,20 @@ def _build_local_requirements(
if path.name != _AGENTKIT_CLI_ARCHIVE
)
try:
return build_studio_offline_runtime(
requirements = build_studio_offline_runtime(
source_root,
package_dir,
veadk_wheel=built_wheels[0],
dependency_sources=dependency_sources,
environment=env,
optimize_cold_start=True,
thin_package_dir=thin_package_dir,
)
except ValueError as error:
raise StudioPublisherError(str(error)) from error
if thin_package_dir is not None:
shutil.copy2(package_dir / _AGENTKIT_CLI_ARCHIVE, thin_package_dir)
return requirements


def _studio_run_script(*, thin: bool = False) -> str:
Expand Down Expand Up @@ -1428,12 +1434,14 @@ def build_studio_release(
)
package_dir = workspace / "package"
package_dir.mkdir()
thin_package_dir = workspace / "thin-package" if thin else None
requirements = _build_local_requirements(
source_root,
package_dir,
resolved_frontend,
dependency_wheels,
env,
thin_package_dir=thin_package_dir,
)
(package_dir / "run.sh").write_text(
_studio_run_script(),
Expand All @@ -1445,15 +1453,15 @@ def build_studio_release(
bundle = output_dir / f"studio-bundle-{version}.zip"
_zip_directory(package_dir, bundle)
thin_bundle: Path | None = None
if thin:
if thin_package_dir is not None:
runtime_epoch, _artifact_dir = stage_studio_thin_runtime(
source_root,
package_dir,
thin_package_dir,
output_dir,
provider=provider,
)
thin_bundle = output_dir / f"studio-bundle-{version}-thin.zip"
_zip_directory(package_dir, thin_bundle)
_zip_directory(thin_package_dir, thin_bundle)
ensure_studio_bundle_agentkit_cli(bundle, dependency_wheels)
content = bundle.read_bytes()
thin_content = thin_bundle.read_bytes() if thin_bundle is not None else b""
Expand Down
Loading
Loading