repro-lambda 0.5.1__tar.gz → 0.6.0__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.
Files changed (66) hide show
  1. {repro_lambda-0.5.1 → repro_lambda-0.6.0}/.github/workflows/build.yml +75 -1
  2. {repro_lambda-0.5.1 → repro_lambda-0.6.0}/CHANGELOG.md +5 -0
  3. {repro_lambda-0.5.1 → repro_lambda-0.6.0}/PKG-INFO +1 -1
  4. {repro_lambda-0.5.1 → repro_lambda-0.6.0}/pyproject.toml +1 -1
  5. {repro_lambda-0.5.1 → repro_lambda-0.6.0}/src/repro_lambda/__init__.py +1 -1
  6. {repro_lambda-0.5.1 → repro_lambda-0.6.0}/src/repro_lambda/cli.py +33 -4
  7. {repro_lambda-0.5.1 → repro_lambda-0.6.0}/src/repro_lambda/docker_runner.py +47 -14
  8. {repro_lambda-0.5.1 → repro_lambda-0.6.0}/tests/test_docker_runner.py +20 -4
  9. {repro_lambda-0.5.1 → repro_lambda-0.6.0}/tests/test_promote.py +71 -0
  10. {repro_lambda-0.5.1 → repro_lambda-0.6.0}/uv.lock +1 -1
  11. {repro_lambda-0.5.1 → repro_lambda-0.6.0}/.github/workflows/ci.yml +0 -0
  12. {repro_lambda-0.5.1 → repro_lambda-0.6.0}/.github/workflows/move-major-tag.yml +0 -0
  13. {repro_lambda-0.5.1 → repro_lambda-0.6.0}/.github/workflows/promote.yml +0 -0
  14. {repro_lambda-0.5.1 → repro_lambda-0.6.0}/.github/workflows/publish.yml +0 -0
  15. {repro_lambda-0.5.1 → repro_lambda-0.6.0}/.gitignore +0 -0
  16. {repro_lambda-0.5.1 → repro_lambda-0.6.0}/.pre-commit-config.yaml +0 -0
  17. {repro_lambda-0.5.1 → repro_lambda-0.6.0}/LICENSE +0 -0
  18. {repro_lambda-0.5.1 → repro_lambda-0.6.0}/README.md +0 -0
  19. {repro_lambda-0.5.1 → repro_lambda-0.6.0}/SETUP.md +0 -0
  20. {repro_lambda-0.5.1 → repro_lambda-0.6.0}/src/repro_lambda/__main__.py +0 -0
  21. {repro_lambda-0.5.1 → repro_lambda-0.6.0}/src/repro_lambda/build.py +0 -0
  22. {repro_lambda-0.5.1 → repro_lambda-0.6.0}/src/repro_lambda/catalog.py +0 -0
  23. {repro_lambda-0.5.1 → repro_lambda-0.6.0}/src/repro_lambda/git_guard.py +0 -0
  24. {repro_lambda-0.5.1 → repro_lambda-0.6.0}/src/repro_lambda/hasher.py +0 -0
  25. {repro_lambda-0.5.1 → repro_lambda-0.6.0}/src/repro_lambda/manifest.py +0 -0
  26. {repro_lambda-0.5.1 → repro_lambda-0.6.0}/src/repro_lambda/promote.py +0 -0
  27. {repro_lambda-0.5.1 → repro_lambda-0.6.0}/src/repro_lambda/s3_uploader.py +0 -0
  28. {repro_lambda-0.5.1 → repro_lambda-0.6.0}/src/repro_lambda/source_locker.py +0 -0
  29. {repro_lambda-0.5.1 → repro_lambda-0.6.0}/src/repro_lambda/source_stager.py +0 -0
  30. {repro_lambda-0.5.1 → repro_lambda-0.6.0}/src/repro_lambda/sources.py +0 -0
  31. {repro_lambda-0.5.1 → repro_lambda-0.6.0}/src/repro_lambda/verify.py +0 -0
  32. {repro_lambda-0.5.1 → repro_lambda-0.6.0}/src/repro_lambda/zip_packager.py +0 -0
  33. {repro_lambda-0.5.1 → repro_lambda-0.6.0}/tests/__init__.py +0 -0
  34. {repro_lambda-0.5.1 → repro_lambda-0.6.0}/tests/conftest.py +0 -0
  35. {repro_lambda-0.5.1 → repro_lambda-0.6.0}/tests/fixtures/sample_nodejs_lambda/handler/index.js +0 -0
  36. {repro_lambda-0.5.1 → repro_lambda-0.6.0}/tests/fixtures/sample_nodejs_lambda/handler/package-lock.json +0 -0
  37. {repro_lambda-0.5.1 → repro_lambda-0.6.0}/tests/fixtures/sample_nodejs_lambda/handler/package.json +0 -0
  38. {repro_lambda-0.5.1 → repro_lambda-0.6.0}/tests/fixtures/sample_nodejs_lambda/lambdas.toml +0 -0
  39. {repro_lambda-0.5.1 → repro_lambda-0.6.0}/tests/fixtures/sample_python_lambda/handler/app.py +0 -0
  40. {repro_lambda-0.5.1 → repro_lambda-0.6.0}/tests/fixtures/sample_python_lambda/handler/requirements.arm64.lock +0 -0
  41. {repro_lambda-0.5.1 → repro_lambda-0.6.0}/tests/fixtures/sample_python_lambda/handler/requirements.in +0 -0
  42. {repro_lambda-0.5.1 → repro_lambda-0.6.0}/tests/fixtures/sample_python_lambda/lambdas.toml +0 -0
  43. {repro_lambda-0.5.1 → repro_lambda-0.6.0}/tests/test_build_integration.py +0 -0
  44. {repro_lambda-0.5.1 → repro_lambda-0.6.0}/tests/test_build_nodejs.py +0 -0
  45. {repro_lambda-0.5.1 → repro_lambda-0.6.0}/tests/test_catalog.py +0 -0
  46. {repro_lambda-0.5.1 → repro_lambda-0.6.0}/tests/test_cli_build.py +0 -0
  47. {repro_lambda-0.5.1 → repro_lambda-0.6.0}/tests/test_cli_lock.py +0 -0
  48. {repro_lambda-0.5.1 → repro_lambda-0.6.0}/tests/test_cli_smoke.py +0 -0
  49. {repro_lambda-0.5.1 → repro_lambda-0.6.0}/tests/test_docker_runner_nodejs.py +0 -0
  50. {repro_lambda-0.5.1 → repro_lambda-0.6.0}/tests/test_e2e_nodejs_lambda.py +0 -0
  51. {repro_lambda-0.5.1 → repro_lambda-0.6.0}/tests/test_e2e_python_lambda.py +0 -0
  52. {repro_lambda-0.5.1 → repro_lambda-0.6.0}/tests/test_extra_files.py +0 -0
  53. {repro_lambda-0.5.1 → repro_lambda-0.6.0}/tests/test_git_guard.py +0 -0
  54. {repro_lambda-0.5.1 → repro_lambda-0.6.0}/tests/test_hasher.py +0 -0
  55. {repro_lambda-0.5.1 → repro_lambda-0.6.0}/tests/test_manifest.py +0 -0
  56. {repro_lambda-0.5.1 → repro_lambda-0.6.0}/tests/test_per_lambda_builder.py +0 -0
  57. {repro_lambda-0.5.1 → repro_lambda-0.6.0}/tests/test_python_byte_compat_regression.py +0 -0
  58. {repro_lambda-0.5.1 → repro_lambda-0.6.0}/tests/test_s3_uploader.py +0 -0
  59. {repro_lambda-0.5.1 → repro_lambda-0.6.0}/tests/test_source_locker.py +0 -0
  60. {repro_lambda-0.5.1 → repro_lambda-0.6.0}/tests/test_source_stager.py +0 -0
  61. {repro_lambda-0.5.1 → repro_lambda-0.6.0}/tests/test_sources.py +0 -0
  62. {repro_lambda-0.5.1 → repro_lambda-0.6.0}/tests/test_sources_hash.py +0 -0
  63. {repro_lambda-0.5.1 → repro_lambda-0.6.0}/tests/test_sources_schema.py +0 -0
  64. {repro_lambda-0.5.1 → repro_lambda-0.6.0}/tests/test_verify.py +0 -0
  65. {repro_lambda-0.5.1 → repro_lambda-0.6.0}/tests/test_zip_excludes.py +0 -0
  66. {repro_lambda-0.5.1 → repro_lambda-0.6.0}/tests/test_zip_packager.py +0 -0
@@ -25,6 +25,27 @@ on:
25
25
  required: false
26
26
  default: ""
27
27
  description: S3 bucket name for prod Lambda artifacts (master push only).
28
+ promotion_repo:
29
+ type: string
30
+ required: false
31
+ default: ""
32
+ description: >-
33
+ owner/repo of the centralized promotion workflow. When set, the build Step
34
+ Summary renders a ready-to-run `gh workflow run` command per function. Generic
35
+ by design - the reusable workflow never hardcodes a promotion target; the caller
36
+ supplies it (keep this empty for public callers).
37
+ promotion_workflow:
38
+ type: string
39
+ required: false
40
+ default: promote-lambda.yml
41
+ description: Workflow file name dispatched by the promotion command in the Step Summary.
42
+ project:
43
+ type: string
44
+ required: false
45
+ default: ""
46
+ description: >-
47
+ Project key the promotion workflow expects (-f project=...). Defaults to the
48
+ caller repo name when empty.
28
49
  secrets:
29
50
  sources_token:
30
51
  required: false
@@ -92,7 +113,60 @@ jobs:
92
113
  env:
93
114
  REPRO_LAMBDA_BUCKET: ${{ inputs.dev_bucket }}
94
115
  REPRO_LAMBDA_SOURCES_TOKEN: ${{ secrets.sources_token }}
95
- run: uvx --from repro-lambda repro-lambda build --manifest "${{ inputs.manifest_path }}" --arch "${{ matrix.arch }}"
116
+ run: uvx --from repro-lambda repro-lambda build --manifest "${{ inputs.manifest_path }}" --arch "${{ matrix.arch }}" --json-out "$RUNNER_TEMP/build-summary.json"
117
+
118
+ # Render the dev build's content shas + a copy-paste promotion command per function
119
+ # into the run's Step Summary (the build JSON is otherwise buried in step logs).
120
+ - name: Build summary (content shas + promote commands)
121
+ if: ${{ hashFiles(format('{0}/build-summary.json', runner.temp)) != '' }}
122
+ env:
123
+ SUMMARY_JSON: ${{ runner.temp }}/build-summary.json
124
+ PROMOTION_REPO: ${{ inputs.promotion_repo }}
125
+ PROMOTION_WORKFLOW: ${{ inputs.promotion_workflow }}
126
+ PROJECT: ${{ inputs.project != '' && inputs.project || github.event.repository.name }}
127
+ ARCH: ${{ matrix.arch }}
128
+ SRC_REPO: ${{ github.repository }}
129
+ SRC_COMMIT: ${{ github.sha }}
130
+ run: |
131
+ python3 - <<'PY'
132
+ import json, os
133
+
134
+ rows = json.load(open(os.environ["SUMMARY_JSON"]))
135
+ repo = os.environ["PROMOTION_REPO"]
136
+ wf = os.environ["PROMOTION_WORKFLOW"]
137
+ project = os.environ["PROJECT"]
138
+ arch = os.environ["ARCH"]
139
+ src = os.environ["SRC_REPO"]
140
+ commit = os.environ["SRC_COMMIT"]
141
+
142
+ out = [
143
+ f"## Lambda build ({arch})",
144
+ "",
145
+ f"Source: `{src}` @ `{commit[:12]}`",
146
+ "",
147
+ "| Function | Outcome | Content SHA | Dev key |",
148
+ "| --- | --- | --- | --- |",
149
+ ]
150
+ for r in rows:
151
+ out.append(
152
+ f"| `{r['logical_name']}` | {r['outcome']} | `{r['sha256']}` | `{r['bucket_key']}` |"
153
+ )
154
+ out += ["", "### Promote to prod", ""]
155
+ for r in rows:
156
+ fn, sha = r["logical_name"], r["sha256"]
157
+ out += [f"**`{fn}`** prod pin: `{fn} = \"{sha}\"`", ""]
158
+ if repo:
159
+ out += [
160
+ "```bash",
161
+ f"gh workflow run {wf} --repo {repo} "
162
+ f"-f project={project} -f function={fn} -f sha={sha}",
163
+ "```",
164
+ "",
165
+ ]
166
+
167
+ with open(os.environ["GITHUB_STEP_SUMMARY"], "a") as f:
168
+ f.write("\n".join(out) + "\n")
169
+ PY
96
170
 
97
171
  - name: Verify reproducible (PR only)
98
172
  if: github.event_name == 'pull_request'
@@ -1,5 +1,10 @@
1
1
  # Changelog
2
2
 
3
+ ## v0.5.2 - 2026-06-21
4
+
5
+ ### Fixed
6
+ - Pass the full compatible manylinux range to `pip install` instead of a single `--platform`, so compiled wheels are selected for every package. A single explicit `--platform` matches (close to) that exact tag - it does not broaden across baselines - so `manylinux_2_17` silently dropped compiled wheels tagged only at a higher baseline. Concretely, `wrapt`'s cp313 wheel is tagged `manylinux_2_28` (no `2_17`), so pip fell back to the pure-Python `py3-none-any` build; that broke `aws-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 catch `pydantic-core`'s 2_17-only wheel) had created the opposite failure - a single floor cannot satisfy both. Now `docker_runner.py` emits repeated `--platform` flags from `manylinux_2_34` down to `manylinux1` (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 using `py3-none-any` when no compiled wheel exists. Verified end to end: a real build now ships `wrapt/_wrappers.*.so` again. The builder version bump re-keys all content hashes, as expected.
7
+
3
8
  ## v0.5.1 - 2026-06-21
4
9
 
5
10
  ### Fixed
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: repro-lambda
3
- Version: 0.5.1
3
+ Version: 0.6.0
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
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "repro-lambda"
3
- version = "0.5.1"
3
+ version = "0.6.0"
4
4
  description = "Build reproducible AWS Lambda packages outside Terraform, optimized for terraform-aws-lambda by serverless.tf."
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.11"
@@ -1,3 +1,3 @@
1
1
  """repro-lambda — reproducible AWS Lambda packaging outside Terraform."""
2
2
 
3
- __version__ = "0.5.1"
3
+ __version__ = "0.6.0"
@@ -70,6 +70,13 @@ def build(
70
70
  "--arch", help="Only build lambdas with this arch (e.g. arm64, x86_64). Empty = all."
71
71
  ),
72
72
  ] = "",
73
+ json_out: Annotated[
74
+ Path | None,
75
+ typer.Option(
76
+ "--json-out",
77
+ help="Also write the build summary JSON array to this file (for CI Step Summary).",
78
+ ),
79
+ ] = None,
73
80
  ) -> None:
74
81
  """Build one lambda (or all) per manifest and upload to S3."""
75
82
  import json
@@ -170,6 +177,9 @@ def build(
170
177
  if not dry_run:
171
178
  catalog.save(catalog_path)
172
179
 
180
+ if json_out is not None:
181
+ json_out.write_text(json.dumps(summary, indent=2))
182
+
173
183
  typer.echo(json.dumps(summary, indent=2))
174
184
 
175
185
 
@@ -196,14 +206,25 @@ def promote(
196
206
  help="Destination (prod) base bucket; us-east-1 variant auto-derived.",
197
207
  ),
198
208
  ] = "",
209
+ sha: Annotated[
210
+ str,
211
+ typer.Option(
212
+ "--sha",
213
+ help="Promote this exact 64-hex content sha instead of the catalog's current. "
214
+ "Requires a single lambda target (not 'all').",
215
+ ),
216
+ ] = "",
199
217
  dry_run: Annotated[bool, typer.Option("--dry-run")] = False,
200
218
  ) -> None:
201
219
  """Copy built artifacts dev -> prod by content hash (no rebuild).
202
220
 
203
- The sha per lambda is read from builds/catalog.json, so the artifact promoted
204
- is exactly the one the source commit built and tested in dev.
221
+ By default the sha per lambda is read from builds/catalog.json, so the artifact
222
+ promoted is exactly the one the source commit built and tested in dev. Pass
223
+ ``--sha`` to promote one explicit content sha (e.g. copy-pasted from a build run's
224
+ Step Summary), bypassing the catalog.
205
225
  """
206
226
  import json
227
+ import re
207
228
 
208
229
  from repro_lambda.manifest import load_manifest
209
230
  from repro_lambda.promote import (
@@ -225,6 +246,14 @@ def promote(
225
246
  typer.echo(f"no lambda named {target!r} in {manifest}", err=True)
226
247
  raise typer.Exit(2)
227
248
 
249
+ if sha:
250
+ if target == "all" or len(selected) != 1:
251
+ typer.echo("--sha requires a single lambda target (not 'all')", err=True)
252
+ raise typer.Exit(2)
253
+ if not re.fullmatch(r"[a-f0-9]{64}", sha):
254
+ typer.echo(f"--sha must be 64 lowercase hex chars, got {sha!r}", err=True)
255
+ raise typer.Exit(2)
256
+
228
257
  if not dry_run and (not dev_bucket or not prod_bucket):
229
258
  typer.echo(
230
259
  "--dev-bucket and --prod-bucket (or REPRO_LAMBDA_DEV_BUCKET / "
@@ -237,10 +266,10 @@ def promote(
237
266
  summary = []
238
267
  for spec in selected:
239
268
  try:
240
- sha = sha_from_catalog(catalog_path, spec.logical_name)
269
+ resolved_sha = sha or sha_from_catalog(catalog_path, spec.logical_name)
241
270
  outcome = promote_one(
242
271
  spec=spec,
243
- sha=sha,
272
+ sha=resolved_sha,
244
273
  dev_bucket=dev_bucket,
245
274
  prod_bucket=prod_bucket,
246
275
  dry_run=dry_run,
@@ -12,17 +12,50 @@ ARCH_TO_DOCKER_PLATFORM: dict[str, str] = {
12
12
  "x86_64": "linux/amd64",
13
13
  }
14
14
 
15
- # manylinux_2_17 (== manylinux2014) is the broadest baseline the AWS Lambda base
16
- # images (Amazon Linux 2023, glibc 2.34) still run, and pip's explicit --platform does
17
- # NOT expand a higher tag (e.g. 2_28) down to lower-baseline wheels. Many compiled
18
- # wheels (e.g. pydantic-core) ship only manylinux_2_17 for a given Python/arch, so a
19
- # 2_28 floor misses them with --only-binary=:all:. 2_17 matches 2_17 wheels and, via
20
- # pip's tag expansion, any lower baseline too.
21
- ARCH_TO_PIP_PLATFORM: dict[str, str] = {
22
- "arm64": "manylinux_2_17_aarch64",
23
- "x86_64": "manylinux_2_17_x86_64",
15
+ # The AWS Lambda base images (Amazon Linux 2023) ship glibc 2.34, so the runtime can load
16
+ # any manylinux wheel up to manylinux_2_34. pip's explicit --platform matches (close to)
17
+ # that exact tag, so a SINGLE platform misses wheels tagged with other baselines in both
18
+ # directions: a 2_28 floor misses 2_17-only wheels (e.g. pydantic-core), and a 2_17 floor
19
+ # misses higher-baseline compiled wheels (e.g. wrapt ships cp313 only as manylinux_2_28 ->
20
+ # pip silently falls back to py3-none-any, which broke aws-xray-sdk's runtime boto3 patching
21
+ # and 500'd a Lambda). A single floor cannot satisfy both. Pass the FULL compatible range as
22
+ # repeated --platform flags, newest -> oldest, capped at 2_34 (the runtime's glibc): pip then
23
+ # selects the most-specific COMPILED wheel each package offers and only falls back to
24
+ # py3-none-any when no compiled wheel exists. Never list a baseline ABOVE 2_34 - that would
25
+ # let pip pick a wheel the runtime cannot load. (arm64 manylinux starts at 2014/2_17; the
26
+ # manylinux1/2010/2_5 legacy aliases are x86_64-only.)
27
+ ARCH_TO_PIP_PLATFORMS: dict[str, list[str]] = {
28
+ "x86_64": [
29
+ "manylinux_2_34_x86_64",
30
+ "manylinux_2_28_x86_64",
31
+ "manylinux_2_24_x86_64",
32
+ "manylinux2014_x86_64",
33
+ "manylinux_2_17_x86_64",
34
+ "manylinux_2_12_x86_64",
35
+ "manylinux2010_x86_64",
36
+ "manylinux_2_5_x86_64",
37
+ "manylinux1_x86_64",
38
+ ],
39
+ "arm64": [
40
+ "manylinux_2_34_aarch64",
41
+ "manylinux_2_28_aarch64",
42
+ "manylinux_2_24_aarch64",
43
+ "manylinux2014_aarch64",
44
+ "manylinux_2_17_aarch64",
45
+ ],
24
46
  }
25
47
 
48
+
49
+ def pip_platform_flags(arch: str) -> str:
50
+ """Repeated ``--platform <tag>`` flags (space-joined) for an arch's manylinux range.
51
+
52
+ Word-split UNQUOTED into the pip command in the container so each tag becomes its own
53
+ flag. pip treats a wheel as compatible with ANY listed platform and prefers the
54
+ earliest (newest baseline) match, so compiled wheels win over the py3-none-any fallback.
55
+ """
56
+ return " ".join(f"--platform {tag}" for tag in ARCH_TO_PIP_PLATFORMS[arch])
57
+
58
+
26
59
  ARCH_TO_NPM_CPU: dict[str, str] = {
27
60
  "arm64": "arm64",
28
61
  "x86_64": "x64",
@@ -31,9 +64,9 @@ ARCH_TO_NPM_CPU: dict[str, str] = {
31
64
  # Invariance: keys must match across all arch lookup tables. Adding a new arch
32
65
  # to one without the other would cause install_nodejs_dependencies to raise
33
66
  # KeyError instead of DockerRunError. Caught at import time.
34
- assert set(ARCH_TO_DOCKER_PLATFORM) == set(ARCH_TO_PIP_PLATFORM) == set(ARCH_TO_NPM_CPU), (
67
+ assert set(ARCH_TO_DOCKER_PLATFORM) == set(ARCH_TO_PIP_PLATFORMS) == set(ARCH_TO_NPM_CPU), (
35
68
  "arch lookup tables must share the same key set; "
36
- f"DOCKER={set(ARCH_TO_DOCKER_PLATFORM)} PIP={set(ARCH_TO_PIP_PLATFORM)} NPM={set(ARCH_TO_NPM_CPU)}"
69
+ f"DOCKER={set(ARCH_TO_DOCKER_PLATFORM)} PIP={set(ARCH_TO_PIP_PLATFORMS)} NPM={set(ARCH_TO_NPM_CPU)}"
37
70
  )
38
71
 
39
72
 
@@ -49,7 +82,7 @@ cp -R /src/source/. "$PKG/"
49
82
 
50
83
  pip install \
51
84
  --no-cache-dir --no-compile --require-hashes --only-binary=:all: \
52
- --platform "$PIP_PLATFORM" \
85
+ $PIP_PLATFORM_FLAGS \
53
86
  --abi "$PIP_ABI" \
54
87
  --python-version "$PIP_PYVER" \
55
88
  --implementation cp \
@@ -130,7 +163,7 @@ def build_python_lambda(
130
163
  python_version: str,
131
164
  ) -> None:
132
165
  """v0.1-compatible: install + pack inside the Python container."""
133
- if arch not in ARCH_TO_PIP_PLATFORM:
166
+ if arch not in ARCH_TO_PIP_PLATFORMS:
134
167
  raise DockerRunError(f"unsupported arch {arch!r}")
135
168
  if shutil.which("docker") is None:
136
169
  raise DockerRunError("docker CLI not found on PATH")
@@ -158,7 +191,7 @@ def build_python_lambda(
158
191
  "-e",
159
192
  "PYTHONPATH=/builder",
160
193
  "-e",
161
- f"PIP_PLATFORM={ARCH_TO_PIP_PLATFORM[arch]}",
194
+ f"PIP_PLATFORM_FLAGS={pip_platform_flags(arch)}",
162
195
  "-e",
163
196
  f"PIP_ABI=cp{pyver_compact}",
164
197
  "-e",
@@ -5,23 +5,39 @@ import pytest
5
5
 
6
6
  from repro_lambda.docker_runner import (
7
7
  ARCH_TO_DOCKER_PLATFORM,
8
- ARCH_TO_PIP_PLATFORM,
8
+ ARCH_TO_PIP_PLATFORMS,
9
9
  DockerRunError,
10
10
  build_python_lambda,
11
+ pip_platform_flags,
11
12
  )
12
13
 
13
14
 
14
15
  def test_arch_mapping_tables_are_complete():
15
16
  for arch in ("arm64", "x86_64"):
16
17
  assert arch in ARCH_TO_DOCKER_PLATFORM
17
- assert arch in ARCH_TO_PIP_PLATFORM
18
+ assert arch in ARCH_TO_PIP_PLATFORMS
18
19
 
19
20
 
20
21
  def test_arch_mapping_values():
21
22
  assert ARCH_TO_DOCKER_PLATFORM["arm64"] == "linux/arm64"
22
23
  assert ARCH_TO_DOCKER_PLATFORM["x86_64"] == "linux/amd64"
23
- assert ARCH_TO_PIP_PLATFORM["arm64"] == "manylinux_2_17_aarch64"
24
- assert ARCH_TO_PIP_PLATFORM["x86_64"] == "manylinux_2_17_x86_64"
24
+
25
+
26
+ def test_pip_platform_flags_span_the_range_and_cap_at_2_34():
27
+ """Multiple --platform flags so pip picks the best COMPILED wheel per package; a single
28
+ tag silently dropped compiled wheels (e.g. wrapt -> py3-none-any). Cap at glibc 2.34."""
29
+ for arch, tag_arch in (("x86_64", "x86_64"), ("arm64", "aarch64")):
30
+ flags = pip_platform_flags(arch)
31
+ tags = ARCH_TO_PIP_PLATFORMS[arch]
32
+ # Repeated --platform, one per tag, in declared (newest-first) order.
33
+ assert flags == " ".join(f"--platform {t}" for t in tags)
34
+ assert flags.count("--platform ") == len(tags)
35
+ # Must include both the historical floors that each broke one direction.
36
+ assert f"manylinux_2_17_{tag_arch}" in flags # pydantic-core (2_17-only) still resolves
37
+ assert f"manylinux_2_28_{tag_arch}" in flags # wrapt's compiled cp313 wheel resolves
38
+ # Never a baseline above the runtime's glibc 2.34 (would be unloadable).
39
+ for n in (35, 36, 38, 40):
40
+ assert f"manylinux_2_{n}_" not in flags
25
41
 
26
42
 
27
43
  def test_build_python_lambda_raises_on_unknown_arch(tmp_path: Path):
@@ -226,3 +226,74 @@ def test_cli_promote_unknown_target_exits_2(tmp_path: Path):
226
226
  ],
227
227
  )
228
228
  assert result.exit_code == 2
229
+
230
+
231
+ _EXPLICIT_SHA = "a" * 64 # 64-hex, deliberately NOT the catalog's "cafef00d"
232
+
233
+
234
+ def test_cli_promote_explicit_sha_bypasses_catalog(tmp_path: Path):
235
+ repo = _consumer(tmp_path)
236
+ with mock_aws():
237
+ c = boto3.client("s3", region_name="eu-west-1")
238
+ _make_bucket(c, DEV, "eu-west-1")
239
+ _make_bucket(c, PROD, "eu-west-1")
240
+ # Only the explicit-sha object exists; the catalog's current sha does not.
241
+ c.put_object(Bucket=DEV, Key=f"lambdas/app/{_EXPLICIT_SHA}.zip", Body=b"z")
242
+
243
+ result = runner.invoke(
244
+ app,
245
+ [
246
+ "promote",
247
+ "app",
248
+ "--manifest",
249
+ str(repo / "lambdas.toml"),
250
+ "--dev-bucket",
251
+ DEV,
252
+ "--prod-bucket",
253
+ PROD,
254
+ "--sha",
255
+ _EXPLICIT_SHA,
256
+ ],
257
+ )
258
+ assert result.exit_code == 0, result.stdout
259
+ assert c.head_object(Bucket=PROD, Key=f"lambdas/app/{_EXPLICIT_SHA}.zip")
260
+
261
+
262
+ def test_cli_promote_sha_rejects_all_target(tmp_path: Path):
263
+ repo = _consumer(tmp_path)
264
+ result = runner.invoke(
265
+ app,
266
+ [
267
+ "promote",
268
+ "all",
269
+ "--manifest",
270
+ str(repo / "lambdas.toml"),
271
+ "--dev-bucket",
272
+ DEV,
273
+ "--prod-bucket",
274
+ PROD,
275
+ "--sha",
276
+ _EXPLICIT_SHA,
277
+ ],
278
+ )
279
+ assert result.exit_code == 2 # --sha forbidden with 'all'
280
+
281
+
282
+ def test_cli_promote_sha_invalid_hex_exits_2(tmp_path: Path):
283
+ repo = _consumer(tmp_path)
284
+ result = runner.invoke(
285
+ app,
286
+ [
287
+ "promote",
288
+ "app",
289
+ "--manifest",
290
+ str(repo / "lambdas.toml"),
291
+ "--dev-bucket",
292
+ DEV,
293
+ "--prod-bucket",
294
+ PROD,
295
+ "--sha",
296
+ "not-a-valid-sha",
297
+ ],
298
+ )
299
+ assert result.exit_code == 2 # not 64-hex
@@ -646,7 +646,7 @@ wheels = [
646
646
 
647
647
  [[package]]
648
648
  name = "repro-lambda"
649
- version = "0.5.1"
649
+ version = "0.6.0"
650
650
  source = { editable = "." }
651
651
  dependencies = [
652
652
  { name = "boto3" },
File without changes
File without changes
File without changes
File without changes