import argparse
from html.parser import HTMLParser
import io
import json
import os
from pathlib import Path
import re
import subprocess
import tarfile
import tempfile
from urllib.parse import unquote, urljoin, urlsplit
import zipfile
ROOT = Path(__file__).resolve().parent.parent
class Page(HTMLParser):
def __init__(self, path):
super().__init__()
self.ids = set()
self.references = []
self.duplicates = []
self.headings = 0
self.redirect = False
self.feed(path.read_text())
def handle_starttag(self, tag, attributes):
attrs = dict(attributes)
if tag == "meta" and attrs.get("http-equiv", "").lower() == "refresh":
self.redirect = True
if "id" in attrs:
if attrs["id"] in self.ids:
self.duplicates.append(attrs["id"])
self.ids.add(attrs["id"])
if tag == "h1":
self.headings += 1
for attr in ("href", "src"):
if attrs.get(attr):
self.references.append(attrs[attr])
def check_links(site):
site = site.resolve()
pages = {path.resolve(): Page(path) for path in site.rglob("*.html")}
required = ["index.html", "release/v1/index.html", "releases/v1/index.html",
"docs/index.html", "cli/index.html"]
required += [f"docs/{name}/index.html" for name in (
"release-workflow", "getting-started", "describing-releases", "resources", "host-requirements",
"recipes", "publishing", "verifying", "release-lists", "self-hosting", "mise",
)]
errors = [f"missing page: {name}" for name in required if not (site / name).is_file()]
count = 0
for path, page in pages.items():
if page.headings != 1 and not page.redirect:
errors.append(f"{path}: expected one H1, found {page.headings}")
for identifier in page.duplicates:
errors.append(f"{path}: duplicate id {identifier}")
route = path.relative_to(site).as_posix().removesuffix("index.html")
for ref in page.references:
url = urlsplit(urljoin(f"https://packslip.dev/{route}", ref))
if url.scheme not in ("https", "http") or url.netloc != "packslip.dev":
continue
target = site / unquote(url.path.lstrip("/"))
if url.path.endswith("/"):
target /= "index.html"
target = target.resolve()
count += 1
if not target.is_file():
errors.append(f"{path.relative_to(site)}: missing target {ref}")
elif url.fragment and target in pages and unquote(url.fragment) not in pages[target].ids:
errors.append(f"{path.relative_to(site)}: missing anchor {ref}")
if errors:
raise RuntimeError("\n".join(errors))
print(f"Checked {len(pages)} pages and {count} local links, anchors, and assets.")
def run(args, cwd, env, success=True):
result = subprocess.run(args, cwd=cwd, env=env, capture_output=True, text=True, timeout=60)
if (result.returncode == 0) != success:
raise RuntimeError(f"Unexpected exit {result.returncode}: {args}\n{result.stdout}{result.stderr}")
return result.stdout
def check_excerpt(example, actual, path="statement"):
if isinstance(example, dict):
for key, value in example.items():
check_excerpt(value, actual[key], f"{path}.{key}")
elif isinstance(example, list):
assert len(example) == len(actual), path
for index, value in enumerate(example):
check_excerpt(value, actual[index], f"{path}[{index}]")
elif example == "...":
assert re.fullmatch(r"[0-9a-f]{64}", actual), path
else:
assert example == actual, f"{path}: example {example!r} differs from {actual!r}"
def check_quickstart(binary):
source = (ROOT / "content/docs/getting-started.md").read_text()
blocks = re.findall(r"<!-- docs-test: quickstart -->\s*```sh\n(.*?)```", source, re.S)
if len(blocks) != 4:
raise RuntimeError("Expected the four marked quickstart blocks: setup, create, verify, show")
with tempfile.TemporaryDirectory(prefix="packslip-docs-") as directory:
cwd = Path(directory)
env = os.environ.copy()
env["PATH"] = str(binary.parent) + os.pathsep + env["PATH"]
for block in blocks:
run(["sh", "-eu", "-c", block], cwd, env)
statement = json.loads(run([str(binary), "show", "dist/packslip.sigstore.json"], cwd, env))
check_excerpt(json.loads((ROOT / "docs/examples/release-excerpt.json").read_text()), statement)
artifact = statement["predicate"]["artifacts"][0]
assert statement["predicate"]["project"] == "mytool.example.com"
assert statement["predicate"]["version"] == "1.2.3"
assert artifact["bin"] == ["bin/mytool"]
assert artifact["format"] == "tar.gz"
assert not any(field in artifact for field in ("os", "arch", "libc"))
assert len(statement["subject"][0]["digest"]["sha256"]) == 64
with (cwd / "dist/mytool-1.2.3.tar.gz").open("ab") as file:
file.write(b"tampered")
run([str(binary), "verify", "dist/packslip.sigstore.json", "--pubkey", "release.pub",
"--allow-unlogged", "--artifact", "dist/mytool-1.2.3.tar.gz"], cwd, env, success=False)
print("Quickstart passed, including manifest fields and rejection of a modified artifact.")
RECIPE_FILES = {
"rust": {"mytool-1.2.3-linux-x64.tar.gz": [
"mytool-1.2.3/bin/mytool", "mytool-1.2.3/share/zsh/site-functions/_mytool",
"mytool-1.2.3/share/man/man1/mytool.1",
]},
"go": {"mytool-1.2.3-linux-x64.tar.gz": ["mytool"]},
"monorepo": {"lint-1.2.3-linux-x64.tar.gz": ["bin/lint-x86_64"]},
"desktop": {
"myapp-1.2.3-linux-x64.tar.gz": ["bin/myapp", "share/applications/myapp.desktop",
"share/icons/hicolor/256x256/apps/myapp.png"],
"myapp-1.2.3-darwin-arm64.zip": ["MyApp.app/Contents/Info.plist"],
},
}
def check_recipes(binary):
source = (ROOT / "content/docs/recipes.md").read_text()
recipes = re.findall(r"<!-- docs-test: recipe (\w+) -->\s*```toml\n(.*?)```", source, re.S)
if {name for name, _ in recipes} != RECIPE_FILES.keys():
raise RuntimeError("Recipe markers and fixture layouts must match")
for name, manifest in recipes:
with tempfile.TemporaryDirectory(prefix=f"packslip-recipe-{name}-") as directory:
cwd = Path(directory)
(cwd / "dist").mkdir()
for filename, members in RECIPE_FILES[name].items():
path = cwd / "dist" / filename
content = b"#!/bin/sh\necho example\n"
if filename.endswith(".zip"):
with zipfile.ZipFile(path, "w") as archive:
for member in members:
archive.writestr(member, content)
else:
with tarfile.open(path, "w:gz") as archive:
for member in members:
entry = tarfile.TarInfo(member)
entry.size = len(content)
entry.mode = 0o755
archive.addfile(entry, io.BytesIO(content))
(cwd / "release.toml").write_text(manifest)
run([str(binary), "keygen", "--out", "release.key"], cwd, None)
run([str(binary), "create", "--manifest", "release.toml", "--out", "out",
"--key", "release.key", "--no-log"], cwd, None)
bundle, = (cwd / "out").glob("packslip*.sigstore.json")
args = [str(binary), "verify", str(bundle), "--pubkey", "release.pub", "--allow-unlogged"]
for filename in RECIPE_FILES[name]:
args += ["--artifact", f"dist/{filename}"]
run(args, cwd, None)
print(f"Checked {len(recipes)} recipe manifests against sample archives.")
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--site", type=Path, default=ROOT / "public")
parser.add_argument("--binary", type=Path, default=ROOT / "target/debug/packslip")
args = parser.parse_args()
check_links(args.site.resolve())
binary = args.binary.resolve()
check_quickstart(binary)
check_recipes(binary)
if __name__ == "__main__":
main()