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
49 changes: 0 additions & 49 deletions .github/scripts/build_lambda_layer.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,18 +14,9 @@
from pathlib import Path


ARCHITECTURE_PLATFORMS = {
"x86_64": "manylinux2014_x86_64",
"arm64": "manylinux2014_aarch64",
}
SUPPORTED_PYTHON_VERSIONS = ("3.11", "3.12", "3.13", "3.14")


@dataclass(frozen=True)
class BuildConfig:
output: Path
target_python: str
architecture: str
sdk_distribution: Path
otel_distribution: Path
build_dir: Path | None = None
Expand All @@ -51,20 +42,6 @@ def build_layer(config: BuildConfig) -> Path:


def _validate_config(config: BuildConfig) -> None:
if config.architecture not in ARCHITECTURE_PLATFORMS:
supported = ", ".join(sorted(ARCHITECTURE_PLATFORMS))
raise ValueError(
f"Unsupported architecture: {config.architecture}. "
f"Supported architectures: {supported}"
)

if config.target_python not in SUPPORTED_PYTHON_VERSIONS:
supported = ", ".join(SUPPORTED_PYTHON_VERSIONS)
raise ValueError(
f"Unsupported Python version: {config.target_python}. "
f"Supported versions: {supported}"
)

for distribution in (config.sdk_distribution, config.otel_distribution):
if not distribution.is_file():
raise FileNotFoundError(distribution)
Expand All @@ -76,10 +53,6 @@ def _validate_output_location(output: Path, build_dir: Path) -> None:


def _install_layer_dependencies(config: BuildConfig, target_dir: Path) -> None:
python_version = config.target_python
abi = f"cp{python_version.replace('.', '')}"
platform = ARCHITECTURE_PLATFORMS[config.architecture]

command = [
sys.executable,
"-m",
Expand All @@ -88,14 +61,6 @@ def _install_layer_dependencies(config: BuildConfig, target_dir: Path) -> None:
"--upgrade",
"--target",
str(target_dir),
"--platform",
platform,
"--implementation",
"cp",
"--python-version",
python_version,
"--abi",
abi,
"--only-binary",
":all:",
"--no-compile",
Expand All @@ -122,18 +87,6 @@ def main(argv: list[str] | None = None) -> int:
description="Build the AWS Durable Execution SDK OTel plugin Lambda layer.",
)
parser.add_argument("--output", type=Path, required=True)
parser.add_argument(
"--target-python",
choices=SUPPORTED_PYTHON_VERSIONS,
required=True,
help="Lambda Python minor version.",
)
parser.add_argument(
"--architecture",
choices=sorted(ARCHITECTURE_PLATFORMS),
required=True,
help="Lambda instruction set architecture.",
)
parser.add_argument("--sdk-distribution", type=Path, required=True)
parser.add_argument("--otel-distribution", type=Path, required=True)
parser.add_argument(
Expand All @@ -146,8 +99,6 @@ def main(argv: list[str] | None = None) -> int:
output = build_layer(
BuildConfig(
output=args.output,
target_python=args.target_python,
architecture=args.architecture,
sdk_distribution=args.sdk_distribution,
otel_distribution=args.otel_distribution,
build_dir=args.build_dir,
Expand Down
42 changes: 4 additions & 38 deletions .github/scripts/tests/test_build_lambda_layer.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,16 +42,16 @@ def fake_run(command: list[str], check: bool) -> subprocess.CompletedProcess[str
output = build_layer(
BuildConfig(
output=tmp_path / "layer.zip",
target_python="3.12",
architecture="arm64",
sdk_distribution=sdk_wheel,
otel_distribution=otel_wheel,
)
)

assert output == tmp_path / "layer.zip"
assert commands[0][commands[0].index("--platform") + 1] == "manylinux2014_aarch64"
assert commands[0][commands[0].index("--abi") + 1] == "cp312"
assert "--platform" not in commands[0]
assert "--implementation" not in commands[0]
assert "--python-version" not in commands[0]
assert "--abi" not in commands[0]
assert "--no-compile" in commands[0]
assert str(sdk_wheel) in commands[0]
assert str(otel_wheel) in commands[0]
Expand All @@ -66,43 +66,11 @@ def fake_run(command: list[str], check: bool) -> subprocess.CompletedProcess[str
)


@pytest.mark.parametrize(
("target_python", "architecture", "error"),
[
("3.10", "x86_64", "Unsupported Python version"),
("3.12", "sparc", "Unsupported architecture"),
],
)
def test_build_layer_rejects_unsupported_targets(
target_python: str,
architecture: str,
error: str,
tmp_path: Path,
) -> None:
sdk_wheel = tmp_path / "sdk.whl"
otel_wheel = tmp_path / "otel.whl"
sdk_wheel.write_text("sdk")
otel_wheel.write_text("otel")

with pytest.raises(ValueError, match=error):
build_layer(
BuildConfig(
output=tmp_path / "layer.zip",
target_python=target_python,
architecture=architecture,
sdk_distribution=sdk_wheel,
otel_distribution=otel_wheel,
)
)


def test_build_layer_requires_built_distributions(tmp_path: Path) -> None:
with pytest.raises(FileNotFoundError):
build_layer(
BuildConfig(
output=tmp_path / "layer.zip",
target_python="3.13",
architecture="x86_64",
sdk_distribution=tmp_path / "missing-sdk.whl",
otel_distribution=tmp_path / "missing-otel.whl",
)
Expand All @@ -120,8 +88,6 @@ def test_build_layer_rejects_output_inside_build_directory(tmp_path: Path) -> No
build_layer(
BuildConfig(
output=build_dir / "layer.zip",
target_python="3.13",
architecture="x86_64",
sdk_distribution=sdk_wheel,
otel_distribution=otel_wheel,
build_dir=build_dir,
Expand Down
18 changes: 18 additions & 0 deletions .github/scripts/tests/test_lambda_layer_publish_workflow.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
from pathlib import Path


REPOSITORY_ROOT = Path(__file__).resolve().parents[3]
WORKFLOW = REPOSITORY_ROOT / ".github" / "workflows" / "lambda-layer-publish.yml"


def test_workflow_builds_and_publishes_one_agnostic_layer() -> None:
workflow = WORKFLOW.read_text()

assert "strategy:" not in workflow
assert "matrix." not in workflow
assert "--target-python" not in workflow
assert "--architecture" not in workflow
assert "--compatible-runtime" not in workflow
assert "--compatible-architecture" not in workflow
Comment on lines +15 to +16

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Codex AI review

[P2] Preserve compatibility metadata on the shared version. These fields drive ListLayers/ListLayerVersions compatibility filtering; without them, consumers resolving the latest Python/architecture-compatible version will not select this release. A single version can declare all supported Python runtimes and both architectures, so publish those lists and assert their presence instead of requiring the flags to be absent.

assert "otel-plugin-layer" in workflow
assert "${{ env.LAYER_NAME }}.zip" in workflow
92 changes: 7 additions & 85 deletions .github/workflows/lambda-layer-publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -143,42 +143,6 @@ jobs:
build-layers:
needs: build-distributions
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
include:
- target_python: "3.11"
runtime: python3.11
runtime_slug: python311
architecture: x86_64
- target_python: "3.11"
runtime: python3.11
runtime_slug: python311
architecture: arm64
- target_python: "3.12"
runtime: python3.12
runtime_slug: python312
architecture: x86_64
- target_python: "3.12"
runtime: python3.12
runtime_slug: python312
architecture: arm64
- target_python: "3.13"
runtime: python3.13
runtime_slug: python313
architecture: x86_64
- target_python: "3.13"
runtime: python3.13
runtime_slug: python313
architecture: arm64
- target_python: "3.14"
runtime: python3.14
runtime_slug: python314
architecture: x86_64
- target_python: "3.14"
runtime: python3.14
runtime_slug: python314
architecture: arm64

steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
Expand All @@ -188,7 +152,7 @@ jobs:
- name: Set up Python
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: ${{ matrix.target_python }}
python-version: "3.11"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Codex AI review

[P1] Build dependencies for every supported target. This resolves the entire layer on CPython 3.11/x86_64. opentelemetry-exporter-otlp pulls native dependencies such as grpcio, so the archive contains CPython 3.11/x86_64 extensions that fail when imported on Python 3.12-3.14 or arm64. Restore target-specific builds, or first ensure every bundled dependency is genuinely pure Python and verify the resulting archive on each supported runtime and architecture.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Claude AI review

Pinning the builder to Python 3.11 on the x86_64 ubuntu-latest runner and dropping the pip cross-targeting flags (--platform/--implementation/--python-version/--abi in build_lambda_layer.py) only yields a portable layer if the dependency closure is pure-Python — it is not.

The OTel plugin declares opentelemetry-exporter-otlp, a meta-package that installs opentelemetry-exporter-otlp-proto-grpc (→ native grpcio, which has no py3-none-any wheel) and protobuf (native C++ extension, arch-specific). With pip install --only-binary :all: and no --platform/--python-version, pip resolves these to the build host's wheels: cp311 / manylinux_x86_64.

Impact: the single artifact — published with --compatible-runtimes/--compatible-architectures removed and documented as "architecture-agnostic" — actually contains x86_64/CPython-3.11 shared objects. On arm64 Lambdas the OTLP HTTP export path fails when loading the x86_64 protobuf extension, and the grpc exporter is unusable; because the compatibility metadata was also removed, Lambda no longer prevents attaching the layer to incompatible runtimes/architectures. This is a regression from the prior matrix, which built correct per-arch/per-version artifacts.

Concrete fixes (pick one): (a) restore the architecture (and, for version-specific wheels, Python) matrix and the targeting flags/metadata; (b) make the closure genuinely pure-Python — depend on opentelemetry-exporter-otlp-proto-http instead of the -otlp meta-package to drop grpcio, and force the pure-Python protobuf backend — then assert the built python/ tree contains no .so/native extensions before zipping; or (c) keep the architecture matrix (2 builds) even if you collapse the Python-version dimension, since arch is the hard portability boundary here.


- name: Download wheels
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
Expand All @@ -199,22 +163,20 @@ jobs:
- name: Build layer zip
id: build-layer
env:
LAYER_ZIP: dist/${{ env.LAYER_NAME }}-${{ matrix.runtime_slug }}-${{ matrix.architecture }}.zip
LAYER_ZIP: dist/${{ env.LAYER_NAME }}.zip
run: |
SDK_WHEEL=$(find release-dists -name 'aws_durable_execution_sdk_python-*.whl' -print -quit)
OTEL_WHEEL=$(find release-dists -name 'aws_durable_execution_sdk_python_otel-*.whl' -print -quit)
python .github/scripts/build_lambda_layer.py \
--sdk-distribution "$SDK_WHEEL" \
--otel-distribution "$OTEL_WHEEL" \
--target-python "${{ matrix.target_python }}" \
--architecture "${{ matrix.architecture }}" \
--output "$LAYER_ZIP"
echo "layer_zip=${LAYER_ZIP}" >> "$GITHUB_OUTPUT"

- name: Upload layer artifact
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: otel-plugin-layer-${{ matrix.runtime_slug }}-${{ matrix.architecture }}
name: otel-plugin-layer
path: ${{ steps.build-layer.outputs.layer_zip }}
if-no-files-found: error
retention-days: 30
Expand All @@ -229,60 +191,24 @@ jobs:
id-token: write
env:
LAYER_REGIONS: ${{ inputs.regions || vars.LAYER_PUBLISH_REGIONS }}
strategy:
fail-fast: false
matrix:
include:
- target_python: "3.11"
runtime: python3.11
runtime_slug: python311
architecture: x86_64
- target_python: "3.11"
runtime: python3.11
runtime_slug: python311
architecture: arm64
- target_python: "3.12"
runtime: python3.12
runtime_slug: python312
architecture: x86_64
- target_python: "3.12"
runtime: python3.12
runtime_slug: python312
architecture: arm64
- target_python: "3.13"
runtime: python3.13
runtime_slug: python313
architecture: x86_64
- target_python: "3.13"
runtime: python3.13
runtime_slug: python313
architecture: arm64
- target_python: "3.14"
runtime: python3.14
runtime_slug: python314
architecture: x86_64
- target_python: "3.14"
runtime: python3.14
runtime_slug: python314
architecture: arm64

steps:
- name: Download layer artifact
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: otel-plugin-layer-${{ matrix.runtime_slug }}-${{ matrix.architecture }}
name: otel-plugin-layer
path: dist/

- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@e6de054238d6b7531b4efff3b6587d9aade6a06c # v6.2.3
with:
role-to-assume: ${{ secrets.LAYER_PUBLISH_ROLE_ARN }}
role-session-name: otelLayerPublish-${{ matrix.runtime_slug }}-${{ matrix.architecture }}
role-session-name: otelLayerPublish
aws-region: us-east-1

- name: Publish layer versions
env:
LAYER_ZIP: dist/${{ env.LAYER_NAME }}-${{ matrix.runtime_slug }}-${{ matrix.architecture }}.zip
LAYER_ZIP: dist/${{ env.LAYER_NAME }}.zip
SDK_VERSION: ${{ needs.build-distributions.outputs.sdk_version }}
OTEL_VERSION: ${{ needs.build-distributions.outputs.otel_version }}
run: |
Expand All @@ -297,11 +223,9 @@ jobs:
continue
fi

LAYER_DESCRIPTION="AWS Durable Execution SDK ${SDK_VERSION} OTel plugin ${OTEL_VERSION} (${{ matrix.runtime }}/${{ matrix.architecture }}) sha256:${LOCAL_CODE_SHA256}"
LAYER_DESCRIPTION="AWS Durable Execution SDK ${SDK_VERSION} OTel plugin ${OTEL_VERSION} sha256:${LOCAL_CODE_SHA256}"
if ! EXISTING_RESULT=$(aws lambda list-layer-versions \
--layer-name "$LAYER_NAME" \
--compatible-runtime "${{ matrix.runtime }}" \
--compatible-architecture "${{ matrix.architecture }}" \
--region "$REGION" \
--query "LayerVersions[?Description=='${LAYER_DESCRIPTION}'] | [0].[LayerVersionArn,Version]" \
--output text 2>&1); then
Expand Down Expand Up @@ -339,8 +263,6 @@ jobs:
--layer-name "$LAYER_NAME" \
--description "$LAYER_DESCRIPTION" \
--zip-file "fileb://${LAYER_ZIP}" \
--compatible-runtimes "${{ matrix.runtime }}" \
--compatible-architectures "${{ matrix.architecture }}" \
--license-info Apache-2.0 \
--region "$REGION" \
--query '[LayerVersionArn,Version,Content.CodeSha256]' \
Expand Down
3 changes: 3 additions & 0 deletions .github/workflows/test-parser.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,14 @@ on:
- '.github/scripts/build_lambda_layer.py'
- '.github/scripts/parse_sdk_branch.py'
- '.github/scripts/tests/**'
- '.github/workflows/lambda-layer-publish.yml'
push:
branches: [ main ]
paths:
- '.github/scripts/build_lambda_layer.py'
- '.github/scripts/parse_sdk_branch.py'
- '.github/scripts/tests/**'
- '.github/workflows/lambda-layer-publish.yml'

permissions:
contents: read
Expand All @@ -29,4 +31,5 @@ jobs:
run: |
python -m pytest \
.github/scripts/tests/test_build_lambda_layer.py \
.github/scripts/tests/test_lambda_layer_publish_workflow.py \
.github/scripts/tests/test_parse_sdk_branch.py
10 changes: 5 additions & 5 deletions RELEASING.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,8 +71,8 @@ The workflow runs on the `release: [published]` event, so it fires whenever a re

Releases containing an `otel-v` tag also trigger the
[`lambda-layer-publish.yml`](.github/workflows/lambda-layer-publish.yml)
workflow. It builds the SDK and OTel plugin into Lambda layers for each
supported Python runtime and architecture, then publishes public versions of the
workflow. It builds the SDK and OTel plugin into a Python-version- and
architecture-agnostic Lambda layer, then publishes a public version of the
`aws-durable-execution-sdk-python-otel-plugin` layer.
For OTel-only releases, the workflow downloads the exact SDK version pinned by
`layer.sdk-version` in `.github/lambda-layer-publish.toml`; that version must
Expand All @@ -85,9 +85,9 @@ environment variable to a comma-separated list of AWS Regions. When unset, the
workflow publishes to every commercial AWS Region supported by Lambda.
The workflow can also be run manually from the Actions tab on `main`; its
optional `regions` input overrides `LAYER_PUBLISH_REGIONS` for that run.
Each runtime and architecture layer archive is built once and retained as a
workflow artifact so retries publish the exact same resolved dependencies. Its
SHA-256 is included in the layer description and verified before reuse.
The layer archive is built once and retained as a workflow artifact so retries
publish the exact same resolved dependencies. Its SHA-256 is included in the
layer description and verified before reuse.
The publishing role must allow `lambda:PublishLayerVersion` and
`lambda:AddLayerVersionPermission`, as well as `lambda:ListLayerVersions` and
`lambda:GetLayerVersion` for identity-checked, idempotent release retries.
Expand Down
Loading