From 9c55cc016f2450233beb24e90a69c777bbf6d5ee Mon Sep 17 00:00:00 2001 From: Frank Chen <65260095+zhongkechen@users.noreply.github.com> Date: Thu, 22 Oct 2026 01:22:41 +0000 Subject: [PATCH] ci: publish emulator images after pypi packages --- .../test_release_workflow_dependencies.py | 67 +++++++++++++++++++ .github/workflows/ecr-release.yml | 22 ++++-- .github/workflows/pypi-publish.yml | 13 ++++ .github/workflows/test-parser.yml | 3 + 4 files changed, 98 insertions(+), 7 deletions(-) create mode 100644 .github/scripts/tests/test_release_workflow_dependencies.py diff --git a/.github/scripts/tests/test_release_workflow_dependencies.py b/.github/scripts/tests/test_release_workflow_dependencies.py new file mode 100644 index 000000000..9ef97b003 --- /dev/null +++ b/.github/scripts/tests/test_release_workflow_dependencies.py @@ -0,0 +1,67 @@ +# SPDX-FileCopyrightText: 2025-present Amazon.com, Inc. or its affiliates. +# +# SPDX-License-Identifier: Apache-2.0 +"""Prevent emulator builds from racing their release's PyPI dependencies.""" + +from pathlib import Path + +import yaml + + +WORKFLOWS = Path(__file__).parents[2] / "workflows" + + +def test_emulator_release_requires_successful_pypi_publication() -> None: + pypi = yaml.safe_load((WORKFLOWS / "pypi-publish.yml").read_text()) + ecr = yaml.safe_load((WORKFLOWS / "ecr-release.yml").read_text()) + # PyYAML's YAML 1.1 loader treats the unquoted "on" key as True. + triggers = ecr[True] + assert set(triggers) == {"workflow_call"} + + caller = pypi["jobs"]["publish-emulator-image"] + assert caller["uses"] == "./.github/workflows/ecr-release.yml" + assert caller["needs"] == "pypi-publish" + assert "if" not in caller # Preserve the default success() gate. + + publisher = pypi["jobs"]["pypi-publish"] + packages = {entry["name"] for entry in publisher["strategy"]["matrix"]["package"]} + assert { + "aws-durable-execution-sdk-python", + "aws-durable-execution-sdk-python-testing", + } <= packages + assert not publisher.get("continue-on-error", False) + + +def test_emulator_checks_out_and_parses_the_published_release_tag() -> None: + pypi = yaml.safe_load((WORKFLOWS / "pypi-publish.yml").read_text()) + ecr = yaml.safe_load((WORKFLOWS / "ecr-release.yml").read_text()) + assert pypi["jobs"]["publish-emulator-image"]["with"]["release_tag"] == ( + "${{ github.event.release.tag_name }}" + ) + release_tag = ecr[True]["workflow_call"]["inputs"]["release_tag"] + assert release_tag["type"] == "string" + assert release_tag["required"] is True + + checkouts = [ + step + for job in ecr["jobs"].values() + for step in job["steps"] + if step.get("uses", "").startswith("actions/checkout@") + ] + assert checkouts + assert all(step["with"]["ref"] == "${{ inputs.release_tag }}" for step in checkouts) + parser = next( + step for step in ecr["jobs"]["preflight"]["steps"] if step.get("id") == "tag" + ) + assert parser["env"]["RELEASE_TAG"] == "${{ inputs.release_tag }}" + + +def test_emulator_call_passes_only_its_required_release_permissions() -> None: + pypi = yaml.safe_load((WORKFLOWS / "pypi-publish.yml").read_text()) + ecr = yaml.safe_load((WORKFLOWS / "ecr-release.yml").read_text()) + caller = pypi["jobs"]["publish-emulator-image"] + assert caller["permissions"] == {"contents": "read", "id-token": "write"} + assert caller["secrets"] == { + "ECR_UPLOAD_IAM_ROLE_ARN": "${{ secrets.ECR_UPLOAD_IAM_ROLE_ARN }}" + } + assert set(ecr[True]["workflow_call"]["secrets"]) == {"ECR_UPLOAD_IAM_ROLE_ARN"} diff --git a/.github/workflows/ecr-release.yml b/.github/workflows/ecr-release.yml index 2a7ede51a..07ea5db7d 100644 --- a/.github/workflows/ecr-release.yml +++ b/.github/workflows/ecr-release.yml @@ -1,8 +1,16 @@ name: Upload Testing SDK Emulator Image on: - release: - types: [published] + workflow_call: + inputs: + release_tag: + description: Release tag whose packages have been published to PyPI + required: true + type: string + secrets: + ECR_UPLOAD_IAM_ROLE_ARN: + description: ECR upload role, required when the release names the testing package + required: false permissions: contents: read @@ -33,7 +41,7 @@ jobs: steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: - ref: ${{ github.event.release.tag_name }} + ref: ${{ inputs.release_tag }} - name: Set up Python uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 @@ -47,7 +55,7 @@ jobs: id: tag shell: bash env: - RELEASE_TAG: ${{ github.event.release.tag_name }} + RELEASE_TAG: ${{ inputs.release_tag }} run: | tag_version="$(python .github/scripts/parse_testing_version.py)" if [[ -z "$tag_version" ]]; then @@ -170,7 +178,7 @@ jobs: steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: - ref: ${{ github.event.release.tag_name }} + ref: ${{ inputs.release_tag }} - name: Set up Python uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 @@ -281,7 +289,7 @@ jobs: steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: - ref: ${{ github.event.release.tag_name }} + ref: ${{ inputs.release_tag }} - name: Set up Python uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 @@ -412,7 +420,7 @@ jobs: steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: - ref: ${{ github.event.release.tag_name }} + ref: ${{ inputs.release_tag }} - name: Set up Python uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 diff --git a/.github/workflows/pypi-publish.yml b/.github/workflows/pypi-publish.yml index 5149419cd..705d01e6f 100644 --- a/.github/workflows/pypi-publish.yml +++ b/.github/workflows/pypi-publish.yml @@ -85,3 +85,16 @@ jobs: uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # v1.14.2 with: packages-dir: dist/ + + publish-emulator-image: + # The testing wheel requires the core SDK from this release on PyPI. + # Keep this in the same run so failed image jobs can be retried after publish. + needs: pypi-publish + permissions: + contents: read + id-token: write + uses: ./.github/workflows/ecr-release.yml + with: + release_tag: ${{ github.event.release.tag_name }} + secrets: + ECR_UPLOAD_IAM_ROLE_ARN: ${{ secrets.ECR_UPLOAD_IAM_ROLE_ARN }} diff --git a/.github/workflows/test-parser.yml b/.github/workflows/test-parser.yml index 4235848de..68c26030d 100644 --- a/.github/workflows/test-parser.yml +++ b/.github/workflows/test-parser.yml @@ -18,6 +18,7 @@ on: - '.github/workflows/opentelemetry-conformance-tests.yml' - '.github/workflows/test-parser.yml' - '.github/workflows/ecr-release.yml' + - '.github/workflows/pypi-publish.yml' - 'packages/aws-durable-execution-sdk-python-otel/pyproject.toml' push: branches: [ main ] @@ -37,6 +38,7 @@ on: - '.github/workflows/opentelemetry-conformance-tests.yml' - '.github/workflows/test-parser.yml' - '.github/workflows/ecr-release.yml' + - '.github/workflows/pypi-publish.yml' - 'packages/aws-durable-execution-sdk-python-otel/pyproject.toml' permissions: @@ -81,5 +83,6 @@ jobs: .github/scripts/tests/test_opentelemetry_conformance_workflow.py \ .github/scripts/tests/test_parse_sdk_branch.py \ .github/scripts/tests/test_parse_testing_version.py \ + .github/scripts/tests/test_release_workflow_dependencies.py \ .github/scripts/tests/e2e/test_ecr_release_workflow.py \ .github/scripts/tests/test_shared_workflow_runners.py