feat: publish Dever core with project scaffolding and versioned AI skills
This commit is contained in:
1 parent
70fa73cdc2
commit
ac458ffa7f
960 files changed
+221319
-1163
No files matched your search
@@ -0,0 +1,96 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Turn a prepared signed release into the three official GitHub release assets."""
|
||||
|
||||
import argparse
|
||||
import ctypes
|
||||
import gzip
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path, PurePosixPath
|
||||
import tarfile
|
||||
import tempfile
|
||||
|
||||
|
||||
def create(release, output):
|
||||
release = release.absolute()
|
||||
output = output.absolute()
|
||||
if release.is_symlink() or not release.is_dir():
|
||||
raise ValueError("release must be a real prepared directory")
|
||||
if output.exists() or output.is_symlink():
|
||||
raise ValueError("output already exists")
|
||||
if not output.parent.is_dir():
|
||||
raise ValueError("output parent must exist")
|
||||
manifest_bytes = (release / "manifest.json").read_bytes()
|
||||
manifest = json.loads(manifest_bytes)
|
||||
if manifest["format"] != "dever-release-v1" or manifest["platform"] not in ("linux-x86_64", "linux-aarch64"):
|
||||
raise ValueError("unsupported release format or platform")
|
||||
artifacts = manifest["artifacts"]
|
||||
if not 1 <= len(artifacts) <= 4096 or sum(entry["bytes"] for entry in artifacts) > 2 * 1024**3:
|
||||
raise ValueError("release exceeds the official asset limits")
|
||||
names = set()
|
||||
for artifact in artifacts:
|
||||
name = artifact["path"]
|
||||
if (not name or str(PurePosixPath(name)) != name or name.startswith("/")
|
||||
or any(part in ("", ".", "..") for part in name.split("/"))
|
||||
or any(character in name for character in ("\\", ":", "\0"))
|
||||
or name in names or name in ("manifest.json", "manifest.sig")):
|
||||
raise ValueError("invalid release artifact path")
|
||||
names.add(name)
|
||||
path = release / name
|
||||
for parent in (path, *path.parents):
|
||||
if parent.is_symlink():
|
||||
raise ValueError("release artifacts must not traverse symlinks")
|
||||
if parent == release:
|
||||
break
|
||||
if not path.is_file() or path.stat().st_size != artifact["bytes"]:
|
||||
raise ValueError("release artifact type/size mismatch")
|
||||
with path.open("rb") as stream:
|
||||
if hashlib.file_digest(stream, "sha256").hexdigest() != artifact["sha256"]:
|
||||
raise ValueError("release artifact digest mismatch")
|
||||
with tempfile.TemporaryDirectory(prefix=".dever-release-assets-", dir=output.parent) as scratch:
|
||||
prepared = Path(scratch) / "assets"
|
||||
prepared.mkdir()
|
||||
stem = f"dever-{manifest['platform']}"
|
||||
(prepared / f"{stem}.manifest.json").write_bytes(manifest_bytes)
|
||||
(prepared / f"{stem}.manifest.sig").write_bytes((release / "manifest.sig").read_bytes())
|
||||
with (prepared / f"{stem}.tar.gz").open("xb") as compressed:
|
||||
with gzip.GzipFile(filename="", fileobj=compressed, mode="wb", mtime=0) as stream:
|
||||
with tarfile.open(fileobj=stream, mode="w|", format=tarfile.USTAR_FORMAT) as archive:
|
||||
for artifact in sorted(artifacts, key=lambda entry: entry["path"]):
|
||||
path = release / artifact["path"]
|
||||
header = tarfile.TarInfo(artifact["path"])
|
||||
header.size = artifact["bytes"]
|
||||
header.mode = 0o644
|
||||
with path.open("rb") as contents:
|
||||
archive.addfile(header, contents)
|
||||
publish(prepared, output)
|
||||
return sorted(path.name for path in output.iterdir())
|
||||
|
||||
|
||||
def publish(prepared, output):
|
||||
# Linux renameat2 publishes the entire owned tree without replacing even an
|
||||
# empty directory created by another publisher. Failure cleans only staging.
|
||||
libc = ctypes.CDLL("libc.so.6", use_errno=True)
|
||||
rename = libc.renameat2
|
||||
rename.argtypes = [ctypes.c_int, ctypes.c_char_p, ctypes.c_int, ctypes.c_char_p, ctypes.c_uint]
|
||||
rename.restype = ctypes.c_int
|
||||
if rename(-100, os.fsencode(prepared), -100, os.fsencode(output), 1) != 0:
|
||||
error = ctypes.get_errno()
|
||||
raise OSError(error, os.strerror(error), str(output))
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("release", type=Path)
|
||||
parser.add_argument("--output", required=True, type=Path)
|
||||
args = parser.parse_args()
|
||||
try:
|
||||
for name in create(args.release, args.output):
|
||||
print(name)
|
||||
except (OSError, ValueError, KeyError, TypeError, tarfile.TarError) as error:
|
||||
parser.exit(1, f"cannot prepare release assets: {error}\n")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in new issue
Block a user