from __future__ import annotations
import argparse
import importlib.util
import io
import json
import shutil
import tarfile
import tempfile
import unittest
import zipfile
from pathlib import Path
import yaml
ROOT = Path(__file__).resolve().parents[3]
def load(name: str):
spec = importlib.util.spec_from_file_location(name, ROOT / "scripts/release" / f"{name}.py")
assert spec and spec.loader
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
return module
version_contract = load("version_contract")
artifact_manifest = load("artifact_manifest")
def workflow(name: str) -> dict:
return yaml.load((ROOT / ".github/workflows" / name).read_text(encoding="utf-8"), Loader=yaml.BaseLoader)
class VersionContractTests(unittest.TestCase):
def test_versions_tags_and_pep440_include_zero_prereleases(self):
expected = {
"1.2.3": "1.2.3",
"1.2.3-alpha.0": "1.2.3a0",
"1.2.3-beta.0": "1.2.3b0",
"1.2.3-rc.0": "1.2.3rc0",
"0.0.0-beta.0": "0.0.0b0",
}
for value, python_value in expected.items():
self.assertEqual(version_contract.version(value), value)
self.assertEqual(version_contract.tag("v" + value), "v" + value)
self.assertEqual(version_contract.pep440(value), python_value)
for value in ("1.2", "1.2.3-dev.1", "01.2.3", "1.2.3-rc.-1", "1.2.3-rc.0/x", "v1.2.3", "1.2.3\nnext=x"):
with self.assertRaises(ValueError):
version_contract.version(value)
for value in ("1.2.3", "vv1.2.3", "v1.2.3+meta", "v1.2.3\nsource_sha=x"):
with self.assertRaises(ValueError):
version_contract.tag(value)
def test_template_is_current_and_rejects_drift(self):
version_contract.check_template(ROOT)
with tempfile.TemporaryDirectory() as temporary:
copy = Path(temporary) / "source"
shutil.copytree(ROOT, copy, ignore=shutil.ignore_patterns(".git", "target", "node_modules"))
package = copy / "bindings/node/package.json"
package.write_text(package.read_text().replace('"version": "0.0.0"', '"version": "0.0.1"'))
with self.assertRaises(ValueError):
version_contract.check_template(copy)
def test_stamping_prerelease_zero_is_isolated_and_verifiable(self):
with tempfile.TemporaryDirectory() as temporary:
source = Path(temporary) / "source"
destination = Path(temporary) / "stamped"
shutil.copytree(ROOT, source, ignore=shutil.ignore_patterns(".git", "target", "node_modules"))
shutil.copytree(source, destination)
version_contract.stamp(destination, "0.0.0-beta.0")
version_contract.verify(destination, "0.0.0-beta.0")
self.assertEqual(json.loads((destination / "bindings/node/package.json").read_text())["version"], "0.0.0-beta.0")
self.assertEqual(json.loads((source / "bindings/node/package.json").read_text())["version"], "0.0.0")
class ManifestTests(unittest.TestCase):
VERSION = "1.2.3-rc.0"
def npm_tarball(self, path: Path, metadata: dict, native: str | bool = True, extra_native: str | None = None) -> None:
data = json.dumps(metadata).encode()
with tarfile.open(path, "w:gz") as archive:
info = tarfile.TarInfo("package/package.json")
info.size = len(data)
archive.addfile(info, io.BytesIO(data))
name = metadata.get("name", "")
prefix = artifact_manifest.ROOT_NODE_PACKAGE + "-"
target = name.removeprefix(prefix) if native is True and name.startswith(prefix) else native
if isinstance(target, str):
native_data = b"native"
info = tarfile.TarInfo(f"package/code2graph-node.{target}.node")
info.size = len(native_data)
archive.addfile(info, io.BytesIO(native_data))
if extra_native is not None:
native_data = b"unexpected-native"
info = tarfile.TarInfo(f"package/{extra_native}")
info.size = len(native_data)
archive.addfile(info, io.BytesIO(native_data))
def make_bundle(self, root: Path) -> list[str]:
npm = root / "npm"
python = root / "python"
npm.mkdir(); python.mkdir()
optional = {name: self.VERSION for name in artifact_manifest.NODE_TARGETS}
root_name = "nodedb-lab-code2graph-1.2.3-rc.0.tgz"
self.npm_tarball(npm / root_name, {"name": artifact_manifest.ROOT_NODE_PACKAGE, "version": self.VERSION, "optionalDependencies": optional})
for name, (os_name, cpu, libc) in artifact_manifest.NODE_TARGETS.items():
metadata = {"name": name, "version": self.VERSION, "os": [os_name], "cpu": [cpu]}
if libc: metadata["libc"] = [libc]
filename = f"{name.removeprefix('@').replace('/', '-')}-{self.VERSION}.tgz"
self.npm_tarball(npm / filename, metadata)
py_version = artifact_manifest.pep440(self.VERSION)
wheel_tags = {
"linux-x64-gnu": "manylinux_2_17_x86_64.manylinux2014_x86_64",
"linux-arm64-gnu": "manylinux_2_17_aarch64.manylinux2014_aarch64",
"linux-x64-musl": "musllinux_1_2_x86_64",
"darwin-x64": "macosx_10_12_x86_64",
"darwin-arm64": "macosx_11_0_arm64",
"win32-x64-msvc": "win_amd64",
}
metadata = f"Name: code2graph-rs\nVersion: {py_version}\n".encode()
for tag in wheel_tags.values():
path = python / f"code2graph_rs-{py_version}-cp39-abi3-{tag}.whl"
with zipfile.ZipFile(path, "w") as archive:
archive.writestr(f"code2graph_rs-{py_version}.dist-info/METADATA", metadata)
sdist = python / f"code2graph_rs-{py_version}.tar.gz"
with tarfile.open(sdist, "w:gz") as archive:
info = tarfile.TarInfo(f"code2graph_rs-{py_version}/PKG-INFO")
info.size = len(metadata)
archive.addfile(info, io.BytesIO(metadata))
return sorted(path.relative_to(root).as_posix() for path in root.rglob("*") if path.is_file())
def args(self, root: Path, files: list[str]) -> argparse.Namespace:
return argparse.Namespace(directory=root, output=root / "release-manifest.json", file=files, repository="nodedb-lab/code2graph", tag="v" + self.VERSION, source_sha="a" * 40, prepare_run_id="42", workflow="release-prepare.yml", version=self.VERSION)
def test_manifest_encodes_and_revalidates_exact_distribution_matrix(self):
with tempfile.TemporaryDirectory() as temporary:
root = Path(temporary)
create = self.args(root, self.make_bundle(root))
artifact_manifest.create(create)
payload = json.loads(create.output.read_text())
self.assertEqual(set(payload["contract"]["npm"]["targets"]), set(artifact_manifest.NODE_TARGETS))
self.assertEqual(set(payload["contract"]["python"]["targets"]), set(artifact_manifest.PYTHON_TARGETS))
verify = argparse.Namespace(**{key: getattr(create, key) for key in ("directory", "repository", "tag", "source_sha", "prepare_run_id", "workflow", "version")}, manifest=create.output)
artifact_manifest.verify(verify)
checksums = root / "SHA256SUMS"
checksum_names = ["release-manifest.json", *create.file]
checksums.write_text("".join(f"{artifact_manifest.digest(root / name)} {name}\n" for name in checksum_names))
artifact_manifest.verify_checksums(argparse.Namespace(directory=root, manifest=create.output, checksums=checksums))
hostile = argparse.Namespace(**vars(verify)); hostile.workflow = "hostile.yml"
with self.assertRaises(ValueError): artifact_manifest.verify(hostile)
platform = next(path for path in (root / "npm").glob("*.tgz") if "linux-" in path.name)
platform.unlink()
with self.assertRaises(ValueError): artifact_manifest.verify(verify)
def test_matrix_rejects_extra_target_and_root_dependency_drift(self):
with tempfile.TemporaryDirectory() as temporary:
root = Path(temporary); self.make_bundle(root)
self.npm_tarball(root / "npm" / "extra.tgz", {"name": "hostile", "version": self.VERSION})
with self.assertRaises(ValueError): artifact_manifest.bundle_contract(root, self.VERSION)
with tempfile.TemporaryDirectory() as temporary:
root = Path(temporary); self.make_bundle(root)
root_tarball = root / "npm" / "nodedb-lab-code2graph-1.2.3-rc.0.tgz"
root_tarball.unlink()
self.npm_tarball(root_tarball, {"name": artifact_manifest.ROOT_NODE_PACKAGE, "version": self.VERSION, "optionalDependencies": {}})
with self.assertRaises(ValueError): artifact_manifest.bundle_contract(root, self.VERSION)
with tempfile.TemporaryDirectory() as temporary:
root = Path(temporary); self.make_bundle(root)
root_tarball = root / "npm" / "nodedb-lab-code2graph-1.2.3-rc.0.tgz"
self.npm_tarball(root_tarball, {"name": artifact_manifest.ROOT_NODE_PACKAGE, "version": self.VERSION, "optionalDependencies": {name: self.VERSION for name in artifact_manifest.NODE_TARGETS}}, native="linux-x64-gnu")
with self.assertRaises(ValueError): artifact_manifest.bundle_contract(root, self.VERSION)
with tempfile.TemporaryDirectory() as temporary:
root = Path(temporary); self.make_bundle(root)
package = next(iter(artifact_manifest.NODE_TARGETS))
platform_tarball = root / "npm" / f"{package.removeprefix('@').replace('/', '-')}-{self.VERSION}.tgz"
os_name, cpu, libc = artifact_manifest.NODE_TARGETS[package]
metadata = {"name": package, "version": self.VERSION, "os": [os_name], "cpu": [cpu]}
if libc: metadata["libc"] = [libc]
self.npm_tarball(platform_tarball, metadata, native=False)
with self.assertRaises(ValueError): artifact_manifest.bundle_contract(root, self.VERSION)
with tempfile.TemporaryDirectory() as temporary:
root = Path(temporary); self.make_bundle(root)
package = next(iter(artifact_manifest.NODE_TARGETS))
platform_tarball = root / "npm" / f"{package.removeprefix('@').replace('/', '-')}-{self.VERSION}.tgz"
os_name, cpu, libc = artifact_manifest.NODE_TARGETS[package]
metadata = {"name": package, "version": self.VERSION, "os": [os_name], "cpu": [cpu]}
if libc: metadata["libc"] = [libc]
self.npm_tarball(platform_tarball, metadata, extra_native="code2graph-node.unexpected.node")
with self.assertRaises(ValueError): artifact_manifest.bundle_contract(root, self.VERSION)
def test_inventory_and_archive_comparison_reject_hostile_content(self):
with tempfile.TemporaryDirectory() as temporary:
root = Path(temporary); (root / "a").write_text("a")
for name in ("../escape", "/absolute", "./relative"):
with self.assertRaises(ValueError): artifact_manifest.safe_name(name)
(root / "extra").write_text("x")
with self.assertRaises(ValueError): artifact_manifest.inventory(root, ["a"])
class WorkflowContractTests(unittest.TestCase):
def test_prepare_is_complete_nonpublishing_and_runs_equivalent_binding_gates(self):
prepare = workflow("release-prepare.yml")
self.assertEqual(prepare["on"]["push"]["tags"], ["v*"])
self.assertEqual(prepare["jobs"]["ci"]["with"]["skip_bindings"], "true")
text = (ROOT / ".github/workflows/release-prepare.yml").read_text()
for required in ("npm run harden-loader", "npm run harden-types", "npm test", "unittest discover", "npm run test:all", "BUNDLE_NPM=\"$GITHUB_WORKSPACE/bundle/npm\""):
self.assertIn(required, text)
self.assertLess(text.index("npx napi artifacts"), text.rindex("npm run harden-loader"))
self.assertIn("npx napi prepublish -t npm --npm-dir ./npm --skip-optional-publish --no-gh-release", text)
self.assertIn('npm pack "./$directory" --pack-destination "$BUNDLE_NPM"', text)
self.assertNotIn('npm pack "$directory" --pack-destination "$BUNDLE_NPM"', text)
self.assertLess(text.index("rm -f ./*.node"), text.index('npm pack --pack-destination "$BUNDLE_NPM"'))
for forbidden in ("npm publish", "cargo publish", "npm install --global"):
self.assertNotIn(forbidden, text)
def test_validation_uses_only_canonical_tag_helper(self):
text = (ROOT / ".github/workflows/release-validate.yml").read_text()
self.assertIn('version_contract.py validate-tag "$TAG"', text)
self.assertNotIn("=~", text)
def test_distribution_provenance_source_and_no_build_contracts(self):
release = workflow("release.yml")
self.assertEqual(set(release["on"]["workflow_dispatch"]["inputs"]), {"tag", "prepare_run_id", "distribution_ref", "crates", "pypi", "npm", "github"})
text = (ROOT / ".github/workflows/release.yml").read_text()
for required in (".repository.full_name", ".head_repository.full_name", ".head_sha", ".path", "tagged-source", "cmp --silent", "unexpected PyPI artifact", "GitHub release attachment set mismatch"):
self.assertIn(required, text)
self.assertNotIn(".head_branch", text)
for forbidden in ("maturin build", "maturin-action", "napi build", "cargo test", "npm test", "cargo build"):
self.assertNotIn(forbidden, text)
self.assertLess(text.index('for file in "${platform_tarballs[@]}"'), text.index('publish "${roots[0]}"'))
def test_action_versions_and_actionlint_invocation_match_repository(self):
test_text = (ROOT / ".github/workflows/test.yml").read_text()
self.assertIn("docker://rhysd/actionlint:1.7.7", test_text)
for workflow_path in (ROOT / ".github/workflows").glob("*.yml"):
text = workflow_path.read_text()
self.assertNotIn("actions/checkout@v5", text)
self.assertNotIn("actions/upload-artifact@v6", text)
self.assertNotIn("actions/download-artifact@v7", text)
if __name__ == "__main__": unittest.main()