Merge branch 'main' into rename

Keep pouch naming and .pouch/ack project state, and bring in ACK
regression mode, deployer test-environment binding, and manage-release
updates from main.
This commit is contained in:
2026-08-25 15:23:48 +08:00
40 changed files with 1968 additions and 224 deletions
+62 -21
View File
@@ -102,9 +102,9 @@ CLASSIFICATIONS = {"development", "staging", "production"}
STOP_POINTS = {"verified", "validation_ready", "review_ready", "released"}
INTENT_FIELDS = {"testEnvironment", "release"}
INTENT_STOP_AT = {
"testEnvironment": "validation_ready",
"release": "released",
}
TEST_ENVIRONMENT_FIELDS = {"via", "env"}
ACTIONS = {
"verify",
"pull-request",
@@ -681,10 +681,47 @@ def _validate_profiles(
errors.append(f"{where}: defaultProfile 不能部署 production 环境")
def _validate_test_environment_intent(
value: Any,
errors: list[str],
project_root: Path | None,
) -> None:
if value is None:
return
if isinstance(value, str):
errors.append(
"intents.testEnvironment: 已改为 deployer 绑定 "
"{via: deployer, env: <env>},不能再使用 profile ID "
f"{value!r}。请迁移后由 ACK 内部调用 deployer skill"
)
return
if not _mapping(value):
errors.append("intents.testEnvironment: 必须是 null 或 {via, env} 对象")
return
_reject_unknown(value, TEST_ENVIRONMENT_FIELDS, "intents.testEnvironment", errors)
if value.get("via") != "deployer":
errors.append("intents.testEnvironment.via 必须是 'deployer'")
env = value.get("env")
if not isinstance(env, str) or ID_RE.fullmatch(env) is None:
errors.append("intents.testEnvironment.env 必须是小写连字符环境名")
return
if project_root is None:
return
env_dir = project_root / ".pouch" / "deployer" / env
if not env_dir.is_dir():
env_dir = project_root / ".skiff" / "deployer" / env
if not env_dir.is_dir():
errors.append(
"intents.testEnvironment.env: 找不到 "
f".pouch/deployer/{env};先按 deployer skill 配置项目测试环境"
)
def _validate_intents(
values: Any,
profiles: dict[str, Any],
errors: list[str],
project_root: Path | None = None,
) -> None:
if values is None:
return
@@ -692,25 +729,29 @@ def _validate_intents(
errors.append("intents: 必须是对象")
return
_reject_unknown(values, INTENT_FIELDS, "intents", errors)
for field in sorted(INTENT_FIELDS):
if field not in values:
errors.append(f"intents.{field}: 必填")
continue
profile_id = values[field]
if profile_id is None:
continue
if not isinstance(profile_id, str) or ID_RE.fullmatch(profile_id) is None:
errors.append(f"intents.{field}: 必须是 null 或小写连字符 profile ID")
continue
profile = profiles.get(profile_id)
if profile is None:
errors.append(f"intents.{field}: 未定义 profile {profile_id!r}")
continue
expected_stop = INTENT_STOP_AT[field]
if _mapping(profile) and profile.get("stopAt") != expected_stop:
errors.append(
f"intents.{field}: profile {profile_id!r} 必须 stopAt {expected_stop}"
)
if "testEnvironment" not in values:
errors.append("intents.testEnvironment: 必填")
else:
_validate_test_environment_intent(
values.get("testEnvironment"), errors, project_root
)
if "release" not in values:
errors.append("intents.release: 必填")
return
profile_id = values["release"]
if profile_id is None:
return
if not isinstance(profile_id, str) or ID_RE.fullmatch(profile_id) is None:
errors.append("intents.release: 必须是 null 或小写连字符 profile ID")
return
profile = profiles.get(profile_id)
if profile is None:
errors.append(f"intents.release: 未定义 profile {profile_id!r}")
return
if _mapping(profile) and profile.get("stopAt") != INTENT_STOP_AT["release"]:
errors.append(
f"intents.release: profile {profile_id!r} 必须 stopAt released"
)
def validate_builtin(data: dict[str, Any], project_root: Path | None = None) -> list[str]:
@@ -758,7 +799,7 @@ def validate_builtin(data: dict[str, Any], project_root: Path | None = None) ->
environments=environments,
errors=errors,
)
_validate_intents(data.get("intents"), profiles, errors)
_validate_intents(data.get("intents"), profiles, errors, project_root)
if enabled:
if default_profile not in profiles: