0.7.2 (2026-06-23)
- add social preview image (b6e0707)
0.7.1 (2026-06-23)
0.7.0 (2026-06-23)
0.6.1 (2026-06-22)
- Pass the full compatible manylinux range to
pip installinstead of a single--platform, so compiled wheels are selected for every package. A single explicit--platformmatches (close to) that exact tag - it does not broaden across baselines - somanylinux_2_17silently dropped compiled wheels tagged only at a higher baseline. Concretely,wrapt's cp313 wheel is taggedmanylinux_2_28(no2_17), so pip fell back to the pure-Pythonpy3-none-anybuild; that brokeaws-xray-sdk's runtime boto3 patching and 500'd a Lambda whose package included it. The v0.4.1 change (2_28 -> 2_17, to catchpydantic-core's 2_17-only wheel) had created the opposite failure - a single floor cannot satisfy both. Nowdocker_runner.pyemits repeated--platformflags frommanylinux_2_34down tomanylinux1(x86_64) /manylinux2014(aarch64), capped at the AL2023 runtime's glibc 2.34, and pip picks the most-specific compiled wheel each package offers, only usingpy3-none-anywhen no compiled wheel exists. Verified end to end: a real build now shipswrapt/_wrappers.*.soagain. The builder version bump re-keys all content hashes, as expected.
- Send a
User-Agentheader on every source fetch. The GitHub REST API rejects requests with no User-Agent (HTTP 403), which brokegithub_releasesource resolution (the asset lookup againstapi.github.com). The header is nowrepro-lambda/<version>on all requests (harmless for plainhttpssources, required for the API). Does not affect any content hash (request headers are not part of artifact identity).
- Declarative sources DSL:
[[lambda.source]]fetches a pinned external artifact into the package before the container build, replacing consumer-side download scripts. Two source types -github_release(repo/tag/asset, resolved via the GitHub API) andhttps(a directurl). Each source is PINNED:sha256is verified before extraction;extractiszip/tar.gz/none; an optionalmemberextracts a single file or a (versioned) directory subtree todest(package-root-relative);executablesets +x. Source names are required and unique per lambda; dest collisions (including tree overlaps and overlaps with the staged source) are refused. version_from(single-level): a source can derive itsversionat lock time from an asdf-stylekey valueline in a referenced source's file (read relative to that source's member-stripped tree).{version}is substituted intourl/tag/asset/member.version_fromis a lock input and never participates in the content hash.lock --sourcesre-resolvesversion_from, re-downloads each source, recomputes its sha256, and rewriteslambdas.tomlin place with tomlkit (comments preserved, atomic). It is idempotent: a run that changes nothing leaves the file byte-for-byte unchanged (no spurious PR).lockkeeps regenerating requirements locks too; use--no-requirements/--no-sourcesto scope it.- The reusable
build.ymlgains a genericsources_tokensecret (exported asREPRO_LAMBDA_SOURCES_TOKEN, used only forgithub_releaseAPI calls) and an arch-scopedactions/cachefor the content-addressed source cache.
- SSRF-hardened fetcher: HTTPS-only with a manual redirect loop; each hop resolves the hostname, refuses any non-global-unicast / reserved / private / loopback / link-local / IPv4-mapped address, and connects to that pinned IP while verifying the TLS cert against the original hostname (no second DNS lookup a rebind could poison).
Authorizationis stripped on any cross-host redirect (e.g. the GitHub API -> asset host), so a private token never leaks. Download size is capped and log URLs are redacted (userinfo + query stripped). - Hardened extraction: sha256 is verified before any archive is opened; entries with absolute /
../ symlink / hardlink / device / fifo paths are rejected; total bytes, entry count, and per-entry bytes are bounded (decompression-bomb limits, with streaming tar reads); files are written to a temp tree then promoted with normalized mtime + perms. The sha256-keyed cache is re-verified on every use (anti cache-poisoning). - Content hashing folds the RESOLVED source metadata (not the bytes), so the artifact key is computable offline and a
member/extract/dest/sha256/version change re-keys, while a re-fetch alone does not.
- Per-lambda builder overrides: any
[[lambda]]may now setbase_image_python,include_patterns, orexclude_patternsto override the[builder]defaults for itself. An override fully REPLACES the matching default (lists are not merged); an unset field inherits[builder]. A per-lambdabase_image_pythonmust still be digest-pinned (validated at manifest load). This lets one lambda build on its own base image or filter its source more tightly than the others - e.g. a lambda that bundles a large prebuilt tree can narrowinclude_patternsto just its runtime modules so unrelated file changes no longer re-key its artifact. The resolved per-lambda builder (base-image digest + include/exclude lists + builder version) folds into the content hash, so changing an override re-keys only that lambda. Manifests with no per-lambda overrides resolve to the[builder]defaults unchanged. The builder version bump re-keys all content hashes, as expected.
- Lower the pip
--platformfloor frommanylinux_2_28tomanylinux_2_17(manylinux2014) for both arches. pip's explicit--platformdoes not expand a higher manylinux tag down to lower-baseline wheels, so with--only-binary=:all:a2_28floor failed to find compiled wheels that ship onlymanylinux_2_17for a given Python/arch (e.g.pydantic-core).manylinux_2_17is the broadest baseline the AWS Lambda base images (Amazon Linux 2023, glibc 2.34) still run, and it matches2_17wheels plus any lower baseline. Pure-Python lambdas are unaffected (theirpy3-none-anywheels never depended on the platform). The builder version bump re-keys all content hashes, as expected.
extra_filesmanifest field: bundle prebuilt files or directories into a lambda package alongside its source. Each[[lambda.extra_files]]entry hassrc(repo-root-relative, where CI materialized it - e.g. a digest-pinned binary or an extracted release tree),dest(package-root-relative), and an optionalexecutableflag (sets +x on a file; ignored for directories, which keep source perms). The bytes fold into the content hash via the staged source tree, and the executable bit folds in separately, so flipping it changes the artifact hash even when bytes are identical. This lets a lambda ship vendored CLIs or release trees the consumer's CI downloads and verifies, while the tool itself stays free of any network/tool-download logic. Paths are validated as relative and..-free. Specs withoutextra_fileshash byte-identically to before.
promotecommand: copy an already-built artifact from the dev bucket to the prod bucket by content hash, with no rebuild. The sha per lambda is read frombuilds/catalog.json, so the promoted object is byte-for-byte the one built and tested in dev. Idempotent (skips keys already present); Lambda@Edge specs resolve to the-us-east-1bucket variant on both sides.S3Uploader.copy()performs the server-sideCopyObject.- Reusable workflow
promote.yml: validatessource_sha(40-char hex + master-lineage), checks it out, assumes a caller-supplied promoter role, and runspromote. Inputs:manifest_path,source_sha,promoter_role_arn,dev_bucket,prod_bucket.
-
Replace a rebuild-on-prod
promote-to-prodjob with a call to the reusable workflow:jobs: promote: uses: antonbabenko/repro-lambda/.github/workflows/promote.yml@v0 with: source_sha: ${{ inputs.source_sha }} promoter_role_arn: arn:aws:iam::<account>:role/<promoter-role> dev_bucket: <env>-my-lambda-artifacts prod_bucket: <env>-my-lambda-artifacts
- Container build no longer shells out to
find/xargs(both absent from the minimal AWS Lambda base images, which causedfind: command not found). The post-install cleanup (Python caches + non-deterministic*.dist-infometadata: RECORD, INSTALLER, direct_url.json, REQUESTED) now happens in the Python zip step via exclude globs, producing the same artifact bytes.
- Python build container staged dependencies under
/build(root-owned), which failed withmkdir: Permission deniedwhen the container runs as a non-root--user(e.g. GitHub-hosted runners, uid 1001). Staging moved to/tmp/build(world-writable).
build --arch <arm64|x86_64>filters the manifest to lambdas of that arch, so a per-arch CI matrix builds each arch natively on its own runner. This avoids cross-archdocker run(which fails without emulation, and emulated builds would break byte-reproducibility). The reusablebuild.ymlpasses--arch ${{ matrix.arch }}.
- Reusable workflow
build.ymlnow takesaws-dev-role-arnandaws-prod-role-arnas inputs instead of secrets. A role ARN is not sensitive (the security boundary is the OIDC trust policy plus the key-level bucket immutability policy), and typing it as a secret blocked callers from passing a derivable literal ARN, since secret inputs reject plain literal values. No package code change: PyPI 0.2.2 is behaviorally identical to 0.2.1. - Artifact bucket names are now
dev-bucket/prod-bucketinputs instead of hardcoded values, so the reusable workflow is consumer-agnostic and carries no environment-specific bucket names.
-
Bump the workflow ref to
uses: antonbabenko/repro-lambda/.github/workflows/build.yml@v0.2.2and moveaws-dev-role-arn/aws-prod-role-arnto thewith:block, addingdev-bucket(andprod-bucketif you upload to prod). They are inputs now, so plain literals are valid:with: aws-dev-role-arn: arn:aws:iam::<account>:role/<role> dev-bucket: <env>-my-lambda-artifacts
- CI workflow:
uvx --from "repro-lambda==<v>" repro-lambda <args>replacesuv pip install --system "repro-lambda==<v>". uv 0.11+ deprecates theuv piplegacy interface for install/uninstall/sync.
- README install instruction switches to
uv tool install repro-lambda(plusuvx repro-lambdaephemeral alternative). - SETUP.md examples bumped to
@v0.2.1/repro_lambda_version: "0.2.1".
- Consumer repos must bump their workflow ref to
uses: antonbabenko/repro-lambda/.github/workflows/build.yml@v0.2.1to receive the uvx-based install. The v0.2.0 workflow ref still works but invokes the deprecated install command.
- Node.js Lambda packaging (
nodejs20.x,nodejs22.x) vianpm ci --omit=dev --ignore-scripts --cpu=${arch} --os=linuxin the digest-pinned Node base image. - Two-container Node build: install in the Node base image, pack the resulting
pkg/directory inside the digest-pinned Python base image (the Python image's zlib is the only deflate implementation invoked, so macOS arm64 hosts and Linux x86_64 CI produce byte-identical output). BuilderConfig.base_image_nodejs(required when any lambda usespackage_manager = "npm").LambdaSpec.package_json(required for npm specs) +package_json_resolvedproperty.ARCH_TO_NPM_CPUmapping (arm64->arm64,x86_64->x64).build_nodejs_lambda+install_nodejs_dependencies+pack_in_python_sidecarindocker_runner.stage_source(... extra_files=[(host_path, dest_relname), ...])for staging artifacts outsidesource_dir(e.g.package.json,package-lock.json).compute_content_hash(... extra_files=...)keyed by destination relname so npmpackage.jsonedits bump the cache key.- SETUP.md sections for Node.js + Lambda@Edge usage + caveats.
- Docker-gated end-to-end Node.js reproducibility test (
test_e2e_nodejs_lambda.py) using atslib@2.7.0fixture. - Python byte-compat regression test (
test_python_byte_compat_regression.py) pinning v0.1 zip output against a digest-pinned base image.
build.pyandverify.pyroute perspec.package_manager(pip | npm). Both pre-stage source + extras BEFORE the cache-key hash so cache-hit and cache-miss branches see the same hash inputs.manifest.pyacceptsnodejs20.xandnodejs22.xruntimes andpackage_manager = "npm".locksubcommand skips npm specs (npm uses package-lock.json directly; regenerate withnpm installupstream).zip_packager.pack_directoryskips symlinks with a stderr warning (zip cannot preserve link semantics).- Docker
--user $(id -u):$(id -g)on POSIX (skipped on Windows;sys.platform == "win32"gate). _PYTHON_INSTALL_SCRIPTnow declares the full pip platform quartet (--platform,--abi,--python-version,--implementation) and stripsREQUESTEDfiles alongsideRECORD/INSTALLER/direct_url.json. Module-level invariance assert keepsARCH_TO_DOCKER_PLATFORM/ARCH_TO_PIP_PLATFORM/ARCH_TO_NPM_CPUkeys in lockstep.
- v0.1 zip byte-output preserved (regression-tested by
test_python_byte_compat_regression.pyonce the operator records the v0.1.0 reference sha against the pinned base image digest). - Content-hash key shifts ONCE at the v0.1 -> v0.2 cut-line because the lockfile is now hashed via the unified
extra_fileschannel. v0.1-built zips remain pullable by their old keys (content-addressed S3 storage), so the shift only affects the next rebuild.
- npm workspaces NOT supported (single
package.jsonper Lambda only). - Native deps must ship a
linux-${arch}binary viaoptionalDependenciesinpackage-lock.json; npm cannot cross-compile native modules. - Symlinks inside
source_dirare skipped (zip cannot preserve link semantics). Replace with file contents if your build relies on them. - Per-arch lockfiles remain Python-only.
- Windows host: the
--userflag is skipped; Docker Desktop's default user mapping applies.
Initial public release.
- Python 3.11/3.12/3.13 Lambda packaging with
pip install --require-hashes - Byte-reproducible zips via deterministic
zipfile.ZipFilewrites (sorted entries, fixed mtime 1980-01-01, 0o755 dirs / 0o644 files / 0o755 executables) - Content-hash sha256 cache key (source tree + lockfile + spec + base image digest + builder version)
- Idempotent S3 upload via
If-None-Match=*, designed for bucket-policy-enforced immutability --verifytwo-pass byte-reproducibility check--dry-runfor hash + catalog inspection--allow-dirtyfor local iterationbuilds/catalog.jsonwith bounded 10-entry history per lambda- Per-arch lockfile generation via
uv pip compile - Reusable GitHub Actions workflow at
.github/workflows/build.yml - Native arm64 + x86_64 build matrix (no QEMU)
- Node.js / npm packaging
- Lambda@Edge-specific constraints (us-east-1 routing, no env vars)
- Rust runtime