feat(observatory): seal blocked run preparations
This commit is contained in:
@@ -43,9 +43,78 @@ def sha256_file(path: Path) -> str:
|
||||
|
||||
def git_revision() -> str:
|
||||
result = subprocess.run(
|
||||
["git", "rev-parse", "HEAD"], cwd=REPOSITORY_ROOT, check=True, capture_output=True, text=True
|
||||
["git", "rev-parse", "HEAD"],
|
||||
cwd=REPOSITORY_ROOT,
|
||||
check=True,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
return result.stdout.strip()
|
||||
revision = result.stdout.strip()
|
||||
if re.fullmatch(r"[a-f0-9]{40}", revision) is None:
|
||||
raise ArtifactBuildError("Git revision is not a full SHA-1")
|
||||
return revision
|
||||
|
||||
|
||||
def materialize_revision(revision: str, destination: Path) -> None:
|
||||
"""Materialize only the declared release inputs from one exact Git commit."""
|
||||
|
||||
if re.fullmatch(r"[a-f0-9]{40}", revision) is None:
|
||||
raise ArtifactBuildError("artifact revision is invalid")
|
||||
if destination.exists():
|
||||
raise ArtifactBuildError("revision destination already exists")
|
||||
|
||||
verified = subprocess.run(
|
||||
["git", "rev-parse", "--verify", f"{revision}^{{commit}}"],
|
||||
cwd=REPOSITORY_ROOT,
|
||||
check=False,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
if verified.returncode != 0 or verified.stdout.strip() != revision:
|
||||
raise ArtifactBuildError("artifact revision does not identify an existing commit")
|
||||
|
||||
archive_path = destination.parent / "source.tar"
|
||||
archived = subprocess.run(
|
||||
[
|
||||
"git",
|
||||
"archive",
|
||||
"--format=tar",
|
||||
"--output",
|
||||
str(archive_path),
|
||||
revision,
|
||||
"--",
|
||||
*(path.as_posix() for path in SOURCES),
|
||||
],
|
||||
cwd=REPOSITORY_ROOT,
|
||||
check=False,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
if archived.returncode != 0:
|
||||
raise ArtifactBuildError("declared release inputs are not present in artifact revision")
|
||||
|
||||
destination.mkdir()
|
||||
root = destination.resolve()
|
||||
with tarfile.open(archive_path, "r:") as archive:
|
||||
members = archive.getmembers()
|
||||
for member in members:
|
||||
target = (destination / member.name).resolve()
|
||||
if target != root and root not in target.parents:
|
||||
raise ArtifactBuildError("Git archive contains an unsafe path")
|
||||
if not member.isdir() and not member.isreg():
|
||||
raise ArtifactBuildError("Git archive contains a non-regular release input")
|
||||
for member in members:
|
||||
target = destination / member.name
|
||||
if member.isdir():
|
||||
target.mkdir(parents=True, exist_ok=True)
|
||||
continue
|
||||
stream = archive.extractfile(member)
|
||||
if stream is None:
|
||||
raise ArtifactBuildError("Git archive release input cannot be read")
|
||||
target.parent.mkdir(parents=True, exist_ok=True)
|
||||
with stream, target.open("wb") as output:
|
||||
while chunk := stream.read(1024 * 1024):
|
||||
output.write(chunk)
|
||||
|
||||
|
||||
def tar_info(path: Path, arcname: str) -> tarfile.TarInfo:
|
||||
@@ -81,24 +150,34 @@ def write_archive(stage: Path, target: Path) -> None:
|
||||
archive.addfile(info, io.BytesIO())
|
||||
|
||||
|
||||
def build(patch_id: str, output_directory: Path, *, revision: str | None = None) -> dict[str, object]:
|
||||
def build(
|
||||
patch_id: str,
|
||||
output_directory: Path,
|
||||
*,
|
||||
revision: str | None = None,
|
||||
) -> dict[str, object]:
|
||||
if PATCH_ID.fullmatch(patch_id) is None:
|
||||
raise ArtifactBuildError("patch id is invalid")
|
||||
sources = tuple(REPOSITORY_ROOT / source for source in SOURCES)
|
||||
if any(path.is_symlink() or not path.is_file() for path in sources):
|
||||
raise ArtifactBuildError("release input is not a regular file")
|
||||
selected_revision = revision or git_revision()
|
||||
if re.fullmatch(r"[a-f0-9]{40}", selected_revision) is None:
|
||||
raise ArtifactBuildError("artifact revision is invalid")
|
||||
with tempfile.TemporaryDirectory(prefix="mission-core-m49-tgs-full-") as directory:
|
||||
stage = Path(directory)
|
||||
snapshot = stage / "source"
|
||||
materialize_revision(selected_revision, snapshot)
|
||||
sources = tuple(snapshot / source for source in SOURCES)
|
||||
if any(path.is_symlink() or not path.is_file() for path in sources):
|
||||
raise ArtifactBuildError("release input is not a regular file")
|
||||
payload = stage / "payload"
|
||||
payload.mkdir()
|
||||
files: dict[str, dict[str, object]] = {}
|
||||
for source in sources:
|
||||
destination = payload / source.name
|
||||
destination.write_bytes(source.read_bytes())
|
||||
files[destination.name] = {"bytes": destination.stat().st_size, "sha256": sha256_file(destination)}
|
||||
files[destination.name] = {
|
||||
"bytes": destination.stat().st_size,
|
||||
"sha256": sha256_file(destination),
|
||||
}
|
||||
release = {
|
||||
"schema_version": "missioncore.m49-tgs-full-shadow-worker-release/v1",
|
||||
"patch_id": patch_id,
|
||||
@@ -106,7 +185,9 @@ def build(patch_id: str, output_directory: Path, *, revision: str | None = None)
|
||||
"worker_id": "worker-006",
|
||||
"candidate_id": "travel-tgs-full-shadow",
|
||||
"license": "GPL-3.0-or-later",
|
||||
"source_pack_sha256": "0685d24219d8236caf8b7f1685e93f6d6b59e7fd015a768d88a92bbe8b154944",
|
||||
"source_pack_sha256": (
|
||||
"0685d24219d8236caf8b7f1685e93f6d6b59e7fd015a768d88a92bbe8b154944"
|
||||
),
|
||||
"images": {
|
||||
"travel": "sha256:7b412020f4d8392d1d1ed1b33beadc44140f0ea8f781e62dd69796042334300f",
|
||||
"parity": "sha256:ceb13548617e4bd3f619766bfdff00af3fa5160946b367828da6d2233dcdcba0",
|
||||
@@ -121,10 +202,14 @@ def build(patch_id: str, output_directory: Path, *, revision: str | None = None)
|
||||
"files": files,
|
||||
}
|
||||
release_path = payload / "release.json"
|
||||
release_path.write_text(json.dumps(release, indent=2, sort_keys=True) + "\n", encoding="utf-8")
|
||||
release_path.write_text(
|
||||
json.dumps(release, indent=2, sort_keys=True) + "\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
payload_names = sorted((*files, release_path.name))
|
||||
(stage / "manifest.env").write_text(
|
||||
f"id={patch_id}\ncomponent=mission-core-worker\ntype=qualification-release\n", encoding="utf-8"
|
||||
f"id={patch_id}\ncomponent=mission-core-worker\ntype=qualification-release\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
(stage / "files.txt").write_text("\n".join(payload_names) + "\n", encoding="utf-8")
|
||||
target = output_directory.resolve() / f"nodedc-{patch_id}.tgz"
|
||||
@@ -142,10 +227,15 @@ def build(patch_id: str, output_directory: Path, *, revision: str | None = None)
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("patch_id")
|
||||
parser.add_argument("--output-directory", type=Path, default=REPOSITORY_ROOT / ".runtime/worker-artifacts")
|
||||
parser.add_argument(
|
||||
"--output-directory",
|
||||
type=Path,
|
||||
default=REPOSITORY_ROOT / ".runtime/worker-artifacts",
|
||||
)
|
||||
parser.add_argument("--revision")
|
||||
arguments = parser.parse_args()
|
||||
try:
|
||||
result = build(arguments.patch_id, arguments.output_directory)
|
||||
result = build(arguments.patch_id, arguments.output_directory, revision=arguments.revision)
|
||||
except (ArtifactBuildError, OSError, subprocess.SubprocessError) as exc:
|
||||
parser.error(str(exc))
|
||||
print(json.dumps(result, indent=2, sort_keys=True))
|
||||
|
||||
Reference in New Issue
Block a user