from __future__ import annotations import copy import os import subprocess import sys import tempfile import unittest from pathlib import Path REPO_ROOT = Path(__file__).resolve().parents[1] SCRIPTS_DIR = REPO_ROOT / "skills" / "ack" / "scripts" VALIDATOR = SCRIPTS_DIR / "validate_delivery.py" EXAMPLE = REPO_ROOT / "skills" / "ack" / "examples" / "delivery.example.yaml" sys.path.insert(0, str(SCRIPTS_DIR)) import validate_delivery # noqa: E402 def valid_contract() -> dict: entrypoints = { name: { "kind": "make", "target": name, "args": [], "requiredSecrets": [], "workingDirectory": ".", "timeoutSeconds": 300, } for name in ("verify", "build", "upload", "deploy", "health", "rollback") } return { "version": 1, "updatedAt": "2026-08-01T10:00:00+08:00", "project": {"name": "demo"}, "enabled": True, "defaultProfile": "review", "entrypoints": entrypoints, "artifacts": { "service-deb": { "type": "deb", "build": "build", "outputs": ["dist/*.deb"], } }, "destinations": { "preview-apt": { "type": "apt-repository", "channel": "preview", "endpoint": "https://packages.example.com", "repository": "testing", "upload": "upload", } }, "environments": { "test-server": { "type": "ssh-host", "classification": "development", "target": "test-server", "deploy": "deploy", "healthCheck": "health", "rollback": "rollback", "mutex": "test-server-deploy", } }, "profiles": { "review": { "stopAt": "review_ready", "steps": [ {"id": "verify", "action": "verify", "entrypoint": "verify"}, { "id": "open-pr", "action": "pull-request", "draft": True, "remote": "origin", "baseBranch": "main", }, {"id": "build", "action": "build", "artifact": "service-deb"}, { "id": "publish", "action": "publish", "artifact": "service-deb", "destination": "preview-apt", }, { "id": "deploy", "action": "deploy", "artifact": "service-deb", "environment": "test-server", }, { "id": "health", "action": "health-check", "environment": "test-server", }, {"id": "ready", "action": "mark-ready"}, ], } }, } class AckDeliveryValidationTests(unittest.TestCase): def test_example_is_valid_with_and_without_site_packages(self) -> None: for no_site_packages in (False, True): command = [sys.executable] if no_site_packages: command.append("-S") result = subprocess.run( [*command, str(VALIDATOR), str(EXAMPLE)], cwd=REPO_ROOT, text=True, capture_output=True, check=False, ) with self.subTest(no_site_packages=no_site_packages): self.assertEqual(result.returncode, 0, result.stderr) self.assertIn("交付契约校验通过", result.stdout) def test_default_profile_rejects_stable_and_production_targets(self) -> None: contract = valid_contract() contract["destinations"]["preview-apt"]["channel"] = "stable" contract["environments"]["test-server"]["classification"] = "production" errors = validate_delivery.validate_builtin(contract) self.assertTrue(any("stable 发布前必须有 release approval" in item for item in errors)) self.assertTrue(any("production 部署前必须有 production approval" in item for item in errors)) self.assertTrue(any("defaultProfile 不能发布 stable" in item for item in errors)) self.assertTrue(any("defaultProfile 不能部署 production" in item for item in errors)) def test_nondefault_release_profile_supports_stable_and_production_with_gates(self) -> None: contract = valid_contract() contract["destinations"]["stable-apt"] = { "type": "apt-repository", "channel": "stable", "endpoint": "https://packages.example.com", "repository": "stable", "upload": "upload", } contract["environments"]["prod-server"] = { "type": "ssh-host", "classification": "production", "target": "prod-server", "deploy": "deploy", "healthCheck": "health", "rollback": "rollback", "mutex": "prod-server-deploy", } contract["profiles"]["release"] = { "stopAt": "released", "steps": [ {"id": "verify-release", "action": "verify", "entrypoint": "verify"}, { "id": "open-release-pr", "action": "pull-request", "draft": True, "remote": "origin", "baseBranch": "main", }, {"id": "build-release", "action": "build", "artifact": "service-deb"}, {"id": "approve-release", "action": "approval", "gate": "release"}, { "id": "publish-release", "action": "publish", "artifact": "service-deb", "destination": "stable-apt", }, {"id": "approve-production", "action": "approval", "gate": "production"}, { "id": "deploy-production", "action": "deploy", "artifact": "service-deb", "environment": "prod-server", }, { "id": "health-production", "action": "health-check", "environment": "prod-server", }, {"id": "ready-release", "action": "mark-ready"}, ], } self.assertEqual(validate_delivery.validate_builtin(contract), []) def test_default_validation_profile_requires_deploy_and_health_check(self) -> None: contract = valid_contract() contract["defaultProfile"] = "local-validation" contract["profiles"]["local-validation"] = { "stopAt": "validation_ready", "steps": [ {"id": "build-local", "action": "build", "artifact": "service-deb"}, { "id": "deploy-local", "action": "deploy", "artifact": "service-deb", "environment": "test-server", }, { "id": "health-local", "action": "health-check", "environment": "test-server", }, ], } self.assertEqual(validate_delivery.validate_builtin(contract), []) contract["profiles"]["local-validation"]["steps"].pop() errors = validate_delivery.validate_builtin(contract) self.assertTrue(any("必须全部完成 health-check" in item for item in errors)) def test_publish_and_health_check_require_prior_steps(self) -> None: contract = valid_contract() steps = contract["profiles"]["review"]["steps"] steps[2], steps[3] = steps[3], steps[2] steps[4], steps[5] = steps[5], steps[4] errors = validate_delivery.validate_builtin(contract) self.assertTrue(any("publish 前必须先 build" in item for item in errors)) self.assertTrue(any("health-check 前必须先 deploy" in item for item in errors)) def test_pull_request_requires_explicit_safe_remote_and_base(self) -> None: contract = valid_contract() step = contract["profiles"]["review"]["steps"][1] del step["remote"] step["baseBranch"] = "../main" errors = validate_delivery.validate_builtin(contract) self.assertTrue(any(".remote: action='pull-request' 时必填" in item for item in errors)) self.assertTrue(any(".remote: 必须是安全的 Git remote 名称" in item for item in errors)) self.assertTrue(any(".baseBranch: 必须是安全的 Git 分支名" in item for item in errors)) def test_unknown_shell_and_inline_secret_are_rejected(self) -> None: contract = valid_contract() contract["entrypoints"]["verify"]["shell"] = "make verify" contract["entrypoints"]["verify"]["requiredSecrets"] = ["token-value"] contract["destinations"]["preview-apt"]["repository"] = ( "token=abcdefghijklmnop" ) contract["destinations"]["preview-apt"]["registry"] = "unexpected.example" errors = validate_delivery.validate_builtin(contract) self.assertTrue(any("未知字段 'shell'" in item for item in errors)) self.assertTrue(any("requiredSecrets" in item for item in errors)) self.assertTrue(any("type='apt-repository' 不允许此字段" in item for item in errors)) self.assertTrue(any("疑似包含敏感信息" in item for item in errors)) def test_project_script_must_be_executable_and_not_a_symlink(self) -> None: contract = valid_contract() contract["entrypoints"]["verify"] = { "kind": "script", "path": "scripts/verify.sh", "args": [], "requiredSecrets": [], "workingDirectory": ".", "timeoutSeconds": 300, } with tempfile.TemporaryDirectory() as temp_dir: root = Path(temp_dir) scripts = root / "scripts" scripts.mkdir() target = scripts / "target.sh" target.write_text("#!/bin/sh\nexit 0\n", encoding="utf-8") target.chmod(0o755) os.symlink("target.sh", scripts / "verify.sh") errors = validate_delivery.validate_builtin(contract, root) self.assertTrue(any("路径不能包含 symlink" in item for item in errors)) (scripts / "verify.sh").unlink() plain = scripts / "verify.sh" plain.write_text("#!/bin/sh\nexit 0\n", encoding="utf-8") plain.chmod(0o644) errors = validate_delivery.validate_builtin(contract, root) self.assertTrue(any("脚本不可执行" in item for item in errors)) def test_tasks_link_requires_fixed_path_runs_and_same_project(self) -> None: contract = valid_contract() tasks = { "project": {"name": "other", "deliveryFile": "delivery.yaml"}, "tasks": [], } errors = validate_delivery.validate_tasks_link(contract, tasks) self.assertIn( "tasks.project.deliveryFile 必须固定为 docs/ack/delivery.yaml", errors, ) self.assertIn("delivery.project.name 必须与 tasks.project.name 一致", errors) self.assertIn("引用 deliveryFile 的任务板必须包含 deliveryRuns 列表", errors) def test_disabled_empty_contract_remains_valid(self) -> None: contract = copy.deepcopy(valid_contract()) contract.update( { "enabled": False, "defaultProfile": None, "intents": {"testEnvironment": None, "release": None}, "entrypoints": {}, "artifacts": {}, "destinations": {}, "environments": {}, "profiles": {}, } ) self.assertEqual(validate_delivery.validate_builtin(contract), []) def test_test_environment_intent_requires_deployer_binding(self) -> None: contract = valid_contract() contract["intents"] = { "testEnvironment": "review", "release": None, } errors = validate_delivery.validate_builtin(contract) self.assertTrue( any("已改为 deployer 绑定" in item for item in errors) ) contract["intents"]["testEnvironment"] = {"via": "deployer", "env": "test"} self.assertEqual(validate_delivery.validate_builtin(contract), []) contract["intents"]["release"] = "missing-release" errors = validate_delivery.validate_builtin(contract) self.assertTrue(any("未定义 profile 'missing-release'" in item for item in errors)) def test_test_environment_binding_requires_deployer_env_directory(self) -> None: contract = valid_contract() contract["intents"] = { "testEnvironment": {"via": "deployer", "env": "test"}, "release": None, } with tempfile.TemporaryDirectory() as temp_dir: root = Path(temp_dir) errors = validate_delivery.validate_builtin(contract, root) self.assertTrue( any(".skiff/deployer/test" in item for item in errors) ) env_dir = root / ".skiff" / "deployer" / "test" env_dir.mkdir(parents=True) self.assertEqual(validate_delivery.validate_builtin(contract, root), []) if __name__ == "__main__": unittest.main()