repro-lambda 0.4.2__tar.gz → 0.5.1__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.4.2 → repro_lambda-0.5.1}/.github/workflows/build.yml +20 -0
- {repro_lambda-0.4.2 → repro_lambda-0.5.1}/CHANGELOG.md +18 -0
- {repro_lambda-0.4.2 → repro_lambda-0.5.1}/PKG-INFO +2 -1
- {repro_lambda-0.4.2 → repro_lambda-0.5.1}/SETUP.md +76 -0
- {repro_lambda-0.4.2 → repro_lambda-0.5.1}/pyproject.toml +2 -2
- {repro_lambda-0.4.2 → repro_lambda-0.5.1}/src/repro_lambda/__init__.py +1 -1
- {repro_lambda-0.4.2 → repro_lambda-0.5.1}/src/repro_lambda/build.py +16 -0
- {repro_lambda-0.4.2 → repro_lambda-0.5.1}/src/repro_lambda/cli.py +31 -1
- {repro_lambda-0.4.2 → repro_lambda-0.5.1}/src/repro_lambda/hasher.py +26 -1
- repro_lambda-0.5.1/src/repro_lambda/manifest.py +478 -0
- repro_lambda-0.5.1/src/repro_lambda/source_locker.py +149 -0
- repro_lambda-0.5.1/src/repro_lambda/sources.py +473 -0
- repro_lambda-0.5.1/tests/test_source_locker.py +137 -0
- repro_lambda-0.5.1/tests/test_sources.py +449 -0
- repro_lambda-0.5.1/tests/test_sources_hash.py +109 -0
- repro_lambda-0.5.1/tests/test_sources_schema.py +246 -0
- {repro_lambda-0.4.2 → repro_lambda-0.5.1}/uv.lock +12 -1
- repro_lambda-0.4.2/src/repro_lambda/manifest.py +0 -228
- {repro_lambda-0.4.2 → repro_lambda-0.5.1}/.github/workflows/ci.yml +0 -0
- {repro_lambda-0.4.2 → repro_lambda-0.5.1}/.github/workflows/move-major-tag.yml +0 -0
- {repro_lambda-0.4.2 → repro_lambda-0.5.1}/.github/workflows/promote.yml +0 -0
- {repro_lambda-0.4.2 → repro_lambda-0.5.1}/.github/workflows/publish.yml +0 -0
- {repro_lambda-0.4.2 → repro_lambda-0.5.1}/.gitignore +0 -0
- {repro_lambda-0.4.2 → repro_lambda-0.5.1}/.pre-commit-config.yaml +0 -0
- {repro_lambda-0.4.2 → repro_lambda-0.5.1}/LICENSE +0 -0
- {repro_lambda-0.4.2 → repro_lambda-0.5.1}/README.md +0 -0
- {repro_lambda-0.4.2 → repro_lambda-0.5.1}/src/repro_lambda/__main__.py +0 -0
- {repro_lambda-0.4.2 → repro_lambda-0.5.1}/src/repro_lambda/catalog.py +0 -0
- {repro_lambda-0.4.2 → repro_lambda-0.5.1}/src/repro_lambda/docker_runner.py +0 -0
- {repro_lambda-0.4.2 → repro_lambda-0.5.1}/src/repro_lambda/git_guard.py +0 -0
- {repro_lambda-0.4.2 → repro_lambda-0.5.1}/src/repro_lambda/promote.py +0 -0
- {repro_lambda-0.4.2 → repro_lambda-0.5.1}/src/repro_lambda/s3_uploader.py +0 -0
- {repro_lambda-0.4.2 → repro_lambda-0.5.1}/src/repro_lambda/source_stager.py +0 -0
- {repro_lambda-0.4.2 → repro_lambda-0.5.1}/src/repro_lambda/verify.py +0 -0
- {repro_lambda-0.4.2 → repro_lambda-0.5.1}/src/repro_lambda/zip_packager.py +0 -0
- {repro_lambda-0.4.2 → repro_lambda-0.5.1}/tests/__init__.py +0 -0
- {repro_lambda-0.4.2 → repro_lambda-0.5.1}/tests/conftest.py +0 -0
- {repro_lambda-0.4.2 → repro_lambda-0.5.1}/tests/fixtures/sample_nodejs_lambda/handler/index.js +0 -0
- {repro_lambda-0.4.2 → repro_lambda-0.5.1}/tests/fixtures/sample_nodejs_lambda/handler/package-lock.json +0 -0
- {repro_lambda-0.4.2 → repro_lambda-0.5.1}/tests/fixtures/sample_nodejs_lambda/handler/package.json +0 -0
- {repro_lambda-0.4.2 → repro_lambda-0.5.1}/tests/fixtures/sample_nodejs_lambda/lambdas.toml +0 -0
- {repro_lambda-0.4.2 → repro_lambda-0.5.1}/tests/fixtures/sample_python_lambda/handler/app.py +0 -0
- {repro_lambda-0.4.2 → repro_lambda-0.5.1}/tests/fixtures/sample_python_lambda/handler/requirements.arm64.lock +0 -0
- {repro_lambda-0.4.2 → repro_lambda-0.5.1}/tests/fixtures/sample_python_lambda/handler/requirements.in +0 -0
- {repro_lambda-0.4.2 → repro_lambda-0.5.1}/tests/fixtures/sample_python_lambda/lambdas.toml +0 -0
- {repro_lambda-0.4.2 → repro_lambda-0.5.1}/tests/test_build_integration.py +0 -0
- {repro_lambda-0.4.2 → repro_lambda-0.5.1}/tests/test_build_nodejs.py +0 -0
- {repro_lambda-0.4.2 → repro_lambda-0.5.1}/tests/test_catalog.py +0 -0
- {repro_lambda-0.4.2 → repro_lambda-0.5.1}/tests/test_cli_build.py +0 -0
- {repro_lambda-0.4.2 → repro_lambda-0.5.1}/tests/test_cli_lock.py +0 -0
- {repro_lambda-0.4.2 → repro_lambda-0.5.1}/tests/test_cli_smoke.py +0 -0
- {repro_lambda-0.4.2 → repro_lambda-0.5.1}/tests/test_docker_runner.py +0 -0
- {repro_lambda-0.4.2 → repro_lambda-0.5.1}/tests/test_docker_runner_nodejs.py +0 -0
- {repro_lambda-0.4.2 → repro_lambda-0.5.1}/tests/test_e2e_nodejs_lambda.py +0 -0
- {repro_lambda-0.4.2 → repro_lambda-0.5.1}/tests/test_e2e_python_lambda.py +0 -0
- {repro_lambda-0.4.2 → repro_lambda-0.5.1}/tests/test_extra_files.py +0 -0
- {repro_lambda-0.4.2 → repro_lambda-0.5.1}/tests/test_git_guard.py +0 -0
- {repro_lambda-0.4.2 → repro_lambda-0.5.1}/tests/test_hasher.py +0 -0
- {repro_lambda-0.4.2 → repro_lambda-0.5.1}/tests/test_manifest.py +0 -0
- {repro_lambda-0.4.2 → repro_lambda-0.5.1}/tests/test_per_lambda_builder.py +0 -0
- {repro_lambda-0.4.2 → repro_lambda-0.5.1}/tests/test_promote.py +0 -0
- {repro_lambda-0.4.2 → repro_lambda-0.5.1}/tests/test_python_byte_compat_regression.py +0 -0
- {repro_lambda-0.4.2 → repro_lambda-0.5.1}/tests/test_s3_uploader.py +0 -0
- {repro_lambda-0.4.2 → repro_lambda-0.5.1}/tests/test_source_stager.py +0 -0
- {repro_lambda-0.4.2 → repro_lambda-0.5.1}/tests/test_verify.py +0 -0
- {repro_lambda-0.4.2 → repro_lambda-0.5.1}/tests/test_zip_excludes.py +0 -0
- {repro_lambda-0.4.2 → repro_lambda-0.5.1}/tests/test_zip_packager.py +0 -0
|
@@ -25,6 +25,14 @@ on:
|
|
|
25
25
|
required: false
|
|
26
26
|
default: ""
|
|
27
27
|
description: S3 bucket name for prod Lambda artifacts (master push only).
|
|
28
|
+
secrets:
|
|
29
|
+
sources_token:
|
|
30
|
+
required: false
|
|
31
|
+
description: >-
|
|
32
|
+
Token with read access to the private release repos referenced by any
|
|
33
|
+
github_release [[lambda.source]]. Exported as REPRO_LAMBDA_SOURCES_TOKEN for the
|
|
34
|
+
build. Omit when every source is a public https URL. Generic by design - the
|
|
35
|
+
reusable workflow never names a consumer repo.
|
|
28
36
|
|
|
29
37
|
jobs:
|
|
30
38
|
detect-arches:
|
|
@@ -64,6 +72,16 @@ jobs:
|
|
|
64
72
|
- uses: actions/checkout@v7
|
|
65
73
|
- uses: astral-sh/setup-uv@v8.2.0
|
|
66
74
|
|
|
75
|
+
# Content-addressed source cache, scoped per-arch (a source's sha256 is its key, so a
|
|
76
|
+
# re-pin changes the manifest hash -> fresh fetch; restore-keys reuses the prior cache).
|
|
77
|
+
- name: Cache fetched sources
|
|
78
|
+
uses: actions/cache@v4
|
|
79
|
+
with:
|
|
80
|
+
path: builds/.sources-cache
|
|
81
|
+
key: repro-sources-${{ matrix.arch }}-${{ hashFiles(inputs.manifest_path) }}
|
|
82
|
+
restore-keys: |
|
|
83
|
+
repro-sources-${{ matrix.arch }}-
|
|
84
|
+
|
|
67
85
|
- name: Configure AWS credentials (dev)
|
|
68
86
|
uses: aws-actions/configure-aws-credentials@v6
|
|
69
87
|
with:
|
|
@@ -73,6 +91,7 @@ jobs:
|
|
|
73
91
|
- name: Build (dev bucket)
|
|
74
92
|
env:
|
|
75
93
|
REPRO_LAMBDA_BUCKET: ${{ inputs.dev_bucket }}
|
|
94
|
+
REPRO_LAMBDA_SOURCES_TOKEN: ${{ secrets.sources_token }}
|
|
76
95
|
run: uvx --from repro-lambda repro-lambda build --manifest "${{ inputs.manifest_path }}" --arch "${{ matrix.arch }}"
|
|
77
96
|
|
|
78
97
|
- name: Verify reproducible (PR only)
|
|
@@ -90,6 +109,7 @@ jobs:
|
|
|
90
109
|
if: github.ref == 'refs/heads/master' && inputs.aws_prod_role_arn != ''
|
|
91
110
|
env:
|
|
92
111
|
REPRO_LAMBDA_BUCKET: ${{ inputs.prod_bucket }}
|
|
112
|
+
REPRO_LAMBDA_SOURCES_TOKEN: ${{ secrets.sources_token }}
|
|
93
113
|
run: uvx --from repro-lambda repro-lambda build --manifest "${{ inputs.manifest_path }}" --arch "${{ matrix.arch }}"
|
|
94
114
|
|
|
95
115
|
- name: Commit catalog drift (master only, dev bot)
|
|
@@ -1,5 +1,23 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## v0.5.1 - 2026-06-21
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
- Send a `User-Agent` header on every source fetch. The GitHub REST API rejects requests with no User-Agent (HTTP 403), which broke `github_release` source resolution (the asset lookup against `api.github.com`). The header is now `repro-lambda/<version>` on all requests (harmless for plain `https` sources, required for the API). Does not affect any content hash (request headers are not part of artifact identity).
|
|
7
|
+
|
|
8
|
+
## v0.5.0 - 2026-06-21
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- 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) and `https` (a direct `url`). Each source is PINNED: `sha256` is verified before extraction; `extract` is `zip`/`tar.gz`/`none`; an optional `member` extracts a single file or a (versioned) directory subtree to `dest` (package-root-relative); `executable` sets +x. Source names are required and unique per lambda; dest collisions (including tree overlaps and overlaps with the staged source) are refused.
|
|
12
|
+
- `version_from` (single-level): a source can derive its `version` at lock time from an asdf-style `key value` line in a referenced source's file (read relative to that source's member-stripped tree). `{version}` is substituted into `url`/`tag`/`asset`/`member`. `version_from` is a lock input and never participates in the content hash.
|
|
13
|
+
- `lock --sources` re-resolves `version_from`, re-downloads each source, recomputes its sha256, and rewrites `lambdas.toml` in place with tomlkit (comments preserved, atomic). It is idempotent: a run that changes nothing leaves the file byte-for-byte unchanged (no spurious PR). `lock` keeps regenerating requirements locks too; use `--no-requirements` / `--no-sources` to scope it.
|
|
14
|
+
- The reusable `build.yml` gains a generic `sources_token` secret (exported as `REPRO_LAMBDA_SOURCES_TOKEN`, used only for `github_release` API calls) and an arch-scoped `actions/cache` for the content-addressed source cache.
|
|
15
|
+
|
|
16
|
+
### Security
|
|
17
|
+
- 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). `Authorization` is 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).
|
|
18
|
+
- 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).
|
|
19
|
+
- 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.
|
|
20
|
+
|
|
3
21
|
## v0.4.2 - 2026-06-21
|
|
4
22
|
|
|
5
23
|
### Added
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: repro-lambda
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.5.1
|
|
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
|
|
@@ -20,6 +20,7 @@ Classifier: Topic :: Software Development :: Build Tools
|
|
|
20
20
|
Requires-Python: >=3.11
|
|
21
21
|
Requires-Dist: boto3>=1.34
|
|
22
22
|
Requires-Dist: repro-zipfile>=0.3.1
|
|
23
|
+
Requires-Dist: tomlkit>=0.13
|
|
23
24
|
Requires-Dist: typer>=0.12
|
|
24
25
|
Provides-Extra: dev
|
|
25
26
|
Requires-Dist: moto[s3]>=5; extra == 'dev'
|
|
@@ -330,6 +330,82 @@ The resolved per-lambda builder (base-image digest + include/exclude lists +
|
|
|
330
330
|
builder version) folds into the content hash, so changing an override re-keys
|
|
331
331
|
that lambda's artifact while leaving the others untouched.
|
|
332
332
|
|
|
333
|
+
## Declarative sources — `[[lambda.source]]`
|
|
334
|
+
|
|
335
|
+
A lambda can bundle pinned external artifacts (a release tarball, a vendored CLI
|
|
336
|
+
binary) fetched at build time, instead of a hand-rolled download script. Each
|
|
337
|
+
`[[lambda.source]]` is fully pinned and fetched + verified + extracted into the
|
|
338
|
+
package before the container build:
|
|
339
|
+
|
|
340
|
+
```toml
|
|
341
|
+
[[lambda]]
|
|
342
|
+
logical_name = "app"
|
|
343
|
+
source_dir = "src/app"
|
|
344
|
+
requirements_lock = "src/app/requirements.${arch}.lock"
|
|
345
|
+
runtime = "python3.13"
|
|
346
|
+
arch = "arm64"
|
|
347
|
+
handler = "app.lambda_handler"
|
|
348
|
+
|
|
349
|
+
# A private GitHub release tarball -> staged at package path "vendor".
|
|
350
|
+
[[lambda.source]]
|
|
351
|
+
name = "vendor"
|
|
352
|
+
type = "github_release"
|
|
353
|
+
repo = "owner/vendor-tool"
|
|
354
|
+
tag = "vendor-v{version}"
|
|
355
|
+
asset = "vendor-{version}.tar.gz"
|
|
356
|
+
sha256 = "<64-hex; written by `repro-lambda lock`>"
|
|
357
|
+
extract = "tar.gz"
|
|
358
|
+
member = "vendor-{version}" # map the versioned top dir to dest
|
|
359
|
+
dest = "vendor"
|
|
360
|
+
version = "1.4.0" # bump this one line; lock re-pins everything
|
|
361
|
+
|
|
362
|
+
# A public binary whose version is derived from the vendor release's .tool-versions.
|
|
363
|
+
[[lambda.source]]
|
|
364
|
+
name = "terraform"
|
|
365
|
+
type = "https"
|
|
366
|
+
url = "https://releases.hashicorp.com/terraform/{version}/terraform_{version}_linux_arm64.zip"
|
|
367
|
+
sha256 = "<64-hex; written by lock>"
|
|
368
|
+
extract = "zip"
|
|
369
|
+
member = "terraform"
|
|
370
|
+
dest = "bin/terraform"
|
|
371
|
+
executable = true
|
|
372
|
+
version = "1.9.0"
|
|
373
|
+
[lambda.source.version_from]
|
|
374
|
+
source = "vendor" # read from the vendor source's extracted tree
|
|
375
|
+
file = ".tool-versions" # relative to its member-stripped root
|
|
376
|
+
key = "terraform"
|
|
377
|
+
```
|
|
378
|
+
|
|
379
|
+
- **Pinning.** `sha256` is verified before the archive is opened. `extract` is
|
|
380
|
+
`zip` / `tar.gz` / `none`. `member` extracts one file or a directory subtree to
|
|
381
|
+
`dest`; omit it to extract the whole archive under `dest`. Source names are
|
|
382
|
+
unique per lambda and dests may not overlap each other or the staged source.
|
|
383
|
+
- **`version_from`** (single-level) derives a source's `version` from an asdf-style
|
|
384
|
+
`key value` line in another source's file, so bumping the root `version`
|
|
385
|
+
cascades to dependents. It is a lock input - it never affects the artifact hash.
|
|
386
|
+
- **`repro-lambda lock`** re-resolves `version_from`, re-downloads, recomputes each
|
|
387
|
+
`sha256`, and rewrites this file (comment-preserving, atomic, idempotent). Run it
|
|
388
|
+
after bumping a `version`. Pass `REPRO_LAMBDA_SOURCES_TOKEN` for private
|
|
389
|
+
`github_release` sources.
|
|
390
|
+
- **Security.** Fetches are HTTPS-only with an SSRF guard (no private/loopback/
|
|
391
|
+
link-local/metadata IPs), strip `Authorization` on cross-host redirects, verify
|
|
392
|
+
sha256 before extraction, and reject path-traversal / link / device entries with
|
|
393
|
+
decompression-bomb bounds. See `src/repro_lambda/sources.py`.
|
|
394
|
+
|
|
395
|
+
In CI, pass the token to the reusable `build.yml` as the `sources_token` secret:
|
|
396
|
+
|
|
397
|
+
```yaml
|
|
398
|
+
jobs:
|
|
399
|
+
build:
|
|
400
|
+
uses: antonbabenko/repro-lambda/.github/workflows/build.yml@v0
|
|
401
|
+
with:
|
|
402
|
+
manifest_path: lambdas.toml
|
|
403
|
+
aws_dev_role_arn: arn:aws:iam::<account>:role/<dev-builder-role>
|
|
404
|
+
dev_bucket: <env>-my-lambda-artifacts
|
|
405
|
+
secrets:
|
|
406
|
+
sources_token: ${{ secrets.MY_RELEASE_TOKEN }}
|
|
407
|
+
```
|
|
408
|
+
|
|
333
409
|
## Terraform consumer — `s3_existing_package`
|
|
334
410
|
|
|
335
411
|
In the Terraform that creates your Lambda function, point at the artifact in
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "repro-lambda"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.5.1"
|
|
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"
|
|
@@ -21,7 +21,7 @@ dependencies = [
|
|
|
21
21
|
"typer>=0.12",
|
|
22
22
|
"repro-zipfile>=0.3.1",
|
|
23
23
|
"boto3>=1.34",
|
|
24
|
-
|
|
24
|
+
"tomlkit>=0.13",
|
|
25
25
|
]
|
|
26
26
|
|
|
27
27
|
[project.optional-dependencies]
|
|
@@ -88,6 +88,7 @@ def compute_sha_for(
|
|
|
88
88
|
payload_exec=[(ef.dest, ef.executable) for ef in spec.extra_files],
|
|
89
89
|
include_patterns=builder.include_patterns,
|
|
90
90
|
exclude_patterns=builder.exclude_patterns,
|
|
91
|
+
sources=spec.sources,
|
|
91
92
|
)
|
|
92
93
|
|
|
93
94
|
|
|
@@ -100,6 +101,8 @@ def build_one(
|
|
|
100
101
|
catalog: Catalog,
|
|
101
102
|
source_commit: str,
|
|
102
103
|
dry_run: bool = False,
|
|
104
|
+
sources_token: str | None = None,
|
|
105
|
+
sources_cache: Path | None = None,
|
|
103
106
|
) -> BuildOutcome:
|
|
104
107
|
"""Build one lambda end-to-end. Returns BuildOutcome with sha + cache verdict."""
|
|
105
108
|
builder = resolve_builder(builder, spec)
|
|
@@ -132,6 +135,7 @@ def build_one(
|
|
|
132
135
|
payload_exec=[(ef.dest, ef.executable) for ef in spec.extra_files],
|
|
133
136
|
include_patterns=builder.include_patterns,
|
|
134
137
|
exclude_patterns=builder.exclude_patterns,
|
|
138
|
+
sources=spec.sources,
|
|
135
139
|
)
|
|
136
140
|
bucket_key = f"lambdas/{spec.logical_name}/{sha}.zip"
|
|
137
141
|
|
|
@@ -143,6 +147,18 @@ def build_one(
|
|
|
143
147
|
_record(catalog, spec, sha, source_commit, builder)
|
|
144
148
|
return BuildOutcome(BuildResult.CACHE_HIT, sha, bucket_key)
|
|
145
149
|
|
|
150
|
+
# Cache miss only: fetch + verify + extract declarative sources into the staged
|
|
151
|
+
# tree (post-filter; collides loudly with already-staged source/payload files).
|
|
152
|
+
if spec.sources:
|
|
153
|
+
from repro_lambda.sources import fetch_sources
|
|
154
|
+
|
|
155
|
+
fetch_sources(
|
|
156
|
+
sources=spec.sources,
|
|
157
|
+
dest_root=stage_dir / "source",
|
|
158
|
+
cache_dir=sources_cache or (repo_root / "builds" / ".sources-cache"),
|
|
159
|
+
github_token=sources_token,
|
|
160
|
+
)
|
|
161
|
+
|
|
146
162
|
out_zip = stage_dir / "lambda.zip"
|
|
147
163
|
if spec.package_manager == "pip":
|
|
148
164
|
build_python_lambda(
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
+
import os
|
|
5
6
|
import subprocess
|
|
6
7
|
from pathlib import Path
|
|
7
8
|
from typing import Annotated
|
|
@@ -128,6 +129,8 @@ def build(
|
|
|
128
129
|
except subprocess.CalledProcessError:
|
|
129
130
|
source_commit = "unknown"
|
|
130
131
|
|
|
132
|
+
sources_token = os.environ.get("REPRO_LAMBDA_SOURCES_TOKEN") or None
|
|
133
|
+
|
|
131
134
|
summary = []
|
|
132
135
|
for spec in selected:
|
|
133
136
|
outcome = build_one(
|
|
@@ -138,6 +141,7 @@ def build(
|
|
|
138
141
|
catalog=catalog,
|
|
139
142
|
source_commit=source_commit,
|
|
140
143
|
dry_run=dry_run,
|
|
144
|
+
sources_token=sources_token,
|
|
141
145
|
)
|
|
142
146
|
summary.append(
|
|
143
147
|
{
|
|
@@ -261,13 +265,39 @@ def promote(
|
|
|
261
265
|
@app.command()
|
|
262
266
|
def lock(
|
|
263
267
|
manifest: Annotated[Path, typer.Option("--manifest", "-m")] = Path("lambdas.toml"),
|
|
268
|
+
requirements: Annotated[
|
|
269
|
+
bool,
|
|
270
|
+
typer.Option(
|
|
271
|
+
"--requirements/--no-requirements",
|
|
272
|
+
help="Regenerate per-arch requirements.${arch}.lock via uv pip compile.",
|
|
273
|
+
),
|
|
274
|
+
] = True,
|
|
275
|
+
sources: Annotated[
|
|
276
|
+
bool,
|
|
277
|
+
typer.Option(
|
|
278
|
+
"--sources/--no-sources",
|
|
279
|
+
help="Re-resolve version_from + re-pin [[lambda.source]] sha256 in the manifest.",
|
|
280
|
+
),
|
|
281
|
+
] = True,
|
|
264
282
|
) -> None:
|
|
265
|
-
"""
|
|
283
|
+
"""Lock pinned inputs: per-arch requirements locks and/or declarative source pins."""
|
|
266
284
|
from repro_lambda.manifest import load_manifest
|
|
267
285
|
|
|
268
286
|
parsed = load_manifest(manifest)
|
|
269
287
|
repo_root = manifest.parent.resolve()
|
|
270
288
|
|
|
289
|
+
if requirements:
|
|
290
|
+
_lock_requirements(parsed, repo_root)
|
|
291
|
+
|
|
292
|
+
if sources:
|
|
293
|
+
from repro_lambda.source_locker import lock_sources
|
|
294
|
+
|
|
295
|
+
token = os.environ.get("REPRO_LAMBDA_SOURCES_TOKEN") or None
|
|
296
|
+
changed = lock_sources(manifest, token)
|
|
297
|
+
typer.echo(f"lock sources: {'updated ' + str(manifest) if changed else 'no changes'}")
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
def _lock_requirements(parsed, repo_root: Path) -> None:
|
|
271
301
|
for spec in parsed.lambdas:
|
|
272
302
|
if spec.package_manager == "npm":
|
|
273
303
|
typer.echo(
|
|
@@ -5,7 +5,7 @@ from __future__ import annotations
|
|
|
5
5
|
import hashlib
|
|
6
6
|
from pathlib import Path
|
|
7
7
|
|
|
8
|
-
from repro_lambda.manifest import LambdaSpec
|
|
8
|
+
from repro_lambda.manifest import LambdaSpec, Source
|
|
9
9
|
|
|
10
10
|
|
|
11
11
|
def _sha256_file(path: Path) -> str:
|
|
@@ -27,6 +27,7 @@ def compute_content_hash(
|
|
|
27
27
|
payload_exec: list[tuple[str, bool]] | None = None,
|
|
28
28
|
include_patterns: list[str] | None = None,
|
|
29
29
|
exclude_patterns: list[str] | None = None,
|
|
30
|
+
sources: tuple[Source, ...] | None = None,
|
|
30
31
|
) -> str:
|
|
31
32
|
"""
|
|
32
33
|
sha256 over: sorted (relative-path, sha256(content)) tuples for the staged tree
|
|
@@ -46,6 +47,12 @@ def compute_content_hash(
|
|
|
46
47
|
not re-key) and only when not None, so an explicit empty list (replace-with-empty)
|
|
47
48
|
hashes differently from an unset/None filter. Callers passing None omit the
|
|
48
49
|
section entirely, preserving hashes for code paths that do not resolve a builder.
|
|
50
|
+
|
|
51
|
+
sources are the declarative [[lambda.source]] entries. Their RESOLVED metadata
|
|
52
|
+
(the {version}-substituted url/tag/asset/member plus type/repo/extract/dest/
|
|
53
|
+
executable/sha256), sorted by name, folds in - NOT the downloaded bytes, so the
|
|
54
|
+
hash stays download-free. version_from is a lock input and never folded; a
|
|
55
|
+
member/extract/dest/sha256 change re-keys. Omitted entirely when there are none.
|
|
49
56
|
"""
|
|
50
57
|
h = hashlib.sha256()
|
|
51
58
|
|
|
@@ -102,4 +109,22 @@ def compute_content_hash(
|
|
|
102
109
|
for dest, executable in sorted(payload_exec):
|
|
103
110
|
h.update(f"{dest}={int(executable)}\n".encode())
|
|
104
111
|
|
|
112
|
+
# Declarative sources: fold resolved metadata only (no bytes), sorted by name.
|
|
113
|
+
# version_from is intentionally excluded (it is a lock input, not artifact identity).
|
|
114
|
+
if sources:
|
|
115
|
+
h.update(b"---sources---\n")
|
|
116
|
+
for s in sorted(sources, key=lambda s: s.name):
|
|
117
|
+
h.update(f"name={s.name}\n".encode())
|
|
118
|
+
h.update(f"type={s.type}\n".encode())
|
|
119
|
+
h.update(f"repo={s.repo}\n".encode())
|
|
120
|
+
h.update(f"url={s.resolved_url}\n".encode())
|
|
121
|
+
h.update(f"tag={s.resolved_tag}\n".encode())
|
|
122
|
+
h.update(f"asset={s.resolved_asset}\n".encode())
|
|
123
|
+
h.update(f"member={s.resolved_member or ''}\n".encode())
|
|
124
|
+
h.update(f"extract={s.extract}\n".encode())
|
|
125
|
+
h.update(f"dest={s.dest}\n".encode())
|
|
126
|
+
h.update(f"executable={int(s.executable)}\n".encode())
|
|
127
|
+
h.update(f"sha256={s.sha256}\n".encode())
|
|
128
|
+
h.update(b"--\n")
|
|
129
|
+
|
|
105
130
|
return h.hexdigest()
|