chore: auto release commit (2026-06-17 09:21:19)
All checks were successful
技能自动化发布 / release (push) Successful in 6s
All checks were successful
技能自动化发布 / release (push) Successful in 6s
This commit is contained in:
@@ -4,17 +4,157 @@ from __future__ import annotations
|
||||
|
||||
import configparser
|
||||
import os
|
||||
import re
|
||||
import unittest
|
||||
|
||||
from _support import get_skill_root
|
||||
|
||||
|
||||
FORBIDDEN_README_DESCRIPTION_TERMS_EN = (
|
||||
"cli",
|
||||
"schema",
|
||||
"references",
|
||||
"development",
|
||||
"playwright",
|
||||
"python",
|
||||
"dom",
|
||||
"rpa",
|
||||
)
|
||||
|
||||
FORBIDDEN_README_DESCRIPTION_TERMS_ZH = (
|
||||
"Agent 参考索引",
|
||||
"契约",
|
||||
"数据结构",
|
||||
"开发规范",
|
||||
)
|
||||
|
||||
|
||||
def _description_has_forbidden_term(description: str) -> str | None:
|
||||
lowered = description.lower()
|
||||
for term in FORBIDDEN_README_DESCRIPTION_TERMS_EN:
|
||||
if term in lowered:
|
||||
return term
|
||||
for term in FORBIDDEN_README_DESCRIPTION_TERMS_ZH:
|
||||
if term in description:
|
||||
return term
|
||||
return None
|
||||
|
||||
|
||||
def _extract_workflow_uses_line(text: str) -> str | None:
|
||||
for line in text.splitlines():
|
||||
stripped = line.strip()
|
||||
if stripped.startswith("uses:"):
|
||||
return stripped
|
||||
return None
|
||||
|
||||
|
||||
class TestDocsStandards(unittest.TestCase):
|
||||
def _read(self, rel_path: str) -> str:
|
||||
path = os.path.join(get_skill_root(), rel_path)
|
||||
with open(path, encoding="utf-8") as f:
|
||||
return f.read()
|
||||
|
||||
def _parse_frontmatter(self, text: str) -> str:
|
||||
parts = text.split("---", 2)
|
||||
if len(parts) < 2:
|
||||
return ""
|
||||
return parts[1]
|
||||
|
||||
def test_root_readme_has_yaml_frontmatter(self) -> None:
|
||||
text = self._read("README.md")
|
||||
self.assertTrue(text.startswith("---"), "README.md must start with YAML frontmatter")
|
||||
parts = text.split("---", 2)
|
||||
self.assertGreaterEqual(len(parts), 3, "README.md frontmatter must be closed")
|
||||
|
||||
def test_root_readme_frontmatter_has_description(self) -> None:
|
||||
frontmatter = self._parse_frontmatter(self._read("README.md"))
|
||||
self.assertIn("description:", frontmatter, "README.md frontmatter must include description")
|
||||
|
||||
def test_root_readme_description_avoids_technical_index_terms(self) -> None:
|
||||
frontmatter = self._parse_frontmatter(self._read("README.md"))
|
||||
match = re.search(r"description:\s*(.+)", frontmatter)
|
||||
self.assertIsNotNone(match, "README.md description field not found")
|
||||
description = match.group(1).strip().strip("\"'")
|
||||
forbidden = _description_has_forbidden_term(description)
|
||||
self.assertIsNone(
|
||||
forbidden,
|
||||
msg=f"README.md description must not contain technical index term {forbidden!r}",
|
||||
)
|
||||
|
||||
def test_description_forbidden_terms_are_case_insensitive_for_english(self) -> None:
|
||||
fail_cases = (
|
||||
"CLI 契约说明",
|
||||
"cli 工具入口",
|
||||
"SCHEMA 定义",
|
||||
"Schema 映射",
|
||||
"schema 字段",
|
||||
"Playwright 自动化",
|
||||
"python 脚本",
|
||||
"RPA 流程",
|
||||
)
|
||||
pass_cases = (
|
||||
"用用户能理解的方式说明这个技能能做什么、适合什么场景,以及如何开始使用。",
|
||||
"帮你自动整理订单并生成处理报告",
|
||||
"适合每天需要重复核对的业务场景",
|
||||
)
|
||||
for description in fail_cases:
|
||||
with self.subTest(description=description):
|
||||
self.assertIsNotNone(
|
||||
_description_has_forbidden_term(description),
|
||||
msg=f"expected forbidden term in {description!r}",
|
||||
)
|
||||
for description in pass_cases:
|
||||
with self.subTest(description=description):
|
||||
self.assertIsNone(
|
||||
_description_has_forbidden_term(description),
|
||||
msg=f"expected no forbidden term in {description!r}",
|
||||
)
|
||||
|
||||
def test_references_readme_has_no_yaml_frontmatter(self) -> None:
|
||||
text = self._read("references/README.md")
|
||||
self.assertFalse(
|
||||
text.lstrip().startswith("---"),
|
||||
"references/README.md must not contain YAML frontmatter",
|
||||
)
|
||||
|
||||
def test_references_readme_has_no_description_field(self) -> None:
|
||||
text = self._read("references/README.md")
|
||||
self.assertNotRegex(
|
||||
text,
|
||||
r"(?m)^description\s*:",
|
||||
msg="references/README.md must not contain description field",
|
||||
)
|
||||
|
||||
def test_skill_md_requires_read_readme_first_rule(self) -> None:
|
||||
text = self._read("SKILL.md")
|
||||
self.assertIn("README.md", text)
|
||||
self.assertTrue(
|
||||
"优先读取" in text or "优先读" in text,
|
||||
msg="SKILL.md must require reading README.md first for user-facing questions",
|
||||
)
|
||||
|
||||
def test_skill_md_states_readme_is_for_users(self) -> None:
|
||||
text = self._read("SKILL.md")
|
||||
self.assertIn("README.md", text)
|
||||
self.assertTrue(
|
||||
any(marker in text for marker in ("面向用户", "普通用户", "用户市场")),
|
||||
msg="SKILL.md must state README.md is user-facing documentation",
|
||||
)
|
||||
|
||||
def test_release_workflow_uses_main_channel(self) -> None:
|
||||
text = self._read(".github/workflows/release_skill.yaml")
|
||||
uses_line = _extract_workflow_uses_line(text)
|
||||
self.assertIsNotNone(uses_line, "release_skill.yaml must contain a uses: line")
|
||||
self.assertIn(
|
||||
"admin/jiangchang-platform-kit/.github/workflows/reusable-release-skill.yaml",
|
||||
uses_line,
|
||||
)
|
||||
self.assertRegex(
|
||||
uses_line,
|
||||
r"reusable-release-skill\.yaml@main\b",
|
||||
msg="workflow uses must reference reusable-release-skill.yaml@main",
|
||||
)
|
||||
|
||||
def test_rpa_md_covers_video_and_playwright_standards(self) -> None:
|
||||
text = self._read("development/RPA.md")
|
||||
self.assertIn("title", text)
|
||||
@@ -64,6 +204,127 @@ class TestDocsStandards(unittest.TestCase):
|
||||
msg=f"{rel} still claims 1.0.11 as current minimum",
|
||||
)
|
||||
|
||||
def test_scripts_has_no_legacy_example_artifacts(self) -> None:
|
||||
scripts_dir = os.path.join(get_skill_root(), "scripts")
|
||||
service_dir = os.path.join(scripts_dir, "service")
|
||||
self.assertFalse(
|
||||
os.path.isfile(os.path.join(service_dir, "example_browser_rpa.py")),
|
||||
"scripts/service/example_browser_rpa.py must not exist",
|
||||
)
|
||||
self.assertFalse(
|
||||
os.path.isdir(os.path.join(service_dir, "example_adapter")),
|
||||
"scripts/service/example_adapter/ must not exist",
|
||||
)
|
||||
|
||||
def test_gitignore_covers_python_runtime_artifacts(self) -> None:
|
||||
text = self._read(".gitignore")
|
||||
self.assertIn("__pycache__", text)
|
||||
self.assertIn(".pytest_cache", text)
|
||||
self.assertTrue(
|
||||
"py[cod]" in text or ".pyc" in text,
|
||||
msg=".gitignore must ignore Python bytecode (e.g. *.py[cod])",
|
||||
)
|
||||
|
||||
def test_scripts_runtime_artifacts_not_tracked_by_git(self) -> None:
|
||||
import subprocess
|
||||
|
||||
try:
|
||||
out = subprocess.run(
|
||||
["git", "ls-files", "scripts"],
|
||||
cwd=get_skill_root(),
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=True,
|
||||
)
|
||||
except (FileNotFoundError, subprocess.CalledProcessError):
|
||||
self.skipTest("git not available or not a git repo")
|
||||
tracked = out.stdout.splitlines()
|
||||
for rel in tracked:
|
||||
lowered = rel.replace("\\", "/").lower()
|
||||
self.assertNotIn("__pycache__", lowered, msg=f"tracked cache path: {rel}")
|
||||
self.assertNotIn(".pytest_cache", lowered, msg=f"tracked cache path: {rel}")
|
||||
self.assertFalse(lowered.endswith(".pyc"), msg=f"tracked .pyc file: {rel}")
|
||||
|
||||
def test_examples_top_level_modes_present(self) -> None:
|
||||
examples_dir = os.path.join(get_skill_root(), "examples")
|
||||
for mode in ("real_api", "real_browser_rpa", "simulator_api", "simulator_browser_rpa"):
|
||||
self.assertTrue(
|
||||
os.path.isdir(os.path.join(examples_dir, mode)),
|
||||
msg=f"examples/{mode}/ must exist",
|
||||
)
|
||||
|
||||
def test_examples_are_not_full_skill_repos(self) -> None:
|
||||
examples_dir = os.path.join(get_skill_root(), "examples")
|
||||
for mode in os.listdir(examples_dir):
|
||||
mode_path = os.path.join(examples_dir, mode)
|
||||
if not os.path.isdir(mode_path):
|
||||
continue
|
||||
self.assertFalse(
|
||||
os.path.isfile(os.path.join(mode_path, "SKILL.md")),
|
||||
msg=f"examples/{mode}/SKILL.md must not exist",
|
||||
)
|
||||
self.assertFalse(
|
||||
os.path.isdir(os.path.join(mode_path, "references")),
|
||||
msg=f"examples/{mode}/references/ must not exist",
|
||||
)
|
||||
self.assertFalse(
|
||||
os.path.isdir(os.path.join(mode_path, "development")),
|
||||
msg=f"examples/{mode}/development/ must not exist",
|
||||
)
|
||||
self.assertFalse(
|
||||
os.path.isdir(os.path.join(mode_path, ".github")),
|
||||
msg=f"examples/{mode}/.github/ must not exist",
|
||||
)
|
||||
|
||||
def test_examples_readme_states_copy_boundary(self) -> None:
|
||||
text = self._read("examples/README.md")
|
||||
self.assertIn("不是完整 skill 仓库", text)
|
||||
self.assertIn("核心实现案例", text)
|
||||
self.assertTrue(
|
||||
"不要整包照搬" in text or "不要整包" in text,
|
||||
msg="examples/README.md must warn against copying whole example tree",
|
||||
)
|
||||
self.assertTrue(
|
||||
"scripts" in text and ("复制" in text or "copy map" in text.lower()),
|
||||
msg="examples/README.md must describe copying into scripts/",
|
||||
)
|
||||
|
||||
def test_placeholder_api_examples_are_not_implementation_references(self) -> None:
|
||||
for rel in ("examples/real_api/README.md", "examples/simulator_api/README.md"):
|
||||
text = self._read(rel)
|
||||
self.assertTrue(
|
||||
"占位" in text or "规划" in text,
|
||||
msg=f"{rel} must state placeholder/planned status",
|
||||
)
|
||||
self.assertTrue(
|
||||
"尚未沉淀" in text or "不可作为实现参考" in text or "不可" in text,
|
||||
msg=f"{rel} must warn not to use as implementation reference",
|
||||
)
|
||||
|
||||
def test_docs_do_not_reference_scripts_legacy_examples(self) -> None:
|
||||
forbidden_fragments = (
|
||||
"scripts/service/example_browser_rpa",
|
||||
"scripts/service/example_adapter",
|
||||
"example_browser_rpa.py",
|
||||
"example_adapter/",
|
||||
)
|
||||
doc_paths = []
|
||||
for root, _dirs, files in os.walk(get_skill_root()):
|
||||
rel_root = os.path.relpath(root, get_skill_root())
|
||||
if rel_root.startswith(".git"):
|
||||
continue
|
||||
for name in files:
|
||||
if name.endswith(".md"):
|
||||
doc_paths.append(os.path.join(rel_root, name).replace("\\", "/"))
|
||||
for rel in doc_paths:
|
||||
text = self._read(rel)
|
||||
for fragment in forbidden_fragments:
|
||||
self.assertNotIn(
|
||||
fragment,
|
||||
text,
|
||||
msg=f"{rel} must not reference legacy scripts example path {fragment!r}",
|
||||
)
|
||||
|
||||
|
||||
class TestPytestIniCollection(unittest.TestCase):
|
||||
def test_pytest_ini_exists_and_limits_python_files(self) -> None:
|
||||
|
||||
Reference in New Issue
Block a user