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
@@ -14,7 +14,6 @@ from pathlib import Path
|
||||
|
||||
|
||||
REPOSITORY_ROOT = Path(__file__).resolve().parents[2]
|
||||
DEPLOY_WORKFLOW = REPOSITORY_ROOT / ".gitea" / "workflows" / "unraid-deploy.yml"
|
||||
VALIDATION_WORKFLOW = REPOSITORY_ROOT / ".gitea" / "workflows" / "managed-validation.yml"
|
||||
|
||||
|
||||
@@ -62,110 +61,15 @@ def _run_script(step: str) -> str:
|
||||
class ProductionDeployPolicyTests(unittest.TestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls) -> None:
|
||||
cls.deploy = DEPLOY_WORKFLOW.read_text(encoding="utf-8")
|
||||
cls.validation = VALIDATION_WORKFLOW.read_text(encoding="utf-8")
|
||||
|
||||
def test_deployment_has_no_automatic_trigger(self) -> None:
|
||||
trigger = _indented_block(self.deploy, "on:")
|
||||
trigger_keys = re.findall(r"^ ([a-z_]+):", trigger, flags=re.MULTILINE)
|
||||
self.assertEqual(trigger_keys, ["workflow_dispatch"])
|
||||
self.assertNotRegex(trigger, r"(?m)^\s+push:")
|
||||
|
||||
def test_dispatch_inputs_are_required_and_fail_safe(self) -> None:
|
||||
inputs = _indented_block(self.deploy, " inputs:")
|
||||
input_names = re.findall(r"^ ([a-z_]+):", inputs, flags=re.MULTILINE)
|
||||
self.assertEqual(input_names, ["release_tag", "release_commit", "action"])
|
||||
|
||||
for input_name in ("release_tag", "release_commit"):
|
||||
block = _indented_block(self.deploy, f" {input_name}:")
|
||||
self.assertRegex(block, r"(?m)^ required: true$")
|
||||
self.assertRegex(block, r"(?m)^ type: string$")
|
||||
|
||||
action = _indented_block(self.deploy, " action:")
|
||||
self.assertRegex(action, r"(?m)^ required: true$")
|
||||
self.assertRegex(action, r"(?m)^ default: VERIFY_ONLY$")
|
||||
self.assertRegex(action, r"(?m)^ type: choice$")
|
||||
options = re.findall(r"^ - (\S+)$", action, flags=re.MULTILINE)
|
||||
self.assertEqual(options, ["VERIFY_ONLY", "DEPLOY_STABLE_TO_PRODUCTION"])
|
||||
|
||||
def test_inputs_are_validated_before_checkout(self) -> None:
|
||||
validation = _named_step(self.deploy, "Validate immutable dispatch inputs")
|
||||
checkout = _named_step(self.deploy, "Check out the exact release commit with full history")
|
||||
self.assertLess(self.deploy.index(validation), self.deploy.index(checkout))
|
||||
self.assertIn('"refs/heads/master"', validation)
|
||||
self.assertIn(
|
||||
r"^v(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$",
|
||||
validation,
|
||||
def test_legacy_production_delivery_workflow_is_retired(self) -> None:
|
||||
self.assertFalse(
|
||||
(REPOSITORY_ROOT / ".gitea" / "workflows" / "unraid-deploy.yml").exists()
|
||||
)
|
||||
self.assertIn(r"^[0-9a-f]{40}$", validation)
|
||||
self.assertIn("VERIFY_ONLY|DEPLOY_STABLE_TO_PRODUCTION", validation)
|
||||
|
||||
tag_pattern = re.search(r'RELEASE_TAG\}" =~ (\^.*\$) \]\]', validation)
|
||||
commit_pattern = re.search(r'RELEASE_COMMIT\}" =~ (\^.*\$) \]\]', validation)
|
||||
self.assertIsNotNone(tag_pattern)
|
||||
self.assertIsNotNone(commit_pattern)
|
||||
assert tag_pattern is not None and commit_pattern is not None
|
||||
for valid_tag in ("v0.0.0", "v1.2.1", "v10.20.30"):
|
||||
self.assertRegex(valid_tag, tag_pattern.group(1))
|
||||
for invalid_tag in ("1.2.1", "v01.2.1", "v1.2.1-rc.1", "v1.2.1+build", "master"):
|
||||
self.assertNotRegex(invalid_tag, tag_pattern.group(1))
|
||||
self.assertRegex("0" * 40, commit_pattern.group(1))
|
||||
for invalid_commit in ("0" * 39, "A" * 40, "g" * 40, "master"):
|
||||
self.assertNotRegex(invalid_commit, commit_pattern.group(1))
|
||||
|
||||
def test_checkout_is_pinned_to_the_validated_commit_with_full_history(self) -> None:
|
||||
checkout = _named_step(self.deploy, "Check out the exact release commit with full history")
|
||||
self.assertRegex(checkout, r"actions/checkout@[0-9a-f]{40}")
|
||||
self.assertIn("ref: ${{ inputs.release_commit }}", checkout)
|
||||
self.assertIn("fetch-depth: 0", checkout)
|
||||
self.assertIn("persist-credentials: true", checkout)
|
||||
|
||||
def test_release_provenance_is_verified_and_exported(self) -> None:
|
||||
provenance = _named_step(self.deploy, "Verify stable release provenance")
|
||||
required_fragments = (
|
||||
"git fetch --force --no-recurse-submodules origin",
|
||||
'git cat-file -t "${tag_ref}"',
|
||||
'git rev-parse "${tag_ref}^{tag}"',
|
||||
'git rev-parse "${tag_ref}^{commit}"',
|
||||
"git rev-parse HEAD",
|
||||
"< VERSION",
|
||||
"git merge-base --is-ancestor",
|
||||
"refs/remotes/origin/master",
|
||||
"DEPLOY_COMMIT=%s",
|
||||
'>> "${GITHUB_ENV}"',
|
||||
)
|
||||
for fragment in required_fragments:
|
||||
with self.subTest(fragment=fragment):
|
||||
self.assertIn(fragment, provenance)
|
||||
self.assertNotIn("GITHUB_SHA", self.deploy)
|
||||
|
||||
def test_all_production_shell_blocks_parse(self) -> None:
|
||||
names = (
|
||||
"Validate immutable dispatch inputs",
|
||||
"Verify stable release provenance",
|
||||
"Verification-only result",
|
||||
"Deploy verified stable release to production",
|
||||
)
|
||||
for name in names:
|
||||
script = _run_script(_named_step(self.deploy, name))
|
||||
result = subprocess.run(
|
||||
["bash", "-n"],
|
||||
input=script.encode("utf-8"),
|
||||
capture_output=True,
|
||||
check=False,
|
||||
)
|
||||
with self.subTest(step=name):
|
||||
self.assertEqual(result.returncode, 0, result.stderr.decode("utf-8", "replace"))
|
||||
|
||||
def test_deployment_requires_the_explicit_action_and_verified_commit(self) -> None:
|
||||
deploy = _named_step(self.deploy, "Deploy verified stable release to production")
|
||||
self.assertIn("inputs.action == 'DEPLOY_STABLE_TO_PRODUCTION'", deploy)
|
||||
self.assertIn('test -n "${DEPLOY_COMMIT:-}"', deploy)
|
||||
self.assertIn('"${GITHUB_REPOSITORY}" "${DEPLOY_COMMIT}"', deploy)
|
||||
self.assertEqual(self.deploy.count("/opt/gitea-deploy/deploy.py deploy"), 1)
|
||||
|
||||
verify_only = _named_step(self.deploy, "Verification-only result")
|
||||
self.assertIn("inputs.action == 'VERIFY_ONLY'", verify_only)
|
||||
def test_managed_validation_uses_the_shared_validation_pool(self) -> None:
|
||||
self.assertRegex(self.validation, r"(?m)^\s*runs-on:\s*linux-validation\s*$")
|
||||
|
||||
def test_managed_validation_runs_policy_before_profile_work(self) -> None:
|
||||
policy_step = _named_step(self.validation, "Production delivery policy")
|
||||
@@ -176,39 +80,83 @@ class ProductionDeployPolicyTests(unittest.TestCase):
|
||||
self.validation.index(" - name: Validate the requested profile"),
|
||||
)
|
||||
|
||||
def test_security_profile_runs_fail_closed_scanners(self) -> None:
|
||||
step = _named_step(self.validation, "Security — secrets and vulnerable dependencies")
|
||||
self.assertIn("env.PROFILE == 'security'", step)
|
||||
self.assertIn("env.PROFILE == 'full'", step)
|
||||
script = _run_script(step)
|
||||
self.assertIn("gitleaks git .", script)
|
||||
self.assertIn("pip_audit --strict --skip-editable", script)
|
||||
self.assertIn("npm audit --audit-level=high --omit=dev", script)
|
||||
self.assertNotRegex(script, r"(?m)(?:\|\|\s*true|continue-on-error)")
|
||||
def test_full_validation_uses_a_disposable_managed_postgres_boundary(self) -> None:
|
||||
job = _indented_block(self.validation, " full:")
|
||||
boundary = _named_step(self.validation, "Managed PostgreSQL boundary")
|
||||
cleanup = _named_step(self.validation, "Remove managed PostgreSQL boundary")
|
||||
|
||||
toolchain = _named_step(self.validation, "Toolchain")
|
||||
toolchain_script = _run_script(toolchain)
|
||||
self.assertIn("command -v gitleaks", toolchain_script)
|
||||
self.assertIn("exit 1", toolchain_script)
|
||||
for variable in ("VALIDATION_COMPOSE_PROJECT", "MODELFORGE_POSTGRES_PORT"):
|
||||
self.assertRegex(job, rf"(?m)^ {variable}: .+$")
|
||||
self.assertIn("modelforge-validation-${{ gitea.run_id }}", job)
|
||||
self.assertIn('MODELFORGE_POSTGRES_PORT: "0"', job)
|
||||
self.assertIn("env.PROFILE == 'full'", boundary)
|
||||
self.assertIn("export MODELFORGE_MIGRATION_DATABASE_URL=", boundary)
|
||||
self.assertIn("export MODELFORGE_RUNTIME_DATABASE_URL=", boundary)
|
||||
self.assertIn("up --detach --wait postgres", boundary)
|
||||
self.assertIn('validation_network="${VALIDATION_COMPOSE_PROJECT}_default"', boundary)
|
||||
self.assertIn('job_container="$(hostname)"', boundary)
|
||||
self.assertIn('docker network connect "${validation_network}" "${job_container}"', boundary)
|
||||
self.assertIn('socket.create_connection(("postgres", 5432)', boundary)
|
||||
self.assertNotIn("port postgres 5432", boundary)
|
||||
for variable in (
|
||||
"MODELFORGE_TEST_POSTGRES_URL",
|
||||
"MODELFORGE_TEST_POSTGRES_OWNER_URL",
|
||||
"MODELFORGE_TEST_POSTGRES_RUNTIME_URL",
|
||||
):
|
||||
self.assertIn(f'echo "{variable}=', boundary)
|
||||
self.assertIn('>> "${GITHUB_ENV}"', boundary)
|
||||
self.assertNotIn("SKIPPED", boundary)
|
||||
self.assertIn("always() && env.PROFILE == 'full'", cleanup)
|
||||
self.assertIn("docker network disconnect", cleanup)
|
||||
self.assertIn('"$(hostname)"', cleanup)
|
||||
self.assertIn("down --volumes --remove-orphans", cleanup)
|
||||
self.assertIn("export MODELFORGE_MIGRATION_DATABASE_URL=", cleanup)
|
||||
self.assertIn("export MODELFORGE_RUNTIME_DATABASE_URL=", cleanup)
|
||||
|
||||
def test_compose_validation_renders_the_production_projection(self) -> None:
|
||||
step = _named_step(self.validation, "Compose projections")
|
||||
script = _run_script(step)
|
||||
self.assertIn(
|
||||
"for overlay in backup dr gpu node-agent node-recovery production runtime-worker",
|
||||
script,
|
||||
def test_full_validation_traverses_the_live_schema_and_tests_the_built_console(self) -> None:
|
||||
backend = _named_step(self.validation, "Backend — install, lint, typecheck, test")
|
||||
console = _named_step(self.validation, "Console — install frozen, typecheck, test, build")
|
||||
boundary = _named_step(self.validation, "Managed PostgreSQL boundary")
|
||||
|
||||
self.assertIn('MODELFORGE_MIGRATION_DATABASE_URL="${MODELFORGE_TEST_POSTGRES_OWNER_URL}"', backend)
|
||||
upgrade_commands = re.findall(
|
||||
r'"\$\{BACKEND_VENV\}/bin/python" -m alembic ([^\n]+)',
|
||||
backend,
|
||||
)
|
||||
self.assertEqual(
|
||||
upgrade_commands,
|
||||
["upgrade head", "downgrade 20260828_0022", "upgrade head"],
|
||||
)
|
||||
self.assertLess(self.validation.index(console), self.validation.index(boundary))
|
||||
self.assertLess(self.validation.index(boundary), self.validation.index(backend))
|
||||
|
||||
def test_managed_postgres_shell_blocks_parse(self) -> None:
|
||||
for name in (
|
||||
"Managed PostgreSQL boundary",
|
||||
"Backend — install, lint, typecheck, test",
|
||||
"Remove managed PostgreSQL boundary",
|
||||
):
|
||||
script = _run_script(_named_step(self.validation, name))
|
||||
result = subprocess.run(
|
||||
["bash", "-n"],
|
||||
input=script.encode("utf-8"),
|
||||
capture_output=True,
|
||||
check=False,
|
||||
)
|
||||
with self.subTest(step=name):
|
||||
self.assertEqual(result.returncode, 0, result.stderr.decode("utf-8", "replace"))
|
||||
|
||||
def test_compose_projection_uses_explicit_non_secret_validation_inputs(self) -> None:
|
||||
projection = _named_step(self.validation, "Compose projections")
|
||||
for variable in (
|
||||
"MODELFORGE_POSTGRES_ADMIN_PASSWORD",
|
||||
"MODELFORGE_MIGRATION_DATABASE_URL",
|
||||
"MODELFORGE_MIGRATION_DB_PASSWORD",
|
||||
"MODELFORGE_RUNTIME_DB_PASSWORD",
|
||||
"MODELFORGE_RUNTIME_DATABASE_URL",
|
||||
"MODELFORGE_OPERATOR_API_KEY",
|
||||
"MODELFORGE_BACKUP_ENCRYPTION_KEY",
|
||||
"MODELFORGE_CORS_ORIGINS",
|
||||
"VITE_API_BASE_URL",
|
||||
"MODELFORGE_MIGRATION_DATABASE_URL",
|
||||
):
|
||||
with self.subTest(variable=variable):
|
||||
self.assertIn(f"export {variable}=", script)
|
||||
self.assertIn(f"export {variable}=", projection)
|
||||
self.assertIn("docker compose --env-file .env.example", projection)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -2,12 +2,13 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
import subprocess
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
REPOSITORY_ROOT = Path(__file__).resolve().parents[2]
|
||||
EXPORTER = REPOSITORY_ROOT / "scripts" / "export-public-source.mjs"
|
||||
VALIDATOR = REPOSITORY_ROOT / "scripts" / "validate-public-source.mjs"
|
||||
@@ -28,8 +29,36 @@ class PublicSourceExportTests(unittest.TestCase):
|
||||
"SECURITY.md": "# Security\n",
|
||||
"CONTRIBUTING.md": "# Contributing\n",
|
||||
"docker-compose.yml": "services: {}\n",
|
||||
"docker-compose.validation-tls.yml": "services: {}\n",
|
||||
"deploy/validation/Caddyfile": "https://modelforge.example.test:8443 {}\n",
|
||||
"backend/Dockerfile": (
|
||||
'LABEL org.opencontainers.image.source="https://git.example.com/'
|
||||
'Jens/ITWorx-ModelForge"\n'
|
||||
),
|
||||
"frontend/Dockerfile": (
|
||||
'LABEL org.opencontainers.image.source="https://git.example.com/'
|
||||
'Jens/ITWorx-ModelForge"\n'
|
||||
),
|
||||
"node-agent/Dockerfile": (
|
||||
'LABEL org.opencontainers.image.source="https://git.example.com/'
|
||||
'Jens/ITWorx-ModelForge"\n'
|
||||
),
|
||||
"runtime-worker/Dockerfile": (
|
||||
'LABEL org.opencontainers.image.source="https://git.example.com/'
|
||||
'Jens/ITWorx-ModelForge"\n'
|
||||
),
|
||||
"VERSION": "1.2.1\n",
|
||||
"backend/pyproject.toml": "[project]\nname = 'fixture-api'\n",
|
||||
"clients/README.md": "# Client usage\n",
|
||||
"clients/python/modelforge_client.py": "# Python client\n",
|
||||
"clients/typescript/modelforge-client.mjs": "// JavaScript client\n",
|
||||
"clients/typescript/modelforge-client.d.mts": "// TypeScript declarations\n",
|
||||
"scripts/m16_chaos.py": "# Chaos scenarios\n",
|
||||
"scripts/m16_invariants.py": "# Invariant checks\n",
|
||||
"scripts/m16_soak.py": "# Soak checks\n",
|
||||
"scripts/operator_identity.py": "# Operator identities\n",
|
||||
"scripts/rc_isolation_preflight.py": "# RC isolation\n",
|
||||
"scripts/rc_stack_retention.py": "# RC retention\n",
|
||||
"frontend/package.json": '{"name":"fixture-web"}\n',
|
||||
"node-agent/pyproject.toml": "[project]\nname = 'fixture-agent'\n",
|
||||
"runtime-worker/pyproject.toml": "[project]\nname = 'fixture-worker'\n",
|
||||
@@ -104,6 +133,59 @@ class PublicSourceExportTests(unittest.TestCase):
|
||||
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
|
||||
validation = self._validate(output)
|
||||
self.assertEqual(validation.returncode, 0, validation.stdout + validation.stderr)
|
||||
self.assertTrue((output / "clients/python/modelforge_client.py").is_file())
|
||||
self.assertTrue((output / "clients/typescript/modelforge-client.mjs").is_file())
|
||||
manifest = json.loads((output / "PUBLIC_SOURCE_MANIFEST.json").read_text())
|
||||
self.assertEqual(
|
||||
manifest["sourceRepository"], "https://git.example.com/example/modelforge"
|
||||
)
|
||||
bound_paths = {entry["path"] for entry in manifest["files"]}
|
||||
for relative_path in (
|
||||
"backend/Dockerfile",
|
||||
"frontend/Dockerfile",
|
||||
"node-agent/Dockerfile",
|
||||
"runtime-worker/Dockerfile",
|
||||
):
|
||||
self.assertIn(relative_path, bound_paths)
|
||||
self.assertIn(
|
||||
'org.opencontainers.image.source="https://git.example.com/example/modelforge"',
|
||||
(output / relative_path).read_text(),
|
||||
)
|
||||
|
||||
dockerfile_path = output / "backend/Dockerfile"
|
||||
original_dockerfile = dockerfile_path.read_bytes()
|
||||
dockerfile_entry = next(
|
||||
entry for entry in manifest["files"] if entry["path"] == "backend/Dockerfile"
|
||||
)
|
||||
forged_dockerfile = original_dockerfile.replace(
|
||||
b"https://git.example.com/example/modelforge",
|
||||
b"https://attacker.invalid/repository",
|
||||
)
|
||||
dockerfile_path.write_bytes(forged_dockerfile)
|
||||
dockerfile_entry["bytes"] = len(forged_dockerfile)
|
||||
dockerfile_entry["sha256"] = hashlib.sha256(forged_dockerfile).hexdigest()
|
||||
(output / "PUBLIC_SOURCE_MANIFEST.json").write_text(
|
||||
json.dumps(manifest), encoding="utf-8"
|
||||
)
|
||||
identity_tampering = self._validate(output)
|
||||
self.assertNotEqual(identity_tampering.returncode, 0)
|
||||
self.assertIn("source label does not match", identity_tampering.stderr)
|
||||
|
||||
dockerfile_path.write_bytes(original_dockerfile)
|
||||
original_manifest = json.loads(
|
||||
(output / "PUBLIC_SOURCE_MANIFEST.json").read_text(encoding="utf-8")
|
||||
)
|
||||
original_manifest["files"] = [
|
||||
{
|
||||
"path": entry["path"],
|
||||
"bytes": len((output / entry["path"]).read_bytes()),
|
||||
"sha256": hashlib.sha256((output / entry["path"]).read_bytes()).hexdigest(),
|
||||
}
|
||||
for entry in manifest["files"]
|
||||
]
|
||||
(output / "PUBLIC_SOURCE_MANIFEST.json").write_text(
|
||||
json.dumps(original_manifest), encoding="utf-8"
|
||||
)
|
||||
|
||||
original_readme = (output / "README.md").read_bytes()
|
||||
(output / "README.md").write_text("tampered\n", encoding="utf-8")
|
||||
@@ -127,6 +209,41 @@ class PublicSourceExportTests(unittest.TestCase):
|
||||
self.assertNotEqual(result.returncode, 0)
|
||||
self.assertIn("commit the exact source tree", result.stderr)
|
||||
|
||||
def test_private_owner_inventory_is_excluded_from_public_source(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as temporary:
|
||||
root = Path(temporary)
|
||||
repository = self._repository(root)
|
||||
private_files = {
|
||||
"scripts/portfolio_gitea_snapshot.py": "ExampleRAG owner inventory\n",
|
||||
"scripts/portfolio_gitea_repository_ids.json": '{"private-repo": 12}\n',
|
||||
"backend/tests/test_portfolio_gitea_snapshot.py": "192." + "168.1.40\n",
|
||||
"docs/operations/PORTFOLIO_GITEA_SNAPSHOT.md": "Private owner audit\n",
|
||||
}
|
||||
for relative_path, contents in private_files.items():
|
||||
destination = repository / relative_path
|
||||
destination.parent.mkdir(parents=True, exist_ok=True)
|
||||
destination.write_text(contents, encoding="utf-8")
|
||||
public_script = repository / "scripts/public.py"
|
||||
public_script.write_text("print('public')\n", encoding="utf-8")
|
||||
with (repository / "public-source.allowlist").open("a", encoding="utf-8") as allowlist:
|
||||
allowlist.write("scripts/**\nbackend/**\ndocs/operations/**\n")
|
||||
subprocess.run(["git", "add", "."], cwd=repository, check=True)
|
||||
subprocess.run(
|
||||
[
|
||||
"git", "-c", "user.name=ModelForge test", "-c", "user.email=test@example.invalid",
|
||||
"commit", "-qm", "private inventory fixture",
|
||||
],
|
||||
cwd=repository,
|
||||
check=True,
|
||||
)
|
||||
output = root / "public"
|
||||
result = self._export(repository, output)
|
||||
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
|
||||
self.assertEqual(self._validate(output).returncode, 0)
|
||||
self.assertTrue((output / "scripts/public.py").is_file())
|
||||
for relative_path in private_files:
|
||||
self.assertFalse((output / relative_path).exists())
|
||||
|
||||
def test_sanitized_destination_collisions_fail_before_writing(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as temporary:
|
||||
root = Path(temporary)
|
||||
|
||||
@@ -2,13 +2,15 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import subprocess
|
||||
import importlib.util
|
||||
import json
|
||||
import os
|
||||
import signal
|
||||
import subprocess
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
from unittest import mock
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
WORKFLOW = ROOT / ".gitea" / "workflows" / "public-candidate-acceptance.yml"
|
||||
@@ -39,9 +41,23 @@ class RcAcceptancePolicyTests(unittest.TestCase):
|
||||
def test_exact_source_and_docker_are_required(self) -> None:
|
||||
self.assertIn(r"^[0-9a-f]{40}$", self.workflow)
|
||||
self.assertIn("ref: ${{ inputs.source_commit }}", self.workflow)
|
||||
self.assertIn("--canonical-source-commit", self.workflow)
|
||||
self.assertIn('"--canonical-source-commit", canonical_commit', self.acceptance)
|
||||
self.assertIn('"source_commit": canonical_commit', self.acceptance)
|
||||
self.assertIn("docker version", self.workflow)
|
||||
self.assertNotIn("SKIPPED: no docker", self.workflow)
|
||||
|
||||
def test_optional_candidate_freeze_retains_only_a_verified_transport_bundle(self) -> None:
|
||||
self.assertIn("retain_image_bundle:", self.workflow)
|
||||
self.assertIn("RETAIN_IMAGE_BUNDLE: ${{ inputs.retain_image_bundle }}", self.workflow)
|
||||
self.assertIn("--retain-image-bundle", self.workflow)
|
||||
self.assertIn("--image-bundle", self.acceptance)
|
||||
self.assertIn("acceptance-evidence/release/*.tar", self.workflow)
|
||||
self.assertIn("acceptance-evidence/release/*.tar.gz", self.workflow)
|
||||
self.assertIn("atexit.register(cleanup_candidate_images", self.acceptance)
|
||||
self.assertIn('canonical_commit="${{ inputs.source_commit }}"', self.workflow)
|
||||
self.assertNotIn("candidate_commit=", self.workflow)
|
||||
|
||||
def test_security_tools_are_downloaded_with_exact_checksums(self) -> None:
|
||||
self.assertIn("GITLEAKS_VERSION: 8.30.1", self.workflow)
|
||||
self.assertIn(
|
||||
@@ -78,18 +94,38 @@ class RcAcceptancePolicyTests(unittest.TestCase):
|
||||
self.assertIn("apt-get purge --yes linux-libc-dev", self.runtime_dockerfile)
|
||||
self.assertNotIn("apt-get autoremove", self.runtime_dockerfile)
|
||||
self.assertIn("pip check", self.runtime_dockerfile)
|
||||
self.assertIn("python -m pip uninstall --yes pip setuptools wheel", self.runtime_dockerfile)
|
||||
for dependency in (
|
||||
"filelock==3.20.3",
|
||||
"h2==4.4.1",
|
||||
"idna==3.15",
|
||||
"lief==0.17.2",
|
||||
"Pillow==12.3.0",
|
||||
"Pygments==2.20.0",
|
||||
"protobuf==5.29.6",
|
||||
"requests==2.33.0",
|
||||
"sentencepiece==0.2.2",
|
||||
"sentence-transformers==6.0.1",
|
||||
"transformers==5.16.1",
|
||||
"urllib3==2.7.0",
|
||||
):
|
||||
self.assertIn(dependency, self.runtime_pyproject)
|
||||
self.assertNotIn("'filelock==3.20.3'", self.runtime_dockerfile)
|
||||
|
||||
def test_runtime_worker_has_only_group_read_access_to_the_agent_credential(self) -> None:
|
||||
self.assertIn(
|
||||
"groupadd --system --gid 101 modelforge-agent", self.runtime_dockerfile
|
||||
)
|
||||
self.assertIn(
|
||||
"--groups modelforge-agent --home /app modelforge-runtime",
|
||||
self.runtime_dockerfile,
|
||||
)
|
||||
|
||||
def test_compose_project_is_isolated_and_always_removed(self) -> None:
|
||||
self.assertIn('project = f"modelforge-rc-', self.acceptance)
|
||||
self.assertIn('project = f"modelforge-validation-', self.acceptance)
|
||||
self.assertIn('"MODELFORGE_SOURCE_REFERENCE": "validation-acceptance"', self.acceptance)
|
||||
self.assertIn('project="modelforge-validation-${suffix}"', self.workflow)
|
||||
self.assertNotIn('project = f"modelforge-rc-', self.acceptance)
|
||||
self.assertIn('"MODELFORGE_API_BIND": "127.0.0.1"', self.acceptance)
|
||||
self.assertIn('"MODELFORGE_API_PUBLISHED_PORT": "0"', self.acceptance)
|
||||
self.assertIn('"production_changed": False', self.acceptance)
|
||||
@@ -100,6 +136,123 @@ class RcAcceptancePolicyTests(unittest.TestCase):
|
||||
self.assertNotIn("docker-compose.runtime-worker.yml", self.acceptance)
|
||||
self.assertNotIn("docker-compose.node-agent.yml", self.acceptance)
|
||||
|
||||
def test_acceptance_override_disables_restarts_for_every_service(self) -> None:
|
||||
spec = importlib.util.spec_from_file_location("rc_server_acceptance", ACCEPTANCE)
|
||||
assert spec and spec.loader
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(module)
|
||||
write_override = getattr(module, "write_acceptance_compose_override", None)
|
||||
self.assertIsNotNone(
|
||||
write_override,
|
||||
"acceptance must expose the same override writer used by the RC run",
|
||||
)
|
||||
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
override = Path(directory) / "acceptance-compose.override.yml"
|
||||
write_override(override)
|
||||
env = os.environ.copy()
|
||||
env.update(
|
||||
{
|
||||
"MODELFORGE_VERSION": "9.9.9-test",
|
||||
"MODELFORGE_COMMIT": "a" * 40,
|
||||
"MODELFORGE_BUILT_AT": "2099-01-01T00:00:00+00:00",
|
||||
"MODELFORGE_POSTGRES_DB": "modelforge_validation_test",
|
||||
"MODELFORGE_POSTGRES_ADMIN_PASSWORD": "admin-test-password",
|
||||
"MODELFORGE_MIGRATION_DB_PASSWORD": "owner-test-password",
|
||||
"MODELFORGE_RUNTIME_DB_PASSWORD": "runtime-test-password",
|
||||
"MODELFORGE_MIGRATION_DATABASE_URL": (
|
||||
"postgresql+psycopg://modelforge:owner-test-password@"
|
||||
"postgres:5432/modelforge_validation_test"
|
||||
),
|
||||
"MODELFORGE_RUNTIME_DATABASE_URL": (
|
||||
"postgresql+psycopg://modelforge_runtime:runtime-test-password@"
|
||||
"postgres:5432/modelforge_validation_test"
|
||||
),
|
||||
"MODELFORGE_BACKUP_ENCRYPTION_KEY": "acceptance-test-key",
|
||||
"MODELFORGE_CORS_ORIGINS": "https://modelforge.example.test",
|
||||
"VITE_API_BASE_URL": "https://modelforge.example.test",
|
||||
}
|
||||
)
|
||||
result = subprocess.run(
|
||||
[
|
||||
"docker",
|
||||
"compose",
|
||||
"-f",
|
||||
str(ROOT / "docker-compose.yml"),
|
||||
"-f",
|
||||
str(ROOT / "docker-compose.production.yml"),
|
||||
"-f",
|
||||
str(override),
|
||||
"config",
|
||||
"--format",
|
||||
"json",
|
||||
],
|
||||
cwd=ROOT,
|
||||
env=env,
|
||||
text=True,
|
||||
capture_output=True,
|
||||
check=False,
|
||||
)
|
||||
self.assertEqual(result.returncode, 0, result.stderr)
|
||||
services = json.loads(result.stdout)["services"]
|
||||
self.assertTrue(services)
|
||||
self.assertEqual(
|
||||
{name: service.get("restart") for name, service in services.items()},
|
||||
{name: "no" for name in services},
|
||||
)
|
||||
|
||||
def test_compose_cleanup_runs_when_acceptance_receives_sigterm(self) -> None:
|
||||
spec = importlib.util.spec_from_file_location("rc_server_acceptance_cleanup", ACCEPTANCE)
|
||||
assert spec and spec.loader
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(module)
|
||||
managed_cleanup = getattr(module, "managed_compose_cleanup", None)
|
||||
self.assertIsNotNone(
|
||||
managed_cleanup,
|
||||
"acceptance must guard its RC project with interruption-safe cleanup",
|
||||
)
|
||||
|
||||
compose = ("docker", "compose", "-p", "modelforge-validation-test")
|
||||
env = {"COMPOSE_PROJECT_NAME": "modelforge-validation-test"}
|
||||
commands: list[tuple[tuple[str, ...], dict[str, object]]] = []
|
||||
registered: list[object] = []
|
||||
unregistered: list[object] = []
|
||||
handlers: dict[int, object] = {}
|
||||
|
||||
def fake_run(*args: str, **kwargs: object) -> str:
|
||||
commands.append((args, kwargs))
|
||||
return ""
|
||||
|
||||
def fake_signal(signum: int, handler: object) -> object:
|
||||
previous = handlers.get(signum, signal.SIG_DFL)
|
||||
handlers[signum] = handler
|
||||
return previous
|
||||
|
||||
with (
|
||||
mock.patch.object(module, "run", side_effect=fake_run),
|
||||
mock.patch.object(module.atexit, "register", side_effect=registered.append),
|
||||
mock.patch.object(module.atexit, "unregister", side_effect=unregistered.append),
|
||||
mock.patch.object(module.signal, "signal", side_effect=fake_signal),
|
||||
self.assertRaises(SystemExit) as raised,
|
||||
managed_cleanup(compose, env),
|
||||
):
|
||||
handler = handlers[signal.SIGTERM]
|
||||
assert callable(handler)
|
||||
handler(signal.SIGTERM, None)
|
||||
|
||||
self.assertEqual(raised.exception.code, 128 + signal.SIGTERM)
|
||||
self.assertEqual(
|
||||
commands,
|
||||
[
|
||||
(
|
||||
compose + ("down", "--volumes", "--remove-orphans"),
|
||||
{"env": env, "check": False},
|
||||
)
|
||||
],
|
||||
)
|
||||
self.assertEqual(len(registered), 1)
|
||||
self.assertEqual(unregistered, registered)
|
||||
|
||||
def test_clean_install_bootstraps_roles_across_a_remote_docker_daemon(self) -> None:
|
||||
self.assertIn("def provision_database_roles(", self.acceptance)
|
||||
self.assertIn('run("docker", "cp", str(bootstrap)', self.acceptance)
|
||||
@@ -152,7 +305,7 @@ class RcAcceptancePolicyTests(unittest.TestCase):
|
||||
self.assertNotIn("docker system prune", cleanup)
|
||||
|
||||
def test_python_entrypoints_compile(self) -> None:
|
||||
for path in (ACCEPTANCE, TRIVY_VALIDATOR):
|
||||
for path in (ACCEPTANCE, TRIVY_VALIDATOR, ROOT / "scripts" / "release_build.py"):
|
||||
result = subprocess.run(
|
||||
["python", "-m", "py_compile", str(path)],
|
||||
capture_output=True,
|
||||
|
||||
@@ -0,0 +1,215 @@
|
||||
"""Unit tests for the fail-closed P12 RC isolation preflight."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib.util
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
SPEC = importlib.util.spec_from_file_location("rc_isolation_preflight", ROOT / "scripts" / "rc_isolation_preflight.py")
|
||||
assert SPEC is not None and SPEC.loader is not None
|
||||
preflight = importlib.util.module_from_spec(SPEC)
|
||||
SPEC.loader.exec_module(preflight)
|
||||
|
||||
|
||||
PROJECT = "modelforge-validation-p12a"
|
||||
|
||||
|
||||
def compose_plan(root: Path) -> dict[str, object]:
|
||||
return {
|
||||
"services": {
|
||||
"api": {
|
||||
"networks": ["default"],
|
||||
"volumes": [
|
||||
{"type": "volume", "source": "postgres-data", "target": "/data"},
|
||||
{"type": "bind", "source": str(root / "config"), "target": "/app/config", "read_only": True},
|
||||
],
|
||||
"ports": [{"host_ip": "127.0.0.1", "published": "0", "target": 8000}],
|
||||
},
|
||||
"node-agent": {
|
||||
"networks": ["default"],
|
||||
"volumes": [{"type": "volume", "source": "node-state", "target": "/data/state"}],
|
||||
},
|
||||
},
|
||||
"volumes": {
|
||||
"postgres-data": {"name": f"{PROJECT}_postgres-data"},
|
||||
"node-state": {"name": f"{PROJECT}_node-state"},
|
||||
},
|
||||
"networks": {"default": {"name": f"{PROJECT}_default"}},
|
||||
}
|
||||
|
||||
|
||||
class RcIsolationPreflightTests(unittest.TestCase):
|
||||
def plan_root(self, *, with_manifest: bool = True) -> tempfile.TemporaryDirectory[str]:
|
||||
temporary = tempfile.TemporaryDirectory()
|
||||
config = Path(temporary.name, "config")
|
||||
config.mkdir()
|
||||
if with_manifest:
|
||||
manifest = config / "models" / "initial-candidates.yaml"
|
||||
manifest.parent.mkdir()
|
||||
manifest.write_text("candidates: []\n", encoding="utf-8")
|
||||
self.addCleanup(temporary.cleanup)
|
||||
return temporary
|
||||
|
||||
def validate(
|
||||
self,
|
||||
plan: dict[str, object],
|
||||
root: Path,
|
||||
*,
|
||||
ports: set[int] | None = None,
|
||||
names: set[str] | None = None,
|
||||
) -> dict[str, object]:
|
||||
return preflight.validate_compose(
|
||||
plan,
|
||||
project=PROJECT,
|
||||
candidate_root=root,
|
||||
protected_ports=ports or set(),
|
||||
protected_resource_names=names or set(),
|
||||
)
|
||||
|
||||
def test_valid_validation_compose_plan_is_accepted(self) -> None:
|
||||
temporary = self.plan_root()
|
||||
root = Path(temporary.name)
|
||||
result = self.validate(
|
||||
compose_plan(root), root, ports={18000, 13000}, names={"modelforge-postgres-1"}
|
||||
)
|
||||
self.assertEqual(result["project"], PROJECT)
|
||||
self.assertEqual(result["published_ports"], [])
|
||||
|
||||
def test_project_validation_rejects_production_and_legacy_rc_names(self) -> None:
|
||||
for project in (
|
||||
"modelforge",
|
||||
"modelforge-rc-p12a",
|
||||
"modelforge-validation-",
|
||||
"modelforge-validation-prod-",
|
||||
):
|
||||
with self.subTest(project=project), self.assertRaises(preflight.IsolationError):
|
||||
preflight.validate_project(project)
|
||||
|
||||
def test_missing_bind_source_is_rejected(self) -> None:
|
||||
temporary = self.plan_root(with_manifest=False)
|
||||
root = Path(temporary.name)
|
||||
plan = compose_plan(root)
|
||||
(root / "config").rmdir()
|
||||
with self.assertRaisesRegex(preflight.IsolationError, "missing bind source"):
|
||||
self.validate(plan, root)
|
||||
|
||||
def test_config_manifest_must_exist_under_exact_bind_source(self) -> None:
|
||||
temporary = self.plan_root(with_manifest=False)
|
||||
root = Path(temporary.name)
|
||||
plan = compose_plan(root)
|
||||
# A manifest elsewhere in the candidate tree cannot satisfy this bind's contract.
|
||||
manifest = root / "models" / "initial-candidates.yaml"
|
||||
manifest.parent.mkdir()
|
||||
manifest.write_text("candidates: []\n", encoding="utf-8")
|
||||
with self.assertRaisesRegex(preflight.IsolationError, "missing models/initial-candidates.yaml under its source"):
|
||||
self.validate(plan, root)
|
||||
|
||||
def test_rendered_compose_network_mapping_is_accepted(self) -> None:
|
||||
temporary = self.plan_root()
|
||||
root = Path(temporary.name)
|
||||
plan = compose_plan(root)
|
||||
plan["services"]["api"]["networks"] = {"default": None} # type: ignore[index]
|
||||
self.assertEqual(self.validate(plan, root)["declared_networks"], ["default"])
|
||||
|
||||
def test_unsafe_compose_plans_are_rejected(self) -> None:
|
||||
cases = [
|
||||
(lambda plan: plan["services"]["api"].update(network_mode="host"), "host network"),
|
||||
(lambda plan: plan["services"]["api"].update(container_name="modelforge-api-1"), "global container name"),
|
||||
(lambda plan: plan["services"]["api"]["ports"].__setitem__(0, {"host_ip": "0.0.0.0", "published": "19000", "target": 8000}), "non-loopback"),
|
||||
(lambda plan: plan["services"]["api"]["ports"].__setitem__(0, {"host_ip": "127.0.0.1", "published": "18000", "target": 8000}), "protected host port"),
|
||||
(lambda plan: plan["services"]["api"]["volumes"].__setitem__(1, {"type": "bind", "source": "/mnt/user/appdata/modelforge", "target": "/data", "read_only": True}), "escapes the candidate root"),
|
||||
(lambda plan: plan["services"]["api"]["volumes"].__setitem__(1, {"type": "bind", "source": "config", "target": "/app/config", "read_only": False}), "writable bind mount"),
|
||||
(lambda plan: plan["volumes"]["postgres-data"].update(external=True), "is external"),
|
||||
(lambda plan: plan["networks"]["default"].update(name="modelforge_default"), "non-project name"),
|
||||
]
|
||||
for mutate, message in cases:
|
||||
with self.subTest(message=message):
|
||||
temporary = self.plan_root()
|
||||
root = Path(temporary.name)
|
||||
plan = compose_plan(root)
|
||||
mutate(plan)
|
||||
with self.assertRaisesRegex(preflight.IsolationError, message):
|
||||
self.validate(plan, root, ports={18000})
|
||||
|
||||
def test_protected_resource_reference_is_rejected(self) -> None:
|
||||
temporary = self.plan_root()
|
||||
root = Path(temporary.name)
|
||||
plan = compose_plan(root)
|
||||
plan["services"]["api"]["environment"] = {"UNSAFE": "modelforge-postgres-1"} # type: ignore[index]
|
||||
with self.assertRaisesRegex(preflight.IsolationError, "protected resources"):
|
||||
self.validate(plan, root, names={"modelforge-postgres-1"})
|
||||
|
||||
def test_only_isolated_tls_service_can_publish_test_port(self) -> None:
|
||||
temporary = self.plan_root()
|
||||
root = Path(temporary.name)
|
||||
source = root / "source" / "deploy" / "validation"
|
||||
source.mkdir(parents=True)
|
||||
(source / "Caddyfile").write_text("https://modelforge.example.com:8443 {}\n", encoding="utf-8")
|
||||
certificates = root / "tls"
|
||||
certificates.mkdir()
|
||||
(certificates / "fullchain.pem").write_text("certificate", encoding="utf-8")
|
||||
(certificates / "privkey.pem").write_text("key", encoding="utf-8")
|
||||
plan = compose_plan(root)
|
||||
plan["networks"]["ingress"] = {"name": f"{PROJECT}_ingress"}
|
||||
plan["services"]["tls"] = {
|
||||
"image": "caddy:2.10-alpine@sha256:4c6e91c6ed0e2fa03efd5b44747b625fec79bc9cd06ac5235a779726618e530d",
|
||||
"networks": ["ingress"], "read_only": True, "cap_drop": ["ALL"],
|
||||
"security_opt": ["no-new-privileges:true"],
|
||||
"volumes": [
|
||||
{"type": "bind", "source": str(source / "Caddyfile"), "target": "/etc/caddy/Caddyfile", "read_only": True},
|
||||
{"type": "bind", "source": str(certificates / "fullchain.pem"), "target": "/run/tls/fullchain.pem", "read_only": True},
|
||||
{"type": "bind", "source": str(certificates / "privkey.pem"), "target": "/run/tls/privkey.pem", "read_only": True},
|
||||
],
|
||||
"ports": [{"host_ip": "0.0.0.0", "published": "18444", "target": 8443, "protocol": "tcp"}],
|
||||
}
|
||||
with self.assertRaisesRegex(preflight.IsolationError, "non-loopback"):
|
||||
self.validate(plan, root)
|
||||
self.assertEqual(preflight.validate_compose(
|
||||
plan, project=PROJECT, candidate_root=root,
|
||||
protected_ports={443, 8443}, protected_resource_names=set(), public_tls_port=18444,
|
||||
)["published_ports"], [18444])
|
||||
plan["services"]["api"]["ports"][0] = {"host_ip": "0.0.0.0", "published": "18444", "target": 8000}
|
||||
with self.assertRaisesRegex(preflight.IsolationError, "non-loopback"):
|
||||
preflight.validate_compose(
|
||||
plan, project=PROJECT, candidate_root=root,
|
||||
protected_ports=set(), protected_resource_names=set(), public_tls_port=18444,
|
||||
)
|
||||
|
||||
def test_observed_resources_require_exact_labels_and_project_names(self) -> None:
|
||||
observed = {
|
||||
"containers": [{"name": f"{PROJECT}-api-1", "labels": {"com.docker.compose.project": PROJECT}}],
|
||||
"volumes": [{"name": f"{PROJECT}_postgres-data", "labels": {"com.docker.compose.project": PROJECT}}],
|
||||
"networks": [{"name": f"{PROJECT}_default", "labels": {"com.docker.compose.project": PROJECT}}],
|
||||
}
|
||||
self.assertEqual(preflight.validate_observed_resources(observed, project=PROJECT)["containers"], [f"{PROJECT}-api-1"])
|
||||
observed["volumes"][0]["labels"] = {}
|
||||
with self.assertRaisesRegex(preflight.IsolationError, "no exact project label"):
|
||||
preflight.validate_observed_resources(observed, project=PROJECT)
|
||||
|
||||
def test_docker_resource_collection_uses_exact_inspection(self) -> None:
|
||||
label = {"com.docker.compose.project": PROJECT}
|
||||
responses = {
|
||||
("container", "ls", "--all", "--quiet", "--filter", f"label=com.docker.compose.project={PROJECT}"): "container-id\n",
|
||||
("volume", "ls", "--quiet", "--filter", f"label=com.docker.compose.project={PROJECT}"): "volume-id\n",
|
||||
("network", "ls", "--quiet", "--filter", f"label=com.docker.compose.project={PROJECT}"): "network-id\n",
|
||||
("container", "inspect", "container-id"): '[{"Name":"/' + PROJECT + '-api-1","Config":{"Labels":{"com.docker.compose.project":"' + PROJECT + '"}}}]',
|
||||
("volume", "inspect", "volume-id"): '[{"Name":"' + PROJECT + '_data","Labels":{"com.docker.compose.project":"' + PROJECT + '"}}]',
|
||||
("network", "inspect", "network-id"): '[{"Name":"' + PROJECT + '_default","Labels":{"com.docker.compose.project":"' + PROJECT + '"}}]',
|
||||
}
|
||||
|
||||
def run(command, **_kwargs):
|
||||
self.assertEqual(command[0], "docker")
|
||||
return type("Result", (), {"returncode": 0, "stdout": responses[tuple(command[1:])]})()
|
||||
|
||||
with patch.object(preflight.subprocess, "run", run):
|
||||
observed = preflight.collect_observed_resources(PROJECT)
|
||||
self.assertEqual(observed["containers"][0]["labels"], label)
|
||||
self.assertEqual(
|
||||
preflight.validate_observed_resources(observed, project=PROJECT)["networks"],
|
||||
[f"{PROJECT}_default"],
|
||||
)
|
||||
@@ -0,0 +1,316 @@
|
||||
"""Focused tests for RC stack inventory and stop-only retention guard."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib.util
|
||||
import io
|
||||
import json
|
||||
import tempfile
|
||||
import unittest
|
||||
from contextlib import redirect_stdout
|
||||
from datetime import UTC, datetime
|
||||
from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import patch
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
SPEC = importlib.util.spec_from_file_location(
|
||||
"rc_stack_retention", ROOT / "scripts" / "rc_stack_retention.py"
|
||||
)
|
||||
assert SPEC is not None and SPEC.loader is not None
|
||||
retention = importlib.util.module_from_spec(SPEC)
|
||||
SPEC.loader.exec_module(retention)
|
||||
|
||||
|
||||
PROJECT = "modelforge-validation-123271"
|
||||
NOW = datetime(2026, 9, 23, 10, 0, tzinfo=UTC)
|
||||
CREATED = "2026-09-20T10:00:00Z"
|
||||
|
||||
|
||||
class MockDocker:
|
||||
def __init__(self, containers: dict[str, dict[str, object]]) -> None:
|
||||
self.containers = containers
|
||||
self.calls: list[tuple[str, ...]] = []
|
||||
|
||||
def run(self, command: tuple[str, ...], **_kwargs: object) -> SimpleNamespace:
|
||||
self.assert_docker(command)
|
||||
args = tuple(command[1:])
|
||||
self.calls.append(args)
|
||||
if args[:4] == (
|
||||
"container", "ls", "--all", "--quiet"
|
||||
):
|
||||
return SimpleNamespace(
|
||||
returncode=0,
|
||||
stdout="\n".join(self.containers) + "\n",
|
||||
stderr="",
|
||||
)
|
||||
if args[:2] == ("container", "inspect"):
|
||||
identifier = args[2]
|
||||
return SimpleNamespace(
|
||||
returncode=0,
|
||||
stdout=json.dumps([self.containers[identifier]]),
|
||||
stderr="",
|
||||
)
|
||||
if args[:1] == ("stop",):
|
||||
identifiers = args[3:]
|
||||
for identifier in identifiers:
|
||||
state = self.containers[identifier]["State"]
|
||||
assert isinstance(state, dict)
|
||||
state["Status"] = "exited"
|
||||
return SimpleNamespace(returncode=0, stdout="\n".join(identifiers), stderr="")
|
||||
raise AssertionError(f"unexpected Docker command: {args}")
|
||||
|
||||
@staticmethod
|
||||
def assert_docker(command: tuple[str, ...]) -> None:
|
||||
if command[0] != "docker":
|
||||
raise AssertionError(f"unexpected executable: {command[0]}")
|
||||
|
||||
|
||||
def docker_container(
|
||||
identifier: str,
|
||||
project: str,
|
||||
service: str,
|
||||
*,
|
||||
status: str = "exited",
|
||||
source: str | None = None,
|
||||
created: str = CREATED,
|
||||
oneoff: str = "False",
|
||||
) -> dict[str, object]:
|
||||
mounts: list[dict[str, str]] = []
|
||||
if source is not None:
|
||||
mounts.append({"Type": "bind", "Source": source, "Destination": "/app/config"})
|
||||
return {
|
||||
"Id": identifier,
|
||||
"Name": f"/{project}-{service}-1",
|
||||
"Created": created,
|
||||
"Config": {
|
||||
"Labels": {
|
||||
retention.PROJECT_LABEL: project,
|
||||
retention.SERVICE_LABEL: service,
|
||||
retention.ONEOFF_LABEL: oneoff,
|
||||
}
|
||||
},
|
||||
"State": {"Status": status, "Running": status == "running"},
|
||||
"Mounts": mounts,
|
||||
}
|
||||
|
||||
|
||||
class RcStackRetentionTests(unittest.TestCase):
|
||||
def fixture(self, *, api_status: str = "exited", service: str = "postgres", oneoff: str = "False") -> tuple[MockDocker, tempfile.TemporaryDirectory[str]]:
|
||||
temporary = tempfile.TemporaryDirectory()
|
||||
config = Path(temporary.name) / "config"
|
||||
config.mkdir()
|
||||
containers = {
|
||||
"validation-api-id": docker_container("validation-api-id", PROJECT, "api", source=str(config)),
|
||||
"validation-db-id": docker_container(
|
||||
"validation-db-id", PROJECT, service, status="running", oneoff=oneoff
|
||||
),
|
||||
"prod-api-id": docker_container("prod-api-id", "modelforge", "api", status="running"),
|
||||
"other-api-id": docker_container(
|
||||
"other-api-id", "staging", "api", status="running"
|
||||
),
|
||||
}
|
||||
if api_status != "exited":
|
||||
containers["validation-api-id"]["State"] = {"Status": api_status, "Running": True}
|
||||
return MockDocker(containers), temporary
|
||||
|
||||
@staticmethod
|
||||
def write_evidence(path: Path, project: str = PROJECT) -> None:
|
||||
path.write_text(
|
||||
json.dumps(
|
||||
{
|
||||
"schema_version": 1,
|
||||
"project": project,
|
||||
"retained": True,
|
||||
"evidence_reference": "acceptance-run-2026-09-20",
|
||||
}
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
def test_inventory_scopes_validation_projects_and_flags_manifest_and_age(self) -> None:
|
||||
docker, temporary = self.fixture()
|
||||
self.addCleanup(temporary.cleanup)
|
||||
with patch.object(retention.subprocess, "run", docker.run):
|
||||
projects = retention.collect_projects(now=NOW, stale_after_hours=24)
|
||||
|
||||
self.assertEqual([row["project"] for row in projects], [PROJECT])
|
||||
self.assertTrue(projects[0]["stale"])
|
||||
self.assertEqual(len(projects[0]["missing_manifests"]), 1)
|
||||
self.assertEqual(projects[0]["missing_bind_sources"], [])
|
||||
self.assertNotIn("volumes", projects[0])
|
||||
self.assertNotIn("images", projects[0])
|
||||
self.assertTrue(all(call[:1] != ("stop",) for call in docker.calls))
|
||||
self.assertTrue(all(call[:1] != ("rm",) for call in docker.calls))
|
||||
|
||||
def test_inventory_ignores_non_validation_partial_compose_labels(self) -> None:
|
||||
docker, temporary = self.fixture()
|
||||
self.addCleanup(temporary.cleanup)
|
||||
docker.containers["unrelated-id"] = {
|
||||
"Id": "unrelated-id",
|
||||
"Name": "/projectbrain-worker-1",
|
||||
"Config": {"Labels": {retention.PROJECT_LABEL: "projectbrain"}},
|
||||
"State": {"Status": "exited"},
|
||||
}
|
||||
|
||||
with patch.object(retention.subprocess, "run", docker.run):
|
||||
projects = retention.collect_projects(now=NOW, stale_after_hours=24)
|
||||
|
||||
self.assertEqual([row["project"] for row in projects], [PROJECT])
|
||||
|
||||
def test_inventory_fails_closed_for_validation_container_missing_service_label(self) -> None:
|
||||
docker, temporary = self.fixture()
|
||||
self.addCleanup(temporary.cleanup)
|
||||
labels = docker.containers["validation-api-id"]["Config"]["Labels"]
|
||||
assert isinstance(labels, dict)
|
||||
del labels[retention.SERVICE_LABEL]
|
||||
|
||||
with patch.object(retention.subprocess, "run", docker.run), self.assertRaisesRegex(
|
||||
retention.RetentionError, "Compose service label is missing"
|
||||
):
|
||||
retention.collect_projects(now=NOW, stale_after_hours=24)
|
||||
|
||||
def test_default_cli_is_read_only(self) -> None:
|
||||
docker, temporary = self.fixture()
|
||||
self.addCleanup(temporary.cleanup)
|
||||
output = io.StringIO()
|
||||
with patch.object(retention.subprocess, "run", docker.run), redirect_stdout(output):
|
||||
status = retention.main([])
|
||||
|
||||
result = json.loads(output.getvalue())
|
||||
self.assertEqual(status, 0)
|
||||
self.assertEqual(result["mode"], "read-only")
|
||||
self.assertTrue(all(call[:1] != ("stop",) for call in docker.calls))
|
||||
|
||||
def test_docker_errors_and_ambiguous_inspection_fail_closed(self) -> None:
|
||||
failed = SimpleNamespace(returncode=1, stdout="", stderr="daemon unavailable")
|
||||
with patch.object(retention.subprocess, "run", return_value=failed), self.assertRaisesRegex(
|
||||
retention.RetentionError, "Docker command failed"
|
||||
):
|
||||
retention.collect_projects(now=NOW)
|
||||
|
||||
docker, temporary = self.fixture()
|
||||
self.addCleanup(temporary.cleanup)
|
||||
docker.containers["validation-api-id"]["Id"] = "different-id"
|
||||
with patch.object(retention.subprocess, "run", docker.run), self.assertRaisesRegex(
|
||||
retention.RetentionError, "different container"
|
||||
):
|
||||
retention.collect_projects(now=NOW)
|
||||
|
||||
def test_project_validation_rejects_production_and_ambiguous_names(self) -> None:
|
||||
for project in (
|
||||
"modelforge",
|
||||
"modelforge-rc-prod-",
|
||||
"modelforge-validation-",
|
||||
"modelforge-validation-prod-",
|
||||
):
|
||||
with self.subTest(project=project), self.assertRaises(retention.RetentionError):
|
||||
retention.validate_project(project)
|
||||
|
||||
def test_stop_requires_explicit_exact_project_confirmation_and_evidence(self) -> None:
|
||||
docker, temporary = self.fixture()
|
||||
self.addCleanup(temporary.cleanup)
|
||||
empty_record = Path(temporary.name) / "empty.json"
|
||||
empty_record.write_text("", encoding="utf-8")
|
||||
with patch.object(retention.subprocess, "run", docker.run):
|
||||
with self.assertRaisesRegex(retention.RetentionError, "exact project name"):
|
||||
retention.stop_project(
|
||||
PROJECT,
|
||||
confirm_stop=None,
|
||||
confirm_no_active_jobs=True,
|
||||
evidence_record=Path("evidence.json"),
|
||||
stale_after_hours=24,
|
||||
now=NOW,
|
||||
)
|
||||
with self.assertRaisesRegex(retention.RetentionError, "nonempty regular file"):
|
||||
retention.stop_project(
|
||||
PROJECT,
|
||||
confirm_stop=PROJECT,
|
||||
confirm_no_active_jobs=True,
|
||||
evidence_record=empty_record,
|
||||
stale_after_hours=24,
|
||||
now=NOW,
|
||||
)
|
||||
mismatch = Path(temporary.name) / "mismatch.json"
|
||||
self.write_evidence(mismatch, project="modelforge-validation-someone-else")
|
||||
with patch.object(retention.subprocess, "run", docker.run), self.assertRaisesRegex(
|
||||
retention.RetentionError, "identify this project"
|
||||
):
|
||||
retention.stop_project(
|
||||
PROJECT,
|
||||
confirm_stop=PROJECT,
|
||||
confirm_no_active_jobs=True,
|
||||
evidence_record=mismatch,
|
||||
stale_after_hours=24,
|
||||
now=NOW,
|
||||
)
|
||||
self.assertEqual(docker.calls, [])
|
||||
|
||||
def test_stop_only_targets_inspected_project_container_ids(self) -> None:
|
||||
docker, temporary = self.fixture()
|
||||
self.addCleanup(temporary.cleanup)
|
||||
evidence = Path(temporary.name) / "evidence.json"
|
||||
self.write_evidence(evidence)
|
||||
with patch.object(retention.subprocess, "run", docker.run):
|
||||
result = retention.stop_project(
|
||||
PROJECT,
|
||||
confirm_stop=PROJECT,
|
||||
confirm_no_active_jobs=True,
|
||||
evidence_record=evidence,
|
||||
stale_after_hours=24,
|
||||
now=NOW,
|
||||
)
|
||||
|
||||
self.assertEqual(result["stopped_container_ids"], ["validation-db-id"])
|
||||
self.assertFalse(result["volumes_or_images_removed"])
|
||||
stop_calls = [call for call in docker.calls if call[:1] == ("stop",)]
|
||||
self.assertEqual(stop_calls, [("stop", "--time", "30", "validation-db-id")])
|
||||
self.assertTrue(all("prod-api-id" not in call for call in stop_calls))
|
||||
self.assertTrue(all(call[:1] not in {("rm",), ("rmi",), ("volume",)} for call in docker.calls))
|
||||
|
||||
def test_stop_fails_if_api_or_oneoff_job_is_running(self) -> None:
|
||||
for options, message in (
|
||||
({"api_status": "running"}, "API is running"),
|
||||
({"api_status": "restarting"}, "API is running"),
|
||||
({"oneoff": "True"}, "one-off job container"),
|
||||
({"service": "migrate"}, "service activity is unknown"),
|
||||
):
|
||||
with self.subTest(message=message):
|
||||
docker, temporary = self.fixture(**options)
|
||||
self.addCleanup(temporary.cleanup)
|
||||
evidence = Path(temporary.name) / "evidence.json"
|
||||
self.write_evidence(evidence)
|
||||
with patch.object(retention.subprocess, "run", docker.run), self.assertRaisesRegex(
|
||||
retention.RetentionError, message
|
||||
):
|
||||
retention.stop_project(
|
||||
PROJECT,
|
||||
confirm_stop=PROJECT,
|
||||
confirm_no_active_jobs=True,
|
||||
evidence_record=evidence,
|
||||
stale_after_hours=24,
|
||||
now=NOW,
|
||||
)
|
||||
self.assertTrue(all(call[:1] != ("stop",) for call in docker.calls))
|
||||
|
||||
def test_stop_requires_job_attestation(self) -> None:
|
||||
docker, temporary = self.fixture()
|
||||
self.addCleanup(temporary.cleanup)
|
||||
evidence = Path(temporary.name) / "evidence.json"
|
||||
self.write_evidence(evidence)
|
||||
with patch.object(retention.subprocess, "run", docker.run), self.assertRaisesRegex(
|
||||
retention.RetentionError, "--confirm-no-active-jobs"
|
||||
):
|
||||
retention.stop_project(
|
||||
PROJECT,
|
||||
confirm_stop=PROJECT,
|
||||
confirm_no_active_jobs=False,
|
||||
evidence_record=evidence,
|
||||
stale_after_hours=24,
|
||||
now=NOW,
|
||||
)
|
||||
self.assertEqual(docker.calls, [])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,111 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
from urllib.parse import urlsplit
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
WORKFLOW = ROOT / ".gitea" / "workflows" / "managed-validation.yml"
|
||||
CANONICAL_ORIGIN = "https://git.example.com"
|
||||
|
||||
|
||||
def _origin_case_script() -> str:
|
||||
text = WORKFLOW.read_text(encoding="utf-8")
|
||||
match = re.search(
|
||||
r'(?ms)^ case "\$\{GITHUB_SERVER_URL%/\}" in\n'
|
||||
r'.*?^ esac$',
|
||||
text,
|
||||
)
|
||||
if match is None:
|
||||
raise AssertionError("managed validation has no GITHUB_SERVER_URL case policy")
|
||||
# Remove workflow indentation before executing only the bounded origin policy.
|
||||
return "\n".join(line[10:] for line in match.group(0).splitlines())
|
||||
|
||||
|
||||
def _evaluate_origin(origin: str) -> subprocess.CompletedProcess[str]:
|
||||
script = (
|
||||
"set -euo pipefail\n"
|
||||
+ _origin_case_script()
|
||||
+ "\nprintf '%s\\n' \"${SOURCE_SERVER_URL}\"\n"
|
||||
)
|
||||
env = os.environ.copy()
|
||||
env["GITHUB_SERVER_URL"] = origin
|
||||
bash = shutil.which("bash") or "bash"
|
||||
if os.name == "nt":
|
||||
# System32/bash.exe starts WSL, which does not inherit arbitrary Windows environment
|
||||
# variables. Git Bash runs natively and tests the exact environment used by this fixture.
|
||||
git = shutil.which("git")
|
||||
git_bash = Path(git).resolve().parents[1] / "bin" / "bash.exe" if git else None
|
||||
if git_bash is None or not git_bash.is_file():
|
||||
raise RuntimeError("Git Bash is required for Windows origin policy tests")
|
||||
bash = str(git_bash)
|
||||
return subprocess.run(
|
||||
[bash, "--noprofile", "--norc", "-c", script],
|
||||
cwd=ROOT,
|
||||
env=env,
|
||||
check=False,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
|
||||
|
||||
def _safe_origin(value: str) -> str:
|
||||
parsed = urlsplit(value)
|
||||
hostname = parsed.hostname or "(missing-host)"
|
||||
port = f":{parsed.port}" if parsed.port is not None else ""
|
||||
return f"{parsed.scheme or '(missing-scheme)'}://{hostname}{port}"
|
||||
|
||||
|
||||
class SecurityOriginPolicyTests(unittest.TestCase):
|
||||
def test_known_runner_and_public_origins_canonicalize_to_https(self) -> None:
|
||||
accepted = (
|
||||
"https://git.example.com",
|
||||
"https://git.example.com/",
|
||||
"http://git.example.com",
|
||||
"http://git.example.com/",
|
||||
"http://192.0.2.10:3000",
|
||||
"http://192.0.2.10:3000/",
|
||||
"http://gitea:3000",
|
||||
"http://gitea:3000/",
|
||||
"http://Gitea:3000",
|
||||
"http://Gitea:3000/",
|
||||
"http://gitea-runner-internal:3000",
|
||||
"http://gitea-runner-internal:3000/",
|
||||
)
|
||||
for origin in accepted:
|
||||
with self.subTest(origin=origin):
|
||||
result = _evaluate_origin(origin)
|
||||
self.assertEqual(result.returncode, 0, result.stderr)
|
||||
self.assertEqual(result.stdout.strip(), CANONICAL_ORIGIN)
|
||||
|
||||
def test_unknown_http_and_https_origins_fail_closed(self) -> None:
|
||||
for origin in (
|
||||
"http://untrusted.invalid",
|
||||
"https://untrusted.invalid",
|
||||
"http://192.0.2.151:3000",
|
||||
"http://gitea:3001",
|
||||
):
|
||||
with self.subTest(origin=origin):
|
||||
result = _evaluate_origin(origin)
|
||||
self.assertNotEqual(result.returncode, 0)
|
||||
self.assertIn("Untrusted Gitea server URL", result.stderr)
|
||||
|
||||
def test_live_runner_origin_is_explicitly_covered_when_present(self) -> None:
|
||||
origin = os.environ.get("GITHUB_SERVER_URL")
|
||||
if not origin:
|
||||
self.skipTest("GITHUB_SERVER_URL is only expected inside the managed runner")
|
||||
result = _evaluate_origin(origin)
|
||||
self.assertEqual(
|
||||
result.returncode,
|
||||
0,
|
||||
f"managed runner origin is not covered: {_safe_origin(origin)}; {result.stderr.strip()}",
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
File diff suppressed because it is too large.
Load diff
+1014
-114
File diff suppressed because it is too large.
Load diff
@@ -1,6 +1,6 @@
|
||||
name: Public candidate server acceptance
|
||||
|
||||
# Builds only an exact, curated public-source commit in a disposable Compose namespace. This job
|
||||
# Builds only an exact, curated public-source commit in a disposable validation Compose namespace. This job
|
||||
# never calls the Unraid deploy controller and cannot select the production deployment action.
|
||||
on:
|
||||
workflow_dispatch:
|
||||
@@ -14,6 +14,11 @@ on:
|
||||
required: true
|
||||
default: https://modelforge.example.test
|
||||
type: string
|
||||
retain_image_bundle:
|
||||
description: Retain the verified four-image Docker bundle for P12-P15 qualification
|
||||
required: false
|
||||
default: false
|
||||
type: boolean
|
||||
workflow_call:
|
||||
inputs:
|
||||
source_commit:
|
||||
@@ -24,6 +29,11 @@ on:
|
||||
description: Bare API origin compiled into the candidate Console image
|
||||
required: true
|
||||
type: string
|
||||
retain_image_bundle:
|
||||
description: Retain the verified four-image Docker bundle for P12-P15 qualification
|
||||
required: false
|
||||
default: false
|
||||
type: boolean
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
@@ -140,14 +150,23 @@ jobs:
|
||||
shell: bash
|
||||
env:
|
||||
PUBLIC_API_ORIGIN: ${{ inputs.public_api_origin }}
|
||||
RETAIN_IMAGE_BUNDLE: ${{ inputs.retain_image_bundle }}
|
||||
SOURCE_COMMIT: ${{ inputs.source_commit }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
cd "${PUBLIC_SOURCE}"
|
||||
python3 scripts/rc_server_acceptance.py \
|
||||
--public-api-origin "${PUBLIC_API_ORIGIN}" \
|
||||
--trivy "${TRIVY}" \
|
||||
--output acceptance-evidence \
|
||||
arguments=(
|
||||
--public-api-origin "${PUBLIC_API_ORIGIN}"
|
||||
--canonical-source-commit "${SOURCE_COMMIT}"
|
||||
--trivy "${TRIVY}"
|
||||
--output acceptance-evidence
|
||||
--project-suffix "${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}"
|
||||
)
|
||||
if [[ "${RETAIN_IMAGE_BUNDLE}" == "true" ]]; then
|
||||
arguments+=(--retain-image-bundle)
|
||||
fi
|
||||
python3 scripts/rc_server_acceptance.py \
|
||||
"${arguments[@]}"
|
||||
|
||||
- name: Always remove acceptance Docker resources
|
||||
if: always()
|
||||
@@ -157,7 +176,7 @@ jobs:
|
||||
cleanup_failed=0
|
||||
suffix="$(printf '%s' "${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}" \
|
||||
| tr '[:upper:]' '[:lower:]' | tr -cd '[:alnum:]' | tail -c 24)"
|
||||
project="modelforge-rc-${suffix}"
|
||||
project="modelforge-validation-${suffix}"
|
||||
for kind in container volume network; do
|
||||
while IFS= read -r resource; do
|
||||
[[ -n "${resource}" ]] || continue
|
||||
@@ -171,7 +190,7 @@ jobs:
|
||||
done
|
||||
|
||||
if [[ -n "${PUBLIC_SOURCE:-}" && -d "${PUBLIC_SOURCE}/.git" ]]; then
|
||||
candidate_commit="$(git -C "${PUBLIC_SOURCE}" rev-parse HEAD 2>/dev/null)"
|
||||
canonical_commit="${{ inputs.source_commit }}"
|
||||
version="$(tr -d '\r\n' < "${PUBLIC_SOURCE}/VERSION" 2>/dev/null)"
|
||||
for image in modelforge-api modelforge-web modelforge-node-agent \
|
||||
modelforge-runtime-worker; do
|
||||
@@ -179,7 +198,7 @@ jobs:
|
||||
revision="$(docker inspect --format \
|
||||
'{{index .Config.Labels "org.opencontainers.image.revision"}}' \
|
||||
"${tag}" 2>/dev/null)"
|
||||
if [[ -n "${candidate_commit}" && "${revision}" == "${candidate_commit}" ]]; then
|
||||
if [[ "${revision}" == "${canonical_commit}" ]]; then
|
||||
docker image rm --force "${tag}" || cleanup_failed=1
|
||||
fi
|
||||
done
|
||||
@@ -208,6 +227,8 @@ jobs:
|
||||
${{ env.PUBLIC_SOURCE }}/acceptance-evidence/*.json
|
||||
${{ env.PUBLIC_SOURCE }}/acceptance-evidence/*.txt
|
||||
${{ env.PUBLIC_SOURCE }}/acceptance-evidence/release/*.json
|
||||
${{ env.PUBLIC_SOURCE }}/acceptance-evidence/release/*.tar
|
||||
${{ env.PUBLIC_SOURCE }}/acceptance-evidence/release/*.tar.gz
|
||||
${{ env.PUBLIC_SOURCE }}/acceptance-evidence/release/*SHA256SUMS
|
||||
if-no-files-found: warn
|
||||
retention-days: 90
|
||||
@@ -1,132 +0,0 @@
|
||||
name: Unraid stable release deployment
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
release_tag:
|
||||
description: Annotated stable release tag (for example, v1.2.1)
|
||||
required: true
|
||||
type: string
|
||||
release_commit:
|
||||
description: Exact lowercase 40-character commit SHA referenced by the tag
|
||||
required: true
|
||||
type: string
|
||||
action:
|
||||
description: Verify provenance only, or deploy the verified stable release
|
||||
required: true
|
||||
default: VERIFY_ONLY
|
||||
type: choice
|
||||
options:
|
||||
- VERIFY_ONLY
|
||||
- DEPLOY_STABLE_TO_PRODUCTION
|
||||
|
||||
concurrency:
|
||||
group: unraid-production-itworx-modelforge
|
||||
cancel-in-progress: false
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
name: Verify and optionally deploy a stable release
|
||||
runs-on: unraid-deploy
|
||||
timeout-minutes: 180
|
||||
steps:
|
||||
- name: Validate immutable dispatch inputs
|
||||
shell: bash
|
||||
env:
|
||||
RELEASE_TAG: ${{ inputs.release_tag }}
|
||||
RELEASE_COMMIT: ${{ inputs.release_commit }}
|
||||
DEPLOY_ACTION: ${{ inputs.action }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
if [[ "${GITHUB_REF:-}" != "refs/heads/master" ]]; then
|
||||
echo "Stable production deployment must be dispatched from refs/heads/master" >&2
|
||||
exit 2
|
||||
fi
|
||||
if [[ ! "${RELEASE_TAG}" =~ ^v(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$ ]]; then
|
||||
echo "release_tag must be a strict stable SemVer tag such as v1.2.1" >&2
|
||||
exit 2
|
||||
fi
|
||||
if [[ ! "${RELEASE_COMMIT}" =~ ^[0-9a-f]{40}$ ]]; then
|
||||
echo "release_commit must be an exact lowercase 40-character SHA" >&2
|
||||
exit 2
|
||||
fi
|
||||
case "${DEPLOY_ACTION}" in
|
||||
VERIFY_ONLY|DEPLOY_STABLE_TO_PRODUCTION) ;;
|
||||
*) echo "action is not allowlisted: ${DEPLOY_ACTION}" >&2; exit 2 ;;
|
||||
esac
|
||||
|
||||
- name: Check out the exact release commit with full history
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
ref: ${{ inputs.release_commit }}
|
||||
fetch-depth: 0
|
||||
# The following provenance step performs an authenticated exact-ref fetch.
|
||||
persist-credentials: true
|
||||
|
||||
- name: Verify stable release provenance
|
||||
shell: bash
|
||||
env:
|
||||
RELEASE_TAG: ${{ inputs.release_tag }}
|
||||
RELEASE_COMMIT: ${{ inputs.release_commit }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
git fetch --force --no-recurse-submodules origin \
|
||||
"refs/tags/${RELEASE_TAG}:refs/tags/${RELEASE_TAG}" \
|
||||
"refs/heads/master:refs/remotes/origin/master"
|
||||
|
||||
tag_ref="refs/tags/${RELEASE_TAG}"
|
||||
if [[ "$(git cat-file -t "${tag_ref}")" != "tag" ]]; then
|
||||
echo "${RELEASE_TAG} must be an annotated tag" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
tag_object="$(git rev-parse "${tag_ref}^{tag}")"
|
||||
tag_commit="$(git rev-parse "${tag_ref}^{commit}")"
|
||||
head_commit="$(git rev-parse HEAD)"
|
||||
if [[ "${tag_commit}" != "${RELEASE_COMMIT}" ]]; then
|
||||
echo "Tag commit ${tag_commit} does not match release_commit ${RELEASE_COMMIT}" >&2
|
||||
exit 1
|
||||
fi
|
||||
if [[ "${head_commit}" != "${RELEASE_COMMIT}" ]]; then
|
||||
echo "Checked-out HEAD ${head_commit} does not match release_commit ${RELEASE_COMMIT}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ ! -f VERSION ]]; then
|
||||
echo "VERSION is missing at the release commit" >&2
|
||||
exit 1
|
||||
fi
|
||||
version="$(tr -d '\r\n' < VERSION)"
|
||||
expected_version="${RELEASE_TAG#v}"
|
||||
if [[ "${version}" != "${expected_version}" ]]; then
|
||||
echo "VERSION ${version} does not match release tag ${RELEASE_TAG}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! git merge-base --is-ancestor "${RELEASE_COMMIT}" refs/remotes/origin/master; then
|
||||
echo "Release commit ${RELEASE_COMMIT} is not an ancestor of origin/master" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
printf 'DEPLOY_COMMIT=%s\n' "${RELEASE_COMMIT}" >> "${GITHUB_ENV}"
|
||||
echo "Verified annotated ${RELEASE_TAG} object ${tag_object} at immutable commit ${RELEASE_COMMIT}"
|
||||
|
||||
- name: Verification-only result
|
||||
if: ${{ inputs.action == 'VERIFY_ONLY' }}
|
||||
shell: bash
|
||||
run: echo "Stable release provenance verified; production was not changed."
|
||||
|
||||
- name: Deploy verified stable release to production
|
||||
if: ${{ inputs.action == 'DEPLOY_STABLE_TO_PRODUCTION' }}
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
test -n "${DEPLOY_COMMIT:-}"
|
||||
docker exec gitea-deploy-control \
|
||||
/opt/gitea-deploy/deploy.py deploy \
|
||||
"${GITHUB_REPOSITORY}" "${DEPLOY_COMMIT}"
|
||||
Reference in new issue
Block a user