Publish curated ITWorx-ModelForge source from 990401737f96c8d46a9f7b0d84994516cadcb6a0
This commit is contained in:
1 parent
7082ab955a
commit
848f0d8f8a
434 files changed
+67298
-4315
No files matched your search
+174
-102
@@ -9,14 +9,16 @@ import json
|
||||
import os
|
||||
import secrets
|
||||
import shutil
|
||||
import signal
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
from collections.abc import Iterator
|
||||
from contextlib import contextmanager
|
||||
from datetime import UTC, datetime
|
||||
from pathlib import Path
|
||||
from urllib.parse import urlparse
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
IMAGES = (
|
||||
"modelforge-api",
|
||||
@@ -24,11 +26,36 @@ IMAGES = (
|
||||
"modelforge-node-agent",
|
||||
"modelforge-runtime-worker",
|
||||
)
|
||||
ACCEPTANCE_SERVICES = ("postgres", "redis", "api", "migrate", "web")
|
||||
CONFIG_SEED_IMAGE = (
|
||||
"redis:7-alpine@sha256:ff02b58f971e7d7d156a1267e283fcbbeee91773b6aa36c49dac28ecfe28eadf"
|
||||
)
|
||||
|
||||
|
||||
def write_acceptance_compose_override(path: Path) -> None:
|
||||
"""Write the disposable RC service policy used by acceptance."""
|
||||
|
||||
services = []
|
||||
for service in ACCEPTANCE_SERVICES:
|
||||
services.append(f' {service}:\n restart: "no"\n')
|
||||
if service == "api":
|
||||
services.append(
|
||||
" volumes:\n"
|
||||
" - type: volume\n"
|
||||
" source: acceptance-config\n"
|
||||
" target: /app/config\n"
|
||||
" read_only: true\n"
|
||||
)
|
||||
path.write_text(
|
||||
"services:\n"
|
||||
f"{''.join(services)}"
|
||||
"volumes:\n"
|
||||
" acceptance-config:\n",
|
||||
encoding="utf-8",
|
||||
newline="\n",
|
||||
)
|
||||
|
||||
|
||||
def run(
|
||||
*args: str,
|
||||
cwd: Path = ROOT,
|
||||
@@ -197,12 +224,57 @@ def cleanup_candidate_images(version: str, commit: str) -> None:
|
||||
run("docker", "image", "rm", "--force", tag, check=False)
|
||||
|
||||
|
||||
def _terminate_after_cleanup(signum: int, _frame: object) -> None:
|
||||
raise SystemExit(128 + signum)
|
||||
|
||||
|
||||
@contextmanager
|
||||
def managed_compose_cleanup(
|
||||
compose: tuple[str, ...], env: dict[str, str]
|
||||
) -> Iterator[None]:
|
||||
"""Remove the disposable project on normal, failed, or interrupted exits."""
|
||||
|
||||
def cleanup() -> None:
|
||||
run(*compose, "down", "--volumes", "--remove-orphans", env=env, check=False)
|
||||
|
||||
atexit.register(cleanup)
|
||||
previous_handlers = {
|
||||
signum: signal.signal(signum, _terminate_after_cleanup)
|
||||
for signum in (signal.SIGINT, signal.SIGTERM)
|
||||
}
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
cleanup_complete = False
|
||||
try:
|
||||
cleanup()
|
||||
cleanup_complete = True
|
||||
finally:
|
||||
if cleanup_complete:
|
||||
atexit.unregister(cleanup)
|
||||
for signum, handler in previous_handlers.items():
|
||||
signal.signal(signum, handler)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--public-api-origin", required=True)
|
||||
parser.add_argument("--output", type=Path, default=Path("acceptance-evidence"))
|
||||
parser.add_argument("--trivy", default="trivy")
|
||||
parser.add_argument("--project-suffix", default=os.environ.get("GITHUB_RUN_ID", "manual"))
|
||||
parser.add_argument(
|
||||
"--canonical-source-commit",
|
||||
default=None,
|
||||
help=(
|
||||
"exact canonical source SHA for a parentless public export; release labels are bound "
|
||||
"to it only when PUBLIC_SOURCE_MANIFEST.json proves the mapping"
|
||||
),
|
||||
)
|
||||
parser.add_argument(
|
||||
"--retain-image-bundle",
|
||||
action="store_true",
|
||||
help="retain the verified four-image Docker transport bundle in the output directory",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
origin = args.public_api_origin.rstrip("/")
|
||||
@@ -217,25 +289,35 @@ def main() -> int:
|
||||
commit = run("git", "rev-parse", "HEAD", capture=True)
|
||||
if run("git", "status", "--porcelain", capture=True):
|
||||
raise SystemExit("Acceptance must run from a clean, exact source commit")
|
||||
canonical_commit = args.canonical_source_commit or commit
|
||||
if len(canonical_commit) != 40 or any(
|
||||
character not in "0123456789abcdef" for character in canonical_commit
|
||||
):
|
||||
raise SystemExit("--canonical-source-commit must be an exact lowercase 40-character SHA")
|
||||
version = (ROOT / "VERSION").read_text(encoding="utf-8").strip()
|
||||
atexit.register(cleanup_candidate_images, version, commit)
|
||||
atexit.register(cleanup_candidate_images, version, canonical_commit)
|
||||
built_at = datetime.now(UTC).isoformat()
|
||||
output = args.output.resolve()
|
||||
output.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
project_suffix = "".join(ch for ch in args.project_suffix.lower() if ch.isalnum())[-24:]
|
||||
project = f"modelforge-rc-{project_suffix or secrets.token_hex(6)}"
|
||||
if not project.startswith("modelforge-rc-"):
|
||||
raise SystemExit("refusing a non-RC Compose project name")
|
||||
project = f"modelforge-validation-{project_suffix or secrets.token_hex(6)}"
|
||||
if not project.startswith("modelforge-validation-"):
|
||||
raise SystemExit("refusing a non-validation Compose project name")
|
||||
|
||||
run(
|
||||
release_command = [
|
||||
sys.executable,
|
||||
str(ROOT / "scripts" / "release_build.py"),
|
||||
"--output",
|
||||
str(output / "release"),
|
||||
"--public-api-origin",
|
||||
origin,
|
||||
)
|
||||
]
|
||||
if args.canonical_source_commit is not None:
|
||||
release_command.extend(("--canonical-source-commit", canonical_commit))
|
||||
if args.retain_image_bundle:
|
||||
release_command.append("--image-bundle")
|
||||
run(*release_command)
|
||||
|
||||
image_records: list[dict[str, str]] = []
|
||||
for image in IMAGES:
|
||||
@@ -275,13 +357,13 @@ def main() -> int:
|
||||
runtime_password = secrets.token_hex(24)
|
||||
operator_key = secrets.token_urlsafe(48)
|
||||
backup_key = base64.urlsafe_b64encode(secrets.token_bytes(32)).decode("ascii")
|
||||
database = "modelforge_rc"
|
||||
database = "modelforge_validation"
|
||||
env = os.environ.copy()
|
||||
env.update(
|
||||
{
|
||||
"COMPOSE_PROJECT_NAME": project,
|
||||
"MODELFORGE_VERSION": version,
|
||||
"MODELFORGE_COMMIT": commit,
|
||||
"MODELFORGE_COMMIT": canonical_commit,
|
||||
"MODELFORGE_BUILT_AT": built_at,
|
||||
"MODELFORGE_API_IMAGE": f"modelforge-api:{version}",
|
||||
"MODELFORGE_WEB_IMAGE": f"modelforge-web:{version}",
|
||||
@@ -301,8 +383,8 @@ def main() -> int:
|
||||
"MODELFORGE_BACKUP_ENCRYPTION_KEY": backup_key,
|
||||
"MODELFORGE_CORS_ORIGINS": origin,
|
||||
"VITE_API_BASE_URL": origin,
|
||||
"MODELFORGE_SOURCE_COMMIT": commit,
|
||||
"MODELFORGE_SOURCE_REFERENCE": "rc-acceptance",
|
||||
"MODELFORGE_SOURCE_COMMIT": canonical_commit,
|
||||
"MODELFORGE_SOURCE_REFERENCE": "validation-acceptance",
|
||||
"MODELFORGE_SOURCE_REPOSITORY": "public-source-candidate",
|
||||
"MODELFORGE_API_BIND": "127.0.0.1",
|
||||
"MODELFORGE_API_PUBLISHED_PORT": "0",
|
||||
@@ -317,19 +399,7 @@ def main() -> int:
|
||||
}
|
||||
)
|
||||
acceptance_override = output / "acceptance-compose.override.yml"
|
||||
acceptance_override.write_text(
|
||||
"services:\n"
|
||||
" api:\n"
|
||||
" volumes:\n"
|
||||
" - type: volume\n"
|
||||
" source: acceptance-config\n"
|
||||
" target: /app/config\n"
|
||||
" read_only: true\n"
|
||||
"volumes:\n"
|
||||
" acceptance-config:\n",
|
||||
encoding="utf-8",
|
||||
newline="\n",
|
||||
)
|
||||
write_acceptance_compose_override(acceptance_override)
|
||||
compose = (
|
||||
"docker",
|
||||
"compose",
|
||||
@@ -346,95 +416,97 @@ def main() -> int:
|
||||
summary: dict[str, object] = {
|
||||
"schema_version": 1,
|
||||
"project": project,
|
||||
"source_commit": commit,
|
||||
"source_commit": canonical_commit,
|
||||
"build_tree_commit": commit,
|
||||
"version": version,
|
||||
"public_api_origin": origin,
|
||||
"image_bundle_retained": args.retain_image_bundle,
|
||||
"production_changed": False,
|
||||
"compute_identity_created": False,
|
||||
"images": image_records,
|
||||
"checks": {},
|
||||
}
|
||||
try:
|
||||
run(*compose, "config", "-q", env=env)
|
||||
run(*compose, "up", "-d", "--no-build", "--wait", "postgres", "redis", env=env)
|
||||
provision_database_roles(compose, env, database)
|
||||
provision_config_volume(project)
|
||||
run(*compose, "up", "-d", "--no-build", "--wait", "api", "web", env=env)
|
||||
api_container = run(*compose, "ps", "--quiet", "api", env=env, capture=True)
|
||||
if not api_container:
|
||||
raise RuntimeError("acceptance API container was not created")
|
||||
api = "http://127.0.0.1:8000"
|
||||
with managed_compose_cleanup(compose, env):
|
||||
try:
|
||||
run(*compose, "config", "-q", env=env)
|
||||
run(*compose, "up", "-d", "--no-build", "--wait", "postgres", "redis", env=env)
|
||||
provision_database_roles(compose, env, database)
|
||||
provision_config_volume(project)
|
||||
run(*compose, "up", "-d", "--no-build", "--wait", "api", "web", env=env)
|
||||
api_container = run(*compose, "ps", "--quiet", "api", env=env, capture=True)
|
||||
if not api_container:
|
||||
raise RuntimeError("acceptance API container was not created")
|
||||
api = "http://127.0.0.1:8000"
|
||||
|
||||
wait_for_container_status(api_container, f"{api}/api/v1/health/live", 200)
|
||||
wait_for_container_status(api_container, f"{api}/api/v1/health/ready", 200)
|
||||
version_status, version_body = container_http_status(
|
||||
api_container, f"{api}/api/v1/version"
|
||||
)
|
||||
unauthenticated, _ = container_http_status(
|
||||
api_container, f"{api}/api/v1/admin/recovery/dashboard"
|
||||
)
|
||||
authenticated, _ = container_http_status(
|
||||
api_container,
|
||||
f"{api}/api/v1/admin/recovery/dashboard",
|
||||
operator=True,
|
||||
)
|
||||
wait_for_container_status(api_container, "http://web:3000/", 200)
|
||||
if version_status != 200 or unauthenticated != 401 or authenticated != 200:
|
||||
raise RuntimeError(
|
||||
"acceptance boundary mismatch: "
|
||||
f"version={version_status}, unauthenticated={unauthenticated}, "
|
||||
f"authenticated={authenticated}"
|
||||
wait_for_container_status(api_container, f"{api}/api/v1/health/live", 200)
|
||||
wait_for_container_status(api_container, f"{api}/api/v1/health/ready", 200)
|
||||
version_status, version_body = container_http_status(
|
||||
api_container, f"{api}/api/v1/version"
|
||||
)
|
||||
version_payload = json.loads(version_body)
|
||||
if version_payload.get("version") != version:
|
||||
raise RuntimeError(f"running version does not match {version}: {version_payload}")
|
||||
unauthenticated, _ = container_http_status(
|
||||
api_container, f"{api}/api/v1/admin/recovery/dashboard"
|
||||
)
|
||||
authenticated, _ = container_http_status(
|
||||
api_container,
|
||||
f"{api}/api/v1/admin/recovery/dashboard",
|
||||
operator=True,
|
||||
)
|
||||
wait_for_container_status(api_container, "http://web:3000/", 200)
|
||||
if version_status != 200 or unauthenticated != 401 or authenticated != 200:
|
||||
raise RuntimeError(
|
||||
"acceptance boundary mismatch: "
|
||||
f"version={version_status}, unauthenticated={unauthenticated}, "
|
||||
f"authenticated={authenticated}"
|
||||
)
|
||||
version_payload = json.loads(version_body)
|
||||
if version_payload.get("version") != version:
|
||||
raise RuntimeError(f"running version does not match {version}: {version_payload}")
|
||||
|
||||
volumes = run(
|
||||
"docker",
|
||||
"volume",
|
||||
"ls",
|
||||
"--filter",
|
||||
f"label=com.docker.compose.project={project}",
|
||||
"--format",
|
||||
"{{.Name}}",
|
||||
capture=True,
|
||||
).splitlines()
|
||||
if not volumes or any(not volume.startswith(f"{project}_") for volume in volumes):
|
||||
raise RuntimeError(f"Compose volumes are not isolated under {project}: {volumes}")
|
||||
summary["checks"] = {
|
||||
"live": 200,
|
||||
"ready": 200,
|
||||
"version": version_status,
|
||||
"admin_without_key": unauthenticated,
|
||||
"admin_with_key": authenticated,
|
||||
"console": 200,
|
||||
"database_roles_provisioned": True,
|
||||
"network_probe_container": "api",
|
||||
"isolated_volumes": sorted(volumes),
|
||||
}
|
||||
summary["result"] = "PASS"
|
||||
(output / "compose-ps.json").write_text(
|
||||
run(*compose, "ps", "--format", "json", env=env, capture=True) + "\n",
|
||||
encoding="utf-8",
|
||||
newline="\n",
|
||||
)
|
||||
except Exception:
|
||||
summary["result"] = "FAIL"
|
||||
(output / "compose-logs.txt").write_text(
|
||||
run(*compose, "logs", "--no-color", env=env, check=False, capture=True) + "\n",
|
||||
encoding="utf-8",
|
||||
newline="\n",
|
||||
)
|
||||
raise
|
||||
finally:
|
||||
run(*compose, "down", "--volumes", "--remove-orphans", env=env, check=False)
|
||||
(output / "acceptance-summary.json").write_text(
|
||||
json.dumps(summary, indent=2, sort_keys=True) + "\n",
|
||||
encoding="utf-8",
|
||||
newline="\n",
|
||||
)
|
||||
volumes = run(
|
||||
"docker",
|
||||
"volume",
|
||||
"ls",
|
||||
"--filter",
|
||||
f"label=com.docker.compose.project={project}",
|
||||
"--format",
|
||||
"{{.Name}}",
|
||||
capture=True,
|
||||
).splitlines()
|
||||
if not volumes or any(not volume.startswith(f"{project}_") for volume in volumes):
|
||||
raise RuntimeError(f"Compose volumes are not isolated under {project}: {volumes}")
|
||||
summary["checks"] = {
|
||||
"live": 200,
|
||||
"ready": 200,
|
||||
"version": version_status,
|
||||
"admin_without_key": unauthenticated,
|
||||
"admin_with_key": authenticated,
|
||||
"console": 200,
|
||||
"database_roles_provisioned": True,
|
||||
"network_probe_container": "api",
|
||||
"isolated_volumes": sorted(volumes),
|
||||
}
|
||||
summary["result"] = "PASS"
|
||||
(output / "compose-ps.json").write_text(
|
||||
run(*compose, "ps", "--format", "json", env=env, capture=True) + "\n",
|
||||
encoding="utf-8",
|
||||
newline="\n",
|
||||
)
|
||||
except BaseException:
|
||||
summary["result"] = "FAIL"
|
||||
(output / "compose-logs.txt").write_text(
|
||||
run(*compose, "logs", "--no-color", env=env, check=False, capture=True) + "\n",
|
||||
encoding="utf-8",
|
||||
newline="\n",
|
||||
)
|
||||
raise
|
||||
finally:
|
||||
(output / "acceptance-summary.json").write_text(
|
||||
json.dumps(summary, indent=2, sort_keys=True) + "\n",
|
||||
encoding="utf-8",
|
||||
newline="\n",
|
||||
)
|
||||
|
||||
print(f"Isolated RC acceptance PASS for {commit} in {project}")
|
||||
print(f"Isolated RC acceptance PASS for {canonical_commit} in {project}")
|
||||
return 0
|
||||
|
||||
|
||||
|
||||
Reference in new issue
Block a user