mirror of
https://github.com/EstrellaXD/Auto_Bangumi.git
synced 2026-10-29 19:57:14 +08:00
183 lines
5.4 KiB
Python
183 lines
5.4 KiB
Python
#!/usr/bin/env python3
|
|
"""Build the native E2E image from an isolated temporary context."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import re
|
|
import shutil
|
|
import subprocess
|
|
import tempfile
|
|
from pathlib import Path
|
|
from typing import Iterable, Sequence
|
|
|
|
DEFAULT_VERSION = "3.3.999-e2e.1"
|
|
_VERSION_RE = re.compile(r"^\d+\.\d+\.\d+(?:-[0-9A-Za-z]+(?:[.-][0-9A-Za-z]+)*)?$")
|
|
|
|
|
|
def repository_root() -> Path:
|
|
return Path(__file__).resolve().parents[2]
|
|
|
|
|
|
def validate_version(version: str) -> str:
|
|
if not _VERSION_RE.fullmatch(version):
|
|
raise ValueError(f"E2E image version must be SemVer-compatible: {version!r}")
|
|
return version
|
|
|
|
|
|
def tracked_backend_files(repo_root: Path) -> tuple[Path, ...]:
|
|
"""Return tracked build inputs while excluding unrelated untracked files."""
|
|
result = subprocess.run(
|
|
[
|
|
"git",
|
|
"ls-files",
|
|
"-z",
|
|
"--",
|
|
".dockerignore",
|
|
"Dockerfile",
|
|
"entrypoint.sh",
|
|
"backend",
|
|
],
|
|
cwd=repo_root,
|
|
check=True,
|
|
capture_output=True,
|
|
)
|
|
tracked = (
|
|
Path(value.decode("utf-8")) for value in result.stdout.split(b"\0") if value
|
|
)
|
|
# `git ls-files` still reports tracked files deleted in the current
|
|
# worktree. Local pre-commit image verification must reflect those
|
|
# deletions instead of trying to copy paths that intentionally no longer
|
|
# exist. Explicit `tracked_files` passed to `prepare_context` remain
|
|
# strict and still surface missing required inputs.
|
|
return tuple(path for path in tracked if (repo_root / path).is_file())
|
|
|
|
|
|
def _safe_relative(path: Path) -> Path:
|
|
if path.is_absolute() or ".." in path.parts:
|
|
raise ValueError(f"Tracked path must stay inside the repository: {path}")
|
|
return path
|
|
|
|
|
|
def prepare_context(
|
|
repo_root: Path,
|
|
dist_dir: Path,
|
|
destination: Path,
|
|
*,
|
|
version: str = DEFAULT_VERSION,
|
|
tracked_files: Iterable[Path] | None = None,
|
|
) -> Path:
|
|
"""Copy tracked build inputs, WebUI dist, and version into *destination*."""
|
|
version = validate_version(version)
|
|
repo_root = repo_root.resolve()
|
|
dist_dir = dist_dir.resolve()
|
|
destination = destination.resolve()
|
|
if not (dist_dir / "index.html").is_file():
|
|
raise FileNotFoundError(f"Built WebUI is missing index.html: {dist_dir}")
|
|
destination.mkdir(parents=True, exist_ok=True)
|
|
|
|
selected = tracked_files or tracked_backend_files(repo_root)
|
|
for relative in selected:
|
|
relative = _safe_relative(Path(relative))
|
|
source = repo_root / relative
|
|
if not source.is_file():
|
|
raise FileNotFoundError(f"Tracked build input is missing: {source}")
|
|
target = destination / relative
|
|
target.parent.mkdir(parents=True, exist_ok=True)
|
|
shutil.copy2(source, target)
|
|
|
|
staged_dist = destination / "backend/src/dist"
|
|
if staged_dist.exists():
|
|
shutil.rmtree(staged_dist)
|
|
shutil.copytree(dist_dir, staged_dist)
|
|
|
|
version_module = destination / "backend/src/module/__version__.py"
|
|
version_module.parent.mkdir(parents=True, exist_ok=True)
|
|
version_module.write_text(f'VERSION = "{version}"\n', encoding="utf-8")
|
|
return destination
|
|
|
|
|
|
def build_command(context: Path, *, image: str, version: str) -> list[str]:
|
|
validate_version(version)
|
|
if not image or any(character.isspace() for character in image):
|
|
raise ValueError(f"Invalid Docker image name: {image!r}")
|
|
return [
|
|
"docker",
|
|
"build",
|
|
"--build-arg",
|
|
f"VERSION={version}",
|
|
"--tag",
|
|
image,
|
|
"--pull=false",
|
|
str(context.resolve()),
|
|
]
|
|
|
|
|
|
def verification_command(*, image: str, version: str) -> list[str]:
|
|
validate_version(version)
|
|
script = (
|
|
"import sys; "
|
|
"from pathlib import Path; "
|
|
"from module.__version__ import VERSION; "
|
|
"expected=sys.argv[1]; "
|
|
"image_version=Path('/app/IMAGE_VERSION').read_text().strip(); "
|
|
"assert VERSION == expected == image_version, "
|
|
"(VERSION, expected, image_version)"
|
|
)
|
|
return [
|
|
"docker",
|
|
"run",
|
|
"--rm",
|
|
"--entrypoint",
|
|
"python",
|
|
image,
|
|
"-c",
|
|
script,
|
|
version,
|
|
]
|
|
|
|
|
|
def build_image(
|
|
*,
|
|
repo_root: Path,
|
|
dist_dir: Path,
|
|
image: str,
|
|
version: str,
|
|
runner=subprocess.run,
|
|
) -> None:
|
|
with tempfile.TemporaryDirectory(prefix="ab-e2e-build-") as temporary:
|
|
context = prepare_context(
|
|
repo_root,
|
|
dist_dir,
|
|
Path(temporary) / "context",
|
|
version=version,
|
|
)
|
|
runner(build_command(context, image=image, version=version), check=True)
|
|
runner(verification_command(image=image, version=version), check=True)
|
|
|
|
|
|
def _parser() -> argparse.ArgumentParser:
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
parser.add_argument("--version", default=DEFAULT_VERSION)
|
|
parser.add_argument("--image", default="auto-bangumi:e2e")
|
|
parser.add_argument("--dist", type=Path)
|
|
return parser
|
|
|
|
|
|
def main(argv: Sequence[str] | None = None) -> int:
|
|
args = _parser().parse_args(argv)
|
|
root = repository_root()
|
|
dist = args.dist or root / "webui/dist"
|
|
build_image(
|
|
repo_root=root,
|
|
dist_dir=dist,
|
|
image=args.image,
|
|
version=args.version,
|
|
)
|
|
print(f"Built and verified {args.image} at version {args.version}")
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|