From 9e71474f57e0e347e7f3f7a602e49770f0d4b7c5 Mon Sep 17 00:00:00 2001 From: Sean Keever <33592180+swkeever@users.noreply.github.com> Date: Mon, 21 Sep 2026 15:59:56 -0400 Subject: [PATCH 1/6] test(acceptance): publish locked installed-package harness --- .github/workflows/acceptance.yml | 48 +++++++++++++++++++++++++++++ .github/workflows/publish.yml | 53 +++++++++++++++++++++++++++++++- acceptance/README.md | 14 +++++++++ acceptance/pyproject.toml | 8 +++++ scripts/build-acceptance.sh | 32 +++++++++++++++++++ scripts/smoke-wheel.sh | 11 +++++++ 6 files changed, 165 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/acceptance.yml create mode 100644 acceptance/README.md create mode 100644 acceptance/pyproject.toml create mode 100755 scripts/build-acceptance.sh create mode 100755 scripts/smoke-wheel.sh diff --git a/.github/workflows/acceptance.yml b/.github/workflows/acceptance.yml new file mode 100644 index 00000000..7f056ef8 --- /dev/null +++ b/.github/workflows/acceptance.yml @@ -0,0 +1,48 @@ +name: Installed package acceptance + +on: + push: + pull_request: + workflow_dispatch: + +permissions: + contents: read + +jobs: + build: + 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 + with: + name: client-acceptance + path: candidate/* + if-no-files-found: error + + attest: + if: github.event_name != 'pull_request' + needs: build + 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: + name: client-acceptance + path: candidate + - uses: actions/attest@1e69f48acb82d1966a394da916b4c1698aa569d6 # v4 + with: + subject-path: candidate/* diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 8cfb6da3..baa32057 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -54,6 +54,7 @@ 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 }} @@ -63,10 +64,42 @@ jobs: name: release-package path: dist/* if-no-files-found: error + - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: acceptance-tests + path: acceptance-output/sdk-acceptance.tar.gz + if-no-files-found: error - publish: + attest: needs: build 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: + name: release-package + path: release + - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: acceptance-tests + 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: gh release upload "$RELEASE_TAG" release/sdk-acceptance.tar.gz + + publish: + needs: [build, attest] + runs-on: ubuntu-latest timeout-minutes: 10 environment: name: pypi @@ -89,6 +122,24 @@ jobs: permissions: contents: write steps: + - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: acceptance-tests + 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 - name: Link the published PyPI version in the release notes env: GH_TOKEN: ${{ github.token }} diff --git a/acceptance/README.md b/acceptance/README.md new file mode 100644 index 00000000..9ed1f978 --- /dev/null +++ b/acceptance/README.md @@ -0,0 +1,14 @@ +# Installed-package acceptance tests + +Run `bash scripts/build-acceptance.sh ` after +building the SDK. The command creates a standalone consumer, installs the exact +package with the language package manager, checks package loading and the public +quickstart, and exports `sdk-acceptance.tar.gz` without runtime source or SDK bytes. +The bundle contains the existing contract bindings and a native dependency lock. + +Hosting authenticates the bundle and customer-registry package, restores that +package at the filename recorded in `acceptance.json`, then performs a frozen +install. Ruby places the gem in `vendor/cache` and sets +`VOLCANO_ACCEPTANCE_VERSION` from the recorded version. 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. diff --git a/acceptance/pyproject.toml b/acceptance/pyproject.toml new file mode 100644 index 00000000..e9a389fb --- /dev/null +++ b/acceptance/pyproject.toml @@ -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 diff --git a/scripts/build-acceptance.sh b/scripts/build-acceptance.sh new file mode 100755 index 00000000..23ab4ebd --- /dev/null +++ b/scripts/build-acceptance.sh @@ -0,0 +1,32 @@ +#!/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 "./$(basename "$artifact")" +uv sync --frozen +uv run --no-sync python -I - "$(basename "$artifact")" <<'PYTHON' +import hashlib, json, sys +from importlib.metadata import distribution +from pathlib import Path +import volcano_sdk +package = distribution("volcano-sdk-python") +assert Path(volcano_sdk.__file__).resolve().is_relative_to(Path.cwd() / ".venv") +artifact = Path(sys.argv[1]) +Path("acceptance.json").write_text(json.dumps({"schema": 1, "language": "python", "package": "volcano-sdk-python", "version": package.version, "filename": artifact.name, "sha256": hashlib.sha256(artifact.read_bytes()).hexdigest()}, indent=2)) +PYTHON +bash scripts/smoke-wheel.sh "$(basename "$artifact")" +rm -rf .venv "$(basename "$artifact")" +tar -czf "$output/sdk-acceptance.tar.gz" . diff --git a/scripts/smoke-wheel.sh b/scripts/smoke-wheel.sh new file mode 100755 index 00000000..1ad84237 --- /dev/null +++ b/scripts/smoke-wheel.sh @@ -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" From bf2ae1a013d42a58ba86fdd3d4a29b5c7420b756 Mon Sep 17 00:00:00 2001 From: Sean Keever <33592180+swkeever@users.noreply.github.com> Date: Mon, 21 Sep 2026 16:05:37 -0400 Subject: [PATCH 2/6] ci(acceptance): isolate registry smoke from release credentials --- .github/workflows/publish.yml | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index baa32057..4eb90535 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -115,12 +115,12 @@ jobs: - name: Publish distributions to PyPI uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # v1.14.2 - release-link: + registry-smoke: needs: publish runs-on: ubuntu-latest - timeout-minutes: 5 + timeout-minutes: 15 permissions: - contents: write + contents: read steps: - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: @@ -140,6 +140,14 @@ jobs: 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 + timeout-minutes: 5 + permissions: + contents: write + steps: - name: Link the published PyPI version in the release notes env: GH_TOKEN: ${{ github.token }} From 789fe8d99d1d24e8b379bde4b5f32af87f68516d Mon Sep 17 00:00:00 2001 From: Sean Keever <33592180+swkeever@users.noreply.github.com> Date: Mon, 21 Sep 2026 16:16:21 -0400 Subject: [PATCH 3/6] ci(release): safely replace acceptance evidence on retry --- .github/workflows/publish.yml | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 4eb90535..32a13791 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -95,7 +95,17 @@ jobs: GH_TOKEN: ${{ github.token }} GH_REPO: ${{ github.repository }} RELEASE_TAG: ${{ github.event.release.tag_name }} - run: gh release upload "$RELEASE_TAG" release/sdk-acceptance.tar.gz + 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 + # A retry may regenerate dependency locks, but must test identical SDK bytes. + 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 + fi + gh release upload "$RELEASE_TAG" release/sdk-acceptance.tar.gz --clobber publish: needs: [build, attest] From b22d59068511eb440c2d76e637c5a98d6f99b2d8 Mon Sep 17 00:00:00 2001 From: Sean Keever <33592180+swkeever@users.noreply.github.com> Date: Mon, 21 Sep 2026 16:31:15 -0400 Subject: [PATCH 4/6] ci(release): retain verified acceptance assets on retry --- .github/workflows/publish.yml | 5 +++-- acceptance/README.md => maintainers/acceptance.md | 5 ++++- 2 files changed, 7 insertions(+), 3 deletions(-) rename acceptance/README.md => maintainers/acceptance.md (87%) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 32a13791..ebec68fd 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -100,12 +100,13 @@ jobs: 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 - # A retry may regenerate dependency locks, but must test identical SDK bytes. + # 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 --clobber + gh release upload "$RELEASE_TAG" release/sdk-acceptance.tar.gz publish: needs: [build, attest] diff --git a/acceptance/README.md b/maintainers/acceptance.md similarity index 87% rename from acceptance/README.md rename to maintainers/acceptance.md index 9ed1f978..54667450 100644 --- a/acceptance/README.md +++ b/maintainers/acceptance.md @@ -1,4 +1,7 @@ -# Installed-package acceptance tests +--- +title: Installed-package acceptance tests +description: Build and publish the locked consumer project used by Hosting. +--- Run `bash scripts/build-acceptance.sh ` after building the SDK. The command creates a standalone consumer, installs the exact From 96c303d29dfdeef20dcce62188815d2f79d469c2 Mon Sep 17 00:00:00 2001 From: Sean Keever <33592180+swkeever@users.noreply.github.com> Date: Mon, 21 Sep 2026 17:05:51 -0400 Subject: [PATCH 5/6] docs(acceptance): explain wheel restoration --- maintainers/acceptance.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/maintainers/acceptance.md b/maintainers/acceptance.md index 54667450..0f2dbffe 100644 --- a/maintainers/acceptance.md +++ b/maintainers/acceptance.md @@ -11,7 +11,7 @@ The bundle contains the existing contract bindings and a native dependency lock. Hosting authenticates the bundle and customer-registry package, restores that package at the filename recorded in `acceptance.json`, then performs a frozen -install. Ruby places the gem in `vendor/cache` and sets -`VOLCANO_ACCEPTANCE_VERSION` from the recorded version. Build candidate bundles +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. From 8170bc37685d9174babd93b8c3f0d6945fc3ebe4 Mon Sep 17 00:00:00 2001 From: Sean Keever <33592180+swkeever@users.noreply.github.com> Date: Mon, 21 Sep 2026 17:41:50 -0400 Subject: [PATCH 6/6] fix(ci): isolate immutable packages from acceptance dependencies --- .github/workflows/acceptance.yml | 14 ++++++- .github/workflows/publish.yml | 32 +++++++++++---- .github/workflows/verify-acceptance.yml | 53 +++++++++++++++++++++++++ maintainers/acceptance.md | 14 +++++-- scripts/build-acceptance.sh | 21 +++++----- scripts/test-acceptance.sh | 14 +++++++ 6 files changed, 124 insertions(+), 24 deletions(-) create mode 100644 .github/workflows/verify-acceptance.yml create mode 100755 scripts/test-acceptance.sh diff --git a/.github/workflows/acceptance.yml b/.github/workflows/acceptance.yml index 7f056ef8..09a466ec 100644 --- a/.github/workflows/acceptance.yml +++ b/.github/workflows/acceptance.yml @@ -10,6 +10,8 @@ permissions: jobs: build: + outputs: + package-artifact-id: ${{ steps.upload-package.outputs.artifact-id }} runs-on: ubuntu-latest timeout-minutes: 20 steps: @@ -24,14 +26,21 @@ jobs: - 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 + needs: [build, acceptance-check] runs-on: ubuntu-latest timeout-minutes: 5 permissions: @@ -41,7 +50,8 @@ jobs: steps: - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: - name: client-acceptance + artifact-ids: ${{ needs.build.outputs.package-artifact-id }} + merge-multiple: true path: candidate - uses: actions/attest@1e69f48acb82d1966a394da916b4c1698aa569d6 # v4 with: diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index ebec68fd..fe89f71f 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -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 @@ -58,20 +61,31 @@ jobs: - 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 - attest: + 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: @@ -81,11 +95,13 @@ jobs: steps: - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: - name: release-package + artifact-ids: ${{ needs.build.outputs.package-artifact-id }} + merge-multiple: true path: release - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: - name: acceptance-tests + artifact-ids: ${{ needs.build.outputs.bundle-artifact-id }} + merge-multiple: true path: release - uses: actions/attest@1e69f48acb82d1966a394da916b4c1698aa569d6 # v4 with: @@ -121,13 +137,14 @@ 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 + needs: [publish, build] runs-on: ubuntu-latest timeout-minutes: 15 permissions: @@ -135,7 +152,8 @@ jobs: steps: - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: - name: acceptance-tests + artifact-ids: ${{ needs.build.outputs.bundle-artifact-id }} + merge-multiple: true path: acceptance-output - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5 with: diff --git a/.github/workflows/verify-acceptance.yml b/.github/workflows/verify-acceptance.yml new file mode 100644 index 00000000..d5230479 --- /dev/null +++ b/.github/workflows/verify-acceptance.yml @@ -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 diff --git a/maintainers/acceptance.md b/maintainers/acceptance.md index 0f2dbffe..5937895e 100644 --- a/maintainers/acceptance.md +++ b/maintainers/acceptance.md @@ -4,10 +4,16 @@ description: Build and publish the locked consumer project used by Hosting. --- Run `bash scripts/build-acceptance.sh ` after -building the SDK. The command creates a standalone consumer, installs the exact -package with the language package manager, checks package loading and the public -quickstart, and exports `sdk-acceptance.tar.gz` without runtime source or SDK bytes. -The bundle contains the existing contract bindings and a native dependency lock. +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 ` 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 diff --git a/scripts/build-acceptance.sh b/scripts/build-acceptance.sh index 23ab4ebd..a3f0471b 100755 --- a/scripts/build-acceptance.sh +++ b/scripts/build-acceptance.sh @@ -15,18 +15,17 @@ 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 "./$(basename "$artifact")" -uv sync --frozen -uv run --no-sync python -I - "$(basename "$artifact")" <<'PYTHON' -import hashlib, json, sys -from importlib.metadata import distribution +uv add --no-sync --no-build "./$(basename "$artifact")" +python3 - "$(basename "$artifact")" <<'PYTHON' +import email, hashlib, json, sys, zipfile from pathlib import Path -import volcano_sdk -package = distribution("volcano-sdk-python") -assert Path(volcano_sdk.__file__).resolve().is_relative_to(Path.cwd() / ".venv") artifact = Path(sys.argv[1]) -Path("acceptance.json").write_text(json.dumps({"schema": 1, "language": "python", "package": "volcano-sdk-python", "version": package.version, "filename": artifact.name, "sha256": hashlib.sha256(artifact.read_bytes()).hexdigest()}, indent=2)) +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 -bash scripts/smoke-wheel.sh "$(basename "$artifact")" -rm -rf .venv "$(basename "$artifact")" +rm "$(basename "$artifact")" tar -czf "$output/sdk-acceptance.tar.gz" . diff --git a/scripts/test-acceptance.sh b/scripts/test-acceptance.sh new file mode 100755 index 00000000..f1c1fef8 --- /dev/null +++ b/scripts/test-acceptance.sh @@ -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")"