Files
qqq-all/scripts/test_release_bundle.py
james.maes 308830a06a fix: preserve SFTP target in release Compose bundle
Use long volume syntax for Compose 2.38.2 rendering and assert that the variable survives. Refs QRun-IO/qqq#921.
2026-10-01 20:18:36 -05:00

77 lines
4.6 KiB
Python

"""Exercise the delivered archive with Compose, without running any containers."""
import json
import os
import subprocess
import tarfile
import tempfile
import unittest
from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent
IMAGE = "ghcr.io/qrun-io/qqq-all@sha256:" + "a" * 64
class ReleaseBundleTest(unittest.TestCase):
def test_bundle_uses_digest_and_keeps_full_profile_infra_without_app_build(self):
with tempfile.TemporaryDirectory() as directory:
result = subprocess.run(["python3", str(ROOT / "scripts/release_bundle.py"),
"--root", str(ROOT), "--version", "4.1.0-RC.1",
"--sha", "b" * 40, "--image", IMAGE, "--output", directory],
capture_output=True, text=True)
self.assertEqual(0, result.returncode, result.stderr)
archive = Path(directory) / "qqq-all-4.1.0-RC.1.tar.gz"
extracted = Path(directory) / "extracted"
with tarfile.open(archive) as bundle:
names = bundle.getnames()
self.assertIn("qqq-all-4.1.0-RC.1/infra/minio/Dockerfile", names)
self.assertIn("qqq-all-4.1.0-RC.1/infra/keycloak/realm.json", names)
self.assertIn("qqq-all-4.1.0-RC.1/.env.example", names)
self.assertNotIn("qqq-all-4.1.0-RC.1/.env", names)
self.assertNotIn("qqq-all-4.1.0-RC.1/Dockerfile", names)
bundle.extractall(extracted, filter="data")
root = extracted / "qqq-all-4.1.0-RC.1"
bundled = json.loads((root / "compose.yaml").read_text())
self.assertEqual("/home/${SFTP_USER:-qqq}/upload",
bundled["services"]["sftp"]["volumes"][0]["target"])
environment = {key: value for key, value in os.environ.items() if not key.startswith("COMPOSE_")}
result = subprocess.run(["docker", "compose", "--env-file", ".env.example", "--profile", "*",
"-f", "compose.yaml", "config", "--format", "json"],
cwd=root, env=environment, capture_output=True, text=True)
self.assertEqual(0, result.returncode, result.stderr)
config = json.loads(result.stdout)
original_result = subprocess.run(["docker", "compose", "--env-file", ".env.example", "--profile", "*",
"-f", "compose.yaml", "config", "--format", "json"],
cwd=ROOT, env=environment, capture_output=True, text=True, check=True)
original = json.loads(original_result.stdout.replace(str(ROOT.resolve()), str(root.resolve())))
for name, service in original["services"].items():
if name in ("app-core", "app-full"):
service.pop("build")
service.update(image=IMAGE, pull_policy="always")
if name == "minio":
service["image"] = "qqq-all-minio:4.1.0-RC.1"
self.assertEqual(service, config["services"][name], name)
for service in ("app-core", "app-full"):
self.assertEqual(IMAGE, config["services"][service]["image"])
self.assertNotIn("build", config["services"][service])
self.assertEqual((root / "infra/minio").resolve(), Path(config["services"]["minio"]["build"]["context"]).resolve())
for service in config["services"].values():
for volume in service.get("volumes", []):
if volume["type"] == "bind":
self.assertTrue(Path(volume["source"]).exists(), volume)
manifest = json.loads((root / "release.json").read_text())
self.assertEqual("4.1.0-RC.1", manifest["version"])
self.assertEqual("b" * 40, manifest["source_sha"])
self.assertEqual(IMAGE, manifest["image"])
def test_bundle_rejects_mutable_image_and_unapproved_version(self):
with tempfile.TemporaryDirectory() as directory:
for version, image in (("4.1.0-RC.2", IMAGE), ("4.1.0-RC.1", "ghcr.io/qrun-io/qqq-all:develop"),
("4.1.0", "ghcr.io/other/qqq-all@sha256:" + "a" * 64)):
result = subprocess.run(["python3", str(ROOT / "scripts/release_bundle.py"),
"--root", str(ROOT), "--version", version, "--sha", "b" * 40,
"--image", image, "--output", directory], capture_output=True, text=True)
self.assertNotEqual(0, result.returncode)
self.assertEqual([], list(Path(directory).iterdir()))