repro-lambda 0.2.2__tar.gz → 0.2.4__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {repro_lambda-0.2.2 → repro_lambda-0.2.4}/.github/workflows/build.yml +4 -4
- {repro_lambda-0.2.2 → repro_lambda-0.2.4}/CHANGELOG.md +13 -0
- {repro_lambda-0.2.2 → repro_lambda-0.2.4}/PKG-INFO +1 -1
- {repro_lambda-0.2.2 → repro_lambda-0.2.4}/pyproject.toml +1 -1
- {repro_lambda-0.2.2 → repro_lambda-0.2.4}/src/repro_lambda/__init__.py +1 -1
- {repro_lambda-0.2.2 → repro_lambda-0.2.4}/src/repro_lambda/cli.py +26 -1
- {repro_lambda-0.2.2 → repro_lambda-0.2.4}/src/repro_lambda/docker_runner.py +4 -10
- {repro_lambda-0.2.2 → repro_lambda-0.2.4}/tests/test_cli_build.py +20 -0
- {repro_lambda-0.2.2 → repro_lambda-0.2.4}/tests/test_cli_smoke.py +2 -1
- repro_lambda-0.2.4/tests/test_zip_excludes.py +38 -0
- {repro_lambda-0.2.2 → repro_lambda-0.2.4}/uv.lock +1 -1
- {repro_lambda-0.2.2 → repro_lambda-0.2.4}/.github/workflows/ci.yml +0 -0
- {repro_lambda-0.2.2 → repro_lambda-0.2.4}/.github/workflows/publish.yml +0 -0
- {repro_lambda-0.2.2 → repro_lambda-0.2.4}/.gitignore +0 -0
- {repro_lambda-0.2.2 → repro_lambda-0.2.4}/.pre-commit-config.yaml +0 -0
- {repro_lambda-0.2.2 → repro_lambda-0.2.4}/LICENSE +0 -0
- {repro_lambda-0.2.2 → repro_lambda-0.2.4}/README.md +0 -0
- {repro_lambda-0.2.2 → repro_lambda-0.2.4}/SETUP.md +0 -0
- {repro_lambda-0.2.2 → repro_lambda-0.2.4}/src/repro_lambda/__main__.py +0 -0
- {repro_lambda-0.2.2 → repro_lambda-0.2.4}/src/repro_lambda/build.py +0 -0
- {repro_lambda-0.2.2 → repro_lambda-0.2.4}/src/repro_lambda/catalog.py +0 -0
- {repro_lambda-0.2.2 → repro_lambda-0.2.4}/src/repro_lambda/git_guard.py +0 -0
- {repro_lambda-0.2.2 → repro_lambda-0.2.4}/src/repro_lambda/hasher.py +0 -0
- {repro_lambda-0.2.2 → repro_lambda-0.2.4}/src/repro_lambda/manifest.py +0 -0
- {repro_lambda-0.2.2 → repro_lambda-0.2.4}/src/repro_lambda/s3_uploader.py +0 -0
- {repro_lambda-0.2.2 → repro_lambda-0.2.4}/src/repro_lambda/source_stager.py +0 -0
- {repro_lambda-0.2.2 → repro_lambda-0.2.4}/src/repro_lambda/verify.py +0 -0
- {repro_lambda-0.2.2 → repro_lambda-0.2.4}/src/repro_lambda/zip_packager.py +0 -0
- {repro_lambda-0.2.2 → repro_lambda-0.2.4}/tests/__init__.py +0 -0
- {repro_lambda-0.2.2 → repro_lambda-0.2.4}/tests/conftest.py +0 -0
- {repro_lambda-0.2.2 → repro_lambda-0.2.4}/tests/fixtures/sample_nodejs_lambda/handler/index.js +0 -0
- {repro_lambda-0.2.2 → repro_lambda-0.2.4}/tests/fixtures/sample_nodejs_lambda/handler/package-lock.json +0 -0
- {repro_lambda-0.2.2 → repro_lambda-0.2.4}/tests/fixtures/sample_nodejs_lambda/handler/package.json +0 -0
- {repro_lambda-0.2.2 → repro_lambda-0.2.4}/tests/fixtures/sample_nodejs_lambda/lambdas.toml +0 -0
- {repro_lambda-0.2.2 → repro_lambda-0.2.4}/tests/fixtures/sample_python_lambda/handler/app.py +0 -0
- {repro_lambda-0.2.2 → repro_lambda-0.2.4}/tests/fixtures/sample_python_lambda/handler/requirements.arm64.lock +0 -0
- {repro_lambda-0.2.2 → repro_lambda-0.2.4}/tests/fixtures/sample_python_lambda/handler/requirements.in +0 -0
- {repro_lambda-0.2.2 → repro_lambda-0.2.4}/tests/fixtures/sample_python_lambda/lambdas.toml +0 -0
- {repro_lambda-0.2.2 → repro_lambda-0.2.4}/tests/test_build_integration.py +0 -0
- {repro_lambda-0.2.2 → repro_lambda-0.2.4}/tests/test_build_nodejs.py +0 -0
- {repro_lambda-0.2.2 → repro_lambda-0.2.4}/tests/test_catalog.py +0 -0
- {repro_lambda-0.2.2 → repro_lambda-0.2.4}/tests/test_cli_lock.py +0 -0
- {repro_lambda-0.2.2 → repro_lambda-0.2.4}/tests/test_docker_runner.py +0 -0
- {repro_lambda-0.2.2 → repro_lambda-0.2.4}/tests/test_docker_runner_nodejs.py +0 -0
- {repro_lambda-0.2.2 → repro_lambda-0.2.4}/tests/test_e2e_nodejs_lambda.py +0 -0
- {repro_lambda-0.2.2 → repro_lambda-0.2.4}/tests/test_e2e_python_lambda.py +0 -0
- {repro_lambda-0.2.2 → repro_lambda-0.2.4}/tests/test_git_guard.py +0 -0
- {repro_lambda-0.2.2 → repro_lambda-0.2.4}/tests/test_hasher.py +0 -0
- {repro_lambda-0.2.2 → repro_lambda-0.2.4}/tests/test_manifest.py +0 -0
- {repro_lambda-0.2.2 → repro_lambda-0.2.4}/tests/test_python_byte_compat_regression.py +0 -0
- {repro_lambda-0.2.2 → repro_lambda-0.2.4}/tests/test_s3_uploader.py +0 -0
- {repro_lambda-0.2.2 → repro_lambda-0.2.4}/tests/test_source_stager.py +0 -0
- {repro_lambda-0.2.2 → repro_lambda-0.2.4}/tests/test_verify.py +0 -0
- {repro_lambda-0.2.2 → repro_lambda-0.2.4}/tests/test_zip_packager.py +0 -0
|
@@ -9,7 +9,7 @@ on:
|
|
|
9
9
|
description: Path to lambdas.toml in the caller repo.
|
|
10
10
|
repro_lambda_version:
|
|
11
11
|
type: string
|
|
12
|
-
default: "0.2.
|
|
12
|
+
default: "0.2.4"
|
|
13
13
|
description: Pinned repro-lambda PyPI version.
|
|
14
14
|
aws-dev-role-arn:
|
|
15
15
|
type: string
|
|
@@ -77,11 +77,11 @@ jobs:
|
|
|
77
77
|
- name: Build (dev bucket)
|
|
78
78
|
env:
|
|
79
79
|
REPRO_LAMBDA_BUCKET: ${{ inputs.dev-bucket }}
|
|
80
|
-
run: uvx --from "repro-lambda==${{ inputs.repro_lambda_version }}" repro-lambda build --manifest "${{ inputs.manifest_path }}"
|
|
80
|
+
run: uvx --from "repro-lambda==${{ inputs.repro_lambda_version }}" repro-lambda build --manifest "${{ inputs.manifest_path }}" --arch "${{ matrix.arch }}"
|
|
81
81
|
|
|
82
82
|
- name: Verify reproducible (PR only)
|
|
83
83
|
if: github.event_name == 'pull_request'
|
|
84
|
-
run: uvx --from "repro-lambda==${{ inputs.repro_lambda_version }}" repro-lambda build --manifest "${{ inputs.manifest_path }}" --verify --dry-run
|
|
84
|
+
run: uvx --from "repro-lambda==${{ inputs.repro_lambda_version }}" repro-lambda build --manifest "${{ inputs.manifest_path }}" --arch "${{ matrix.arch }}" --verify --dry-run
|
|
85
85
|
|
|
86
86
|
- name: Configure AWS credentials (prod)
|
|
87
87
|
if: github.ref == 'refs/heads/master' && inputs.aws-prod-role-arn != ''
|
|
@@ -94,7 +94,7 @@ jobs:
|
|
|
94
94
|
if: github.ref == 'refs/heads/master' && inputs.aws-prod-role-arn != ''
|
|
95
95
|
env:
|
|
96
96
|
REPRO_LAMBDA_BUCKET: ${{ inputs.prod-bucket }}
|
|
97
|
-
run: uvx --from "repro-lambda==${{ inputs.repro_lambda_version }}" repro-lambda build --manifest "${{ inputs.manifest_path }}"
|
|
97
|
+
run: uvx --from "repro-lambda==${{ inputs.repro_lambda_version }}" repro-lambda build --manifest "${{ inputs.manifest_path }}" --arch "${{ matrix.arch }}"
|
|
98
98
|
|
|
99
99
|
- name: Commit catalog drift (master only, dev bot)
|
|
100
100
|
if: github.ref == 'refs/heads/master'
|
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## v0.2.4 - 2026-06-20
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
- Container build no longer shells out to `find`/`xargs` (both absent from the minimal AWS Lambda base images, which caused `find: command not found`). The post-install cleanup (Python caches + non-deterministic `*.dist-info` metadata: RECORD, INSTALLER, direct_url.json, REQUESTED) now happens in the Python zip step via exclude globs, producing the same artifact bytes.
|
|
7
|
+
|
|
8
|
+
## v0.2.3 - 2026-06-20
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
- Python build container staged dependencies under `/build` (root-owned), which failed with `mkdir: Permission denied` when the container runs as a non-root `--user` (e.g. GitHub-hosted runners, uid 1001). Staging moved to `/tmp/build` (world-writable).
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
- `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-arch `docker run` (which fails without emulation, and emulated builds would break byte-reproducibility). The reusable `build.yml` passes `--arch ${{ matrix.arch }}`.
|
|
15
|
+
|
|
3
16
|
## v0.2.2 - 2026-06-20
|
|
4
17
|
|
|
5
18
|
### Changed
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: repro-lambda
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.4
|
|
4
4
|
Summary: Build reproducible AWS Lambda packages outside Terraform, optimized for terraform-aws-lambda by serverless.tf.
|
|
5
5
|
Project-URL: Homepage, https://github.com/antonbabenko/repro-lambda
|
|
6
6
|
Project-URL: Repository, https://github.com/antonbabenko/repro-lambda
|
|
@@ -63,6 +63,12 @@ def build(
|
|
|
63
63
|
verify: Annotated[bool, typer.Option("--verify")] = False,
|
|
64
64
|
dry_run: Annotated[bool, typer.Option("--dry-run")] = False,
|
|
65
65
|
allow_dirty: Annotated[bool, typer.Option("--allow-dirty")] = False,
|
|
66
|
+
arch: Annotated[
|
|
67
|
+
str,
|
|
68
|
+
typer.Option(
|
|
69
|
+
"--arch", help="Only build lambdas with this arch (e.g. arm64, x86_64). Empty = all."
|
|
70
|
+
),
|
|
71
|
+
] = "",
|
|
66
72
|
) -> None:
|
|
67
73
|
"""Build one lambda (or all) per manifest and upload to S3."""
|
|
68
74
|
import json
|
|
@@ -84,6 +90,12 @@ def build(
|
|
|
84
90
|
typer.echo(f"no lambda named {target!r} in {manifest}", err=True)
|
|
85
91
|
raise typer.Exit(2)
|
|
86
92
|
|
|
93
|
+
if arch:
|
|
94
|
+
selected = [s for s in selected if s.arch == arch]
|
|
95
|
+
if not selected:
|
|
96
|
+
typer.echo(f"no lambdas with arch {arch!r} in {manifest}; nothing to build")
|
|
97
|
+
raise typer.Exit(0)
|
|
98
|
+
|
|
87
99
|
if not dry_run and not bucket:
|
|
88
100
|
typer.echo(
|
|
89
101
|
"--bucket or REPRO_LAMBDA_BUCKET env var is required for non-dry-run",
|
|
@@ -208,11 +220,24 @@ def init() -> None:
|
|
|
208
220
|
raise typer.Exit(0)
|
|
209
221
|
|
|
210
222
|
|
|
223
|
+
# Stripped from every lambda zip so the container build needs no findutils/xargs
|
|
224
|
+
# (absent from minimal Lambda base images): Python caches and the non-deterministic
|
|
225
|
+
# dist-info metadata files pip writes (RECORD, INSTALLER, direct_url.json, REQUESTED).
|
|
226
|
+
_LAMBDA_ZIP_EXCLUDES = [
|
|
227
|
+
"*__pycache__*",
|
|
228
|
+
"*.pyc",
|
|
229
|
+
"*.dist-info/RECORD",
|
|
230
|
+
"*.dist-info/INSTALLER",
|
|
231
|
+
"*.dist-info/direct_url.json",
|
|
232
|
+
"*.dist-info/REQUESTED",
|
|
233
|
+
]
|
|
234
|
+
|
|
235
|
+
|
|
211
236
|
def _zip_impl(src: Path, out: Path) -> None:
|
|
212
237
|
"""Pack a directory into a deterministic zip (used inside container)."""
|
|
213
238
|
from repro_lambda.zip_packager import pack_directory
|
|
214
239
|
|
|
215
|
-
pack_directory(src, out)
|
|
240
|
+
pack_directory(src, out, exclude_glob=_LAMBDA_ZIP_EXCLUDES)
|
|
216
241
|
|
|
217
242
|
|
|
218
243
|
@app.command(name="zip")
|
|
@@ -37,7 +37,7 @@ class DockerRunError(RuntimeError):
|
|
|
37
37
|
|
|
38
38
|
_PYTHON_INSTALL_SCRIPT = r"""
|
|
39
39
|
set -euxo pipefail
|
|
40
|
-
PKG=/build/pkg
|
|
40
|
+
PKG=/tmp/build/pkg
|
|
41
41
|
mkdir -p "$PKG"
|
|
42
42
|
cp -R /src/source/. "$PKG/"
|
|
43
43
|
|
|
@@ -50,15 +50,9 @@ pip install \
|
|
|
50
50
|
--target "$PKG" \
|
|
51
51
|
--requirement /src/requirements.lock
|
|
52
52
|
|
|
53
|
-
#
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
find "$PKG" -type d -name "*.dist-info" -exec sh -c '
|
|
57
|
-
for d; do
|
|
58
|
-
rm -f -- "$d/RECORD" "$d/INSTALLER" "$d/direct_url.json" "$d/REQUESTED"
|
|
59
|
-
done
|
|
60
|
-
' _ {} +
|
|
61
|
-
|
|
53
|
+
# Byte-output cleanup (caches + non-deterministic dist-info metadata) happens in the
|
|
54
|
+
# Python zip step below, so this script needs no findutils/xargs (both absent from the
|
|
55
|
+
# minimal AWS Lambda base images).
|
|
62
56
|
python3 -m repro_lambda zip --src "$PKG" --out /out/lambda.zip
|
|
63
57
|
"""
|
|
64
58
|
|
|
@@ -109,3 +109,23 @@ def test_cli_build_allow_dirty_bypasses_guard(consumer_repo: Path, mocker):
|
|
|
109
109
|
],
|
|
110
110
|
)
|
|
111
111
|
assert result.exit_code == 0, result.stdout
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def test_cli_build_arch_filter_skips_nonmatching(consumer_repo: Path, mocker):
|
|
115
|
+
mock_docker = mocker.patch("repro_lambda.build.build_python_lambda")
|
|
116
|
+
result = runner.invoke(
|
|
117
|
+
app,
|
|
118
|
+
[
|
|
119
|
+
"build",
|
|
120
|
+
"app",
|
|
121
|
+
"--manifest",
|
|
122
|
+
str(consumer_repo / "lambdas.toml"),
|
|
123
|
+
"--arch",
|
|
124
|
+
"x86_64",
|
|
125
|
+
"--dry-run",
|
|
126
|
+
],
|
|
127
|
+
)
|
|
128
|
+
# consumer_repo's only lambda 'app' is arm64; an x86_64 filter -> nothing to build.
|
|
129
|
+
assert result.exit_code == 0, result.stdout
|
|
130
|
+
assert "nothing to build" in result.stdout
|
|
131
|
+
mock_docker.assert_not_called()
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
from typer.testing import CliRunner
|
|
2
2
|
|
|
3
|
+
from repro_lambda import __version__
|
|
3
4
|
from repro_lambda.cli import app
|
|
4
5
|
|
|
5
6
|
runner = CliRunner()
|
|
@@ -8,7 +9,7 @@ runner = CliRunner()
|
|
|
8
9
|
def test_cli_version_shows_package_version():
|
|
9
10
|
result = runner.invoke(app, ["--version"])
|
|
10
11
|
assert result.exit_code == 0
|
|
11
|
-
assert
|
|
12
|
+
assert __version__ in result.stdout
|
|
12
13
|
|
|
13
14
|
|
|
14
15
|
def test_cli_build_subcommand_exists():
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
from zipfile import ZipFile
|
|
3
|
+
|
|
4
|
+
from typer.testing import CliRunner
|
|
5
|
+
|
|
6
|
+
from repro_lambda.cli import app
|
|
7
|
+
|
|
8
|
+
runner = CliRunner()
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def test_zip_excludes_caches_and_dist_info_metadata(tmp_path: Path):
|
|
12
|
+
"""`repro-lambda zip` strips caches + non-deterministic dist-info metadata,
|
|
13
|
+
so the container build needs no find/xargs (absent from minimal base images)."""
|
|
14
|
+
pkg = tmp_path / "pkg"
|
|
15
|
+
(pkg / "mymod").mkdir(parents=True)
|
|
16
|
+
(pkg / "mymod" / "__init__.py").write_text("x = 1\n")
|
|
17
|
+
(pkg / "mymod" / "__pycache__").mkdir()
|
|
18
|
+
(pkg / "mymod" / "__pycache__" / "__init__.cpython-313.pyc").write_bytes(b"\x00")
|
|
19
|
+
(pkg / "mymod" / "stale.pyc").write_bytes(b"\x00")
|
|
20
|
+
dist = pkg / "req-1.0.dist-info"
|
|
21
|
+
dist.mkdir()
|
|
22
|
+
(dist / "RECORD").write_text("mymod/__init__.py,,\n")
|
|
23
|
+
(dist / "INSTALLER").write_text("pip\n")
|
|
24
|
+
(dist / "METADATA").write_text("Name: req\n")
|
|
25
|
+
|
|
26
|
+
out = tmp_path / "lambda.zip"
|
|
27
|
+
result = runner.invoke(app, ["zip", "--src", str(pkg), "--out", str(out)])
|
|
28
|
+
assert result.exit_code == 0, result.stdout
|
|
29
|
+
|
|
30
|
+
names = ZipFile(out).namelist()
|
|
31
|
+
# kept: real code + stable dist-info metadata
|
|
32
|
+
assert "mymod/__init__.py" in names
|
|
33
|
+
assert "req-1.0.dist-info/METADATA" in names
|
|
34
|
+
# stripped: caches + non-deterministic metadata
|
|
35
|
+
assert not any("__pycache__" in n for n in names)
|
|
36
|
+
assert not any(n.endswith(".pyc") for n in names)
|
|
37
|
+
assert "req-1.0.dist-info/RECORD" not in names
|
|
38
|
+
assert "req-1.0.dist-info/INSTALLER" not in names
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{repro_lambda-0.2.2 → repro_lambda-0.2.4}/tests/fixtures/sample_nodejs_lambda/handler/index.js
RENAMED
|
File without changes
|
|
File without changes
|
{repro_lambda-0.2.2 → repro_lambda-0.2.4}/tests/fixtures/sample_nodejs_lambda/handler/package.json
RENAMED
|
File without changes
|
|
File without changes
|
{repro_lambda-0.2.2 → repro_lambda-0.2.4}/tests/fixtures/sample_python_lambda/handler/app.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|