Skip to content
Closed
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
58 changes: 58 additions & 0 deletions .github/workflows/acceptance.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
name: Installed package acceptance

on:
push:
pull_request:
workflow_dispatch:

permissions:
contents: read

jobs:
build:
outputs:
package-artifact-id: ${{ steps.upload-package.outputs.artifact-id }}
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5
with:
python-version: '3.14'
- uses: astral-sh/setup-uv@d0cc045d04ccac9d8b7881df0226f9e82c39688e # v6
- run: uv sync --frozen
- run: uv run python -m build --wheel --outdir candidate
- run: bash scripts/build-acceptance.sh candidate/*.whl candidate
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
id: upload-package
with:
name: client-acceptance
path: candidate/*
if-no-files-found: error

acceptance-check:
needs: build
uses: ./.github/workflows/verify-acceptance.yml
with:
package-artifact-id: ${{ needs.build.outputs.package-artifact-id }}

attest:
if: github.event_name != 'pull_request'
needs: [build, acceptance-check]
runs-on: ubuntu-latest
timeout-minutes: 5
permissions:
contents: read
id-token: write
attestations: write
steps:
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
artifact-ids: ${{ needs.build.outputs.package-artifact-id }}
merge-multiple: true
path: candidate
- uses: actions/attest@1e69f48acb82d1966a394da916b4c1698aa569d6 # v4
with:
subject-path: candidate/*
94 changes: 91 additions & 3 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,9 @@ jobs:
uses: ./.github/workflows/ci.yml

build:
outputs:
package-artifact-id: ${{ steps.upload-package.outputs.artifact-id }}
bundle-artifact-id: ${{ steps.upload-acceptance.outputs.artifact-id }}
needs: [validate, check]
runs-on: ubuntu-latest
timeout-minutes: 15
Expand All @@ -54,18 +57,75 @@ jobs:
- uses: astral-sh/setup-uv@d0cc045d04ccac9d8b7881df0226f9e82c39688e # v6
- run: uv sync --frozen
- run: uv run python -m build
- run: bash scripts/build-acceptance.sh dist/*.whl acceptance-output
- name: Check package identity and release version
env:
RELEASE_TAG: ${{ github.event.release.tag_name }}
run: bash scripts/check_package.sh "${RELEASE_TAG#v}"
run: |
python -c 'import os,tomllib; p=tomllib.load(open("pyproject.toml","rb"))["project"]; assert p["name"]=="volcano-sdk-python" and "v"+p["version"]==os.environ["RELEASE_TAG"]'
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
id: upload-package
with:
name: release-package
path: dist/*
if-no-files-found: error
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
id: upload-acceptance
with:
name: acceptance-tests
path: acceptance-output/sdk-acceptance.tar.gz
if-no-files-found: error

publish:
acceptance-check:
needs: build
uses: ./.github/workflows/verify-acceptance.yml
with:
package-artifact-id: ${{ needs.build.outputs.package-artifact-id }}
bundle-artifact-id: ${{ needs.build.outputs.bundle-artifact-id }}
release-version: ${{ github.event.release.tag_name }}

attest:
needs: [build, acceptance-check]
runs-on: ubuntu-latest
timeout-minutes: 5
permissions:
contents: write
id-token: write
attestations: write
steps:
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
artifact-ids: ${{ needs.build.outputs.package-artifact-id }}
merge-multiple: true
path: release
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
artifact-ids: ${{ needs.build.outputs.bundle-artifact-id }}
merge-multiple: true
path: release
- uses: actions/attest@1e69f48acb82d1966a394da916b4c1698aa569d6 # v4
with:
subject-path: release/*
- name: Publish acceptance bundle
env:
GH_TOKEN: ${{ github.token }}
GH_REPO: ${{ github.repository }}
RELEASE_TAG: ${{ github.event.release.tag_name }}
run: |
if gh release view "$RELEASE_TAG" --json assets --jq '.assets[].name' | grep -Fxq sdk-acceptance.tar.gz; then
mkdir previous
gh release download "$RELEASE_TAG" --pattern sdk-acceptance.tar.gz --dir previous
gh attestation verify previous/sdk-acceptance.tar.gz --repo "$GH_REPO" --signer-workflow "$GH_REPO/.github/workflows/publish.yml" --source-ref "refs/tags/$RELEASE_TAG" --source-digest "$GITHUB_SHA" --deny-self-hosted-runners
# Keep the authenticated original when a retry rebuilds the same SDK.
tar -xOf previous/sdk-acceptance.tar.gz ./acceptance.json | jq -S . > previous.json
tar -xOf release/sdk-acceptance.tar.gz ./acceptance.json | jq -S . > current.json
cmp previous.json current.json
exit 0
fi
gh release upload "$RELEASE_TAG" release/sdk-acceptance.tar.gz

publish:
needs: [build, attest]
runs-on: ubuntu-latest
timeout-minutes: 10
environment:
Expand All @@ -77,11 +137,39 @@ jobs:
# Publishing receives checked artifacts without executing SDK source.
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: release-package
artifact-ids: ${{ needs.build.outputs.package-artifact-id }}
merge-multiple: true
path: dist
- name: Publish distributions to PyPI
uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # v1.14.2

registry-smoke:
needs: [publish, build]
runs-on: ubuntu-latest
timeout-minutes: 15
permissions:
contents: read
steps:
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
artifact-ids: ${{ needs.build.outputs.bundle-artifact-id }}
merge-multiple: true
path: acceptance-output
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5
with:
python-version: '3.14'
- uses: astral-sh/setup-uv@d0cc045d04ccac9d8b7881df0226f9e82c39688e # v6
- name: Check the registry download in a fresh consumer
env:
RELEASE_TAG: ${{ github.event.release.tag_name }}
run: |
mkdir consumer
tar -xzf acceptance-output/sdk-acceptance.tar.gz -C consumer
cd consumer
python -m pip download --no-deps --only-binary :all: --index-url https://pypi.org/simple "volcano-sdk-python==${RELEASE_TAG#v}"
python -c 'import hashlib,json,pathlib; a=json.loads(pathlib.Path("acceptance.json").read_text()); assert hashlib.sha256(pathlib.Path(a["filename"]).read_bytes()).hexdigest()==a["sha256"], "Registry package differs from build"'
bash scripts/smoke-wheel.sh ./*.whl

release-link:
needs: publish
runs-on: ubuntu-latest
Expand Down
53 changes: 53 additions & 0 deletions .github/workflows/verify-acceptance.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
name: Verify immutable acceptance artifacts

on:
workflow_call:
inputs:
package-artifact-id:
required: true
type: string
bundle-artifact-id:
default: ''
type: string
release-version:
default: ''
type: string

permissions:
contents: read

jobs:
verify:
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0
with:
persist-credentials: false
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065
with:
python-version: '3.14'
- uses: astral-sh/setup-uv@d0cc045d04ccac9d8b7881df0226f9e82c39688e
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c
with:
artifact-ids: ${{ inputs.package-artifact-id }}
merge-multiple: true
path: package
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c
if: inputs.bundle-artifact-id != ''
with:
artifact-ids: ${{ inputs.bundle-artifact-id }}
merge-multiple: true
path: acceptance
# Fresh dependencies can modify these copies, never the uploaded artifacts.
- name: Verify the installed package and fresh consumer
env:
ACCEPTANCE_BUNDLE: ${{ inputs.bundle-artifact-id != '' && 'acceptance/sdk-acceptance.tar.gz' || 'package/sdk-acceptance.tar.gz' }}
run: bash scripts/test-acceptance.sh package/*.whl "$ACCEPTANCE_BUNDLE"
- name: Check wheel, source distribution and installed types
if: inputs.release-version != ''
env:
RELEASE_TAG: ${{ inputs.release-version }}
run: |
uv sync --frozen
bash scripts/check_package.sh "${RELEASE_TAG#v}" package
8 changes: 8 additions & 0 deletions acceptance/pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
[project]
name = "volcano-sdk-acceptance"
version = "0.0.0"
requires-python = ">=3.11"
dependencies = ["behave>=1.3.3,<2", "pytest>=8.3,<10"]

[tool.uv]
package = false
23 changes: 23 additions & 0 deletions maintainers/acceptance.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
---
title: Installed-package acceptance tests
description: Build and publish the locked consumer project used by Hosting.
---

Run `bash scripts/build-acceptance.sh <package-file> <output-directory>` after
building the SDK. The command creates a standalone consumer and native dependency
lock without installing or executing newly resolved dependencies. It exports
`sdk-acceptance.tar.gz` with the existing contract bindings, without SDK runtime
source or package bytes.

CI uploads the package and bundle before running
`bash scripts/test-acceptance.sh <package-file> <bundle-file>` in a separate job.
That job checks frozen installation, package loading, and the public quickstart.
Signing and publishing download the original immutable artifact IDs after those
checks pass; dependencies cannot modify the originals.

Hosting authenticates the bundle and customer-registry package, restores that
package at the filename recorded in `acceptance.json`, then performs a frozen
install with `uv sync --frozen`. Place the wheel beside `pyproject.toml` and
`uv.lock`, then run tests with `uv run --no-sync`. Build candidate bundles
with the same command before publishing; package and bundle must come from the
same source commit. Never modify imports or lockfile text in the consumer.
31 changes: 31 additions & 0 deletions scripts/build-acceptance.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
#!/usr/bin/env bash
set -euo pipefail
if [ "$#" -ne 2 ]; then echo 'usage: build-acceptance.sh sdk.whl output-directory' >&2; exit 1; fi
repo_dir="$(cd "$(dirname "$0")/.." && pwd)"
artifact="$(cd "$(dirname "$1")" && pwd)/$(basename "$1")"
mkdir -p "$2"
output="$(cd "$2" && pwd)"
work="$(mktemp -d)"
trap 'rm -rf "$work"' EXIT
cp "$repo_dir/acceptance/pyproject.toml" "$work/"
cp "$artifact" "$work/"
cp -R "$repo_dir/features" "$work/"
mkdir -p "$work/tests/package" "$work/docs" "$work/scripts"
cp "$repo_dir/tests/package/quickstart.py" "$work/tests/package/"
cp "$repo_dir/docs/README.md" "$work/docs/"
cp "$repo_dir/scripts/smoke-wheel.sh" "$work/scripts/"
cd "$work"
uv add --no-sync --no-build "./$(basename "$artifact")"
python3 - "$(basename "$artifact")" <<'PYTHON'
import email, hashlib, json, sys, zipfile
from pathlib import Path
artifact = Path(sys.argv[1])
with zipfile.ZipFile(artifact) as wheel:
names = [name for name in wheel.namelist() if name.endswith(".dist-info/METADATA")]
assert len(names) == 1
metadata = email.message_from_bytes(wheel.read(names[0]))
assert metadata["Name"] == "volcano-sdk-python"
Path("acceptance.json").write_text(json.dumps({"schema": 1, "language": "python", "package": metadata["Name"], "version": metadata["Version"], "filename": artifact.name, "sha256": hashlib.sha256(artifact.read_bytes()).hexdigest()}, indent=2))
PYTHON
rm "$(basename "$artifact")"
tar -czf "$output/sdk-acceptance.tar.gz" .
11 changes: 11 additions & 0 deletions scripts/smoke-wheel.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
#!/usr/bin/env bash
set -euo pipefail
if [ "$#" -ne 1 ]; then echo 'usage: smoke-wheel.sh sdk.whl' >&2; exit 1; fi
artifact="$(cd "$(dirname "$1")" && pwd)/$(basename "$1")"
repo_dir="$(cd "$(dirname "$0")/.." && pwd)"
work="$(mktemp -d)"
trap 'rm -rf "$work"' EXIT
uv venv "$work/venv"
uv pip install --python "$work/venv/bin/python" --only-binary :all: "$artifact"
cd "$work"
env -i PATH="$PATH" HOME="$work" "$work/venv/bin/python" -I "$repo_dir/tests/package/quickstart.py"
14 changes: 14 additions & 0 deletions scripts/test-acceptance.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
#!/usr/bin/env bash
set -euo pipefail
if [ "$#" -ne 2 ]; then echo 'usage: test-acceptance.sh package-file sdk-acceptance.tar.gz' >&2; exit 1; fi
artifact="$(cd "$(dirname "$1")" && pwd)/$(basename "$1")"
bundle="$(cd "$(dirname "$2")" && pwd)/$(basename "$2")"
work="$(mktemp -d)"
trap 'rm -rf "$work"' EXIT
tar -xzf "$bundle" -C "$work"
cd "$work"
cp "$artifact" .
python3 -c 'import hashlib,json,pathlib; a=json.loads(pathlib.Path("acceptance.json").read_text()); assert hashlib.sha256(pathlib.Path(a["filename"]).read_bytes()).hexdigest()==a["sha256"], "Package differs from acceptance bundle"'
uv sync --frozen --no-build
uv run --no-sync python -I -c 'from pathlib import Path; import volcano_sdk; assert Path(volcano_sdk.__file__).resolve().is_relative_to(Path.cwd()/".venv")'
bash scripts/smoke-wheel.sh "$(basename "$artifact")"
Loading