Skip to content
Merged
Show file tree
Hide file tree
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 Sep 12, 2026
fcb6a7b
Replay complete source fixtures against reproducible wheel artifacts
erinepshovel-code Sep 12, 2026
66c299c
Preserve BOMs across adapters and complete Node option arity
erinepshovel-code Sep 12, 2026
6023357
Handle Node test runner value options and aliases
erinepshovel-code Sep 12, 2026
a07c16d
Pin isolated backend and replay the clean source installation
erinepshovel-code Sep 12, 2026
57c46a2
Preserve shell quoting, URL entrypoints, and canonical parser bytes
erinepshovel-code Sep 12, 2026
ee60fdb
Preserve literal source data and concurrent edits during examination
erinepshovel-code Sep 12, 2026
c65f403
Preserve source inodes and publish without replacing competing edits
erinepshovel-code Sep 12, 2026
08acfb0
Enforce canonical placement and expose safe source recovery
erinepshovel-code Sep 12, 2026
22193d9
Keep recovery CLI regressions valid after fixture annotation
erinepshovel-code Sep 12, 2026
f0f4224
Close audit context gaps and preserve source metadata and stale evidence
erinepshovel-code Sep 12, 2026
03aa395
Keep unresolved shell expansion contexts out of literal path findings
erinepshovel-code Sep 12, 2026
e5c5dee
Use declared boundary values for release tooling
erinepshovel-code Sep 12, 2026
8d351da
Keep literal entrypoints visible around dynamic child arguments
erinepshovel-code Sep 12, 2026
71c6369
Close shell operand and undecodable evidence hash gaps
erinepshovel-code Sep 12, 2026
80a935a
Restrict assignment tilde expansion to its first separator or colons
erinepshovel-code Sep 12, 2026
311bcc6
Merge remote-tracking branch 'origin/main' into release/v0.2.0
erinepshovel-code Sep 12, 2026
1aeff0c
Record the build interpreter in release provenance
erinepshovel-code Sep 12, 2026
785b2d8
Ignore generated packaging output during release verification
erinepshovel-code Sep 12, 2026
b9e20e8
Normalize wheel metadata across build environments
erinepshovel-code Sep 12, 2026
9ad8f81
Bind release metadata and CI to the selected source commit
erinepshovel-code Sep 12, 2026
4001c46
Filter source archive extraction before release builds
erinepshovel-code Sep 12, 2026
24622c9
Bind release builds to the qualified zlib compressor
erinepshovel-code Sep 12, 2026
0c305f0
Install build dependencies with the selected uv environment
erinepshovel-code Sep 12, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ jobs:
steps:
- uses: actions/checkout@v7
with:
ref: ${{ github.event.pull_request.head.sha || github.sha }}
persist-credentials: false
- uses: actions/setup-python@v7
with:
Expand All @@ -27,3 +28,28 @@ jobs:
/tmp/pubskill-wheel-venv/bin/python -m pip install --no-deps /tmp/pubskill-wheel/pubskill_lib-*.whl
cd /tmp
/tmp/pubskill-wheel-venv/bin/python -c "from pubskill_lib import evidence; assert evidence._comment_markers()['.py'] == '#'"
- name: Reproduce release artifacts and replay installed wheel
run: |
python -m pip install uv==0.11.18
uv venv --managed-python --python 3.11.15 /tmp/pubskill-build-venv
uv pip install --python /tmp/pubskill-build-venv/bin/python -r requirements-build.txt
(
umask 022
/tmp/pubskill-build-venv/bin/python tools/build_release.py --out /tmp/pubskill-release-a
)
(
umask 077
/tmp/pubskill-build-venv/bin/python tools/build_release.py --out /tmp/pubskill-release-b
)
diff /tmp/pubskill-release-a/SHA256SUMS /tmp/pubskill-release-b/SHA256SUMS
/tmp/pubskill-wheel-venv/bin/python -m pip install --no-deps --force-reinstall /tmp/pubskill-release-a/*.whl
mkdir /tmp/pubskill-replay
tar -xzf /tmp/pubskill-release-a/*.tar.gz -C /tmp/pubskill-replay
cd /tmp/pubskill-replay/pubskill_lib-0.2.0
/tmp/pubskill-wheel-venv/bin/python -m unittest discover -s tests
/tmp/pubskill-wheel-venv/bin/python -m pubskill_lib.audit examples/neglected-repo --out /tmp/release-findings.json
/tmp/pubskill-wheel-venv/bin/python -c "import json; from pubskill_lib.audit import _read_source_pin; assert json.load(open('/tmp/release-findings.json'))['source_pin'] == _read_source_pin() != 'hmmm'"
python -m venv /tmp/pubskill-sdist-venv
/tmp/pubskill-sdist-venv/bin/python -m pip install --no-deps /tmp/pubskill-release-a/*.tar.gz
/tmp/pubskill-sdist-venv/bin/python -m unittest discover -s tests
/tmp/pubskill-sdist-venv/bin/python -c "from pubskill_lib.audit import _read_source_pin; assert _read_source_pin() != 'hmmm'"
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
__pycache__/
*.pyc
*.egg-info/
build/
dist/
.env
.env.*
!.env.example
Expand Down
8 changes: 8 additions & 0 deletions MANIFEST.in
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]
35 changes: 33 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,14 @@ Clone this repo when you want a command that inspects a local repository and wri

## Status

The inspect CLI implementation passes the repository gate; the `v0.2.0` release tag is not published yet.
The inspect CLI version is `0.2.0`. Published versions and their immutable
artifacts are listed on [GitHub Releases](https://github.com/The-Interdependency/pubskill-lib/releases).

| Claim | State |
|---|---|
| Canon | `The-Interdependency/skill-lib` |
| This repo | distribution + public CLI + fixtures |
| Clone / run / findings | **implementation ready** — release pending |
| Clone / run / findings | Source and built-artifact gates described below |
| VM populate | `HANDOFF.vm.md` |
| Source pin | `SOURCE.md` |

Expand All @@ -31,6 +32,36 @@ python -m pubskill_lib.audit examples/neglected-repo --out /tmp/findings.json

Those commands are the definition of done for the first utility tag (`v0.2.0`). They run in GitHub CI from a clean checkout; publish the tag only after the release gate is explicitly completed.

## Reproduce release artifacts

From the release's exact Git commit, install the pinned build tools and build
into two empty directories:

```bash
python -m pip install uv==0.11.18
uv venv --managed-python --python 3.11.15 /tmp/pubskill-build-env
. /tmp/pubskill-build-env/bin/activate
uv pip install --python /tmp/pubskill-build-env/bin/python -r requirements-build.txt
python tools/build_release.py --out /tmp/pubskill-build-a
python tools/build_release.py --out /tmp/pubskill-build-b
diff /tmp/pubskill-build-a/SHA256SUMS /tmp/pubskill-build-b/SHA256SUMS
```

The builder uses only committed source, normalizes source and wheel archive
headers, ordering, and permissions, and
requires zlib 1.3.1 at compile time and runtime, and records its identity along
with source, doctrine, toolchain, and artifact digests in `release-manifest.json`.
CI compares builds under both 022 and 077 file-creation masks; wheel payloads
and their RECORD hashes remain unchanged by archive normalization.
It does not publish. Before publication, install the exact wheel in a fresh venv,
run the tests and fixture from the extracted sdist, and inspect a real consumer.
The wheel retains its canonical skill-lib source pin without requiring a checkout.

Download the wheel, source archive, manifest, and `SHA256SUMS` from the chosen
release. Verify the downloaded files with `sha256sum -c SHA256SUMS`, then install
the verified wheel with `python -m pip install --no-deps ./pubskill_lib-0.2.0-py3-none-any.whl`.
Checksums establish byte identity; they are not a signature or a blanket health claim.

## Inspect CLI

`v0.2` inspects one **local repository path** without executing the target repository:
Expand Down
5 changes: 4 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[build-system]
requires = ["setuptools>=68"]
requires = ["setuptools==84.0.0", "wheel==0.48.0"]
build-backend = "setuptools.build_meta"

[project]
Expand All @@ -16,3 +16,6 @@ pubskill-examine = "pubskill_lib.examine:main"

[tool.setuptools.packages.find]
where = ["src"]

[tool.setuptools.package-data]
pubskill_lib = ["_source.json"]
5 changes: 5 additions & 0 deletions requirements-build.txt
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
6 changes: 6 additions & 0 deletions src/pubskill_lib/_source.json
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"
}
8 changes: 4 additions & 4 deletions src/pubskill_lib/audit.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

import argparse
import json
from importlib.resources import files
import re
import shlex
from urllib.parse import unquote, urlsplit
Expand Down Expand Up @@ -491,10 +492,9 @@ def _check_package_scripts(target, sink, unresolved):


def _read_source_pin():
root = Path(__file__).resolve().parents[2]
text = _read_text(root / "SOURCE.md") or ""
match = PIN_PATTERN.search(text)
return match.group(1) if match else "hmmm"
"""Read the canonical identity shipped with both source and wheel installs."""
data = json.loads(files("pubskill_lib").joinpath("_source.json").read_text(encoding="utf-8"))
return data["commit"]


def audit_path(target_path, source_pin=None):
Expand Down
10 changes: 7 additions & 3 deletions tests/test_provenance.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ def _source_pin() -> str:


class PublicationProvenanceTests(unittest.TestCase):
def test_installed_source_pin_matches_publication_pin(self):
from pubskill_lib.audit import _read_source_pin
self.assertEqual(_source_pin(), _read_source_pin())

def test_source_pin_matches_vendored_skill_manifest(self):
vendored = (REPO / ".agents" / "skills" / "README.md").read_text(encoding="utf-8")
vendored_pin = PIN_RE.search(vendored)
Expand All @@ -42,10 +46,10 @@ def test_local_secret_files_are_ignored(self):
self.assertIn(".env.*", ignore)
self.assertIn("*.egg-info/", ignore)

def test_readme_does_not_claim_unpublished_v020_tag(self):
def test_readme_exposes_reproducible_release_gate(self):
readme = (REPO / "README.md").read_text(encoding="utf-8")
self.assertIn("release pending", readme)
self.assertNotIn("**shipped** — `v0.2`", readme)
self.assertIn("tools/build_release.py", readme)
self.assertIn("sha256sum -c SHA256SUMS", readme)


if __name__ == "__main__":
Expand Down
14 changes: 14 additions & 0 deletions tests/test_release_compressor.py
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()
146 changes: 146 additions & 0 deletions tools/build_release.py
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)
Comment thread
erinepshovel-code marked this conversation as resolved.


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()