mirror of
https://github.com/Cheviiot/Nivora.git
synced 2026-08-03 15:51:12 +00:00
feat: create autonomous Nivora package repository
Maintain a validated cross-distribution package catalog. Automate upstream updates, isolated builds, diagnostics, and direct publication. Build the official GitHub Desktop sources for Linux with working OAuth.
This commit is contained in:
Executable
+323
@@ -0,0 +1,323 @@
|
||||
#!/usr/bin/env python3
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import os
|
||||
import re
|
||||
import shlex
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from urllib.parse import unquote, urlsplit
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
|
||||
EXPECTED_PACKAGES = (
|
||||
"adwyra",
|
||||
"anidesk",
|
||||
"balena-etcher",
|
||||
"chatbox",
|
||||
"clash-verge-rev",
|
||||
"claude-desktop",
|
||||
"codex",
|
||||
"fisher",
|
||||
"github-desktop",
|
||||
"happ",
|
||||
"netbird",
|
||||
"nivora-stplr",
|
||||
"opencode",
|
||||
"parsec",
|
||||
"pineconemc",
|
||||
"tailscale",
|
||||
"ventoy",
|
||||
"vual",
|
||||
"yandex-browser-stable",
|
||||
)
|
||||
|
||||
REQUIRED_ROOT_FILES = {
|
||||
Path("README.md"),
|
||||
Path("CHANGELOG.md"),
|
||||
Path("CONTRIBUTING.md"),
|
||||
Path("SECURITY.md"),
|
||||
Path("LICENSE"),
|
||||
Path("stapler-repo.toml"),
|
||||
Path("docs/maintenance.md"),
|
||||
Path("docs/security-model.md"),
|
||||
Path("docs/packages/claude-desktop.md"),
|
||||
Path("docs/packages/codex.md"),
|
||||
Path("docs/packages/github-desktop.md"),
|
||||
Path("docs/packages/opencode.md"),
|
||||
Path("docs/packages/nivora-stplr.md"),
|
||||
Path("docs/packages/ventoy.md"),
|
||||
}
|
||||
|
||||
CHECKSUM_RE = re.compile(r"(?:sha256:)?[0-9a-f]{64}\Z")
|
||||
MARKDOWN_LINK_RE = re.compile(r"!?\[[^\]]*\]\(([^)\s]+)(?:\s+[^)]*)?\)")
|
||||
HTML_LINK_RE = re.compile(r"(?:src|href)=[\"']([^\"']+)[\"']")
|
||||
SECRET_PATTERNS = (
|
||||
re.compile(r"-----BEGIN (?:RSA |OPENSSH |EC )?PRIVATE KEY-----"),
|
||||
re.compile(r"\bgh[pousr]_[A-Za-z0-9_]{20,}\b"),
|
||||
re.compile(r"\bgithub_pat_[A-Za-z0-9_]{20,}\b"),
|
||||
re.compile(r"\bAKIA[0-9A-Z]{16}\b"),
|
||||
)
|
||||
|
||||
|
||||
def scalar(text: str, field: str) -> str | None:
|
||||
match = re.search(
|
||||
rf"^{re.escape(field)}=(?:'([^']*)'|\"([^\"]*)\"|([^#\n]+))",
|
||||
text,
|
||||
re.MULTILINE,
|
||||
)
|
||||
if not match:
|
||||
return None
|
||||
return next(value.strip() for value in match.groups() if value is not None)
|
||||
|
||||
|
||||
def array(text: str, field: str) -> list[str] | None:
|
||||
match = re.search(
|
||||
rf"^{re.escape(field)}=\((.*?)\)", text, re.MULTILINE | re.DOTALL
|
||||
)
|
||||
if not match:
|
||||
return None
|
||||
try:
|
||||
return shlex.split(match.group(1), comments=True, posix=True)
|
||||
except ValueError:
|
||||
return None
|
||||
|
||||
|
||||
def source_arrays(text: str) -> dict[str, list[str]]:
|
||||
result: dict[str, list[str]] = {}
|
||||
for match in re.finditer(r"^(sources(?:_[a-z0-9_]+)?)=\(", text, re.MULTILINE):
|
||||
name = match.group(1)
|
||||
values = array(text, name)
|
||||
if values is not None:
|
||||
result[name] = values
|
||||
return result
|
||||
|
||||
|
||||
def sha256(path: Path) -> str:
|
||||
digest = hashlib.sha256()
|
||||
with path.open("rb") as stream:
|
||||
for chunk in iter(lambda: stream.read(1024 * 1024), b""):
|
||||
digest.update(chunk)
|
||||
return digest.hexdigest()
|
||||
|
||||
|
||||
def local_source_name(source: str) -> str | None:
|
||||
if not source.startswith("local:///"):
|
||||
return None
|
||||
value = unquote(source.removeprefix("local:///").split("?", 1)[0])
|
||||
path = Path(value)
|
||||
if not value or path.is_absolute() or ".." in path.parts:
|
||||
return ""
|
||||
return value
|
||||
|
||||
|
||||
def markdown_targets(text: str) -> set[str]:
|
||||
return set(MARKDOWN_LINK_RE.findall(text)) | set(HTML_LINK_RE.findall(text))
|
||||
|
||||
|
||||
def validate_links(path: Path, errors: list[str]) -> None:
|
||||
text = path.read_text(encoding="utf-8")
|
||||
for target in markdown_targets(text):
|
||||
parsed = urlsplit(target)
|
||||
if parsed.scheme or target.startswith(("mailto:", "#")):
|
||||
continue
|
||||
local = unquote(parsed.path)
|
||||
if not local:
|
||||
continue
|
||||
if local.startswith("/"):
|
||||
errors.append(f"{path.relative_to(ROOT)}: unsafe local link: {target}")
|
||||
continue
|
||||
resolved = (path.parent / local).resolve()
|
||||
try:
|
||||
resolved.relative_to(ROOT)
|
||||
except ValueError:
|
||||
errors.append(f"{path.relative_to(ROOT)}: link escapes repository: {target}")
|
||||
continue
|
||||
if not resolved.exists():
|
||||
errors.append(f"{path.relative_to(ROOT)}: missing link target: {target}")
|
||||
|
||||
|
||||
def validate_package(package: str, errors: list[str]) -> dict[str, object]:
|
||||
directory = ROOT / package
|
||||
staplerfile = directory / "Staplerfile"
|
||||
text = staplerfile.read_text(encoding="utf-8")
|
||||
|
||||
name = scalar(text, "name")
|
||||
version = scalar(text, "version")
|
||||
release = scalar(text, "release")
|
||||
architectures = array(text, "architectures")
|
||||
provides = array(text, "provides")
|
||||
replaces = array(text, "replaces")
|
||||
conflicts = array(text, "conflicts")
|
||||
|
||||
if name != package:
|
||||
errors.append(f"{package}: directory and name differ: {name!r}")
|
||||
if not version:
|
||||
errors.append(f"{package}: version is missing")
|
||||
if not release or not release.isdigit() or int(release) < 1:
|
||||
errors.append(f"{package}: release must be a positive integer")
|
||||
if not architectures:
|
||||
errors.append(f"{package}: architectures are missing")
|
||||
elif any(item not in {"amd64", "arm64", "all"} for item in architectures):
|
||||
errors.append(f"{package}: unsupported architecture value: {architectures}")
|
||||
|
||||
if provides != [] or conflicts != []:
|
||||
errors.append(f"{package}: provides/conflicts must not contain binary aliases")
|
||||
if replaces != [package]:
|
||||
errors.append(f"{package}: replaces must contain only its own base package name")
|
||||
|
||||
if "package()" not in text or "files()" not in text:
|
||||
errors.append(f"{package}: package() or files() is missing")
|
||||
|
||||
arrays = source_arrays(text)
|
||||
if not arrays:
|
||||
errors.append(f"{package}: sources are missing")
|
||||
for source_field, sources in arrays.items():
|
||||
checksum_field = source_field.replace("sources", "checksums", 1)
|
||||
checksums = array(text, checksum_field)
|
||||
if checksums is None:
|
||||
errors.append(f"{package}: {checksum_field} is missing")
|
||||
continue
|
||||
if len(sources) != len(checksums):
|
||||
errors.append(
|
||||
f"{package}: {source_field}/{checksum_field} lengths differ "
|
||||
f"({len(sources)} != {len(checksums)})"
|
||||
)
|
||||
continue
|
||||
for source, checksum in zip(sources, checksums, strict=True):
|
||||
if checksum == "SKIP" or not CHECKSUM_RE.fullmatch(checksum):
|
||||
errors.append(f"{package}: invalid checksum for {source}: {checksum}")
|
||||
continue
|
||||
if source.startswith("http://"):
|
||||
errors.append(f"{package}: insecure source URL: {source}")
|
||||
if source.startswith("git+") and "#" not in source:
|
||||
errors.append(f"{package}: unpinned Git source: {source}")
|
||||
|
||||
local_name = local_source_name(source)
|
||||
if local_name is None:
|
||||
continue
|
||||
if local_name == "":
|
||||
errors.append(f"{package}: unsafe local source: {source}")
|
||||
continue
|
||||
local_path = directory / local_name
|
||||
if not local_path.is_file():
|
||||
errors.append(f"{package}: missing local source: {local_name}")
|
||||
continue
|
||||
expected = checksum.removeprefix("sha256:")
|
||||
actual = sha256(local_path)
|
||||
if actual != expected:
|
||||
errors.append(
|
||||
f"{package}: checksum mismatch for {local_name}: {actual} != {expected}"
|
||||
)
|
||||
|
||||
for hook in re.findall(r"\['[^']+'\]='([^']+)'", text):
|
||||
hook_path = directory / hook
|
||||
if not hook_path.is_file():
|
||||
errors.append(f"{package}: missing lifecycle script: {hook}")
|
||||
elif not os.access(hook_path, os.X_OK):
|
||||
errors.append(f"{package}: lifecycle script is not executable: {hook}")
|
||||
|
||||
update_check = directory / ".stapler/update-check"
|
||||
if not update_check.is_file() or not os.access(update_check, os.X_OK):
|
||||
errors.append(f"{package}: executable .stapler/update-check is required")
|
||||
|
||||
return {
|
||||
"name": name,
|
||||
"version": version,
|
||||
"architectures": architectures or [],
|
||||
}
|
||||
|
||||
|
||||
def validate_readme(metadata: dict[str, dict[str, object]], errors: list[str]) -> None:
|
||||
path = ROOT / "README.md"
|
||||
text = path.read_text(encoding="utf-8")
|
||||
count_match = re.search(r"<!--\s*package-count\s*-->\s*\*\*(\d+) пакет", text)
|
||||
if not count_match or int(count_match.group(1)) != len(EXPECTED_PACKAGES):
|
||||
errors.append("README.md: package counter is stale")
|
||||
if text.count("### ") != 6:
|
||||
errors.append("README.md: catalog must contain exactly six categories")
|
||||
|
||||
for package, values in metadata.items():
|
||||
command = f"stplr install nivora/{package}"
|
||||
catalog_rows = [
|
||||
line for line in text.splitlines() if line.startswith("|") and command in line
|
||||
]
|
||||
if len(catalog_rows) != 1:
|
||||
errors.append(f"README.md: expected one catalog command for {package}")
|
||||
version = str(values["version"])
|
||||
if f"`{version}`" not in text:
|
||||
errors.append(f"README.md: version {version} is missing for {package}")
|
||||
|
||||
|
||||
def validate_repository_text(errors: list[str]) -> None:
|
||||
for path in ROOT.rglob("*"):
|
||||
if not path.is_file() or ".git" in path.parts:
|
||||
continue
|
||||
relative = path.relative_to(ROOT)
|
||||
if path.suffix.lower() in {".png", ".ico", ".zip", ".gz"}:
|
||||
continue
|
||||
try:
|
||||
text = path.read_text(encoding="utf-8")
|
||||
except UnicodeDecodeError:
|
||||
continue
|
||||
|
||||
scans_validator_source = relative == Path("tools/validate_repo.py")
|
||||
if not scans_validator_source and (
|
||||
"/home/cheviiot" in text or "/.codex/attachments/" in text
|
||||
):
|
||||
errors.append(f"{relative}: personal path is forbidden")
|
||||
if not scans_validator_source and "chmod 777" in text:
|
||||
errors.append(f"{relative}: chmod 777 is forbidden")
|
||||
if not scans_validator_source and re.search(r"\brm\s+-rf\b", text):
|
||||
allowed_purge = relative in {
|
||||
Path("tailscale/tailscale-purge-data"),
|
||||
Path("netbird/netbird-purge-data"),
|
||||
}
|
||||
if not allowed_purge or "--yes" not in text:
|
||||
errors.append(f"{relative}: unsafe rm -rf")
|
||||
for pattern in SECRET_PATTERNS:
|
||||
if pattern.search(text):
|
||||
errors.append(f"{relative}: possible secret detected")
|
||||
|
||||
if text.startswith("#!") and not os.access(path, os.X_OK):
|
||||
errors.append(f"{relative}: script is not executable")
|
||||
|
||||
|
||||
def main() -> int:
|
||||
errors: list[str] = []
|
||||
|
||||
for required in sorted(REQUIRED_ROOT_FILES):
|
||||
if not (ROOT / required).is_file():
|
||||
errors.append(f"missing required file: {required}")
|
||||
|
||||
package_dirs = tuple(
|
||||
sorted(path.name for path in ROOT.iterdir() if (path / "Staplerfile").is_file())
|
||||
)
|
||||
if package_dirs != EXPECTED_PACKAGES:
|
||||
errors.append(
|
||||
"package list mismatch: "
|
||||
f"expected {', '.join(EXPECTED_PACKAGES)}; got {', '.join(package_dirs)}"
|
||||
)
|
||||
|
||||
metadata: dict[str, dict[str, object]] = {}
|
||||
for package in package_dirs:
|
||||
metadata[package] = validate_package(package, errors)
|
||||
|
||||
validate_readme(metadata, errors)
|
||||
for path in sorted([*ROOT.glob("*.md"), *ROOT.glob("docs/**/*.md")]):
|
||||
validate_links(path, errors)
|
||||
validate_repository_text(errors)
|
||||
|
||||
if errors:
|
||||
print("Nivora validation failed:", file=sys.stderr)
|
||||
for error in errors:
|
||||
print(f"- {error}", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
print(f"OK: validated {len(package_dirs)} Nivora packages")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in New Issue
Block a user