From 7555db1b0d76a8c8ea07234e7f1fd35f63c4acd5 Mon Sep 17 00:00:00 2001 From: Frank Chen Date: Tue, 11 Aug 2026 23:12:06 +0000 Subject: [PATCH 1/5] ci: publish Java OTel Lambda layer --- .github/workflows/lambda-layer-publish.yml | 281 +++++++++++++++++++++ 1 file changed, 281 insertions(+) create mode 100644 .github/workflows/lambda-layer-publish.yml diff --git a/.github/workflows/lambda-layer-publish.yml b/.github/workflows/lambda-layer-publish.yml new file mode 100644 index 000000000..b67112cc8 --- /dev/null +++ b/.github/workflows/lambda-layer-publish.yml @@ -0,0 +1,281 @@ +name: Publish OTel Plugin Lambda Layer + +on: + release: + types: [published] + workflow_dispatch: + inputs: + regions: + description: "Comma-separated AWS Regions; defaults to all commercial Regions" + required: false + type: string + +permissions: + contents: read + +concurrency: + group: lambda-layer-publish-${{ github.event.release.tag_name || github.ref_name }} + cancel-in-progress: false + +env: + LAYER_NAME: aws-durable-execution-sdk-java-otel-plugin + SOURCE_REF: ${{ github.event_name == 'workflow_dispatch' && github.event.repository.default_branch || github.event.release.tag_name }} + DEFAULT_LAYER_REGIONS: >- + af-south-1, + ap-east-1, + ap-east-2, + ap-northeast-1, + ap-northeast-2, + ap-northeast-3, + ap-south-1, + ap-south-2, + ap-southeast-1, + ap-southeast-2, + ap-southeast-3, + ap-southeast-4, + ap-southeast-5, + ap-southeast-6, + ap-southeast-7, + ca-central-1, + ca-west-1, + eu-central-1, + eu-central-2, + eu-north-1, + eu-south-1, + eu-south-2, + eu-west-1, + eu-west-2, + eu-west-3, + il-central-1, + me-central-1, + me-south-1, + mx-central-1, + sa-east-1, + us-east-1, + us-east-2, + us-west-1, + us-west-2 + +jobs: + build-layer: + if: >- + github.event_name == 'release' || + (github.event_name == 'workflow_dispatch' && + github.ref == format('refs/heads/{0}', github.event.repository.default_branch)) + runs-on: ubuntu-latest + outputs: + sdk_version: ${{ steps.version.outputs.sdk_version }} + + steps: + - name: Checkout source + uses: actions/checkout@v7 + with: + ref: ${{ env.SOURCE_REF }} + + - name: Setup Java + uses: actions/setup-java@v5 + with: + distribution: corretto + java-version: '17' + cache: maven + + - name: Determine SDK version + id: version + env: + RELEASE_TAG: ${{ github.event.release.tag_name }} + shell: bash + run: | + set -euo pipefail + + sdk_version="$(mvn -q help:evaluate -Dexpression=project.version -DforceStdout)" + if [ "$GITHUB_EVENT_NAME" = "release" ]; then + if [[ ! "$RELEASE_TAG" =~ ^v([0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z][0-9A-Za-z.-]*)?)$ ]]; then + echo "Release tag must be a semantic version prefixed with v (for example, v1.2.0)." >&2 + exit 1 + fi + if [ "$sdk_version" != "${BASH_REMATCH[1]}" ]; then + echo "Release tag $RELEASE_TAG points to Maven version $sdk_version." >&2 + exit 1 + fi + fi + + echo "sdk_version=$sdk_version" >> "$GITHUB_OUTPUT" + + - name: Build layer zip + id: build-layer + env: + LAYER_ZIP: dist/${{ env.LAYER_NAME }}.zip + SDK_VERSION: ${{ steps.version.outputs.sdk_version }} + shell: bash + run: | + set -euo pipefail + + mvn -B -q -pl otel-plugin -am install -DskipTests --no-transfer-progress + mkdir -p layer/java/lib dist + mvn -B -q -pl otel-plugin dependency:copy-dependencies \ + -DincludeScope=runtime \ + -DoutputDirectory="$GITHUB_WORKSPACE/layer/java/lib" \ + --no-transfer-progress + cp "otel-plugin/target/aws-durable-execution-sdk-java-plugin-otel-${SDK_VERSION}.jar" \ + layer/java/lib/ + cp LICENSE NOTICE layer/ + ( + cd layer + zip -q -r "$GITHUB_WORKSPACE/$LAYER_ZIP" . + ) + echo "layer_zip=$LAYER_ZIP" >> "$GITHUB_OUTPUT" + + - name: Verify layer contents + env: + LAYER_ZIP: ${{ steps.build-layer.outputs.layer_zip }} + SDK_VERSION: ${{ steps.version.outputs.sdk_version }} + shell: bash + run: | + set -euo pipefail + + unzip -Z1 "$LAYER_ZIP" > layer-contents.txt + grep -Fx "java/lib/aws-durable-execution-sdk-java-${SDK_VERSION}.jar" layer-contents.txt + grep -Fx "java/lib/aws-durable-execution-sdk-java-plugin-otel-${SDK_VERSION}.jar" layer-contents.txt + grep -Fx "LICENSE" layer-contents.txt + grep -Fx "NOTICE" layer-contents.txt + + - name: Upload layer artifact + uses: actions/upload-artifact@v7 + with: + name: otel-plugin-layer + path: ${{ steps.build-layer.outputs.layer_zip }} + if-no-files-found: error + retention-days: 30 + + publish-layer: + needs: build-layer + runs-on: ubuntu-latest + environment: + name: lambda-layer-publish + permissions: + contents: read + id-token: write + env: + LAYER_REGIONS: ${{ inputs.regions || vars.LAYER_PUBLISH_REGIONS }} + + steps: + - name: Download layer artifact + uses: actions/download-artifact@v8 + with: + 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 + aws-region: us-east-1 + + - name: Publish layer versions + env: + LAYER_ZIP: dist/${{ env.LAYER_NAME }}.zip + SDK_VERSION: ${{ needs.build-layer.outputs.sdk_version }} + shell: bash + run: | + set -euo pipefail + + PUBLISHED=false + FAILED_REGIONS=() + LOCAL_CODE_SHA256=$(openssl dgst -sha256 -binary "$LAYER_ZIP" | openssl base64 -A) + REGION_LIST=${LAYER_REGIONS:-$DEFAULT_LAYER_REGIONS} + IFS=',' read -ra REGIONS <<< "$REGION_LIST" + for REGION in "${REGIONS[@]}"; do + REGION=$(echo "$REGION" | xargs) + if [ -z "$REGION" ]; then + continue + fi + + LAYER_DESCRIPTION="AWS Durable Execution SDK for Java ${SDK_VERSION} OTel plugin sha256:${LOCAL_CODE_SHA256}" + if ! EXISTING_RESULT=$(aws lambda list-layer-versions \ + --layer-name "$LAYER_NAME" \ + --region "$REGION" \ + --query "LayerVersions[?Description=='${LAYER_DESCRIPTION}'] | [0].[LayerVersionArn,Version]" \ + --output text 2>&1); then + if [[ "$EXISTING_RESULT" == *"ResourceNotFoundException"* ]]; then + EXISTING_RESULT="" + else + echo "$EXISTING_RESULT" >&2 + echo "::warning::Failed to list existing layer versions in ${REGION}" + FAILED_REGIONS+=("$REGION") + continue + fi + fi + + if [ -n "$EXISTING_RESULT" ] && [ "$EXISTING_RESULT" != "None" ]; then + read -r LAYER_VERSION_ARN VERSION_NUMBER <<< "$EXISTING_RESULT" + if ! EXISTING_CODE_SHA256=$(aws lambda get-layer-version \ + --layer-name "$LAYER_NAME" \ + --version-number "$VERSION_NUMBER" \ + --region "$REGION" \ + --query 'Content.CodeSha256' \ + --output text 2>&1); then + echo "$EXISTING_CODE_SHA256" >&2 + echo "::warning::Failed to read ${LAYER_VERSION_ARN}" + FAILED_REGIONS+=("$REGION") + continue + fi + if [ "$EXISTING_CODE_SHA256" != "$LOCAL_CODE_SHA256" ]; then + echo "::error::Artifact hash mismatch for ${LAYER_VERSION_ARN}" + FAILED_REGIONS+=("$REGION") + continue + fi + echo "Reusing ${LAYER_VERSION_ARN}" + else + if ! PUBLISH_RESULT=$(aws lambda publish-layer-version \ + --layer-name "$LAYER_NAME" \ + --description "$LAYER_DESCRIPTION" \ + --zip-file "fileb://${LAYER_ZIP}" \ + --compatible-runtimes java17 java21 java25 \ + --compatible-architectures x86_64 arm64 \ + --license-info Apache-2.0 \ + --region "$REGION" \ + --query '[LayerVersionArn,Version,Content.CodeSha256]' \ + --output text); then + echo "::warning::Failed to publish the layer in ${REGION}" + FAILED_REGIONS+=("$REGION") + continue + fi + read -r LAYER_VERSION_ARN VERSION_NUMBER PUBLISHED_CODE_SHA256 <<< "$PUBLISH_RESULT" + if [ "$PUBLISHED_CODE_SHA256" != "$LOCAL_CODE_SHA256" ]; then + echo "::error::Published artifact hash mismatch for ${LAYER_VERSION_ARN}" + FAILED_REGIONS+=("$REGION") + continue + fi + fi + + if ! PERMISSION_RESULT=$(aws lambda add-layer-version-permission \ + --layer-name "$LAYER_NAME" \ + --version-number "$VERSION_NUMBER" \ + --statement-id public-layer-access \ + --action lambda:GetLayerVersion \ + --principal "*" \ + --region "$REGION" \ + 2>&1); then + if [[ "$PERMISSION_RESULT" != *"ResourceConflictException"* ]]; then + echo "$PERMISSION_RESULT" >&2 + echo "::warning::Failed to grant public access to ${LAYER_VERSION_ARN}" + FAILED_REGIONS+=("$REGION") + continue + fi + fi + + echo "Available ${LAYER_VERSION_ARN}" + echo "- \`${LAYER_VERSION_ARN}\`" >> "$GITHUB_STEP_SUMMARY" + PUBLISHED=true + done + + if [ "${#FAILED_REGIONS[@]}" -gt 0 ]; then + echo "::error::Layer publishing failed in: ${FAILED_REGIONS[*]}" + exit 1 + fi + + if [ "$PUBLISHED" != true ]; then + echo "No AWS regions were configured for layer publishing." + exit 1 + fi From 8a8556ed68a5e0dd93eac515cc33b43af4d0224b Mon Sep 17 00:00:00 2001 From: Frank Chen Date: Tue, 11 Aug 2026 23:35:03 +0000 Subject: [PATCH 2/5] ci: keep OTel layer plugin-only --- .github/workflows/lambda-layer-publish.yml | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/.github/workflows/lambda-layer-publish.yml b/.github/workflows/lambda-layer-publish.yml index b67112cc8..c1d0c9f11 100644 --- a/.github/workflows/lambda-layer-publish.yml +++ b/.github/workflows/lambda-layer-publish.yml @@ -110,12 +110,8 @@ jobs: run: | set -euo pipefail - mvn -B -q -pl otel-plugin -am install -DskipTests --no-transfer-progress + mvn -B -q -pl otel-plugin -am package -DskipTests --no-transfer-progress mkdir -p layer/java/lib dist - mvn -B -q -pl otel-plugin dependency:copy-dependencies \ - -DincludeScope=runtime \ - -DoutputDirectory="$GITHUB_WORKSPACE/layer/java/lib" \ - --no-transfer-progress cp "otel-plugin/target/aws-durable-execution-sdk-java-plugin-otel-${SDK_VERSION}.jar" \ layer/java/lib/ cp LICENSE NOTICE layer/ @@ -134,10 +130,13 @@ jobs: set -euo pipefail unzip -Z1 "$LAYER_ZIP" > layer-contents.txt - grep -Fx "java/lib/aws-durable-execution-sdk-java-${SDK_VERSION}.jar" layer-contents.txt grep -Fx "java/lib/aws-durable-execution-sdk-java-plugin-otel-${SDK_VERSION}.jar" layer-contents.txt grep -Fx "LICENSE" layer-contents.txt grep -Fx "NOTICE" layer-contents.txt + if [ "$(grep -Ec '^java/lib/[^/]+\.jar$' layer-contents.txt)" -ne 1 ]; then + echo "The layer must contain only the OTel plugin JAR." >&2 + exit 1 + fi - name: Upload layer artifact uses: actions/upload-artifact@v7 From f05bab725193123d9dbcb0fabd85c11d8612df84 Mon Sep 17 00:00:00 2001 From: Frank Chen Date: Wed, 12 Aug 2026 02:26:39 +0000 Subject: [PATCH 3/5] ci: harden layer release provenance --- .github/workflows/lambda-layer-publish.yml | 27 ++++++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/.github/workflows/lambda-layer-publish.yml b/.github/workflows/lambda-layer-publish.yml index c1d0c9f11..f4810a662 100644 --- a/.github/workflows/lambda-layer-publish.yml +++ b/.github/workflows/lambda-layer-publish.yml @@ -19,7 +19,7 @@ concurrency: env: LAYER_NAME: aws-durable-execution-sdk-java-otel-plugin - SOURCE_REF: ${{ github.event_name == 'workflow_dispatch' && github.event.repository.default_branch || github.event.release.tag_name }} + SOURCE_REF: ${{ github.event_name == 'workflow_dispatch' && github.sha || github.event.release.tag_name }} DEFAULT_LAYER_REGIONS: >- af-south-1, ap-east-1, @@ -65,13 +65,20 @@ jobs: runs-on: ubuntu-latest outputs: sdk_version: ${{ steps.version.outputs.sdk_version }} + source_sha: ${{ steps.source.outputs.sha }} steps: - name: Checkout source uses: actions/checkout@v7 with: + fetch-depth: 0 ref: ${{ env.SOURCE_REF }} + - name: Record source revision + id: source + shell: bash + run: echo "sha=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT" + - name: Setup Java uses: actions/setup-java@v5 with: @@ -101,6 +108,21 @@ jobs: echo "sdk_version=$sdk_version" >> "$GITHUB_OUTPUT" + - name: Verify release source + if: github.event_name == 'release' + env: + BASE_BRANCH: ${{ github.event.repository.default_branch }} + RELEASE_TAG: ${{ github.event.release.tag_name }} + shell: bash + run: | + set -euo pipefail + + git fetch origin "$BASE_BRANCH" + if ! git merge-base --is-ancestor "${RELEASE_TAG}^{commit}" "origin/${BASE_BRANCH}"; then + echo "Release tag $RELEASE_TAG does not point to a commit on $BASE_BRANCH." >&2 + exit 1 + fi + - name: Build layer zip id: build-layer env: @@ -175,6 +197,7 @@ jobs: env: LAYER_ZIP: dist/${{ env.LAYER_NAME }}.zip SDK_VERSION: ${{ needs.build-layer.outputs.sdk_version }} + SOURCE_SHA: ${{ needs.build-layer.outputs.source_sha }} shell: bash run: | set -euo pipefail @@ -190,7 +213,7 @@ jobs: continue fi - LAYER_DESCRIPTION="AWS Durable Execution SDK for Java ${SDK_VERSION} OTel plugin sha256:${LOCAL_CODE_SHA256}" + LAYER_DESCRIPTION="AWS Durable Execution SDK for Java ${SDK_VERSION} OTel plugin source:${SOURCE_SHA}" if ! EXISTING_RESULT=$(aws lambda list-layer-versions \ --layer-name "$LAYER_NAME" \ --region "$REGION" \ From 11cea0da6643fb30f3a63a73f5acdcbd9b5646d0 Mon Sep 17 00:00:00 2001 From: Frank Chen Date: Wed, 12 Aug 2026 04:35:28 +0000 Subject: [PATCH 4/5] ci: make layer publishing reproducible --- .github/workflows/lambda-layer-publish.yml | 86 +++++++++++++++++++--- 1 file changed, 75 insertions(+), 11 deletions(-) diff --git a/.github/workflows/lambda-layer-publish.yml b/.github/workflows/lambda-layer-publish.yml index f4810a662..13f999277 100644 --- a/.github/workflows/lambda-layer-publish.yml +++ b/.github/workflows/lambda-layer-publish.yml @@ -14,7 +14,7 @@ permissions: contents: read concurrency: - group: lambda-layer-publish-${{ github.event.release.tag_name || github.ref_name }} + group: lambda-layer-publish-${{ github.sha }} cancel-in-progress: false env: @@ -77,7 +77,11 @@ jobs: - name: Record source revision id: source shell: bash - run: echo "sha=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT" + run: | + set -euo pipefail + + echo "epoch=$(git show -s --format=%ct HEAD)" >> "$GITHUB_OUTPUT" + echo "sha=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT" - name: Setup Java uses: actions/setup-java@v5 @@ -128,19 +132,41 @@ jobs: env: LAYER_ZIP: dist/${{ env.LAYER_NAME }}.zip SDK_VERSION: ${{ steps.version.outputs.sdk_version }} + SOURCE_DATE_EPOCH: ${{ steps.source.outputs.epoch }} + SOURCE_SHA: ${{ steps.source.outputs.sha }} + TZ: UTC shell: bash run: | set -euo pipefail - mvn -B -q -pl otel-plugin -am package -DskipTests --no-transfer-progress - mkdir -p layer/java/lib dist - cp "otel-plugin/target/aws-durable-execution-sdk-java-plugin-otel-${SDK_VERSION}.jar" \ - layer/java/lib/ - cp LICENSE NOTICE layer/ - ( - cd layer - zip -q -r "$GITHUB_WORKSPACE/$LAYER_ZIP" . - ) + build_layer() { + local output_zip=$1 + + mvn -B -q -pl otel-plugin -am clean package \ + -DskipTests \ + -Dproject.build.outputTimestamp="$SOURCE_DATE_EPOCH" \ + --no-transfer-progress + rm -rf layer + rm -f "$output_zip" + mkdir -p layer/java/lib "$(dirname "$output_zip")" + cp "otel-plugin/target/aws-durable-execution-sdk-java-plugin-otel-${SDK_VERSION}.jar" \ + layer/java/lib/ + cp LICENSE NOTICE layer/ + find layer -exec touch -d "@${SOURCE_DATE_EPOCH}" {} + + ( + cd layer + find . -type f -print | LC_ALL=C sort | zip -X -q "$GITHUB_WORKSPACE/$output_zip" -@ + ) + } + + rebuild_zip="${LAYER_ZIP%.zip}-rebuild.zip" + build_layer "$LAYER_ZIP" + build_layer "$rebuild_zip" + if ! cmp --silent "$LAYER_ZIP" "$rebuild_zip"; then + echo "Layer builds for source $SOURCE_SHA are not reproducible." >&2 + exit 1 + fi + rm "$rebuild_zip" echo "layer_zip=$LAYER_ZIP" >> "$GITHUB_OUTPUT" - name: Verify layer contents @@ -287,6 +313,44 @@ jobs: fi fi + if ! POLICY_RESULT=$(aws lambda get-layer-version-policy \ + --layer-name "$LAYER_NAME" \ + --version-number "$VERSION_NUMBER" \ + --region "$REGION" \ + --query Policy \ + --output text 2>&1); then + echo "$POLICY_RESULT" >&2 + echo "::warning::Failed to read the access policy for ${LAYER_VERSION_ARN}" + FAILED_REGIONS+=("$REGION") + continue + fi + if ! jq -e ' + .Statement + | if type == "array" then . else [.] end + | any(.[]; + .Sid == "public-layer-access" + and .Effect == "Allow" + and ( + .Action == "lambda:GetLayerVersion" + or ( + (.Action | type) == "array" + and (.Action | index("lambda:GetLayerVersion")) != null + ) + ) + and ( + .Principal == "*" + or ( + (.Principal | type) == "object" + and .Principal.AWS == "*" + ) + ) + ) + ' <<< "$POLICY_RESULT" > /dev/null; then + echo "::error::Expected public access policy is missing for ${LAYER_VERSION_ARN}" + FAILED_REGIONS+=("$REGION") + continue + fi + echo "Available ${LAYER_VERSION_ARN}" echo "- \`${LAYER_VERSION_ARN}\`" >> "$GITHUB_STEP_SUMMARY" PUBLISHED=true From 27c2681c7c4721aa18dc9a342a775befb086fd36 Mon Sep 17 00:00:00 2001 From: Frank Chen Date: Wed, 12 Aug 2026 05:06:51 +0000 Subject: [PATCH 5/5] ci: fix paginated layer lookup --- .github/workflows/lambda-layer-publish.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/lambda-layer-publish.yml b/.github/workflows/lambda-layer-publish.yml index 13f999277..7443be957 100644 --- a/.github/workflows/lambda-layer-publish.yml +++ b/.github/workflows/lambda-layer-publish.yml @@ -243,7 +243,7 @@ jobs: if ! EXISTING_RESULT=$(aws lambda list-layer-versions \ --layer-name "$LAYER_NAME" \ --region "$REGION" \ - --query "LayerVersions[?Description=='${LAYER_DESCRIPTION}'] | [0].[LayerVersionArn,Version]" \ + --query "LayerVersions[?Description=='${LAYER_DESCRIPTION}'].[LayerVersionArn,Version]" \ --output text 2>&1); then if [[ "$EXISTING_RESULT" == *"ResourceNotFoundException"* ]]; then EXISTING_RESULT="" @@ -255,7 +255,7 @@ jobs: fi fi - if [ -n "$EXISTING_RESULT" ] && [ "$EXISTING_RESULT" != "None" ]; then + if [ -n "$EXISTING_RESULT" ]; then read -r LAYER_VERSION_ARN VERSION_NUMBER <<< "$EXISTING_RESULT" if ! EXISTING_CODE_SHA256=$(aws lambda get-layer-version \ --layer-name "$LAYER_NAME" \