feat(layer): publish Lambda Layer to GitHub Releases and ~17 commercial regions - #15
Merged
Conversation
Adds a new openspec change for publishing a Lambda Layer to GitHub Releases (a ZIP artifact) AND across all default-enabled commercial AWS regions (~17 today; opt-in regions added later by editing layer/regions.txt). New capability: lambda-layer-publishing Key design decisions captured in design.md: - One universal layer ZIP per release (pure Python zero-deps; works across all CompatibleRuntimes [py3.10..3.14] and Architectures [x86_64, arm64]). - All layer artifacts under a top-level layer/ directory: layer/README.md (consumer), layer/MAINTAINER.md (publisher), layer/iam-publisher.cfn.yaml (CFN for OIDC role), layer/regions.txt (single source of truth for region matrix). - OIDC-federated IAM role; trust policy scoped to repo:igorlg/cfn-handler:environment:layer-publisher. - Permissions least-privilege: lambda:*LayerVersion* on arn:aws:lambda:*:<account>:layer:cfn-handler*; ssm:*Parameter* on /cfn-handler/*. - Per-region SSM parameter for ARN discovery: /cfn-handler/<region>/layer-arn/latest and .../v<version>. - Public read access via lambda:AddLayerVersionPermission with Principal='*'. - fail-fast: false on the publish matrix so a single bad region doesnt block the rest. Tasks.md walks through the full implementation including the maintainer one-time setup steps that gate the PR merge (deploy CFN, create GitHub environment, save secret). Validated --strict; ready for /opsx-apply.
…lds badge
Two enhancements to the openspec change before applying, per Igor's feedback:
1. ARN inventory — three public discovery surfaces:
- GitHub Release body augmentation: aggregate-arns job appends a
per-region ARN markdown table to the release notes.
- layer-arns.json release asset: structured JSON manifest
(version, layer_name, regions) uploaded to the release.
Fetchable at predictable URL via gh release / curl.
- shields.io badge: GitHub-native release endpoint renders current
Layer version inline with PyPI / Python / License badges in
README; AWS-orange (#ff9900); links to latest release page.
2. SSM parameters reframed as maintainer-operational, NOT user-
facing. Users in other AWS accounts cannot read SSM in the
maintainer's account without cross-account sharing infra (out of
scope). The public surfaces above cover the user need.
Workflow change: new aggregate-arns job after publish-layer matrix.
Per-region jobs upload an arn-<region>.json artifact; aggregate
downloads all, builds the consolidated JSON + markdown table,
uploads asset, edits release body. Runs if: always() so partial-
region success still produces an inventory.
design.md grows D10 (ARN discovery surfaces) and refines D4 (SSM
operational role). spec.md adds requirement 'Public ARN discovery
via GitHub Release surfaces' with 5 scenarios. tasks.md expands
section 2 with new aggregate task (2.8) and the per-region upload
artifact task (2.6); section 8 verification adds public-surface
checks (8.5).
Validated --strict; ready for /opsx-apply.
…al regions
Implements the publish-lambda-layer openspec change. New top-level
layer/ directory holds the publishing infrastructure:
- layer/regions.txt — canonical region list (17 commercial regions
enabled by default in any AWS account; opt-in regions added later
by editing the file).
- layer/iam-publisher.cfn.yaml — CloudFormation template creating the
OIDC-federated IAM role assumed by GitHub Actions during release.yml.
Trust scoped to repo + 'layer-publisher' environment; permissions
scoped to cfn-handler* layers and /cfn-handler/* SSM parameters.
- layer/MAINTAINER.md — operational guide for the AWS-side setup
(deploy CFN, create GitHub environment, save secret, add regions,
troubleshoot).
- layer/README.md — consumer-facing guide (ARN format, three discovery
surfaces, SAM/CDK snippets, deploy-it-yourself path).
release.yml gains four new jobs, all gated on
release_created == 'true':
- build-layer-zip: builds the wheel and repackages it into a Lambda-
layer-compatible ZIP (top-level python/cfn_handler/, no dist-info).
Uploads to GH Release.
- set-layer-matrix: reads layer/regions.txt and emits a JSON array
matrix output for the publish job.
- publish-layer: matrix over regions; uses environment 'layer-publisher'
with secrets.LAYER_PUBLISHER_ROLE_ARN; assume role via OIDC; publish
layer version; grant public read; write SSM parameters
(/cfn-handler/<region>/layer-arn/{latest, <tag>}); upload per-region
ARN artifact for aggregate. fail-fast: false; max-parallel: 10.
- aggregate-arns: if: always(); downloads per-region artifacts; builds
layer-arns.json (uploaded as a release asset); appends a per-region
ARN markdown table to the GitHub Release notes idempotently.
User-facing ARN discovery (no AWS credentials needed):
1. GitHub Release body markdown table (browser).
2. layer-arns.json release asset (programmatic;
curl https://github.com/igorlg/cfn-handler/releases/latest/download/layer-arns.json).
3. shields.io badge in README using GitHub-native release endpoint
(renders current Layer version inline with PyPI / Python / License).
SSM parameters live in the maintainer's account; they're an
operational record, NOT user-facing (cross-account SSM access requires
extra setup we're not doing).
docs/CI.md: workflow inventory updated; release-pipeline section
expanded with the layer publishing diagram + the three discovery
surfaces; cross-links to layer/README.md and layer/MAINTAINER.md.
README.md: new Lambda Layer badge in the badge row; new 'Or use the
AWS Lambda Layer' subsection under Installation.
Verified locally:
- just ci-check: 103 passed, 99.48% coverage.
- uv run cfn-lint layer/iam-publisher.cfn.yaml: clean.
- Built the layer ZIP via the same steps the workflow uses; confirmed
ZIP contents match the wheel (sans dist-info) and lay out under
python/cfn_handler/.
- actionlint .github/workflows/release.yml: clean (one info-level
shellcheck false positive on literal markdown backticks; harmless).
- openspec validate publish-lambda-layer --strict: valid.
Maintainer setup BEFORE merging this PR (per layer/MAINTAINER.md):
1. Deploy layer/iam-publisher.cfn.yaml in the AWS account.
2. Create the 'layer-publisher' GitHub environment in
igorlg/cfn-handler (no protection rules).
3. Save the role ARN as the LAYER_PUBLISHER_ROLE_ARN secret of that
environment.
The new jobs only run after release-please's PR merges (release_created
true), so this PR's CI does not require the AWS setup to pass.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 99bacf4ef6
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
When walking through the maintainer setup with a real account, two
ergonomics gaps surfaced:
1. The GitHub-environment + secret steps were UI-only; the gh CLI
does both ('gh api -X PUT /repos/.../environments/<name>' for the
environment, 'gh secret set --env <name>' for the secret). Both
are now documented as the recommended path with the UI as
fallback.
2. AWS commands assumed default credentials. Most maintainers use
named profiles ('--profile personal-opensource' etc.); added a
note + a snippet for checking the OIDC provider's prior
existence.
Status: 31/38 tasks complete. Remaining are post-merge work:
8.1-8.6: merge + first-release verification (waits on Igor merging
PR #15 and the subsequent release-please PR)
9.2: archive the change once it's shipped
Two design simplifications surfaced during PR review:
1. Hard-coded --compatible-runtimes was duplicating the supported-Python
list that already lives in pyproject.toml's classifiers. Add a new
set-layer-matrix.outputs.runtimes derivation step that reads the
classifiers via tomllib and emits 'python3.10 python3.11 ...'. The
publish-layer step consumes this via env. Single source of truth;
bumping a Python version edits one file.
2. SSM Parameter Store does not support cross-account public read, so
the per-region /cfn-handler/<region>/layer-arn/{latest,vX.Y.Z}
parameters were not actually a user-facing surface. The same ARN
info is already public via three other paths (release-body table,
layer-arns.json release asset, lambda:GetLayerVersion direct query).
Drop the SSM put-parameter calls from the workflow and the
WriteCfnHandlerSsmParameters policy from the IAM CFN template.
Maintainer follow-up (task 6.5): re-deploy the IAM CFN stack post-merge
to remove the now-unused SSM policy from the live role:
aws cloudformation deploy \
--stack-name cfn-handler-layer-publisher \
--template-file layer/iam-publisher.cfn.yaml \
--capabilities CAPABILITY_NAMED_IAM \
--region us-east-1
Verify: aws iam get-role-policy \
--role-name cfn-handler-layer-publisher \
--policy-name WriteCfnHandlerSsmParameters
returns NoSuchEntity.
OpenSpec change updated (proposal/design/spec/tasks); validates --strict.
All 103 tests pass; coverage 99.48%; ruff/cfn-lint clean.
… P1) The previous `|| echo "(PublicRead already granted; idempotent)"` swallowed every `add-layer-version-permission` failure, not just the intended duplicate-statement case. A ThrottlingException, AccessDenied, or any transient service error would let the job report success while leaving the published layer without its public read grant — silently breaking the main user-facing API (other accounts get AccessDenied when calling `get-layer-version` against the ARN). Replaced with explicit error inspection: capture stderr, check exit code, swallow only when the error message contains `ResourceConflictException`, fail loudly on any other error. Verified with three local bash test cases (success / conflict / throttle); the throttle case now exits 1 as required. Refs: PR #15 review by chatgpt-codex-connector[bot] (P1).
igorlg
added a commit
that referenced
this pull request
May 21, 2026
All 38 tasks complete:
* 1-7 (scaffolding, workflow, docs, local + cloud verification)
* 8.1-8.5 (merge, first release, post-release validation including
real Lambda invoke from cross-account principal — see PR #15
history; layer SHA-256 byte-identical us-east-1 == ap-southeast-2
== GH Release ZIP)
* 9.1 (openspec validate --strict pre-merge)
* 6.5 (CFN redeployed to drop unused SSM IAM policy from live role
cfn-handler-layer-publisher)
The new `lambda-layer-publishing` capability moves into the baseline
specs at openspec/specs/lambda-layer-publishing/spec.md (6 added
requirements). The change directory is preserved at
openspec/changes/archive/2026-05-21-publish-lambda-layer/ for
historical reference.
Also fills in the spec's Purpose section (the archive command leaves
a TBD placeholder by default).
igorlg
added a commit
that referenced
this pull request
May 22, 2026
Move `ReplayRequestType` and `ReplayStatus` into the
`if TYPE_CHECKING:` block. They were imported at module scope but only
referenced in annotations (resolved as strings via
`from __future__ import annotations`) and in a string-form
`cast("ReplayRequestType", ...)` call (per ruff's TC006 rule, which
prefers the string form to keep type-only symbols out of the runtime
import graph). CodeQL's py/unused-import sees the runtime import and
the string-only references, and reasonably concludes the import is
unused.
Cleaner resolution: keep the imports type-only, expand the explanatory
comment so future readers see why both static analyzers (CodeQL +
ruff TC006) end up happy with this shape.
The two CodeQL py/cyclic-import alerts on the surrounding
`if TYPE_CHECKING:` block remain false positives — CodeQL does not
model conditional imports, and the runtime cycle is broken by the
lazy import in `CustomResource.replay` (resource.py:354). Both have
been dismissed in the GitHub UI with rationale (alerts #15, #16).
igorlg
added a commit
that referenced
this pull request
May 22, 2026
Move `ReplayRequestType` and `ReplayStatus` into the
`if TYPE_CHECKING:` block. They were imported at module scope but only
referenced in annotations (resolved as strings via
`from __future__ import annotations`) and in a string-form
`cast("ReplayRequestType", ...)` call (per ruff's TC006 rule, which
prefers the string form to keep type-only symbols out of the runtime
import graph). CodeQL's py/unused-import sees the runtime import and
the string-only references, and reasonably concludes the import is
unused.
Cleaner resolution: keep the imports type-only, expand the explanatory
comment so future readers see why both static analyzers (CodeQL +
ruff TC006) end up happy with this shape.
The two CodeQL py/cyclic-import alerts on the surrounding
`if TYPE_CHECKING:` block remain false positives — CodeQL does not
model conditional imports, and the runtime cycle is broken by the
lazy import in `CustomResource.replay` (resource.py:354). Both have
been dismissed in the GitHub UI with rationale (alerts #15, #16).
igorlg
added a commit
that referenced
this pull request
May 22, 2026
Move `ReplayRequestType` and `ReplayStatus` into the
`if TYPE_CHECKING:` block. They were imported at module scope but only
referenced in annotations (resolved as strings via
`from __future__ import annotations`) and in a string-form
`cast("ReplayRequestType", ...)` call (per ruff's TC006 rule, which
prefers the string form to keep type-only symbols out of the runtime
import graph). CodeQL's py/unused-import sees the runtime import and
the string-only references, and reasonably concludes the import is
unused.
Cleaner resolution: keep the imports type-only, expand the explanatory
comment so future readers see why both static analyzers (CodeQL +
ruff TC006) end up happy with this shape.
The two CodeQL py/cyclic-import alerts on the surrounding
`if TYPE_CHECKING:` block remain false positives — CodeQL does not
model conditional imports, and the runtime cycle is broken by the
lazy import in `CustomResource.replay` (resource.py:354). Both have
been dismissed in the GitHub UI with rationale (alerts #15, #16).
igorlg
added a commit
that referenced
this pull request
May 22, 2026
…rs (#24) * chore(openspec): propose v1.3 testing helpers + roadmap Adds the OpenSpec change `add-testing-helpers` documenting: * proposal.md - rationale, scope (replay(), Replay, factories, assertions, pytest fixtures), explicit non-goals (no integration helpers, no fluent builders, no async support), and the soft-deprecation path for the existing `test_mode` flag. * design.md - architecture, the dispatch flow diagram, eight key decisions each with at least one rejected alternative, risks, and open questions reserved for the implementation phase. * specs/testing-helpers/spec.md - 9 ADDED requirements with ~25 scenarios covering the full public contract. * tasks.md - 13 phases / 43 verifiable tasks in TDD order. Adds `docs/ROADMAP.md` capturing the wider library-surface evolution discussion: testing helpers (this change), better logging, optional idempotency module, typed events for v2.0, and the cfn-lint plugin parallel-track work. The Decision Log section records explicit non-goals (CDK construct, SFN polling, CFN macros, async handlers) with rationale. This commit lands the plan only - no implementation. Subsequent commits in this PR implement it, ending with the test-suite migration. * feat(testing): add cfn_handler.testing module with replay() and helpers Introduces a public testing surface so users can unit-test custom-resource handlers without HTTP, without boto3, and without reaching into `cfn_handler._internal/`. Public API (`cfn_handler.testing`): * `Replay` - frozen dataclass capturing the dispatch outcome (status / data / reason / no_echo / payload / request_type / physical_resource_id). The `status` field is one of "SUCCESS" / "FAILED" / "DEFERRED"; the last is a replay-only sentinel signalling "would have entered polling". * `CustomResource.replay(event, context=None)` - drives the full dispatch pipeline in-process using internal seams to capture the response payload. No HTTP, no boto3 import. Polling is stubbed: a deferred replay mutates the event with marker keys and returns `Replay(status="DEFERRED")`; a follow-up `replay()` with the mutated event correctly resumes through the poll handler. * `make_event(...)` / `make_context(...)` - factories with safe defaults (RFC 6761 `example.invalid`, AWS-reserved 111111111111 account ID). `make_event` enforces `physical_resource_id` for Update/Delete events. * `assert_success` / `assert_failed` / `assert_deferred` - pytest-style assertion helpers with informative AssertionError messages on mismatch. * pytest fixtures `cfn_create_event`, `cfn_update_event`, `cfn_delete_event`, `cfn_lambda_context` - auto-discovered via a new `pytest11` entry point. No `pytest_plugins` declaration required in user conftest. Internal seams (private API, used by replay() but also available to power users via the constructor kwargs `transport=`, `provision_poller=`, `teardown_poller=`): * `Transport` - `Callable[[str, dict], None]` replacing the default urllib PUT (`send_response`). Late-bound default lookup so existing tests that `patch("cfn_handler.resource.send_response")` continue to work. * `PollerProvision` / `PollerTeardown` - mirror seams for the boto3-using polling provisioning/teardown calls. Late-bound for backwards compatibility with existing patches. * `CustomResource._replay_seams(...)` - context manager that swaps all three seams atomically and restores them on exit (including exceptional return). Used by the runner; not part of the public API contract. CI / tooling: * `pyproject.toml`: registers the `pytest11` entry point. * `justfile` + `.github/workflows/ci.yml`: switch `test-cov` from `pytest --cov` to `coverage run -m pytest`. The pytest11 entry point causes pytest to import `cfn_handler.testing.fixtures` (and transitively `cfn_handler`) during plugin collection, BEFORE the pytest-cov instrumentation hooks attach. Module-level code in `__init__.py` then runs uninstrumented and the report shows artificial 0% on those lines, dropping aggregate coverage to ~68%. `coverage run` ensures the tracer is active before any imports. Documented in both the recipe and the workflow step. Tests added: * `tests/unit/test_transport_seam.py` - the seam intercepts; default behaviour preserved. * `tests/unit/test_poller_seam.py` - poller stubs work; boto3 is never imported during a stubbed deferral. * `tests/unit/testing/test_replay.py` - SUCCESS/FAILED replays; `Replay` is frozen; no HTTP I/O; default context fallback. * `tests/unit/testing/test_replay_polling.py` - DEFERRED status, event mutation, two-step deferral->resume flow. * `tests/unit/testing/test_factories.py` - make_event / make_context with overrides + validation. * `tests/unit/testing/test_assertions.py` - all helpers, both pass and fail paths, message contents on failure. * `tests/integration/test_replay_parity.py` - same handler via `__call__` (moto + fake transport) vs `replay()` produce equivalent payloads. * `tests/integration/test_fixture_discovery.py` - pytest11 entry point auto-discovers fixtures in a fresh subprocess project, fixture invocations are independent. No breaking changes. The legacy `test_mode` flag continues to work. The deprecation of `test_mode` and `last_response` lands in the next commit. * feat(resource): deprecate test_mode and last_response in favour of replay() The legacy `test_mode=True` constructor flag and `last_response` capture attribute are superseded by the new `replay()` method shipped in the previous commit. They had known issues: * Mutable state on the resource (tests must reset `last_response` between assertions or risk false positives). * Sentinel string `__cfn_handler_polling__` for the polling-defer case lives on the public `last_response` surface with no type or documentation guarantees. * Polling re-invocation can't be tested: `test_mode` short-circuits `setup_polling` entirely, so the marker keys never get added to the event and a follow-up dispatch can't be simulated. This commit: * Emits a `DeprecationWarning` from `CustomResource.__init__` when `test_mode=True`, with a message pointing at `replay()` and the `cfn_handler.testing` module. `stacklevel=2` so the warning surfaces at the user's call site. * Updates the docstrings on `test_mode` (constructor parameter) and `last_response` (instance attribute) to mark them as deprecated and reference `replay()`. * Adds `tests/unit/test_test_mode_deprecation.py` covering the warning, that `test_mode=False` does NOT warn, and that the legacy behaviour still functions verbatim (so existing user code keeps working in v1.x). Removal is scheduled for v2.0 (separate change). The behaviour is unchanged in v1.3 - users see only the warning. The internal test suite migrates onto `replay()` in the next commit; until that lands the project's own pytest `filterwarnings` would promote the warning to an error in tests that still use `test_mode=True`. To avoid artificial failures during the migration window, that single warning is filtered to `ignore` in pyproject.toml's `[tool.pytest.ini_options] .filterwarnings` block - reverted in the migration commit. * test: migrate existing test suite from test_mode to replay() Bulk migration of the 100+ pre-existing references to `CustomResource(test_mode=True)` and `resource.last_response` over to the new `replay()` API. Verbatim translation: CustomResource(test_mode=True) → CustomResource() resource(event, ctx) → replay = resource.replay(event, ctx) resource.last_response["Status"] == "X" → replay.status == "X" resource.last_response["Data"] → replay.data resource.last_response["Reason"] → replay.reason resource.last_response["PhysicalResourceId"] → replay.physical_resource_id resource.last_response["NoEcho"] → replay.no_echo Where the assertion shape matches, the migration uses the helpers `assert_success(replay, data=...)` / `assert_failed(replay, reason_contains=...)` for clearer intent and informative error messages. Files migrated: * tests/unit/test_resource.py - decorator registration tests, lifecycle dispatch, exception handling, PhysicalResourceId semantics, init_failure, log_level acceptance. * tests/unit/test_backstops.py - the lone test_mode-specific test (`test_safe_teardown_skipped_in_test_mode`) was moved to `test_test_mode_deprecation.py` since it tests deprecated behaviour we keep working until v2.0. * tests/unit/test_polling_dispatch.py - the `__cfn_handler_polling__` sentinel test (also deprecated-only behaviour) was moved to test_test_mode_deprecation.py. * tests/unit/test_state_machine.py - hypothesis-driven invariant tests, all migrated to inspect `Replay` fields rather than `last_response`. After this commit `grep -rn 'test_mode\|last_response' tests/` returns hits only from `test_test_mode_deprecation.py` (intentional, testing the deprecated path) and a single docstring mention in `test_resource.py`. The temporary `filterwarnings` exception added in the previous commit is removed - any new test using `test_mode=True` would now fail the suite (as intended), forcing the test author to use the new API. Coverage stays at 98%. * docs(readme): add Testing section showing replay() workflow Adds a 'Testing your handlers' section between 'Examples' and 'Project status' that: * Shows a minimal one-screen example: build a CustomResource, register a handler, call `replay(make_event())`, assert via `assert_success`. * Lists the public testing surface (`replay`, `make_event`, `make_context`, the three assertion helpers, and the four auto-discovered pytest fixtures). * Calls out the polling-deferral semantics: first `replay()` returns `Replay(status='DEFERRED')` and mutates the event; a second `replay()` resumes through the poll handler. Useful for testing both halves of a polled lifecycle without provisioning EventBridge rules. The detailed contract lives in docstrings, the spec, and the roadmap doc; the README only carries enough context to nudge a new reader toward unit-testing their handlers from day one. * ci(justfile): skip CodeQL in gha-pre-release (act/CodeQL incompatibility) The CodeQL GitHub Action's post-analysis step calls the GH REST API (`/repos/{owner}/{repo}/actions/runs/{run_id}`) for telemetry and status-page reporting. Under `act`, the synthesized GITHUB_RUN_ID doesn't exist on github.com, so the call 404s and the action sets the job status to JOB_STATUS_CONFIGURATION_ERROR even when: * 174/174 queries loaded successfully * 42/42 Python files extracted * SARIF generated and post-processed * Zero findings in our code The result is gha-pre-release ALWAYS failing on the codeql step under act, which masks real failures in upstream steps. Cleanest fix: drop CodeQL from the local replay sequence with a clear note about why, and rely on the real GH Actions run on every PR (which has actual access to the workflow-runs endpoint via GITHUB_TOKEN). Changes: * Recipe header documents the skip with rationale and a one-liner showing how to run CodeQL locally on demand for ad-hoc inspection. The user runs `act push` directly and inspects the SARIF; a configuration-error exit with a successfully-generated SARIF means no findings. * Step counters renumbered from N/7 to N/6 throughout the recipe. * Step 5 prints an explicit SKIPPED notice explaining the situation so the user isn't left wondering whether CodeQL ran. * Final "safe to merge" message clarifies CodeQL still gates merge on the actual PR via real GH Actions. Discovered while running gha-pre-release on the testing-helpers PR - CodeQL kept reporting failure despite analyzing cleanly. * fix(testing): silence CodeQL py/unused-import on runner module imports Move `ReplayRequestType` and `ReplayStatus` into the `if TYPE_CHECKING:` block. They were imported at module scope but only referenced in annotations (resolved as strings via `from __future__ import annotations`) and in a string-form `cast("ReplayRequestType", ...)` call (per ruff's TC006 rule, which prefers the string form to keep type-only symbols out of the runtime import graph). CodeQL's py/unused-import sees the runtime import and the string-only references, and reasonably concludes the import is unused. Cleaner resolution: keep the imports type-only, expand the explanatory comment so future readers see why both static analyzers (CodeQL + ruff TC006) end up happy with this shape. The two CodeQL py/cyclic-import alerts on the surrounding `if TYPE_CHECKING:` block remain false positives — CodeQL does not model conditional imports, and the runtime cycle is broken by the lazy import in `CustomResource.replay` (resource.py:354). Both have been dismissed in the GitHub UI with rationale (alerts #15, #16).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Implements the
publish-lambda-layeropenspec change. Three coordinated additions:Layer ZIP attached to every GitHub Release —
cfn_handler-<version>-layer.zipis the wheel repackaged for Lambda (python/cfn_handler/...layout, nodist-info). Users who want to deploy the layer themselves just download the ZIP.Public Lambda Layer published across ~17 commercial regions — every release calls
aws lambda publish-layer-versionper region withPrincipal=*public read; users reference the resulting ARN.fail-fast: falseso a single bad region doesnt block the rest.Three public ARN discovery surfaces — no AWS credentials needed:
aggregate-arnsjob).layer-arns.jsonrelease asset for programmatic use (curl …/releases/latest/download/layer-arns.json).OpenSpec change
openspec/changes/publish-lambda-layer/— proposal, design, spec, tasks. New capabilitylambda-layer-publishing. Validated--strict.What landed
layer/regions.txt#comments allowedlayer/iam-publisher.cfn.yamllayer/MAINTAINER.mdlayer/README.md.github/workflows/release.ymlbuild-layer-zip,set-layer-matrix,publish-layer(matrix),aggregate-arnsdocs/CI.mdREADME.mdlambda layerbadge + "Or use the AWS Lambda Layer" subsectionMaintainer setup BEFORE merging
The new jobs run after
release-pleasereportsrelease_created=true, so this PRs CI does NOT exercise the AWS-facing path. But a future release would fail without:Deploy
layer/iam-publisher.cfn.yamlto your AWS account:If your account already has the GitHub Actions OIDC provider (from another project), pass
--parameter-overrides CreateOidcProvider=false.Capture the role ARN:
aws cloudformation describe-stacks \ --stack-name cfn-handler-layer-publisher --region us-east-1 \ --query "Stacks[0].Outputs[?OutputKey==\`RoleArn\`].OutputValue" --output textCreate GitHub environment
layer-publisherinigorlg/cfn-handler(Settings → Environments). No protection rules — they would block the bot-driven release.Save the role ARN as the
LAYER_PUBLISHER_ROLE_ARNsecret inside thelayer-publisherenvironment (NOT at the repo level).Full walkthrough in
layer/MAINTAINER.md.What runs on this PRs CI
The 4 new jobs are gated on
release_created=true, so PR CI exercises only:CI passed(test matrix + lint)analyze (python)(CodeQL)review dependenciesensure SHA-pinned actions(validates the new action SHAs we added)secure-workflows.ymlwill re-validate the new SHAs:aws-actions/configure-aws-credentials@00943011d9042930efac3dcd3a170e4273319bc8# v5.1.0actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4# v5.0.0actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53# v6.0.0Local verification
just ci-check✓ — 103 passed, 99.48% coverageuv run cfn-lint layer/iam-publisher.cfn.yaml✓ — cleandist-info, layout ispython/cfn_handler/...actionlint .github/workflows/release.yml✓ — clean (one info-level shellcheck false positive on literal markdown backticks; harmless)openspec validate publish-lambda-layer --strict✓After merge
The next release-please PR (whatever feat/fix commits accumulate) will trigger the new pipeline end-to-end. Watch the
release.ymlrun for ~17 region publishes + the aggregate. Verify a layer viaaws lambda get-layer-version --layer-name cfn-handler --version-number 1 --region us-east-1from any AWS account; verify public read.Non-goals