diff --git a/.github/actions/node-layer-setup/action.yml b/.github/actions/node-layer-setup/action.yml index 4a0c206d..d4ebab5a 100644 --- a/.github/actions/node-layer-setup/action.yml +++ b/.github/actions/node-layer-setup/action.yml @@ -22,7 +22,8 @@ runs: id: check-tag shell: bash run: | - if [[ ${{ github.event.ref }} =~ ^refs/tags/v[0-9]+(\.[0-9]+)*_nodejs$ ]]; then + if [[ "${{ github.event_name }}" == "workflow_dispatch" ]] || \ + [[ "${{ github.event.ref }}" =~ ^refs/tags/v[0-9]+(\.[0-9]+)*_nodejs$ ]]; then echo "match=true" >> $GITHUB_OUTPUT fi - name: Run Node unit tests diff --git a/.github/actions/notify-slack-layer/action.yml b/.github/actions/notify-slack-layer/action.yml new file mode 100644 index 00000000..dc729c77 --- /dev/null +++ b/.github/actions/notify-slack-layer/action.yml @@ -0,0 +1,173 @@ +name: Notify Slack Layer Release +description: Builds and sends a structured Slack notification for lambda layer releases + +inputs: + language_name: + description: Language display name, e.g. "Node.js" + required: true + versions_json: + description: | + JSON array describing each runtime version to include in the Slack message. + Schema (one object per version): + - key: unique identifier, e.g. "3.9" + - label: display name shown in Slack, e.g. "Python 3.9" + - job: GitHub Actions job name for API result lookup, e.g. "publish-python (3.9)" + - fallback: result to use when the API lookup finds no match, e.g. "success" or "failure" + - failure_key: job_key used by region-retry for per-version region failure detail, e.g. "python-3.9" + Example: + [ + {"key":"3.9","label":"Python 3.9","job":"publish-python (3.9)","fallback":"success","failure_key":"python-3.9"}, + {"key":"3.10","label":"Python 3.10","job":"publish-python (3.10)","fallback":"success","failure_key":"python-3.10"} + ] + required: true + failure_summaries: + description: Newline-separated failure_summary strings from publish jobs (used for ECR failures) + required: false + default: "" + slack_webhook: + description: Slack incoming webhook URL + required: true + gh_token: + required: true + repo: + required: true + run_id: + required: true + run_attempt: + description: Pass github.run_attempt from the calling workflow. + required: false + default: "1" + ref_name: + required: true + actor: + required: true + server_url: + required: true + +runs: + using: composite + steps: + - name: Build Slack payload + shell: bash + env: + LANGUAGE_NAME: ${{ inputs.language_name }} + VERSIONS_JSON: ${{ inputs.versions_json }} + FAILURE_SUMMARIES: ${{ inputs.failure_summaries }} + GH_TOKEN: ${{ inputs.gh_token }} + REPO: ${{ inputs.repo }} + RUN_ID: ${{ inputs.run_id }} + RUN_ATTEMPT: ${{ inputs.run_attempt }} + TAG: ${{ inputs.ref_name }} + ACTOR: ${{ inputs.actor }} + SERVER_URL: ${{ inputs.server_url }} + run: | + python3 << 'PYEOF' + import json, os, subprocess, glob, re + + versions = json.loads(os.environ["VERSIONS_JSON"]) + run_id = os.environ["RUN_ID"] + run_attempt = os.environ.get("RUN_ATTEMPT", "1") + repo = os.environ["REPO"] + + # ── Fetch per-job results from the GitHub API ──────────────────────── + r = subprocess.run( + ["gh", "api", "--paginate", + f"repos/{repo}/actions/runs/{run_id}/jobs", + "-q", ".jobs[]"], + capture_output=True, text=True + ) + api_jobs = {} + for line in r.stdout.strip().splitlines(): + try: + job = json.loads(line) + api_jobs[job["name"]] = job.get("conclusion") or "in_progress" + except Exception: + pass + + results = {} + for v in versions: + results[v["key"]] = api_jobs.get(v["job"], v["fallback"]) + + # ── Download per-version region failure artifacts ───────────────────── + # Artifacts are named: failed-regions-{job_key}-{run_id}-attempt-{N} + subprocess.run( + ["gh", "run", "download", run_id, + "--pattern", "failed-regions-*", + "--dir", "/tmp/region-artifacts", + "--repo", repo], + capture_output=True, text=True + ) + + version_failures = {} # job_key -> comma-separated failed regions + artifact_base = "/tmp/region-artifacts" + if os.path.isdir(artifact_base): + for artifact_dir in sorted(glob.glob(f"{artifact_base}/failed-regions-*")): + artifact_name = os.path.basename(artifact_dir) + # Strip "failed-regions-" prefix, then split off "-{run_id}-attempt-{N}" + stripped = artifact_name[len("failed-regions-"):] + parts = stripped.rsplit(f"-{run_id}-attempt-", 1) + if len(parts) != 2: + continue + job_key, attempt_num = parts + if attempt_num != run_attempt: + continue + txt_file = os.path.join(artifact_dir, f"failed-regions-{job_key}.txt") + if os.path.isfile(txt_file): + content = open(txt_file).read().strip() + if content: + version_failures[job_key] = content + + # ── Build Slack message ─────────────────────────────────────────────── + total = len(versions) + failed_count = sum(1 for v in versions if results.get(v["key"]) != "success") + all_ok = failed_count == 0 + + lang = os.environ["LANGUAGE_NAME"] + icon = ":white_check_mark:" if all_ok else ":x:" + status = "Succeeded" if all_ok else f"Failed ({failed_count}/{total} versions failed)" + lines = [ + f"{icon} *{lang} Layer Release {status}*", + f"Tag: `{os.environ['TAG']}`", + f"Triggered by: {os.environ['ACTOR']}", + "", + "*Layer Results:*", + ] + for v in versions: + res = results.get(v["key"], "unknown") + em = ":white_check_mark:" if res == "success" else ":x:" + if res == "success": + detail = "Layer published successfully" + else: + fk = v.get("failure_key", "") + regions_str = version_failures.get(fk, "") + if regions_str: + regions = [r for r in regions_str.split(",") if r] + detail = f"{len(regions)} region(s) failed: {', '.join(regions)}" + else: + detail = "Layer publish to AWS failed" + lines.append(f"{em} {v['label']} — {detail}") + + # ECR failures (from failure_summaries — not stored in artifacts) + skipped_ecr = [] + for fs in os.environ.get("FAILURE_SUMMARIES", "").splitlines(): + fs = fs.strip() + if "ECR images failed:" in fs: + for img in fs.split("ECR images failed:")[-1].strip().split(): + if img not in skipped_ecr: + skipped_ecr.append(img) + if skipped_ecr: + lines += ["", ":warning: *ECR images failed to publish:*"] + lines += [f"• {i}" for i in skipped_ecr] + + run_url = f"{os.environ['SERVER_URL']}/{repo}/actions/runs/{run_id}" + lines += ["", f"<{run_url}|View Run>"] + + with open("/tmp/slack-payload.json", "w") as f: + json.dump({"text": "\n".join(lines)}, f) + PYEOF + - name: Notify Slack + uses: slackapi/slack-github-action@v2.1.0 + with: + webhook: ${{ inputs.slack_webhook }} + webhook-type: incoming-webhook + payload-file-path: /tmp/slack-payload.json diff --git a/.github/actions/python-layer-setup/action.yml b/.github/actions/python-layer-setup/action.yml index ce8149b9..95763f70 100644 --- a/.github/actions/python-layer-setup/action.yml +++ b/.github/actions/python-layer-setup/action.yml @@ -13,7 +13,8 @@ runs: id: check-tag shell: bash run: | - if [[ ${{ github.event.ref }} =~ ^refs/tags/v[0-9]+(\.[0-9]+)*_python$ ]]; then + if [[ "${{ github.event_name }}" == "workflow_dispatch" ]] || \ + [[ "${{ github.event.ref }}" =~ ^refs/tags/v[0-9]+(\.[0-9]+)*_python$ ]]; then echo "match=true" >> $GITHUB_OUTPUT fi - name: Install python dependencies diff --git a/.github/actions/region-retry/action.yml b/.github/actions/region-retry/action.yml new file mode 100644 index 00000000..a115c16a --- /dev/null +++ b/.github/actions/region-retry/action.yml @@ -0,0 +1,105 @@ +name: Region Retry State +description: | + Save failed regions after a publish attempt, or load them before a re-run. + On attempt N > 1, load automatically restricts publishing to the regions + that failed in attempt N-1. This makes "Re-run failed jobs" smart: it only + retries the regions that actually need it. + + Usage in a publish job: + # Before publish step: + - uses: ./.github/actions/region-retry + id: region-retry-load + with: { mode: load, job_key: nodejs-20, run_id: ..., run_attempt: ... } + + # In publish step env: + # PUBLISH_REGIONS: steps.region-retry-load.outputs.publish_regions || inputs.regions + + # After publish step (if: always()): + - uses: ./.github/actions/region-retry + with: { mode: save, job_key: nodejs-20, failure_summary: ..., run_id: ..., run_attempt: ... } + +inputs: + mode: + description: '"save" — write failed regions artifact after publish. "load" — read it before publish.' + required: true + job_key: + description: 'Unique key for this publish job, e.g. "nodejs-20", "python-3.9", "java-java21".' + required: true + failure_summary: + description: 'failure_summary output from the publish step (save mode only).' + required: false + default: '' + run_id: + description: 'Pass github.run_id from the calling workflow.' + required: true + run_attempt: + description: 'Pass github.run_attempt from the calling workflow.' + required: true + +outputs: + publish_regions: + description: 'Comma-separated regions to target (empty = all). Set PUBLISH_REGIONS env var to this value.' + value: ${{ steps.load-regions.outputs.publish_regions }} + +runs: + using: composite + steps: + # ── SAVE: write failed regions to an artifact named by attempt number ───── + - name: Write failed-regions file + if: inputs.mode == 'save' + id: write-file + shell: bash + run: | + fs="${{ inputs.failure_summary }}" + outfile="/tmp/failed-regions-${{ inputs.job_key }}.txt" + if [[ "$fs" == *"regions failed:"* ]]; then + regions="${fs#*regions failed: }" + printf '%s' "${regions// /,}" > "$outfile" + echo "has_failures=true" >> "$GITHUB_OUTPUT" + echo "Captured failed regions: ${regions}" + else + echo "has_failures=false" >> "$GITHUB_OUTPUT" + fi + + - name: Upload failed-regions artifact + if: inputs.mode == 'save' && steps.write-file.outputs.has_failures == 'true' + uses: actions/upload-artifact@v4 + with: + name: failed-regions-${{ inputs.job_key }}-${{ inputs.run_id }}-attempt-${{ inputs.run_attempt }} + path: /tmp/failed-regions-${{ inputs.job_key }}.txt + retention-days: 7 + + # ── LOAD: on re-run, download the previous attempt's failed-regions ──────── + - name: Compute previous attempt artifact name + if: inputs.mode == 'load' + id: artifact-name + shell: bash + run: | + prev=$(( ${{ inputs.run_attempt }} - 1 )) + echo "name=failed-regions-${{ inputs.job_key }}-${{ inputs.run_id }}-attempt-${prev}" >> "$GITHUB_OUTPUT" + + - name: Download previous failed-regions artifact + if: inputs.mode == 'load' && fromJSON(inputs.run_attempt) > 1 + id: download + continue-on-error: true + uses: actions/download-artifact@v4 + with: + name: ${{ steps.artifact-name.outputs.name }} + path: /tmp/prev-attempt-${{ inputs.job_key }} + + - name: Set publish_regions output + if: inputs.mode == 'load' + id: load-regions + shell: bash + run: | + regions="" + f="/tmp/prev-attempt-${{ inputs.job_key }}/failed-regions-${{ inputs.job_key }}.txt" + if [[ -f "$f" ]]; then + regions=$(cat "$f") + if [[ -n "$regions" ]]; then + echo "Re-run: restricting to previously failed regions: ${regions}" + else + echo "Previous attempt had no failures — publishing to all regions." + fi + fi + echo "publish_regions=${regions}" >> "$GITHUB_OUTPUT" diff --git a/.github/scripts/poll-validation.sh b/.github/scripts/poll-validation.sh new file mode 100755 index 00000000..6b06bbba --- /dev/null +++ b/.github/scripts/poll-validation.sh @@ -0,0 +1,121 @@ +#!/usr/bin/env bash +# Call the orchestrator API then poll S3 until the validation summary appears. +# Exit 0 = PASS, exit 1 = FAIL or timeout. +# +# Required env vars: +# ORCHESTRATOR_API_URL full URL of the /validate endpoint +# ORCHESTRATOR_API_KEY x-api-key value +# RESULTS_BUCKET S3 bucket where the orchestrator writes summary.json +# +# Usage: +# poll-validation.sh --runtime \ +# --arn-x86 --arn-arm64 \ +# [--arn-x86-slim ] [--arn-arm64-slim ] \ +# [--os al2|al2023] \ +# [--run-id ] [--timeout ] + +set -euo pipefail + +RUNTIME="" +ARN_X86="" +ARN_ARM64="" +ARN_X86_SLIM="" +ARN_ARM64_SLIM="" +OS_TIER="" +RUN_ID="" +TIMEOUT=${VALIDATION_TIMEOUT_S:-1800} + +while [[ $# -gt 0 ]]; do + case $1 in + --runtime) RUNTIME=$2; shift 2 ;; + --arn-x86) ARN_X86=$2; shift 2 ;; + --arn-arm64) ARN_ARM64=$2; shift 2 ;; + --arn-x86-slim) ARN_X86_SLIM=$2; shift 2 ;; + --arn-arm64-slim) ARN_ARM64_SLIM=$2; shift 2 ;; + --os) OS_TIER=$2; shift 2 ;; + --run-id) RUN_ID=$2; shift 2 ;; + --timeout) TIMEOUT=$2; shift 2 ;; + *) echo "Unknown argument: $1"; exit 1 ;; + esac +done + +[[ -z "$RUNTIME" ]] && { echo "ERROR: --runtime is required"; exit 1; } +[[ -z "$ARN_X86" ]] && { echo "ERROR: --arn-x86 is required"; exit 1; } +[[ -z "$ARN_ARM64" ]] && { echo "ERROR: --arn-arm64 is required"; exit 1; } +[[ -z "${ORCHESTRATOR_API_URL:-}" ]] && { echo "ERROR: ORCHESTRATOR_API_URL is not set"; exit 1; } +[[ -z "${ORCHESTRATOR_API_KEY:-}" ]] && { echo "ERROR: ORCHESTRATOR_API_KEY is not set"; exit 1; } +[[ -z "${RESULTS_BUCKET:-}" ]] && { echo "ERROR: RESULTS_BUCKET is not set"; exit 1; } + +if [[ -z "$RUN_ID" ]]; then + RUN_ID="gh-${GITHUB_RUN_ID:-$(date +%s)}-${RUNTIME}" +fi + +# Build JSON payload +PAYLOAD=$(python3 - </dev/null || true) + + if [[ -n "$result" ]]; then + echo "" + echo "=== Validation result for ${RUNTIME} ===" + echo "$result" | python3 -m json.tool + + final_status=$(echo "$result" | python3 -c "import json,sys; print(json.load(sys.stdin)['status'])") + + if [[ "$final_status" == "PASS" ]]; then + echo "PASSED: ${RUNTIME}" + exit 0 + else + echo "FAILED: ${RUNTIME}" + echo "$result" | python3 - <<'PYEOF' +import json, sys +d = json.load(sys.stdin) +for variant in ("standard", "slim"): + section = d.get(variant) or {} + for f in section.get("functions", []): + if f["status"] != "PASS": + print(f" [{variant}] FAIL: {f['function']} — {f['reason']} (error_type={f.get('error_type','?')})") +PYEOF + exit 1 + fi + fi + + remaining=$((end - SECONDS)) + echo " Waiting... (${remaining}s remaining)" + sleep 300 +done + +echo "ERROR: Timeout after ${TIMEOUT}s — no result at s3://${RESULTS_BUCKET}/${KEY}" +exit 1 diff --git a/.github/workflows/publish-dotnet.yml b/.github/workflows/publish-dotnet.yml index 41fedbc3..e870f357 100644 --- a/.github/workflows/publish-dotnet.yml +++ b/.github/workflows/publish-dotnet.yml @@ -4,28 +4,135 @@ on: push: tags: - v**_dotnet + workflow_dispatch: + inputs: + regions: + description: 'Comma-separated regions to (re)publish. Leave empty to publish all regions.' + required: false + default: '' jobs: - publish-dotnet: + # ── Job 1: Publish staging (dotnet10) ──────────────────────────────────── + stage-dotnet: runs-on: ubuntu-latest + outputs: + tag-match: ${{ steps.dotnet-check-tag.outputs.match }} + arn_x86: ${{ steps.staging.outputs.arn_x86 }} + arn_arm64: ${{ steps.staging.outputs.arn_arm64 }} steps: + - uses: actions/checkout@v4 - name: Check Tag id: dotnet-check-tag run: | - if [[ ${{ github.event.ref }} =~ ^refs/tags/v[0-9]+(\.[0-9]+)*_dotnet ]]; then + if [[ "${{ github.event_name }}" == "workflow_dispatch" ]] || \ + [[ "${{ github.event.ref }}" =~ ^refs/tags/v[0-9]+(\.[0-9]+)*_dotnet ]]; then echo "match=true" >> $GITHUB_OUTPUT fi + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + with: + platforms: arm64, amd64 + - name: Publish staging layers (dotnet10) + if: steps.dotnet-check-tag.outputs.match == 'true' + id: staging + env: + AWS_ACCESS_KEY_ID: ${{ secrets.STAGING_AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.STAGING_AWS_SECRET_ACCESS_KEY }} + STAGING_REGION: us-east-1 + run: | + cd dotnet + ./publish-layers.sh publish-staging-dotnet + + # ── Job 2: Validate (calls reusable workflow) ───────────────────────────── + validate-dotnet: + needs: stage-dotnet + if: needs.stage-dotnet.outputs.tag-match == 'true' + uses: ./.github/workflows/validate-layer.yml + with: + runtime: dotnet10 + arn_x86: ${{ needs.stage-dotnet.outputs.arn_x86 }} + arn_arm64: ${{ needs.stage-dotnet.outputs.arn_arm64 }} + secrets: inherit + + # ── Job 3: Cleanup staging (always) ────────────────────────────────────── + cleanup-dotnet: + needs: [stage-dotnet, validate-dotnet] + if: always() && needs.stage-dotnet.outputs.arn_x86 != '' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Cleanup staging layers + env: + AWS_ACCESS_KEY_ID: ${{ secrets.STAGING_AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.STAGING_AWS_SECRET_ACCESS_KEY }} + STAGING_REGION: us-east-1 + ARN_X86: ${{ needs.stage-dotnet.outputs.arn_x86 }} + ARN_ARM64: ${{ needs.stage-dotnet.outputs.arn_arm64 }} + run: | + cd dotnet + ./publish-layers.sh cleanup-staging-dotnet + + # ── Job 4: Release dotnet layer — gated on validate passing ─────────────── + publish-dotnet: + needs: [stage-dotnet, validate-dotnet] + if: needs.stage-dotnet.outputs.tag-match == 'true' + runs-on: ubuntu-latest + outputs: + failure_summary: ${{ steps.publish.outputs.failure_summary }} + ecr_failure_summary: ${{ steps.publish.outputs.ecr_failure_summary }} + steps: - uses: actions/checkout@v4 - name: Set up QEMU uses: docker/setup-qemu-action@v3 with: platforms: arm64, amd64 + - name: Load failed regions for re-run + id: region-retry-load + uses: ./.github/actions/region-retry + with: + mode: load + job_key: dotnet + run_id: ${{ github.run_id }} + run_attempt: ${{ github.run_attempt }} - name: Publish Dotnet Layer - if: steps.dotnet-check-tag.outputs.match == 'true' + id: publish env: AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} - AGENT_VERSION: ${{ steps.dotnet-check-tag.outputs.version }} + PUBLISH_REGIONS: ${{ steps.region-retry-load.outputs.publish_regions || inputs.regions }} + LAYER_REGIONS: ${{ vars.LAYER_REGIONS }} + S3_BUCKET_PREFIX: ${{ vars.S3_BUCKET_PREFIX }} + ECR_REPOSITORY: ${{ vars.ECR_REPOSITORY }} run: | cd dotnet ./publish-layers.sh + - name: Save failed regions for re-run + if: always() + uses: ./.github/actions/region-retry + with: + mode: save + job_key: dotnet + failure_summary: ${{ steps.publish.outputs.failure_summary }} + run_id: ${{ github.run_id }} + run_attempt: ${{ github.run_attempt }} + + # ── Job 5: Slack release summary — only after validate passed ───────────── + notify-slack: + needs: [stage-dotnet, validate-dotnet, publish-dotnet] + if: always() && needs.validate-dotnet.result == 'success' && needs.stage-dotnet.outputs.tag-match == 'true' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: ./.github/actions/notify-slack-layer + with: + language_name: Dotnet + versions_json: '[{"key":"dotnet10","label":"Dotnet 10","job":"publish-dotnet","fallback":"${{ needs.publish-dotnet.result }}","failure_key":"dotnet"}]' + failure_summaries: ${{ needs.publish-dotnet.outputs.ecr_failure_summary }} + slack_webhook: ${{ secrets.SLACK_VALIDATION_WEBHOOK }} + gh_token: ${{ github.token }} + repo: ${{ github.repository }} + run_id: ${{ github.run_id }} + run_attempt: ${{ github.run_attempt }} + ref_name: ${{ github.ref_name }} + actor: ${{ github.actor }} + server_url: ${{ github.server_url }} diff --git a/.github/workflows/publish-extension.yml b/.github/workflows/publish-extension.yml index 19785d0d..c7afb890 100644 --- a/.github/workflows/publish-extension.yml +++ b/.github/workflows/publish-extension.yml @@ -4,19 +4,28 @@ on: push: tags: - v**_extension + workflow_dispatch: + inputs: + regions: + description: 'Comma-separated regions to (re)publish. Leave empty to publish all regions.' + required: false + default: '' jobs: - publish-extension: + # ── Job 1: Publish staging (extension) ─────────────────────────────────── + stage-extension: runs-on: ubuntu-latest - strategy: - matrix: - python-version: [ 3.12 ] + outputs: + tag-match: ${{ steps.extension-check-tag.outputs.match }} + arn_x86: ${{ steps.staging.outputs.arn_x86 }} + arn_arm64: ${{ steps.staging.outputs.arn_arm64 }} steps: - uses: actions/checkout@v4 - name: Check Tag id: extension-check-tag run: | - if [[ ${{ github.event.ref }} =~ ^refs/tags/v[0-9]+(\.[0-9]+)*_extension$ ]]; then + if [[ "${{ github.event_name }}" == "workflow_dispatch" ]] || \ + [[ "${{ github.event.ref }}" =~ ^refs/tags/v[0-9]+(\.[0-9]+)*_extension$ ]]; then echo "match=true" >> $GITHUB_OUTPUT fi - name: Install publish dependencies @@ -26,11 +35,124 @@ jobs: uses: docker/setup-qemu-action@v3 with: platforms: arm64, amd64 - - name: Publish extension layer + - name: Publish staging layers if: steps.extension-check-tag.outputs.match == 'true' + id: staging + env: + AWS_ACCESS_KEY_ID: ${{ secrets.STAGING_AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.STAGING_AWS_SECRET_ACCESS_KEY }} + STAGING_REGION: us-east-1 + run: | + cd extension + ./publish-layer.sh publish-staging + + # ── Job 2a: Validate go/al2 (calls reusable workflow) ──────────────────── + validate-al2: + needs: stage-extension + if: needs.stage-extension.outputs.tag-match == 'true' + uses: ./.github/workflows/validate-layer.yml + with: + runtime: go + label: go/al2 + arn_x86: ${{ needs.stage-extension.outputs.arn_x86 }} + arn_arm64: ${{ needs.stage-extension.outputs.arn_arm64 }} + os: al2 + secrets: inherit + + # ── Job 2b: Validate go/al2023 (calls reusable workflow, runs in parallel) ─ + validate-al2023: + needs: stage-extension + if: needs.stage-extension.outputs.tag-match == 'true' + uses: ./.github/workflows/validate-layer.yml + with: + runtime: go + label: go/al2023 + arn_x86: ${{ needs.stage-extension.outputs.arn_x86 }} + arn_arm64: ${{ needs.stage-extension.outputs.arn_arm64 }} + os: al2023 + secrets: inherit + + # ── Job 3: Cleanup staging (always, after both validates) ───────────────── + cleanup-extension: + needs: [stage-extension, validate-al2, validate-al2023] + if: always() && needs.stage-extension.outputs.arn_x86 != '' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Cleanup staging layers + env: + AWS_ACCESS_KEY_ID: ${{ secrets.STAGING_AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.STAGING_AWS_SECRET_ACCESS_KEY }} + STAGING_REGION: us-east-1 + ARN_X86: ${{ needs.stage-extension.outputs.arn_x86 }} + ARN_ARM64: ${{ needs.stage-extension.outputs.arn_arm64 }} + run: | + cd extension + ./publish-layer.sh cleanup-staging + + # ── Job 4: Release extension — gated on BOTH validates passing ──────────── + publish-extension: + needs: [stage-extension, validate-al2, validate-al2023] + if: needs.stage-extension.outputs.tag-match == 'true' + runs-on: ubuntu-latest + outputs: + failure_summary: ${{ steps.publish.outputs.failure_summary }} + ecr_failure_summary: ${{ steps.publish.outputs.ecr_failure_summary }} + steps: + - uses: actions/checkout@v4 + - name: Install publish dependencies + run: pip install -U awscli + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + with: + platforms: arm64, amd64 + - name: Load failed regions for re-run + id: region-retry-load + uses: ./.github/actions/region-retry + with: + mode: load + job_key: extension + run_id: ${{ github.run_id }} + run_attempt: ${{ github.run_attempt }} + - name: Publish extension layer + id: publish env: AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + PUBLISH_REGIONS: ${{ steps.region-retry-load.outputs.publish_regions || inputs.regions }} + LAYER_REGIONS: ${{ vars.LAYER_REGIONS }} + S3_BUCKET_PREFIX: ${{ vars.S3_BUCKET_PREFIX }} + ECR_REPOSITORY: ${{ vars.ECR_REPOSITORY }} run: | cd extension ./publish-layer.sh + - name: Save failed regions for re-run + if: always() + uses: ./.github/actions/region-retry + with: + mode: save + job_key: extension + failure_summary: ${{ steps.publish.outputs.failure_summary }} + run_id: ${{ github.run_id }} + run_attempt: ${{ github.run_attempt }} + + # ── Job 5: Slack release summary — only after both validates passed ──────── + notify-slack: + needs: [stage-extension, validate-al2, validate-al2023, publish-extension] + if: always() && needs.validate-al2.result == 'success' && needs.validate-al2023.result == 'success' && needs.stage-extension.outputs.tag-match == 'true' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: ./.github/actions/notify-slack-layer + with: + language_name: Extension (Go/AL2+AL2023) + versions_json: '[{"key":"extension","label":"Extension (go/al2+al2023)","job":"publish-extension","fallback":"${{ needs.publish-extension.result }}","failure_key":"extension"}]' + failure_summaries: ${{ needs.publish-extension.outputs.ecr_failure_summary }} + slack_webhook: ${{ secrets.SLACK_VALIDATION_WEBHOOK }} + gh_token: ${{ github.token }} + repo: ${{ github.repository }} + run_id: ${{ github.run_id }} + run_attempt: ${{ github.run_attempt }} + ref_name: ${{ github.ref_name }} + actor: ${{ github.actor }} + server_url: ${{ github.server_url }} diff --git a/.github/workflows/publish-java.yml b/.github/workflows/publish-java.yml index 91d63560..2747e593 100644 --- a/.github/workflows/publish-java.yml +++ b/.github/workflows/publish-java.yml @@ -4,14 +4,90 @@ on: push: tags: - v**_java + workflow_dispatch: + inputs: + regions: + description: 'Comma-separated regions to (re)publish. Leave empty to publish all regions.' + required: false + default: '' jobs: + # ── Job 1: Build + publish staging (java21 only) ────────────────────────── + stage-java: + runs-on: ubuntu-latest + outputs: + tag-match: ${{ steps.java-check-tag.outputs.match }} + arn_x86: ${{ steps.staging.outputs.arn_x86 }} + arn_arm64: ${{ steps.staging.outputs.arn_arm64 }} + steps: + - uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # pin@v4 + - name: Use Java 21 + uses: actions/setup-java@v4 + with: + distribution: 'corretto' + java-version: '21' + - name: Check Tag + id: java-check-tag + run: | + if [[ "${{ github.event_name }}" == "workflow_dispatch" ]] || \ + [[ "${{ github.event.ref }}" =~ ^refs/tags/v[0-9]+(\.[0-9]+)*_java$ ]]; then + echo "match=true" >> $GITHUB_OUTPUT + fi + - name: Build layer artifacts (java21) + if: steps.java-check-tag.outputs.match == 'true' + run: make extract-java21-artifacts + - name: Publish staging layers (java21) + if: steps.java-check-tag.outputs.match == 'true' + id: staging + env: + AWS_ACCESS_KEY_ID: ${{ secrets.STAGING_AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.STAGING_AWS_SECRET_ACCESS_KEY }} + STAGING_REGION: us-east-1 + run: | + cd java + DIST_DIR=../dist/java21 ./publish-layers.sh publish-staging-java21 + + # ── Job 2: Validate (calls reusable workflow) ───────────────────────────── + validate-java: + needs: stage-java + if: needs.stage-java.outputs.tag-match == 'true' + uses: ./.github/workflows/validate-layer.yml + with: + runtime: java21 + arn_x86: ${{ needs.stage-java.outputs.arn_x86 }} + arn_arm64: ${{ needs.stage-java.outputs.arn_arm64 }} + secrets: inherit + + # ── Job 3: Cleanup staging (always) ────────────────────────────────────── + cleanup-java: + needs: [stage-java, validate-java] + if: always() && needs.stage-java.outputs.arn_x86 != '' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Cleanup staging layers + env: + AWS_ACCESS_KEY_ID: ${{ secrets.STAGING_AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.STAGING_AWS_SECRET_ACCESS_KEY }} + STAGING_REGION: us-east-1 + ARN_X86: ${{ needs.stage-java.outputs.arn_x86 }} + ARN_ARM64: ${{ needs.stage-java.outputs.arn_arm64 }} + run: | + cd java + ./publish-layers.sh cleanup-staging-java21 + + # ── Job 4: Release all Java versions — gated on validate passing ────────── publish-java: + needs: [stage-java, validate-java] + if: needs.stage-java.outputs.tag-match == 'true' runs-on: ubuntu-latest strategy: + fail-fast: false matrix: - java-version: [ java8al2, java11, java17, java21 ] - + java-version: [java8al2, java11, java17, java21] + outputs: + failure_summary: ${{ steps.publish.outputs.failure_summary }} + ecr_failure_summary: ${{ steps.publish-ecr.outputs.ecr_failure_summary }} steps: - uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # pin@v4 - name: Set up Java version @@ -30,27 +106,96 @@ jobs: with: distribution: 'corretto' java-version: ${{ env.JAVA_NUMERIC_VERSION }} - - name: Check Tag - id: java-check-tag - run: | - if [[ ${{ github.event.ref }} =~ ^refs/tags/v[0-9]+(\.[0-9]+)*_java$ ]]; then - echo "match=true" >> $GITHUB_OUTPUT - fi + - name: Build layer artifacts (java21 only) + if: matrix.java-version == 'java21' + run: make extract-java21-artifacts + - name: Load failed regions for re-run + id: region-retry-load + uses: ./.github/actions/region-retry + with: + mode: load + job_key: java-${{ matrix.java-version }} + run_id: ${{ github.run_id }} + run_attempt: ${{ github.run_attempt }} - name: Publish ${{ matrix.java-version }} layer - if: steps.java-check-tag.outputs.match == 'true' + id: publish env: AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} - run: make publish-${{ matrix.java-version }}-ci + PUBLISH_REGIONS: ${{ steps.region-retry-load.outputs.publish_regions || inputs.regions }} + LAYER_REGIONS: ${{ vars.LAYER_REGIONS }} + S3_BUCKET_PREFIX: ${{ vars.S3_BUCKET_PREFIX }} + run: | + if [[ "${{ matrix.java-version }}" == "java21" ]]; then + failed_file="$(mktemp)" + chmod a+rw "$failed_file" "$GITHUB_STEP_SUMMARY" + set +e + docker run \ + -e AWS_ACCESS_KEY_ID -e AWS_SECRET_ACCESS_KEY \ + -e GITHUB_STEP_SUMMARY \ + -e PUBLISH_REGIONS \ + -e LAYER_REGIONS \ + -e S3_BUCKET_PREFIX \ + -e "FAILED_REGIONS_FILE=${failed_file}" \ + -v "$GITHUB_STEP_SUMMARY:$GITHUB_STEP_SUMMARY" \ + -v "${failed_file}:${failed_file}" \ + newrelic-lambda-layers-java21 + docker_exit=$? + set -e + [[ -s "${failed_file}" ]] && cat "${failed_file}" >> "$GITHUB_OUTPUT" || true + rm -f "${failed_file}" + exit "${docker_exit}" + else + make publish-${{ matrix.java-version }}-ci + fi + - name: Save failed regions for re-run + if: always() + uses: ./.github/actions/region-retry + with: + mode: save + job_key: java-${{ matrix.java-version }} + failure_summary: ${{ steps.publish.outputs.failure_summary }} + run_id: ${{ github.run_id }} + run_attempt: ${{ github.run_attempt }} - name: Set up QEMU + if: always() uses: docker/setup-qemu-action@v3 with: platforms: arm64, amd64 - name: Publish ECR image for ${{ matrix.java-version }} - if: steps.java-check-tag.outputs.match == 'true' + if: always() + id: publish-ecr env: AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + ECR_REPOSITORY: ${{ vars.ECR_REPOSITORY }} run: | cd java ./publish-layers.sh build-publish-${{ matrix.java-version }}-ecr-image + + # ── Job 5: Slack release summary — only after validate passed ───────────── + notify-slack: + needs: [stage-java, validate-java, publish-java] + if: always() && needs.validate-java.result == 'success' && needs.stage-java.outputs.tag-match == 'true' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: ./.github/actions/notify-slack-layer + with: + language_name: Java + versions_json: |- + [ + {"key":"java8al2","label":"Java 8 (AL2)","job":"publish-java (java8al2)","fallback":"${{ needs.publish-java.result }}","failure_key":"java-java8al2"}, + {"key":"java11","label":"Java 11","job":"publish-java (java11)","fallback":"${{ needs.publish-java.result }}","failure_key":"java-java11"}, + {"key":"java17","label":"Java 17","job":"publish-java (java17)","fallback":"${{ needs.publish-java.result }}","failure_key":"java-java17"}, + {"key":"java21","label":"Java 21","job":"publish-java (java21)","fallback":"${{ needs.publish-java.result }}","failure_key":"java-java21"} + ] + failure_summaries: ${{ needs.publish-java.outputs.ecr_failure_summary }} + slack_webhook: ${{ secrets.SLACK_VALIDATION_WEBHOOK }} + gh_token: ${{ github.token }} + repo: ${{ github.repository }} + run_id: ${{ github.run_id }} + run_attempt: ${{ github.run_attempt }} + ref_name: ${{ github.ref_name }} + actor: ${{ github.actor }} + server_url: ${{ github.server_url }} diff --git a/.github/workflows/publish-node.yml b/.github/workflows/publish-node.yml index 974ad377..a1412df5 100644 --- a/.github/workflows/publish-node.yml +++ b/.github/workflows/publish-node.yml @@ -4,71 +4,243 @@ on: push: tags: - v**_nodejs + workflow_dispatch: + inputs: + regions: + description: 'Comma-separated regions to (re)publish. Leave empty to publish all regions.' + required: false + default: '' jobs: + # ── Job 1: Build + publish staging (node24 only) ────────────────────────── + stage-node: + runs-on: ubuntu-latest + outputs: + tag-match: ${{ steps.setup.outputs.tag-match }} + arn_x86: ${{ steps.staging.outputs.arn_x86 }} + arn_arm64: ${{ steps.staging.outputs.arn_arm64 }} + arn_x86_slim: ${{ steps.staging.outputs.arn_x86_slim }} + arn_arm64_slim: ${{ steps.staging.outputs.arn_arm64_slim }} + steps: + - uses: actions/checkout@v4 + - name: Setup + id: setup + uses: ./.github/actions/node-layer-setup + with: + node-version: 24 + - name: Build layer artifacts (node24) + if: steps.setup.outputs.tag-match == 'true' + run: make extract-nodejs24-artifacts + - name: Publish staging layers (node24) + if: steps.setup.outputs.tag-match == 'true' + id: staging + env: + AWS_ACCESS_KEY_ID: ${{ secrets.STAGING_AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.STAGING_AWS_SECRET_ACCESS_KEY }} + STAGING_REGION: us-east-1 + run: | + cd nodejs + DIST_DIR=../dist/nodejs24 ./publish-layers.sh publish-staging-24 + + # ── Job 2: Validate (calls reusable workflow) ───────────────────────────── + validate-node: + needs: stage-node + if: needs.stage-node.outputs.tag-match == 'true' + uses: ./.github/workflows/validate-layer.yml + with: + runtime: nodejs24 + arn_x86: ${{ needs.stage-node.outputs.arn_x86 }} + arn_arm64: ${{ needs.stage-node.outputs.arn_arm64 }} + arn_x86_slim: ${{ needs.stage-node.outputs.arn_x86_slim }} + arn_arm64_slim: ${{ needs.stage-node.outputs.arn_arm64_slim }} + secrets: inherit + + # ── Job 3: Cleanup staging (always, even if validate failed) ───────────── + cleanup-node: + needs: [stage-node, validate-node] + if: always() && needs.stage-node.outputs.arn_x86 != '' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Cleanup staging layers + env: + AWS_ACCESS_KEY_ID: ${{ secrets.STAGING_AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.STAGING_AWS_SECRET_ACCESS_KEY }} + STAGING_REGION: us-east-1 + ARN_X86: ${{ needs.stage-node.outputs.arn_x86 }} + ARN_ARM64: ${{ needs.stage-node.outputs.arn_arm64 }} + ARN_X86_SLIM: ${{ needs.stage-node.outputs.arn_x86_slim }} + ARN_ARM64_SLIM: ${{ needs.stage-node.outputs.arn_arm64_slim }} + run: | + cd nodejs + ./publish-layers.sh cleanup-staging-24 + + # ── Job 4: Release node20/22/24 — gated on validate passing ────────────── publish-node: + needs: [stage-node, validate-node] + if: needs.stage-node.outputs.tag-match == 'true' runs-on: ubuntu-latest strategy: + fail-fast: false matrix: node-version: [20, 22, 24] + outputs: + failure_summary: ${{ steps.publish.outputs.failure_summary }} + ecr_failure_summary: ${{ steps.publish-ecr.outputs.ecr_failure_summary }} steps: - uses: actions/checkout@v4 - name: Setup - id: setup uses: ./.github/actions/node-layer-setup with: node-version: ${{ matrix.node-version }} - - name: Publish layer - if: steps.setup.outputs.tag-match == 'true' + - name: Build layer artifacts + run: make extract-nodejs${{ matrix.node-version }}-artifacts + - name: Load failed regions for re-run + id: region-retry-load + uses: ./.github/actions/region-retry + with: + mode: load + job_key: nodejs-${{ matrix.node-version }} + run_id: ${{ github.run_id }} + run_attempt: ${{ github.run_attempt }} + - name: Publish release layers + id: publish env: AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} - run: make publish-nodejs${{ matrix.node-version }}-ci + PUBLISH_REGIONS: ${{ steps.region-retry-load.outputs.publish_regions || inputs.regions }} + LAYER_REGIONS: ${{ vars.LAYER_REGIONS }} + S3_BUCKET_PREFIX: ${{ vars.S3_BUCKET_PREFIX }} + run: | + failed_file="$(mktemp)" + chmod a+rw "$failed_file" "$GITHUB_STEP_SUMMARY" + set +e + docker run \ + -e AWS_ACCESS_KEY_ID \ + -e AWS_SECRET_ACCESS_KEY \ + -e GITHUB_STEP_SUMMARY \ + -e PUBLISH_REGIONS \ + -e LAYER_REGIONS \ + -e S3_BUCKET_PREFIX \ + -e "FAILED_REGIONS_FILE=${failed_file}" \ + -v "$GITHUB_STEP_SUMMARY:$GITHUB_STEP_SUMMARY" \ + -v "${failed_file}:${failed_file}" \ + newrelic-lambda-layers-nodejs${{ matrix.node-version }} + docker_exit=$? + set -e + [[ -s "${failed_file}" ]] && cat "${failed_file}" >> "$GITHUB_OUTPUT" || true + rm -f "${failed_file}" + exit "${docker_exit}" + - name: Save failed regions for re-run + if: always() + uses: ./.github/actions/region-retry + with: + mode: save + job_key: nodejs-${{ matrix.node-version }} + failure_summary: ${{ steps.publish.outputs.failure_summary }} + run_id: ${{ github.run_id }} + run_attempt: ${{ github.run_attempt }} - name: Publish ECR image for nodejs${{ matrix.node-version }} - if: steps.setup.outputs.tag-match == 'true' + if: always() + id: publish-ecr env: AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + ECR_REPOSITORY: ${{ vars.ECR_REPOSITORY }} run: | cd nodejs ./publish-layers.sh build-publish-${{ matrix.node-version }}-ecr-image - name: Upload Unit Test Coverage - if: steps.setup.outputs.tag-match == 'true' uses: codecov/codecov-action@v5.3.1 with: - token: ${{ secrets.CODECOV_TOKEN }} - files: ./nodejs/coverage/unit/lcov.info - fail_ci_if_error: true - flags: unit-tests + token: ${{ secrets.CODECOV_TOKEN }} + files: ./nodejs/coverage/unit/lcov.info + fail_ci_if_error: true + flags: unit-tests + # ── Job 5: Release universal — gated on validate passing ───────────────── publish-node-universal: + needs: [stage-node, validate-node] + if: needs.stage-node.outputs.tag-match == 'true' runs-on: ubuntu-latest + outputs: + failure_summary: ${{ steps.publish-universal.outputs.failure_summary }} + ecr_failure_summary: ${{ steps.publish-ecr-universal.outputs.ecr_failure_summary }} steps: - uses: actions/checkout@v4 - name: Setup - id: setup uses: ./.github/actions/node-layer-setup with: node-version: 22 + - name: Load failed regions for re-run + id: region-retry-load + uses: ./.github/actions/region-retry + with: + mode: load + job_key: nodejs-universal + run_id: ${{ github.run_id }} + run_attempt: ${{ github.run_attempt }} - name: Publish universal layer - if: steps.setup.outputs.tag-match == 'true' + id: publish-universal env: AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + PUBLISH_REGIONS: ${{ steps.region-retry-load.outputs.publish_regions || inputs.regions }} + LAYER_REGIONS: ${{ vars.LAYER_REGIONS }} + S3_BUCKET_PREFIX: ${{ vars.S3_BUCKET_PREFIX }} run: make publish-nodejs-universal-ci + - name: Save failed regions for re-run + if: always() + uses: ./.github/actions/region-retry + with: + mode: save + job_key: nodejs-universal + failure_summary: ${{ steps.publish-universal.outputs.failure_summary }} + run_id: ${{ github.run_id }} + run_attempt: ${{ github.run_attempt }} - name: Publish universal ECR image - if: steps.setup.outputs.tag-match == 'true' + if: always() + id: publish-ecr-universal env: AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + ECR_REPOSITORY: ${{ vars.ECR_REPOSITORY }} run: | cd nodejs ./publish-layers.sh build-publish-universal-ecr-image - name: Upload Unit Test Coverage - if: steps.setup.outputs.tag-match == 'true' uses: codecov/codecov-action@v5.3.1 with: - token: ${{ secrets.CODECOV_TOKEN }} - files: ./nodejs/coverage/unit/lcov.info - fail_ci_if_error: true - flags: unit-tests + token: ${{ secrets.CODECOV_TOKEN }} + files: ./nodejs/coverage/unit/lcov.info + fail_ci_if_error: true + flags: unit-tests + + # ── Job 6: Slack release summary — only after validate passed ───────────── + notify-slack: + needs: [stage-node, validate-node, publish-node, publish-node-universal] + if: always() && needs.validate-node.result == 'success' && needs.stage-node.outputs.tag-match == 'true' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: ./.github/actions/notify-slack-layer + with: + language_name: Node.js + versions_json: |- + [ + {"key":"20","label":"Node.js 20","job":"publish-node (20)","fallback":"${{ needs.publish-node.result }}","failure_key":"nodejs-20"}, + {"key":"22","label":"Node.js 22","job":"publish-node (22)","fallback":"${{ needs.publish-node.result }}","failure_key":"nodejs-22"}, + {"key":"24","label":"Node.js 24","job":"publish-node (24)","fallback":"${{ needs.publish-node.result }}","failure_key":"nodejs-24"}, + {"key":"universal","label":"Node.js Universal","job":"publish-node-universal","fallback":"${{ needs.publish-node-universal.result }}","failure_key":"nodejs-universal"} + ] + failure_summaries: | + ${{ needs.publish-node.outputs.ecr_failure_summary }} + ${{ needs.publish-node-universal.outputs.ecr_failure_summary }} + slack_webhook: ${{ secrets.SLACK_VALIDATION_WEBHOOK }} + gh_token: ${{ github.token }} + repo: ${{ github.repository }} + run_id: ${{ github.run_id }} + run_attempt: ${{ github.run_attempt }} + ref_name: ${{ github.ref_name }} + actor: ${{ github.actor }} + server_url: ${{ github.server_url }} diff --git a/.github/workflows/publish-python.yml b/.github/workflows/publish-python.yml index 886c8cb7..8aa8413e 100644 --- a/.github/workflows/publish-python.yml +++ b/.github/workflows/publish-python.yml @@ -4,39 +4,182 @@ on: push: tags: - v**_python + workflow_dispatch: + inputs: + regions: + description: 'Comma-separated regions to (re)publish. Leave empty to publish all regions.' + required: false + default: '' jobs: + # ── Job 1: Publish staging (python3.14 only) ────────────────────────────── + stage-python: + runs-on: ubuntu-latest + outputs: + tag-match: ${{ steps.setup.outputs.tag-match }} + arn_x86: ${{ steps.staging.outputs.arn_x86 }} + arn_arm64: ${{ steps.staging.outputs.arn_arm64 }} + steps: + - uses: actions/checkout@v4 + - name: Setup + id: setup + uses: ./.github/actions/python-layer-setup + - name: Publish staging layers (python3.14) + if: steps.setup.outputs.tag-match == 'true' + id: staging + env: + AWS_ACCESS_KEY_ID: ${{ secrets.STAGING_AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.STAGING_AWS_SECRET_ACCESS_KEY }} + STAGING_REGION: us-east-1 + run: | + cd python + ./publish-layers.sh publish-staging-python3.14 + + # ── Job 2: Validate (calls reusable workflow) ───────────────────────────── + validate-python: + needs: stage-python + if: needs.stage-python.outputs.tag-match == 'true' + uses: ./.github/workflows/validate-layer.yml + with: + runtime: python314 + arn_x86: ${{ needs.stage-python.outputs.arn_x86 }} + arn_arm64: ${{ needs.stage-python.outputs.arn_arm64 }} + secrets: inherit + + # ── Job 3: Cleanup staging (always) ────────────────────────────────────── + cleanup-python: + needs: [stage-python, validate-python] + if: always() && needs.stage-python.outputs.arn_x86 != '' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Cleanup staging layers + env: + AWS_ACCESS_KEY_ID: ${{ secrets.STAGING_AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.STAGING_AWS_SECRET_ACCESS_KEY }} + STAGING_REGION: us-east-1 + ARN_X86: ${{ needs.stage-python.outputs.arn_x86 }} + ARN_ARM64: ${{ needs.stage-python.outputs.arn_arm64 }} + run: | + cd python + ./publish-layers.sh cleanup-staging-python3.14 + + # ── Job 4: Release all Python versions — gated on validate passing ──────── publish-python: + needs: [stage-python, validate-python] + if: needs.stage-python.outputs.tag-match == 'true' runs-on: ubuntu-latest strategy: + fail-fast: false matrix: - python-version: [ '3.9', '3.10', '3.11', '3.12', '3.13', '3.14' ] + python-version: ['3.9', '3.10', '3.11', '3.12', '3.13', '3.14'] + outputs: + failure_summary: ${{ steps.publish.outputs.failure_summary }} + ecr_failure_summary: ${{ steps.publish.outputs.ecr_failure_summary }} steps: - uses: actions/checkout@v4 - name: Setup - id: setup uses: ./.github/actions/python-layer-setup + - name: Load failed regions for re-run + id: region-retry-load + uses: ./.github/actions/region-retry + with: + mode: load + job_key: python-${{ matrix.python-version }} + run_id: ${{ github.run_id }} + run_attempt: ${{ github.run_attempt }} - name: Publish Python ${{ matrix.python-version }} layer - if: steps.setup.outputs.tag-match == 'true' + id: publish env: AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + PUBLISH_REGIONS: ${{ steps.region-retry-load.outputs.publish_regions || inputs.regions }} + LAYER_REGIONS: ${{ vars.LAYER_REGIONS }} + S3_BUCKET_PREFIX: ${{ vars.S3_BUCKET_PREFIX }} + ECR_REPOSITORY: ${{ vars.ECR_REPOSITORY }} run: | cd python ./publish-layers.sh python${{ matrix.python-version }} + - name: Save failed regions for re-run + if: always() + uses: ./.github/actions/region-retry + with: + mode: save + job_key: python-${{ matrix.python-version }} + failure_summary: ${{ steps.publish.outputs.failure_summary }} + run_id: ${{ github.run_id }} + run_attempt: ${{ github.run_attempt }} + # ── Job 5: Release universal — gated on validate passing ───────────────── publish-python-universal: + needs: [stage-python, validate-python] + if: needs.stage-python.outputs.tag-match == 'true' runs-on: ubuntu-latest + outputs: + failure_summary: ${{ steps.publish-universal.outputs.failure_summary }} + ecr_failure_summary: ${{ steps.publish-universal.outputs.ecr_failure_summary }} steps: - uses: actions/checkout@v4 - name: Setup - id: setup uses: ./.github/actions/python-layer-setup + - name: Load failed regions for re-run + id: region-retry-load + uses: ./.github/actions/region-retry + with: + mode: load + job_key: python-universal + run_id: ${{ github.run_id }} + run_attempt: ${{ github.run_attempt }} - name: Publish universal Python layer - if: steps.setup.outputs.tag-match == 'true' + id: publish-universal env: AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + PUBLISH_REGIONS: ${{ steps.region-retry-load.outputs.publish_regions || inputs.regions }} + LAYER_REGIONS: ${{ vars.LAYER_REGIONS }} + S3_BUCKET_PREFIX: ${{ vars.S3_BUCKET_PREFIX }} + ECR_REPOSITORY: ${{ vars.ECR_REPOSITORY }} run: | cd python ./publish-layers.sh python + - name: Save failed regions for re-run + if: always() + uses: ./.github/actions/region-retry + with: + mode: save + job_key: python-universal + failure_summary: ${{ steps.publish-universal.outputs.failure_summary }} + run_id: ${{ github.run_id }} + run_attempt: ${{ github.run_attempt }} + + # ── Job 6: Slack release summary — only after validate passed ───────────── + notify-slack: + needs: [stage-python, validate-python, publish-python, publish-python-universal] + if: always() && needs.validate-python.result == 'success' && needs.stage-python.outputs.tag-match == 'true' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: ./.github/actions/notify-slack-layer + with: + language_name: Python + versions_json: |- + [ + {"key":"3.9","label":"Python 3.9","job":"publish-python (3.9)","fallback":"${{ needs.publish-python.result }}","failure_key":"python-3.9"}, + {"key":"3.10","label":"Python 3.10","job":"publish-python (3.10)","fallback":"${{ needs.publish-python.result }}","failure_key":"python-3.10"}, + {"key":"3.11","label":"Python 3.11","job":"publish-python (3.11)","fallback":"${{ needs.publish-python.result }}","failure_key":"python-3.11"}, + {"key":"3.12","label":"Python 3.12","job":"publish-python (3.12)","fallback":"${{ needs.publish-python.result }}","failure_key":"python-3.12"}, + {"key":"3.13","label":"Python 3.13","job":"publish-python (3.13)","fallback":"${{ needs.publish-python.result }}","failure_key":"python-3.13"}, + {"key":"3.14","label":"Python 3.14","job":"publish-python (3.14)","fallback":"${{ needs.publish-python.result }}","failure_key":"python-3.14"}, + {"key":"universal","label":"Python Universal","job":"publish-python-universal","fallback":"${{ needs.publish-python-universal.result }}","failure_key":"python-universal"} + ] + failure_summaries: | + ${{ needs.publish-python.outputs.ecr_failure_summary }} + ${{ needs.publish-python-universal.outputs.ecr_failure_summary }} + slack_webhook: ${{ secrets.SLACK_VALIDATION_WEBHOOK }} + gh_token: ${{ github.token }} + repo: ${{ github.repository }} + run_id: ${{ github.run_id }} + run_attempt: ${{ github.run_attempt }} + ref_name: ${{ github.ref_name }} + actor: ${{ github.actor }} + server_url: ${{ github.server_url }} diff --git a/.github/workflows/publish-ruby.yml b/.github/workflows/publish-ruby.yml index 7881259b..88f5756c 100644 --- a/.github/workflows/publish-ruby.yml +++ b/.github/workflows/publish-ruby.yml @@ -4,45 +4,163 @@ on: push: tags: - v**_ruby + workflow_dispatch: + inputs: + regions: + description: 'Comma-separated regions to (re)publish. Leave empty to publish all regions.' + required: false + default: '' jobs: + # ── Job 1: Publish staging (ruby3.4 only) ──────────────────────────────── + stage-ruby: + runs-on: ubuntu-latest + outputs: + tag-match: ${{ steps.ruby-check-tag.outputs.match }} + arn_x86: ${{ steps.staging.outputs.arn_x86 }} + arn_arm64: ${{ steps.staging.outputs.arn_arm64 }} + steps: + - uses: actions/checkout@v4.2.2 + - name: Use Ruby 3.4 + uses: ruby/setup-ruby@v1.226.0 + with: + ruby-version: 3.4 + - name: Check Tag + id: ruby-check-tag + run: | + if [[ "${{ github.event_name }}" == "workflow_dispatch" ]] || \ + [[ "${{ github.event.ref }}" =~ ^refs/tags/v[0-9]+(\.[0-9]+)*_ruby$ ]]; then + echo "match=true" >> $GITHUB_OUTPUT + fi + - name: Clean the workspace + run: ./bin/clean + working-directory: ruby + - name: Install Ruby Dependencies + run: bundle + working-directory: ruby + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + with: + platforms: arm64, amd64 + - name: Publish staging layers (ruby3.4) + if: steps.ruby-check-tag.outputs.match == 'true' + id: staging + env: + AWS_ACCESS_KEY_ID: ${{ secrets.STAGING_AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.STAGING_AWS_SECRET_ACCESS_KEY }} + STAGING_REGION: us-east-1 + run: | + cd ruby + ./publish-layers.sh publish-staging-ruby3.4 + + # ── Job 2: Validate (calls reusable workflow) ───────────────────────────── + validate-ruby: + needs: stage-ruby + if: needs.stage-ruby.outputs.tag-match == 'true' + uses: ./.github/workflows/validate-layer.yml + with: + runtime: ruby34 + arn_x86: ${{ needs.stage-ruby.outputs.arn_x86 }} + arn_arm64: ${{ needs.stage-ruby.outputs.arn_arm64 }} + secrets: inherit + + # ── Job 3: Cleanup staging (always) ────────────────────────────────────── + cleanup-ruby: + needs: [stage-ruby, validate-ruby] + if: always() && needs.stage-ruby.outputs.arn_x86 != '' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Cleanup staging layers + env: + AWS_ACCESS_KEY_ID: ${{ secrets.STAGING_AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.STAGING_AWS_SECRET_ACCESS_KEY }} + STAGING_REGION: us-east-1 + ARN_X86: ${{ needs.stage-ruby.outputs.arn_x86 }} + ARN_ARM64: ${{ needs.stage-ruby.outputs.arn_arm64 }} + run: | + cd ruby + ./publish-layers.sh cleanup-staging-ruby3.4 + + # ── Job 4: Release all Ruby versions — gated on validate passing ────────── publish-ruby: + needs: [stage-ruby, validate-ruby] + if: needs.stage-ruby.outputs.tag-match == 'true' runs-on: ubuntu-latest strategy: + fail-fast: false matrix: ruby-version: [3.2, 3.3, 3.4] + outputs: + failure_summary: ${{ steps.publish.outputs.failure_summary }} + ecr_failure_summary: ${{ steps.publish.outputs.ecr_failure_summary }} steps: - uses: actions/checkout@v4.2.2 - name: Use Ruby ${{ matrix.ruby-version }} uses: ruby/setup-ruby@v1.226.0 with: ruby-version: ${{ matrix.ruby-version }} - - name: Check Tag - id: ruby-check-tag - run: | - if [[ ${{ github.event.ref }} =~ ^refs/tags/v[0-9]+(\.[0-9]+)*_ruby$ ]]; then - echo "match=true" >> $GITHUB_OUTPUT - fi - name: Clean the workspace run: ./bin/clean working-directory: ruby - name: Install Ruby Dependencies run: bundle working-directory: ruby - - name: Obtain Ruby version without the dot - id: ruby-version-without-dot - run: >- - echo "::set-output name=VERSION::$( - echo ${{ matrix.ruby-version }} | sed 's/\.//' - )" - name: Set up QEMU uses: docker/setup-qemu-action@v3 with: platforms: arm64, amd64 + - name: Load failed regions for re-run + id: region-retry-load + uses: ./.github/actions/region-retry + with: + mode: load + job_key: ruby-${{ matrix.ruby-version }} + run_id: ${{ github.run_id }} + run_attempt: ${{ github.run_attempt }} - name: Build and Publish layer - if: steps.ruby-check-tag.outputs.match == 'true' + id: publish env: AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + PUBLISH_REGIONS: ${{ steps.region-retry-load.outputs.publish_regions || inputs.regions }} + LAYER_REGIONS: ${{ vars.LAYER_REGIONS }} + S3_BUCKET_PREFIX: ${{ vars.S3_BUCKET_PREFIX }} + ECR_REPOSITORY: ${{ vars.ECR_REPOSITORY }} run: ./publish-layers.sh ruby${{ matrix.ruby-version }} working-directory: ruby + - name: Save failed regions for re-run + if: always() + uses: ./.github/actions/region-retry + with: + mode: save + job_key: ruby-${{ matrix.ruby-version }} + failure_summary: ${{ steps.publish.outputs.failure_summary }} + run_id: ${{ github.run_id }} + run_attempt: ${{ github.run_attempt }} + + # ── Job 5: Slack release summary — only after validate passed ───────────── + notify-slack: + needs: [stage-ruby, validate-ruby, publish-ruby] + if: always() && needs.validate-ruby.result == 'success' && needs.stage-ruby.outputs.tag-match == 'true' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: ./.github/actions/notify-slack-layer + with: + language_name: Ruby + versions_json: |- + [ + {"key":"3.2","label":"Ruby 3.2","job":"publish-ruby (3.2)","fallback":"${{ needs.publish-ruby.result }}","failure_key":"ruby-3.2"}, + {"key":"3.3","label":"Ruby 3.3","job":"publish-ruby (3.3)","fallback":"${{ needs.publish-ruby.result }}","failure_key":"ruby-3.3"}, + {"key":"3.4","label":"Ruby 3.4","job":"publish-ruby (3.4)","fallback":"${{ needs.publish-ruby.result }}","failure_key":"ruby-3.4"} + ] + failure_summaries: ${{ needs.publish-ruby.outputs.ecr_failure_summary }} + slack_webhook: ${{ secrets.SLACK_VALIDATION_WEBHOOK }} + gh_token: ${{ github.token }} + repo: ${{ github.repository }} + run_id: ${{ github.run_id }} + run_attempt: ${{ github.run_attempt }} + ref_name: ${{ github.ref_name }} + actor: ${{ github.actor }} + server_url: ${{ github.server_url }} diff --git a/.github/workflows/validate-layer.yml b/.github/workflows/validate-layer.yml new file mode 100644 index 00000000..e6f359cb --- /dev/null +++ b/.github/workflows/validate-layer.yml @@ -0,0 +1,78 @@ +name: Validate Layer (reusable) + +on: + workflow_call: + inputs: + runtime: + required: true + type: string + label: + description: Display name used in Slack messages (defaults to runtime) + required: false + type: string + default: '' + arn_x86: + required: true + type: string + arn_arm64: + required: true + type: string + arn_x86_slim: + required: false + type: string + default: '' + arn_arm64_slim: + required: false + type: string + default: '' + os: + required: false + type: string + default: '' + secrets: + ORCHESTRATOR_API_URL: + required: true + ORCHESTRATOR_API_KEY: + required: true + VALIDATION_RESULTS_BUCKET: + required: true + STAGING_AWS_ACCESS_KEY_ID: + required: true + STAGING_AWS_SECRET_ACCESS_KEY: + required: true + SLACK_VALIDATION_WEBHOOK: + required: true + +jobs: + validate: + runs-on: ubuntu-latest + timeout-minutes: 35 + steps: + - uses: actions/checkout@v4 + + - name: Validate staging layers + id: validate + env: + ORCHESTRATOR_API_URL: ${{ secrets.ORCHESTRATOR_API_URL }} + ORCHESTRATOR_API_KEY: ${{ secrets.ORCHESTRATOR_API_KEY }} + RESULTS_BUCKET: ${{ secrets.VALIDATION_RESULTS_BUCKET }} + AWS_ACCESS_KEY_ID: ${{ secrets.STAGING_AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.STAGING_AWS_SECRET_ACCESS_KEY }} + run: | + chmod +x .github/scripts/poll-validation.sh + ARGS="--runtime ${{ inputs.runtime }} --arn-x86 ${{ inputs.arn_x86 }} --arn-arm64 ${{ inputs.arn_arm64 }}" + [[ -n "${{ inputs.arn_x86_slim }}" ]] && ARGS="$ARGS --arn-x86-slim ${{ inputs.arn_x86_slim }}" + [[ -n "${{ inputs.arn_arm64_slim }}" ]] && ARGS="$ARGS --arn-arm64-slim ${{ inputs.arn_arm64_slim }}" + [[ -n "${{ inputs.os }}" ]] && ARGS="$ARGS --os ${{ inputs.os }}" + .github/scripts/poll-validation.sh $ARGS + + - name: Notify Slack — validation failed + if: failure() + uses: slackapi/slack-github-action@v2.1.0 + with: + webhook: ${{ secrets.SLACK_VALIDATION_WEBHOOK }} + webhook-type: incoming-webhook + payload: | + { + "text": ":x: *${{ inputs.label || inputs.runtime }}* layer validation FAILED — release blocked — <${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}|Run #${{ github.run_id }}>" + } diff --git a/Makefile b/Makefile index 4841d253..da9101d4 100644 --- a/Makefile +++ b/Makefile @@ -74,6 +74,13 @@ publish-java21-local: build-java21 -v "${HOME}/.aws:/home/newrelic-lambda-layers/.aws" \ newrelic-lambda-layers-java21 +extract-java21-artifacts: build-java21 + @docker rm -f java21-artifacts 2>/dev/null || true + mkdir -p dist/java21 + docker create --name java21-artifacts newrelic-lambda-layers-java21 + docker cp java21-artifacts:/home/newrelic-lambda-layers/java/dist/. dist/java21/ + docker rm java21-artifacts + build-nodejs20: docker build \ --no-cache \ @@ -131,6 +138,28 @@ publish-nodejs24-local: build-nodejs24 -v "${HOME}/.aws:/home/newrelic-lambda-layers/.aws" \ newrelic-lambda-layers-nodejs24 +# Extract built zips from the Docker image into dist/nodejs/ for staging publish. +extract-nodejs20-artifacts: build-nodejs20 + @docker rm -f nodejs20-artifacts 2>/dev/null || true + mkdir -p dist/nodejs20 + docker create --name nodejs20-artifacts newrelic-lambda-layers-nodejs20 + docker cp nodejs20-artifacts:/home/newrelic-lambda-layers/nodejs/dist/. dist/nodejs20/ + docker rm nodejs20-artifacts + +extract-nodejs22-artifacts: build-nodejs22 + @docker rm -f nodejs22-artifacts 2>/dev/null || true + mkdir -p dist/nodejs22 + docker create --name nodejs22-artifacts newrelic-lambda-layers-nodejs22 + docker cp nodejs22-artifacts:/home/newrelic-lambda-layers/nodejs/dist/. dist/nodejs22/ + docker rm nodejs22-artifacts + +extract-nodejs24-artifacts: build-nodejs24 + @docker rm -f nodejs24-artifacts 2>/dev/null || true + mkdir -p dist/nodejs24 + docker create --name nodejs24-artifacts newrelic-lambda-layers-nodejs24 + docker cp nodejs24-artifacts:/home/newrelic-lambda-layers/nodejs/dist/. dist/nodejs24/ + docker rm nodejs24-artifacts + build-python-universal: docker build \ --no-cache \ diff --git a/dotnet/publish-layers.sh b/dotnet/publish-layers.sh index 7d62d34e..abf29b14 100755 --- a/dotnet/publish-layers.sh +++ b/dotnet/publish-layers.sh @@ -3,7 +3,7 @@ set -Eeuo pipefail BUILD_DIR=lib # for .net can either be lib or bin. See: https://docs.aws.amazon.com/lambda/latest/dg/packaging-layers.html -DIST_DIR=dist +DIST_DIR=${DIST_DIR:-dist} DOTNET_DIST_ARM64=$DIST_DIR/dotnet.arm64.zip DOTNET_DIST_X86_64=$DIST_DIR/dotnet.x86_64.zip @@ -36,11 +36,7 @@ function publish-dotnet-x86-64 { exit 1 fi - for region in "${REGIONS[@]}"; do - publish_layer $DOTNET_DIST_X86_64 $region dotnet x86_64 $NEWRELIC_AGENT_VERSION - done - - publish_docker_ecr $DOTNET_DIST_X86_64 dotnet x86_64 + run_region_loop "$DOTNET_DIST_X86_64" dotnet x86_64 "$NEWRELIC_AGENT_VERSION" } function build-dotnet-arm64 { @@ -61,11 +57,7 @@ function publish-dotnet-arm64 { exit 1 fi - for region in "${REGIONS[@]}"; do - publish_layer $DOTNET_DIST_ARM64 $region dotnet arm64 $NEWRELIC_AGENT_VERSION - done - - publish_docker_ecr $DOTNET_DIST_ARM64 dotnet arm64 + run_region_loop "$DOTNET_DIST_ARM64" dotnet arm64 "$NEWRELIC_AGENT_VERSION" } # exmaple https://download.newrelic.com/dot_net_agent/latest_release/newrelic-dotnet-agent_amd64.tar.gz @@ -96,8 +88,31 @@ function get_agent { } -build-dotnet-arm64 -publish-dotnet-arm64 -build-dotnet-x86-64 -publish-dotnet-x86-64 +case "${1:-}" in +"publish-staging-dotnet") + build-dotnet-arm64 + build-dotnet-x86-64 + arn_arm64=$(publish_staging_layer "$DOTNET_DIST_ARM64" dotnet arm64 "$NEWRELIC_AGENT_VERSION") + echo "arn_arm64=${arn_arm64}" >> "${GITHUB_OUTPUT:-/dev/stderr}" + arn_x86=$(publish_staging_layer "$DOTNET_DIST_X86_64" dotnet x86_64 "$NEWRELIC_AGENT_VERSION") + echo "arn_x86=${arn_x86}" >> "${GITHUB_OUTPUT:-/dev/stderr}" + ;; +"cleanup-staging-dotnet") + for arn in "${ARN_X86:-}" "${ARN_ARM64:-}"; do + [[ -z "$arn" ]] && continue + delete_staging_layer "$(echo "$arn" | cut -d: -f8)" "$(echo "$arn" | cut -d: -f9)" + done + ;; +*) + layer_rc=0 + build-dotnet-arm64 + publish-dotnet-arm64 || layer_rc=$? + publish_ecr_safe $DOTNET_DIST_ARM64 dotnet arm64 + build-dotnet-x86-64 + publish-dotnet-x86-64 || layer_rc=$? + publish_ecr_safe $DOTNET_DIST_X86_64 dotnet x86_64 + finalize_ecr_results "dotnet" + [[ $layer_rc -eq 0 ]] || exit $layer_rc + ;; +esac diff --git a/extension/publish-layer.sh b/extension/publish-layer.sh index 96a400e8..c2f3974f 100755 --- a/extension/publish-layer.sh +++ b/extension/publish-layer.sh @@ -31,9 +31,7 @@ function publish-layer-x86 { exit 1 fi - for region in "${REGIONS[@]}"; do - publish_layer $EXTENSION_DIST_ZIP_X86_64 $region provided x86_64 provided - done + run_region_loop "$EXTENSION_DIST_ZIP_X86_64" provided x86_64 provided } function publish-layer-arm64 { @@ -42,15 +40,41 @@ function publish-layer-arm64 { exit 1 fi - for region in "${REGIONS[@]}"; do - publish_layer $EXTENSION_DIST_ZIP_ARM64 $region provided arm64 provided + run_region_loop "$EXTENSION_DIST_ZIP_ARM64" provided arm64 provided +} + +function publish-staging { + build-layer-x86 + build-layer-arm64 + + arn_x86=$(publish_staging_layer "$EXTENSION_DIST_ZIP_X86_64" provided x86_64) + echo "arn_x86=${arn_x86}" >> "${GITHUB_OUTPUT:-/dev/stderr}" + + arn_arm64=$(publish_staging_layer "$EXTENSION_DIST_ZIP_ARM64" provided arm64) + echo "arn_arm64=${arn_arm64}" >> "${GITHUB_OUTPUT:-/dev/stderr}" +} + +function cleanup-staging { + for arn in "${ARN_X86:-}" "${ARN_ARM64:-}"; do + [[ -z "$arn" ]] && continue + delete_staging_layer "$(echo "$arn" | cut -d: -f8)" "$(echo "$arn" | cut -d: -f9)" done } -build-layer-x86 -publish-layer-x86 -publish_docker_ecr $EXTENSION_DIST_ZIP_X86_64 extension x86_64 +case "${1:-publish}" in + "publish-staging") publish-staging ;; + "cleanup-staging") cleanup-staging ;; + *) + layer_rc=0 + build-layer-x86 + publish-layer-x86 || layer_rc=$? + publish_ecr_safe $EXTENSION_DIST_ZIP_X86_64 extension x86_64 + + build-layer-arm64 + publish-layer-arm64 || layer_rc=$? + publish_ecr_safe $EXTENSION_DIST_ZIP_ARM64 extension arm64 -build-layer-arm64 -publish-layer-arm64 -publish_docker_ecr $EXTENSION_DIST_ZIP_ARM64 extension arm64 + finalize_ecr_results "extension" + [[ $layer_rc -eq 0 ]] || exit $layer_rc + ;; +esac diff --git a/java/publish-layers.sh b/java/publish-layers.sh index d5d211a2..e0b2545e 100755 --- a/java/publish-layers.sh +++ b/java/publish-layers.sh @@ -5,7 +5,7 @@ set -Eeuo pipefail BUILD_DIR=build GRADLE_ARCHIVE=$BUILD_DIR/distributions/NewRelicJavaLayer.zip -DIST_DIR=dist +DIST_DIR=${DIST_DIR:-dist} JAVA8_DIST_ARM64=$DIST_DIR/java8.arm64.zip JAVA8_DIST_X86_64=$DIST_DIR/java8.x86_64.zip JAVA11_DIST_ARM64=$DIST_DIR/java11.arm64.zip @@ -65,9 +65,7 @@ function publish-java8al2-arm64 { exit 1 fi - for region in "${REGIONS[@]}"; do - publish_layer $JAVA8_DIST_ARM64 $region java8.al2 arm64 - done + run_region_loop "$JAVA8_DIST_ARM64" java8.al2 arm64 } function publish-java8al2-x86 { @@ -76,9 +74,7 @@ function publish-java8al2-x86 { exit 1 fi - for region in "${REGIONS[@]}"; do - publish_layer $JAVA8_DIST_X86_64 $region java8.al2 x86_64 - done + run_region_loop "$JAVA8_DIST_X86_64" java8.al2 x86_64 } function build-java11-arm64 { @@ -95,9 +91,7 @@ function publish-java11-arm64 { exit 1 fi - for region in "${REGIONS[@]}"; do - publish_layer $JAVA11_DIST_ARM64 $region java11 arm64 - done + run_region_loop "$JAVA11_DIST_ARM64" java11 arm64 } function publish-java11-x86 { @@ -106,9 +100,7 @@ function publish-java11-x86 { exit 1 fi - for region in "${REGIONS[@]}"; do - publish_layer $JAVA11_DIST_X86_64 $region java11 x86_64 - done + run_region_loop "$JAVA11_DIST_X86_64" java11 x86_64 } function build-java17-arm64 { @@ -125,9 +117,7 @@ function publish-java17-arm64 { exit 1 fi - for region in "${REGIONS[@]}"; do - publish_layer $JAVA17_DIST_ARM64 $region java17 arm64 - done + run_region_loop "$JAVA17_DIST_ARM64" java17 arm64 } function publish-java17-x86 { @@ -136,9 +126,7 @@ function publish-java17-x86 { exit 1 fi - for region in "${REGIONS[@]}"; do - publish_layer $JAVA17_DIST_X86_64 $region java17 x86_64 - done + run_region_loop "$JAVA17_DIST_X86_64" java17 x86_64 } function build-java21-arm64 { @@ -155,9 +143,7 @@ function publish-java21-arm64 { exit 1 fi - for region in "${REGIONS[@]}"; do - publish_layer $JAVA21_DIST_ARM64 $region java21 arm64 - done + run_region_loop "$JAVA21_DIST_ARM64" java21 arm64 } function publish-java21-x86 { @@ -166,9 +152,7 @@ function publish-java21-x86 { exit 1 fi - for region in "${REGIONS[@]}"; do - publish_layer $JAVA21_DIST_X86_64 $region java21 x86_64 - done + run_region_loop "$JAVA21_DIST_X86_64" java21 x86_64 } case "$1" in @@ -206,27 +190,31 @@ case "$1" in ;; "build-publish-java8al2-ecr-image") build-java8al2-arm64 - publish_docker_ecr $JAVA8_DIST_ARM64 java8 arm64 + publish_ecr_safe $JAVA8_DIST_ARM64 java8 arm64 build-java8al2-x86 - publish_docker_ecr $JAVA8_DIST_X86_64 java8 x86_64 + publish_ecr_safe $JAVA8_DIST_X86_64 java8 x86_64 + finalize_ecr_results "java8al2" ;; "build-publish-java11-ecr-image") build-java11-arm64 - publish_docker_ecr $JAVA11_DIST_ARM64 java11 arm64 + publish_ecr_safe $JAVA11_DIST_ARM64 java11 arm64 build-java11-x86 - publish_docker_ecr $JAVA11_DIST_X86_64 java11 x86_64 + publish_ecr_safe $JAVA11_DIST_X86_64 java11 x86_64 + finalize_ecr_results "java11" ;; "build-publish-java17-ecr-image") build-java17-arm64 - publish_docker_ecr $JAVA17_DIST_ARM64 java17 arm64 + publish_ecr_safe $JAVA17_DIST_ARM64 java17 arm64 build-java17-x86 - publish_docker_ecr $JAVA17_DIST_X86_64 java17 x86_64 + publish_ecr_safe $JAVA17_DIST_X86_64 java17 x86_64 + finalize_ecr_results "java17" ;; "build-publish-java21-ecr-image") build-java21-arm64 - publish_docker_ecr $JAVA21_DIST_ARM64 java21 arm64 + publish_ecr_safe $JAVA21_DIST_ARM64 java21 arm64 build-java21-x86 - publish_docker_ecr $JAVA21_DIST_X86_64 java21 x86_64 + publish_ecr_safe $JAVA21_DIST_X86_64 java21 x86_64 + finalize_ecr_results "java21" ;; "java8al2") $0 build-java8al2 @@ -244,6 +232,20 @@ case "$1" in $0 build-java21 $0 publish-java21 ;; +"publish-staging-java21") + build-java21-arm64 + build-java21-x86 + arn_arm64=$(publish_staging_layer "$JAVA21_DIST_ARM64" java21 arm64) + echo "arn_arm64=${arn_arm64}" >> "${GITHUB_OUTPUT:-/dev/stderr}" + arn_x86=$(publish_staging_layer "$JAVA21_DIST_X86_64" java21 x86_64) + echo "arn_x86=${arn_x86}" >> "${GITHUB_OUTPUT:-/dev/stderr}" + ;; +"cleanup-staging-java21") + for arn in "${ARN_X86:-}" "${ARN_ARM64:-}"; do + [[ -z "$arn" ]] && continue + delete_staging_layer "$(echo "$arn" | cut -d: -f8)" "$(echo "$arn" | cut -d: -f9)" + done + ;; *) usage ;; diff --git a/libBuild.sh b/libBuild.sh index 85c85821..d9fee8af 100644 --- a/libBuild.sh +++ b/libBuild.sh @@ -31,6 +31,21 @@ REGIONS=( us-west-2 ) +# Override the default region list for testing: +# LAYER_REGIONS="us-east-1,us-west-2" ./publish-layers.sh ... +if [[ -n "${LAYER_REGIONS:-}" ]]; then + IFS=',' read -ra REGIONS <<< "${LAYER_REGIONS//[[:space:]]/}" + echo "=== LAYER_REGIONS override: publishing to ${#REGIONS[@]} region(s): ${REGIONS[*]} ===" +fi + +# S3 bucket name prefix — bucket per region is "-". +# Override for testing: S3_BUCKET_PREFIX="my-test-bucket-prefix" +S3_BUCKET_PREFIX="${S3_BUCKET_PREFIX:-nr-layers}" + +# Public ECR repository alias — override for testing: +# ECR_REPOSITORY="q6k3q1g1" ./publish-layers.sh build-publish-20-ecr-image +ECR_REPOSITORY="${ECR_REPOSITORY:-x6n7b2o2}" + EXTENSION_DIST_DIR=extensions EXTENSION_DIST_ZIP=extension.zip EXTENSION_DIST_PREVIEW_FILE=preview-extensions-ggqizro707 @@ -250,7 +265,7 @@ function publish_public_layer { --compatible-runtimes ${compat_list[*]} \ --region "$region" \ --output text \ - --query Version) + --query Version) || return 1 echo "Published ${runtime_name} layer version ${layer_version} to ${region}" echo "Setting public permissions for ${runtime_name} layer version ${layer_version} in ${region}" @@ -278,7 +293,7 @@ function publish_layer { hash=$( hash_file $layer_archive | awk '{ print $1 }' ) - bucket_name="nr-layers-${region}" + bucket_name="${S3_BUCKET_PREFIX}-${region}" s3_key="$( s3_prefix $runtime_name )/${hash}.${arch}.zip" compat_list=( $runtime_name ) @@ -337,6 +352,57 @@ function publish_layer { } +# Staging region/bucket — overridable via environment for local testing. +STAGING_REGION="${STAGING_REGION:-us-east-1}" + +# Publish a single layer zip to the staging account as -staging[-slim]. +# Prints the full layer ARN to stdout; all progress messages go to stderr. +# Uses direct zip upload (no S3 hop) so no bucket policy changes are needed. +function publish_staging_layer { + layer_archive=$1 + runtime_name=$2 + arch=$3 + newrelic_agent_version=${4:-"none"} + slim=${5:-""} + + layer_name=$( layer_name_str $runtime_name $arch ) + staging_layer_name="${layer_name}-staging" + if [[ $slim == "slim" ]]; then + staging_layer_name="${layer_name}-slim-staging" + fi + + compat_list=( $runtime_name ) + if [[ $runtime_name == "provided" ]]; then compat_list=("provided" "provided.al2" "provided.al2023" "dotnetcore3.1"); fi + if [[ $runtime_name == "dotnet" ]]; then compat_list=("dotnet6" "dotnet8" "dotnet10"); fi + if [[ $runtime_name == "python" ]]; then compat_list=("python3.9" "python3.10" "python3.11" "python3.12" "python3.13" "python3.14"); fi + if [[ $runtime_name == "nodejs" ]]; then compat_list=("nodejs20.x" "nodejs22.x" "nodejs24.x"); fi + + echo "Publishing staging layer ${staging_layer_name} (${arch}) to ${STAGING_REGION}" >&2 + layer_version=$(aws lambda publish-layer-version \ + --layer-name "${staging_layer_name}" \ + --region "${STAGING_REGION}" \ + --zip-file "fileb://${layer_archive}" \ + --compatible-architectures "${arch}" \ + --compatible-runtimes "${compat_list[@]}" \ + --query 'Version' \ + --output text) + + echo "Staged ${staging_layer_name}:${layer_version}" >&2 + account_id=$(aws sts get-caller-identity --query Account --output text) + echo "arn:aws:lambda:${STAGING_REGION}:${account_id}:layer:${staging_layer_name}:${layer_version}" +} + +# Delete a staging layer version (best-effort; never fails the caller). +function delete_staging_layer { + layer_name=$1 + version=$2 + echo "Deleting staging layer ${layer_name}:${version} in ${STAGING_REGION}" + aws lambda delete-layer-version \ + --layer-name "${layer_name}" \ + --version-number "${version}" \ + --region "${STAGING_REGION}" 2>/dev/null || true +} + function publish_docker_ecr { layer_archive=$1 @@ -385,9 +451,8 @@ function publish_docker_ecr { echo "File does not start with 'dist/': $file_without_dist" fi - # public ecr repository name - # maintainer can use this("q6k3q1g1") repo name for testing - repository="x6n7b2o2" + # Public ECR repository alias — set ECR_REPOSITORY env var to override + repository="${ECR_REPOSITORY}" # copy dockerfile cp ../Dockerfile.ecrImage . @@ -452,3 +517,136 @@ function publish_docker_hub { echo "docker push newrelic/newrelic-lambda-layers:${language_flag}-${version_flag}${arch_flag}" docker push newrelic/newrelic-lambda-layers:${language_flag}-${version_flag}${arch_flag} } + +# Calls publish_layer for one region; prints [OK]/[FAIL] and returns 0/1. +# Args: [version] [slim] +publish_layer_safe() { + local region="${2}" + if publish_layer "$@"; then + echo " [OK] ${region}" + return 0 + else + echo " [FAIL] ${region} — skipped (see AWS error above)" + return 1 + fi +} + +# Iterates REGIONS[@], calling publish_layer_safe per region. +# Writes a table to $GITHUB_STEP_SUMMARY and failure_summary to $GITHUB_OUTPUT. +# Exits 1 (after all regions are attempted) if any region failed. +# Usage: run_region_loop [version] [slim] +run_region_loop() { + local zip=$1 + shift + local extra_args=("$@") + local -a failed=() passed=() + + # If PUBLISH_REGIONS is set, restrict to that comma-separated list of regions. + # Used by workflow_dispatch re-runs to retry only specific failed regions. + local -a target_regions=("${REGIONS[@]}") + if [[ -n "${PUBLISH_REGIONS:-}" ]]; then + local -a filter + IFS=',' read -ra filter <<< "${PUBLISH_REGIONS//[[:space:]]/}" + target_regions=() + for r in "${REGIONS[@]}"; do + for f in "${filter[@]}"; do + [[ "$r" == "$f" ]] && { target_regions+=("$r"); break; } + done + done + echo "=== Region filter active: targeting ${#target_regions[@]}/${#REGIONS[@]} region(s): ${target_regions[*]:-none} ===" + fi + + for region in "${target_regions[@]}"; do + if publish_layer_safe "$zip" "$region" "${extra_args[@]}"; then + passed+=("$region") + else + failed+=("$region") + fi + done + + local total=$(( ${#passed[@]} + ${#failed[@]} )) + local label="${extra_args[0]:-layer} ${extra_args[1]:-}" + + if [[ -n "${GITHUB_STEP_SUMMARY:-}" ]]; then + { + printf "### Region Publish: %s\n" "$label" + printf "| Region | Status |\n|--------|--------|\n" + for r in "${passed[@]}"; do printf "| \`%s\` | ✅ passed |\n" "$r"; done + for r in "${failed[@]}"; do printf "| \`%s\` | ❌ FAILED |\n" "$r"; done + } >> "$GITHUB_STEP_SUMMARY" 2>/dev/null || true + fi + + if [[ ${#failed[@]} -gt 0 ]]; then + local summary="${#failed[@]}/${total} regions failed: ${failed[*]}" + # Non-Docker steps: write directly to GITHUB_OUTPUT + if [[ -n "${GITHUB_OUTPUT:-}" ]]; then + echo "failure_summary=${summary}" >> "$GITHUB_OUTPUT" 2>/dev/null || true + fi + # Docker steps: write to FAILED_REGIONS_FILE; host runner relays to GITHUB_OUTPUT + if [[ -n "${FAILED_REGIONS_FILE:-}" ]]; then + echo "failure_summary=${summary}" >> "${FAILED_REGIONS_FILE}" 2>/dev/null || true + fi + fi + + echo "" + echo "=== Region Publish Summary: ${label} ===" + echo " Passed (${#passed[@]}/${total}): ${passed[*]:-none}" + echo " Failed (${#failed[@]}/${total}): ${failed[*]:-none}" + + if [[ ${#failed[@]} -gt 0 ]]; then + echo "ERROR: ${#failed[@]}/${total} region(s) failed" + return 1 + fi +} + +# Global accumulators for ECR publish results within one script invocation. +declare -ga _ECR_PASSED=() +declare -ga _ECR_FAILED=() + +# Calls publish_docker_ecr; accumulates pass/fail into _ECR_PASSED/_ECR_FAILED. +# Never propagates failure — call finalize_ecr_results at the end instead. +# Args: same as publish_docker_ecr ( [slim]) +publish_ecr_safe() { + local label="${2}-${3}${4:+-$4}" + if publish_docker_ecr "$@"; then + echo " [ECR OK] ${label}" + _ECR_PASSED+=("$label") + else + echo " [ECR FAIL] ${label} — see Docker/AWS error above" + _ECR_FAILED+=("$label") + fi + return 0 +} + +# Writes ECR summary table to $GITHUB_STEP_SUMMARY and ecr_failure_summary to +# $GITHUB_OUTPUT, then returns 1 if any images failed. +# Call once after all publish_ecr_safe calls for a given publish block. +# Args: [label] — optional human label for the summary heading +finalize_ecr_results() { + local label="${1:-ECR}" + local total=$(( ${#_ECR_PASSED[@]} + ${#_ECR_FAILED[@]} )) + + if [[ -n "${GITHUB_STEP_SUMMARY:-}" ]]; then + { + printf "### ECR Image Publish: %s\n" "$label" + printf "| Image | Status |\n|-------|--------|\n" + for i in "${_ECR_PASSED[@]}"; do printf "| \`%s\` | ✅ passed |\n" "$i"; done + for i in "${_ECR_FAILED[@]}"; do printf "| \`%s\` | ❌ FAILED |\n" "$i"; done + } >> "$GITHUB_STEP_SUMMARY" 2>/dev/null || true + fi + + echo "" + echo "=== ECR Publish Summary: ${label} ===" + echo " Passed (${#_ECR_PASSED[@]}/${total}): ${_ECR_PASSED[*]:-none}" + echo " Failed (${#_ECR_FAILED[@]}/${total}): ${_ECR_FAILED[*]:-none}" + + if [[ ${#_ECR_FAILED[@]} -gt 0 ]]; then + if [[ -n "${GITHUB_OUTPUT:-}" ]]; then + echo "ecr_failure_summary=${#_ECR_FAILED[@]}/${total} ECR images failed: ${_ECR_FAILED[*]}" \ + >> "$GITHUB_OUTPUT" 2>/dev/null || true + fi + echo "ERROR: ${#_ECR_FAILED[@]}/${total} ECR image(s) failed" + return 1 + fi + return 0 +} diff --git a/nodejs/publish-layers.sh b/nodejs/publish-layers.sh index e60ca244..a5d4ccef 100755 --- a/nodejs/publish-layers.sh +++ b/nodejs/publish-layers.sh @@ -3,7 +3,7 @@ set -Eeuo pipefail BUILD_DIR=nodejs -DIST_DIR=dist +DIST_DIR=${DIST_DIR:-dist} source ../libBuild.sh @@ -64,9 +64,7 @@ function publish_universal_wrapper { exit 1 fi - for region in "${REGIONS[@]}"; do - publish_layer $ZIP $region nodejs ${arch} $NEWRELIC_AGENT_VERSION $slim - done + run_region_loop "$ZIP" nodejs "${arch}" "$NEWRELIC_AGENT_VERSION" "$slim" } function build_wrapper { @@ -117,8 +115,42 @@ function publish_wrapper { exit 1 fi - for region in "${REGIONS[@]}"; do - publish_layer $ZIP $region nodejs${node_version}.x ${arch} $NEWRELIC_AGENT_VERSION $slim + run_region_loop "$ZIP" "nodejs${node_version}.x" "${arch}" "$NEWRELIC_AGENT_VERSION" "$slim" +} + +# Publish staging layers for a given node version (us-east-1, -staging suffix). +# Writes arn_x86/arn_arm64/arn_x86_slim/arn_arm64_slim to $GITHUB_OUTPUT. +function publish_staging_wrapper { + node_version=$1 + source $DIST_DIR/nr-env + + for arch in x86_64 arm64; do + ZIP=$DIST_DIR/nodejs${node_version}x.${arch}.zip + ZIP_SLIM=$DIST_DIR/nodejs${node_version}x.${arch}.slim.zip + if [ ! -f "$ZIP" ]; then echo "Package not found: ${ZIP}"; exit 1; fi + if [ ! -f "$ZIP_SLIM" ]; then echo "Package not found: ${ZIP_SLIM}"; exit 1; fi + + arn=$(publish_staging_layer "$ZIP" nodejs${node_version}.x "$arch" "$NEWRELIC_AGENT_VERSION") + arn_slim=$(publish_staging_layer "$ZIP_SLIM" nodejs${node_version}.x "$arch" "$NEWRELIC_AGENT_VERSION" slim) + + if [[ $arch == "x86_64" ]]; then + echo "arn_x86=${arn}" >> "${GITHUB_OUTPUT:-/dev/stderr}" + echo "arn_x86_slim=${arn_slim}" >> "${GITHUB_OUTPUT:-/dev/stderr}" + else + echo "arn_arm64=${arn}" >> "${GITHUB_OUTPUT:-/dev/stderr}" + echo "arn_arm64_slim=${arn_slim}" >> "${GITHUB_OUTPUT:-/dev/stderr}" + fi + done +} + +# Delete the 4 staging layer versions published by publish_staging_wrapper. +# Reads ARNs from env vars: ARN_X86, ARN_ARM64, ARN_X86_SLIM, ARN_ARM64_SLIM. +function cleanup_staging_wrapper { + for arn in "${ARN_X86:-}" "${ARN_ARM64:-}" "${ARN_X86_SLIM:-}" "${ARN_ARM64_SLIM:-}"; do + [[ -z "$arn" ]] && continue + layer_name=$(echo "$arn" | cut -d: -f8) + version=$(echo "$arn" | cut -d: -f9) + delete_staging_layer "$layer_name" "$version" done } @@ -137,13 +169,14 @@ case "$1" in ;; "build-publish-universal-ecr-image") build_universal_wrapper arm64 - publish_docker_ecr $DIST_DIR/nodejs.arm64.zip nodejs arm64 + publish_ecr_safe $DIST_DIR/nodejs.arm64.zip nodejs arm64 build_universal_wrapper arm64 slim - publish_docker_ecr $DIST_DIR/nodejs.arm64.slim.zip nodejs arm64 slim + publish_ecr_safe $DIST_DIR/nodejs.arm64.slim.zip nodejs arm64 slim build_universal_wrapper x86_64 - publish_docker_ecr $DIST_DIR/nodejs.x86_64.zip nodejs x86_64 + publish_ecr_safe $DIST_DIR/nodejs.x86_64.zip nodejs x86_64 build_universal_wrapper x86_64 slim - publish_docker_ecr $DIST_DIR/nodejs.x86_64.slim.zip nodejs x86_64 slim + publish_ecr_safe $DIST_DIR/nodejs.x86_64.slim.zip nodejs x86_64 slim + finalize_ecr_results "nodejs-universal" ;; "nodejs") $0 build-universal @@ -192,34 +225,37 @@ case "$1" in publish_wrapper 24 x86_64 slim ;; "build-publish-20-ecr-image") - build_wrapper 20 arm64 - publish_docker_ecr $DIST_DIR/nodejs20x.arm64.zip nodejs20.x arm64 + build_wrapper 20 arm64 + publish_ecr_safe $DIST_DIR/nodejs20x.arm64.zip nodejs20.x arm64 build_wrapper 20 arm64 slim - publish_docker_ecr $DIST_DIR/nodejs20x.arm64.slim.zip nodejs20.x arm64 slim - build_wrapper 20 x86_64 - publish_docker_ecr $DIST_DIR/nodejs20x.x86_64.zip nodejs20.x x86_64 + publish_ecr_safe $DIST_DIR/nodejs20x.arm64.slim.zip nodejs20.x arm64 slim + build_wrapper 20 x86_64 + publish_ecr_safe $DIST_DIR/nodejs20x.x86_64.zip nodejs20.x x86_64 build_wrapper 20 x86_64 slim - publish_docker_ecr $DIST_DIR/nodejs20x.x86_64.slim.zip nodejs20.x x86_64 slim + publish_ecr_safe $DIST_DIR/nodejs20x.x86_64.slim.zip nodejs20.x x86_64 slim + finalize_ecr_results "nodejs20.x" ;; "build-publish-22-ecr-image") - build_wrapper 22 arm64 - publish_docker_ecr $DIST_DIR/nodejs22x.arm64.zip nodejs22.x arm64 + build_wrapper 22 arm64 + publish_ecr_safe $DIST_DIR/nodejs22x.arm64.zip nodejs22.x arm64 build_wrapper 22 arm64 slim - publish_docker_ecr $DIST_DIR/nodejs22x.arm64.slim.zip nodejs22.x arm64 - build_wrapper 22 x86_64 - publish_docker_ecr $DIST_DIR/nodejs22x.x86_64.zip nodejs22.x x86_64 + publish_ecr_safe $DIST_DIR/nodejs22x.arm64.slim.zip nodejs22.x arm64 slim + build_wrapper 22 x86_64 + publish_ecr_safe $DIST_DIR/nodejs22x.x86_64.zip nodejs22.x x86_64 build_wrapper 22 x86_64 slim - publish_docker_ecr $DIST_DIR/nodejs22x.x86_64.slim.zip nodejs22.x x86_64 slim + publish_ecr_safe $DIST_DIR/nodejs22x.x86_64.slim.zip nodejs22.x x86_64 slim + finalize_ecr_results "nodejs22.x" ;; "build-publish-24-ecr-image") - build_wrapper 24 arm64 - publish_docker_ecr $DIST_DIR/nodejs24x.arm64.zip nodejs24.x arm64 + build_wrapper 24 arm64 + publish_ecr_safe $DIST_DIR/nodejs24x.arm64.zip nodejs24.x arm64 build_wrapper 24 arm64 slim - publish_docker_ecr $DIST_DIR/nodejs24x.arm64.slim.zip nodejs24.x arm64 - build_wrapper 24 x86_64 - publish_docker_ecr $DIST_DIR/nodejs24x.x86_64.zip nodejs24.x x86_64 + publish_ecr_safe $DIST_DIR/nodejs24x.arm64.slim.zip nodejs24.x arm64 slim + build_wrapper 24 x86_64 + publish_ecr_safe $DIST_DIR/nodejs24x.x86_64.zip nodejs24.x x86_64 build_wrapper 24 x86_64 slim - publish_docker_ecr $DIST_DIR/nodejs24x.x86_64.slim.zip nodejs24.x x86_64 slim + publish_ecr_safe $DIST_DIR/nodejs24x.x86_64.slim.zip nodejs24.x x86_64 slim + finalize_ecr_results "nodejs24.x" ;; "nodejs20") $0 build-20 @@ -233,6 +269,24 @@ case "$1" in $0 build-24 $0 publish-24 ;; +"publish-staging-20") + publish_staging_wrapper 20 + ;; +"publish-staging-22") + publish_staging_wrapper 22 + ;; +"publish-staging-24") + publish_staging_wrapper 24 + ;; +"cleanup-staging-20") + cleanup_staging_wrapper + ;; +"cleanup-staging-22") + cleanup_staging_wrapper + ;; +"cleanup-staging-24") + cleanup_staging_wrapper + ;; *) usage ;; diff --git a/python/publish-layers.sh b/python/publish-layers.sh index 8cfd1010..62f567bd 100755 --- a/python/publish-layers.sh +++ b/python/publish-layers.sh @@ -3,7 +3,7 @@ set -Eeuo pipefail BUILD_DIR=python -DIST_DIR=dist +DIST_DIR=${DIST_DIR:-dist} NEWRELIC_AGENT_VERSION="" VERSION_REGEX="v?([0-9]+\.[0-9]+\.[0-9]+)\.[0-9]+_python" PY39_DIST_ARM64=$DIST_DIR/python39.arm64.zip @@ -78,10 +78,7 @@ function publish_python_layer { REGIONS=("${REGIONS[@]}"); fi - for region in "${REGIONS[@]}"; do - echo "Publishing layer for python${python_version} (${arch}) to region ${region}" - publish_layer ${ZIP} $region python${python_version} ${arch} $NEWRELIC_AGENT_VERSION - done + run_region_loop "$ZIP" "python${python_version}" "${arch}" "$NEWRELIC_AGENT_VERSION" } @@ -127,10 +124,7 @@ function publish_universal_python_layer { exit 1 fi - for region in "${REGIONS[@]}"; do - echo "Publishing universal Python layer (${arch}) to region ${region}" - publish_layer ${ZIP} $region python ${arch} $NEWRELIC_AGENT_VERSION - done + run_region_loop "$ZIP" python "${arch}" "$NEWRELIC_AGENT_VERSION" } @@ -150,60 +144,95 @@ case "$1" in publish_docker_ecr $PY_DIST_X86_64 python x86_64 ;; "python") + layer_rc=0 build_universal_python_layer arm64 - publish_universal_python_layer arm64 - publish_docker_ecr $PY_DIST_ARM64 python arm64 + publish_universal_python_layer arm64 || layer_rc=$? + publish_ecr_safe $PY_DIST_ARM64 python arm64 build_universal_python_layer x86_64 - publish_universal_python_layer x86_64 - publish_docker_ecr $PY_DIST_X86_64 python x86_64 + publish_universal_python_layer x86_64 || layer_rc=$? + publish_ecr_safe $PY_DIST_X86_64 python x86_64 + finalize_ecr_results "python-universal" + [[ $layer_rc -eq 0 ]] || exit $layer_rc ;; "python3.9") + layer_rc=0 build_python_layer 3.9 arm64 - publish_python_layer 3.9 arm64 - publish_docker_ecr $PY39_DIST_ARM64 python3.9 arm64 + publish_python_layer 3.9 arm64 || layer_rc=$? + publish_ecr_safe $PY39_DIST_ARM64 python3.9 arm64 build_python_layer 3.9 x86_64 - publish_python_layer 3.9 x86_64 - publish_docker_ecr $PY39_DIST_X86_64 python3.9 x86_64 + publish_python_layer 3.9 x86_64 || layer_rc=$? + publish_ecr_safe $PY39_DIST_X86_64 python3.9 x86_64 + finalize_ecr_results "python3.9" + [[ $layer_rc -eq 0 ]] || exit $layer_rc ;; "python3.10") + layer_rc=0 build_python_layer 3.10 arm64 - publish_python_layer 3.10 arm64 - publish_docker_ecr $PY310_DIST_ARM64 python3.10 arm64 + publish_python_layer 3.10 arm64 || layer_rc=$? + publish_ecr_safe $PY310_DIST_ARM64 python3.10 arm64 build_python_layer 3.10 x86_64 - publish_python_layer 3.10 x86_64 - publish_docker_ecr $PY310_DIST_X86_64 python3.10 x86_64 + publish_python_layer 3.10 x86_64 || layer_rc=$? + publish_ecr_safe $PY310_DIST_X86_64 python3.10 x86_64 + finalize_ecr_results "python3.10" + [[ $layer_rc -eq 0 ]] || exit $layer_rc ;; "python3.11") + layer_rc=0 build_python_layer 3.11 arm64 - publish_python_layer 3.11 arm64 - publish_docker_ecr $PY311_DIST_ARM64 python3.11 arm64 + publish_python_layer 3.11 arm64 || layer_rc=$? + publish_ecr_safe $PY311_DIST_ARM64 python3.11 arm64 build_python_layer 3.11 x86_64 - publish_python_layer 3.11 x86_64 - publish_docker_ecr $PY311_DIST_X86_64 python3.11 x86_64 + publish_python_layer 3.11 x86_64 || layer_rc=$? + publish_ecr_safe $PY311_DIST_X86_64 python3.11 x86_64 + finalize_ecr_results "python3.11" + [[ $layer_rc -eq 0 ]] || exit $layer_rc ;; "python3.12") + layer_rc=0 build_python_layer 3.12 arm64 - publish_python_layer 3.12 arm64 - publish_docker_ecr $PY312_DIST_ARM64 python3.12 arm64 + publish_python_layer 3.12 arm64 || layer_rc=$? + publish_ecr_safe $PY312_DIST_ARM64 python3.12 arm64 build_python_layer 3.12 x86_64 - publish_python_layer 3.12 x86_64 - publish_docker_ecr $PY312_DIST_X86_64 python3.12 x86_64 + publish_python_layer 3.12 x86_64 || layer_rc=$? + publish_ecr_safe $PY312_DIST_X86_64 python3.12 x86_64 + finalize_ecr_results "python3.12" + [[ $layer_rc -eq 0 ]] || exit $layer_rc ;; "python3.13") + layer_rc=0 build_python_layer 3.13 arm64 - publish_python_layer 3.13 arm64 - publish_docker_ecr $PY313_DIST_ARM64 python3.13 arm64 + publish_python_layer 3.13 arm64 || layer_rc=$? + publish_ecr_safe $PY313_DIST_ARM64 python3.13 arm64 build_python_layer 3.13 x86_64 - publish_python_layer 3.13 x86_64 - publish_docker_ecr $PY313_DIST_X86_64 python3.13 x86_64 + publish_python_layer 3.13 x86_64 || layer_rc=$? + publish_ecr_safe $PY313_DIST_X86_64 python3.13 x86_64 + finalize_ecr_results "python3.13" + [[ $layer_rc -eq 0 ]] || exit $layer_rc ;; "python3.14") + layer_rc=0 build_python_layer 3.14 arm64 - publish_python_layer 3.14 arm64 - publish_docker_ecr $PY314_DIST_ARM64 python3.14 arm64 + publish_python_layer 3.14 arm64 || layer_rc=$? + publish_ecr_safe $PY314_DIST_ARM64 python3.14 arm64 build_python_layer 3.14 x86_64 - publish_python_layer 3.14 x86_64 - publish_docker_ecr $PY314_DIST_X86_64 python3.14 x86_64 + publish_python_layer 3.14 x86_64 || layer_rc=$? + publish_ecr_safe $PY314_DIST_X86_64 python3.14 x86_64 + finalize_ecr_results "python3.14" + [[ $layer_rc -eq 0 ]] || exit $layer_rc + ;; + "publish-staging-python3.14") + build_python_layer 3.14 arm64 + build_python_layer 3.14 x86_64 + arn_arm64=$(publish_staging_layer "$PY314_DIST_ARM64" python3.14 arm64 "$NEWRELIC_AGENT_VERSION") + echo "arn_arm64=${arn_arm64}" >> "${GITHUB_OUTPUT:-/dev/stderr}" + arn_x86=$(publish_staging_layer "$PY314_DIST_X86_64" python3.14 x86_64 "$NEWRELIC_AGENT_VERSION") + echo "arn_x86=${arn_x86}" >> "${GITHUB_OUTPUT:-/dev/stderr}" + ;; + "cleanup-staging-python3.14") + for arn in "${ARN_X86:-}" "${ARN_ARM64:-}"; do + [[ -z "$arn" ]] && continue + delete_staging_layer "$(echo "$arn" | cut -d: -f8)" "$(echo "$arn" | cut -d: -f9)" + done ;; *) usage diff --git a/ruby/publish-layers.sh b/ruby/publish-layers.sh index d55b0bb2..9df8e8ad 100755 --- a/ruby/publish-layers.sh +++ b/ruby/publish-layers.sh @@ -15,7 +15,7 @@ set -Eeuo pipefail # preview-extensions-* -- Extensions preview file RUBY_DIR=ruby -DIST_DIR=dist +DIST_DIR=${DIST_DIR:-dist} WRAPPER_FILE=newrelic_lambda_wrapper.rb NEWRELIC_AGENT_VERSION="" # Set this to a path to a clone of newrelic-lambda-extension to build @@ -175,38 +175,57 @@ function publish_ruby_for_arch { local arch=$2 local dist_file=$3 - for region in "${REGIONS[@]}"; do - echo "Publishing $dist_file for region=$region, ruby=$ruby_version, arch=$arch" - publish_layer $dist_file $region "ruby${ruby_version}" $arch $NEWRELIC_AGENT_VERSION - done - echo 'Publishing complete' + run_region_loop "$dist_file" "ruby${ruby_version}" "$arch" "$NEWRELIC_AGENT_VERSION" } set +u # permit $1 to be unbound so that '*' matches it when no args are present case "$1" in "ruby3.4") + layer_rc=0 build-ruby34-arm64 - publish-ruby34-arm64 - publish_docker_ecr $RB34_DIST_ARM64 ruby3.4 arm64 + publish-ruby34-arm64 || layer_rc=$? + publish_ecr_safe $RB34_DIST_ARM64 ruby3.4 arm64 build-ruby34-x86 - publish-ruby34-x86 - publish_docker_ecr $RB34_DIST_X86_64 ruby3.4 x86_64 + publish-ruby34-x86 || layer_rc=$? + publish_ecr_safe $RB34_DIST_X86_64 ruby3.4 x86_64 + finalize_ecr_results "ruby3.4" + [[ $layer_rc -eq 0 ]] || exit $layer_rc ;; "ruby3.3") + layer_rc=0 build-ruby33-arm64 - publish-ruby33-arm64 - publish_docker_ecr $RB33_DIST_ARM64 ruby3.3 arm64 + publish-ruby33-arm64 || layer_rc=$? + publish_ecr_safe $RB33_DIST_ARM64 ruby3.3 arm64 build-ruby33-x86 - publish-ruby33-x86 - publish_docker_ecr $RB33_DIST_X86_64 ruby3.3 x86_64 + publish-ruby33-x86 || layer_rc=$? + publish_ecr_safe $RB33_DIST_X86_64 ruby3.3 x86_64 + finalize_ecr_results "ruby3.3" + [[ $layer_rc -eq 0 ]] || exit $layer_rc ;; "ruby3.2") + layer_rc=0 build-ruby32-arm64 - publish-ruby32-arm64 - publish_docker_ecr $RB32_DIST_ARM64 ruby3.2 arm64 + publish-ruby32-arm64 || layer_rc=$? + publish_ecr_safe $RB32_DIST_ARM64 ruby3.2 arm64 build-ruby32-x86 - publish-ruby32-x86 - publish_docker_ecr $RB32_DIST_X86_64 ruby3.2 x86_64 + publish-ruby32-x86 || layer_rc=$? + publish_ecr_safe $RB32_DIST_X86_64 ruby3.2 x86_64 + finalize_ecr_results "ruby3.2" + [[ $layer_rc -eq 0 ]] || exit $layer_rc + ;; + "publish-staging-ruby3.4") + build-ruby34-arm64 + build-ruby34-x86 + arn_arm64=$(publish_staging_layer "$RB34_DIST_ARM64" ruby3.4 arm64 "$NEWRELIC_AGENT_VERSION") + echo "arn_arm64=${arn_arm64}" >> "${GITHUB_OUTPUT:-/dev/stderr}" + arn_x86=$(publish_staging_layer "$RB34_DIST_X86_64" ruby3.4 x86_64 "$NEWRELIC_AGENT_VERSION") + echo "arn_x86=${arn_x86}" >> "${GITHUB_OUTPUT:-/dev/stderr}" + ;; + "cleanup-staging-ruby3.4") + for arn in "${ARN_X86:-}" "${ARN_ARM64:-}"; do + [[ -z "$arn" ]] && continue + delete_staging_layer "$(echo "$arn" | cut -d: -f8)" "$(echo "$arn" | cut -d: -f9)" + done ;; *) usage