-
Notifications
You must be signed in to change notification settings - Fork 0
Ship reproducible pubskill-lib v0.2.0 artifacts and installed provenance #5
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
24 commits
Select commit
Hold shift + click to select a range
9fee916
Make v0.2 distribution provenance and artifact builds reproducible
erinepshovel-code fcb6a7b
Replay complete source fixtures against reproducible wheel artifacts
erinepshovel-code 66c299c
Preserve BOMs across adapters and complete Node option arity
erinepshovel-code 6023357
Handle Node test runner value options and aliases
erinepshovel-code a07c16d
Pin isolated backend and replay the clean source installation
erinepshovel-code 57c46a2
Preserve shell quoting, URL entrypoints, and canonical parser bytes
erinepshovel-code ee60fdb
Preserve literal source data and concurrent edits during examination
erinepshovel-code c65f403
Preserve source inodes and publish without replacing competing edits
erinepshovel-code 08acfb0
Enforce canonical placement and expose safe source recovery
erinepshovel-code 22193d9
Keep recovery CLI regressions valid after fixture annotation
erinepshovel-code f0f4224
Close audit context gaps and preserve source metadata and stale evidence
erinepshovel-code 03aa395
Keep unresolved shell expansion contexts out of literal path findings
erinepshovel-code e5c5dee
Use declared boundary values for release tooling
erinepshovel-code 8d351da
Keep literal entrypoints visible around dynamic child arguments
erinepshovel-code 71c6369
Close shell operand and undecodable evidence hash gaps
erinepshovel-code 80a935a
Restrict assignment tilde expansion to its first separator or colons
erinepshovel-code 311bcc6
Merge remote-tracking branch 'origin/main' into release/v0.2.0
erinepshovel-code 1aeff0c
Record the build interpreter in release provenance
erinepshovel-code 785b2d8
Ignore generated packaging output during release verification
erinepshovel-code b9e20e8
Normalize wheel metadata across build environments
erinepshovel-code 9ad8f81
Bind release metadata and CI to the selected source commit
erinepshovel-code 4001c46
Filter source archive extraction before release builds
erinepshovel-code 24622c9
Bind release builds to the qualified zlib compressor
erinepshovel-code 0c305f0
Install build dependencies with the selected uv environment
erinepshovel-code File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
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
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2,6 +2,8 @@ | |
| __pycache__/ | ||
| *.pyc | ||
| *.egg-info/ | ||
| build/ | ||
| dist/ | ||
| .env | ||
| .env.* | ||
| !.env.example | ||
|
|
||
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,8 @@ | ||
| include AGENTS.md SOURCE.md HANDOFF.md HANDOFF.vm.md .env.example | ||
| include .gitignore | ||
| include requirements-build.txt | ||
| graft .agents/skills | ||
| graft examples | ||
| graft tests | ||
| graft tools | ||
| global-exclude __pycache__ *.py[cod] |
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
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
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| build==1.6.1 | ||
| packaging==26.3 | ||
| pyproject-hooks==1.2.0 | ||
| setuptools==84.0.0 | ||
| wheel==0.48.0 |
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| { | ||
| "schema": "pubskill-lib.source", | ||
| "version": 1, | ||
| "repository": "The-Interdependency/skill-lib", | ||
| "commit": "8de4f12d0f31ff94f41e4a0196c447c0cbe20faf" | ||
| } |
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
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
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,14 @@ | ||
| """Usage: python -m unittest discover -s tests. Reject an unqualified compressor.""" | ||
| import unittest | ||
| from unittest.mock import patch | ||
| from tools.build_release import check_compressor | ||
|
|
||
|
|
||
| class CompressorTest(unittest.TestCase): | ||
| def test_compressor_identity_is_enforced(self): | ||
| with patch("tools.build_release.zlib.ZLIB_VERSION", "1.3.1"), patch("tools.build_release.zlib.ZLIB_RUNTIME_VERSION", "1.3.1"): | ||
| self.assertEqual(check_compressor()["runtime_version"], "1.3.1") | ||
| for compile_version, runtime_version in (("1.3", "1.3.1"), ("1.3.1", "1.3")): | ||
| with patch("tools.build_release.zlib.ZLIB_VERSION", compile_version), patch("tools.build_release.zlib.ZLIB_RUNTIME_VERSION", runtime_version): | ||
| with self.assertRaisesRegex(RuntimeError, "require zlib"): | ||
| check_compressor() |
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,146 @@ | ||
| # === MODULE_BUILD === | ||
| # id: pubskill_release_builder | ||
| # module_name: build_release | ||
| # module_kind: instrument | ||
| # summary: builds normalized immutable wheel and sdist artifacts from a clean exact Git commit | ||
| # owner: The Interdependency | ||
| # public_surface: python tools/build_release.py --out DIRECTORY | ||
| # internal_surface: normalize_sdist, normalize_wheel, main | ||
| # auth_boundary: none | ||
| # storage_boundary: write | ||
| # storage_notes: temporary build directory and explicit output directory | ||
| # network_boundary: none | ||
| # network_notes: build dependencies must already be installed | ||
| # user_data_boundary: none | ||
| # admin_only: false | ||
| # tests: clean-install repository suite and two-build digest comparison documented in README | ||
| # rollout: explicit release build command | ||
| # rollback: return to previous published immutable release | ||
| # === END MODULE_BUILD === | ||
| # === CONTRACTS === | ||
| # id: release_build_binds_exact_source | ||
| # given: a clean source checkout and the pinned build toolchain | ||
| # then: artifacts derive only from Git HEAD; the manifest records source, doctrine, toolchain and output digests | ||
| # class: provenance | ||
| # === END CONTRACTS === | ||
|
|
||
| """Usage: install requirements-build.txt, then run with --out /tmp/release. | ||
|
|
||
| Run twice into separate empty directories and compare wheel/sdist SHA-256 values. | ||
| The builder performs no publication. Clean-install and consumer gates are required | ||
| before publishing these bytes. Archive headers, order, and permissions are | ||
| normalized to the commit timestamp. Wheel payloads and RECORD are unchanged. | ||
| """ | ||
| from __future__ import annotations | ||
|
|
||
| import argparse | ||
| import gzip | ||
| import hashlib | ||
| import importlib.metadata | ||
| import io | ||
| import json | ||
| import os | ||
| from pathlib import Path | ||
| import subprocess | ||
| import sys | ||
| import tarfile | ||
| import tempfile | ||
| import time | ||
| import zipfile | ||
| import zlib | ||
|
|
||
|
|
||
| def check_compressor() -> dict[str, str]: | ||
| expected = "1.3.1" | ||
| actual = {"implementation": "zlib", "compile_version": zlib.ZLIB_VERSION, "runtime_version": zlib.ZLIB_RUNTIME_VERSION} | ||
| if actual["compile_version"] != expected or actual["runtime_version"] != expected: | ||
| raise RuntimeError(f"release builds require zlib {expected} at compile time and runtime: {actual}") | ||
| return actual | ||
|
|
||
|
|
||
| def normalize_sdist(path: Path, destination: Path, epoch: int) -> None: | ||
| with path.open("rb") as raw, tarfile.open(fileobj=raw, mode="r:gz") as source: | ||
| with destination.open("wb") as output, gzip.GzipFile(filename="", mode="wb", fileobj=output, mtime=epoch) as compressed: | ||
| with tarfile.open(fileobj=compressed, mode="w", format=tarfile.PAX_FORMAT) as target: | ||
| for member in sorted(source.getmembers(), key=lambda item: item.name): | ||
| if not (member.isfile() or member.isdir()): | ||
| raise ValueError(f"unexpected sdist member: {member.name}") | ||
| member.uid = member.gid = 0 | ||
| member.uname = member.gname = "" | ||
| member.mtime = epoch | ||
| member.pax_headers = {} | ||
| member.mode = 0o755 if member.isdir() or member.mode & 0o111 else 0o644 | ||
| if member.isfile(): | ||
| with source.extractfile(member) as stream: | ||
| target.addfile(member, stream) | ||
| else: | ||
| target.addfile(member) | ||
|
|
||
|
|
||
| def normalize_wheel(path: Path, destination: Path, epoch: int) -> None: | ||
| with zipfile.ZipFile(path) as source, zipfile.ZipFile(destination, "w") as target: | ||
| for member in sorted(source.infolist(), key=lambda item: item.filename): | ||
| normalized = zipfile.ZipInfo(member.filename, time.gmtime(epoch)[:6]) | ||
| normalized.create_system = 3 | ||
| mode = 0o40755 if member.is_dir() else 0o100755 if (member.external_attr >> 16) & 0o111 else 0o100644 | ||
| normalized.external_attr = (mode << 16) | (0x10 if member.is_dir() else 0) | ||
| target.writestr(normalized, source.read(member), compress_type=zipfile.ZIP_DEFLATED, compresslevel=9) | ||
|
|
||
|
|
||
| def main() -> None: | ||
| parser = argparse.ArgumentParser(description=__doc__) | ||
| parser.add_argument("--out", type=Path, required=True) | ||
| args = parser.parse_args() | ||
| root = Path(__file__).resolve().parents[1] | ||
| def git(*args): | ||
| return subprocess.check_output(["git", "-C", str(root), *args], text=True).strip() | ||
| if git("status", "--porcelain"): | ||
| raise SystemExit("release build requires a clean Git checkout") | ||
| compressor = check_compressor() | ||
| commit = git("rev-parse", "HEAD") | ||
| epoch = int(git("show", "-s", "--format=%ct", commit)) | ||
| out = args.out.resolve() | ||
| out.mkdir(parents=True, exist_ok=True) | ||
| if any(out.iterdir()): | ||
| raise SystemExit("release output directory must be empty") | ||
| versions = {} | ||
| for requirement in git("show", f"{commit}:requirements-build.txt").splitlines(): | ||
| name, version = requirement.split("==") | ||
| versions[name] = importlib.metadata.version(name) | ||
| if versions[name] != version: | ||
| raise SystemExit(f"build toolchain mismatch: {name}") | ||
| with tempfile.TemporaryDirectory(prefix="pubskill-release-") as directory: | ||
| temporary = Path(directory) | ||
| source = temporary / "source" | ||
| source.mkdir() | ||
| archive = subprocess.check_output(["git", "-C", str(root), "archive", commit]) | ||
| if not hasattr(tarfile, "data_filter"): | ||
| raise SystemExit("release builds require Python with tarfile.data_filter support") | ||
| with tarfile.open(fileobj=io.BytesIO(archive)) as tree: | ||
| for member in tree.getmembers(): | ||
| if member.name.startswith("/") or ".." in Path(member.name).parts or not (member.isfile() or member.isdir()): | ||
| raise ValueError("unsafe source archive") | ||
| tree.extractall(source, filter="data") | ||
| environment = dict(os.environ, SOURCE_DATE_EPOCH=str(epoch), PYTHONHASHSEED="0") | ||
| environment.pop("PYTHONPATH", None) | ||
| subprocess.run([sys.executable, "-m", "build", "--no-isolation", "--outdir", str(temporary / "dist"), str(source)], check=True, env=environment) | ||
| for artifact in sorted((temporary / "dist").iterdir()): | ||
| if artifact.name.endswith(".tar.gz"): | ||
| normalize_sdist(artifact, out / artifact.name, epoch) | ||
| elif artifact.suffix == ".whl": | ||
| normalize_wheel(artifact, out / artifact.name, epoch) | ||
| else: | ||
| raise ValueError(f"unexpected build artifact: {artifact.name}") | ||
| hashes = {path.name: hashlib.sha256(path.read_bytes()).hexdigest() for path in sorted(out.iterdir())} | ||
| doctrine = json.loads(git("show", f"{commit}:src/pubskill_lib/_source.json")) | ||
| manifest = {"schema": "pubskill-lib.release-manifest", "version": 1, "source_commit": commit, "source_tree": git("rev-parse", f"{commit}^{{tree}}"), "source_date_epoch": epoch, "skill_lib_commit": doctrine["commit"], "build_toolchain": versions, "build_python": sys.version, "compressor": compressor, "artifacts_sha256": hashes} | ||
| receipt = out / "release-manifest.json" | ||
| receipt.write_text(json.dumps(manifest, indent=2, sort_keys=True) + "\n") | ||
| hashes = dict(hashes) | ||
| hashes[receipt.name] = hashlib.sha256(receipt.read_bytes()).hexdigest() | ||
| (out / "SHA256SUMS").write_text("".join(f"{digest} {name}\n" for name, digest in sorted(hashes.items()))) | ||
| print(json.dumps(manifest, indent=2)) | ||
|
|
||
|
|
||
| if __name__ == "__main__": | ||
| main() | ||
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.
Uh oh!
There was an error while loading. Please reload this page.