chore: auto release commit (2026-06-17 09:48:59)
All checks were successful
技能自动化发布 / release (push) Successful in 5s
All checks were successful
技能自动化发布 / release (push) Successful in 5s
This commit is contained in:
@@ -215,6 +215,28 @@ scripts/
|
||||
|
||||
`health` 应委托 `collect_runtime_diagnostics`,不要在 `scripts/util/` 自建 `runtime_diagnostics.py`。
|
||||
|
||||
### 3.4 发布打包约束(PyArmor 与编码)
|
||||
|
||||
release workflow 会对 `scripts/` 下的 Python 源码做加密/打包。当前发布链路可能运行在 **PyArmor 免费/试用模式**,因此有两条**发布硬约束**:
|
||||
|
||||
**单文件行数(仅 `scripts/**/*.py`)**
|
||||
|
||||
- 每个 `scripts/` 下 Python 文件必须 **小于 1000 行**(`>= 1000` 可能在 CI 加密阶段失败)。
|
||||
- 推荐单文件控制在 **900 行以内**;接近上限时应拆分模块。
|
||||
- 不要把完整 RPA 主流程、DOM 选择器、数据库、CLI、日志全部堆进一个大文件。
|
||||
- 正确做法是拆分到 `scripts/cli/`、`scripts/service/`、`scripts/db/`、`scripts/util/`。
|
||||
|
||||
`examples/` 里的 Python 示例也建议保持模块化,但**行数限制不作为 examples 的发布硬约束**;不要对 README、HTML、测试数据等文档/资源套用 PyArmor 行数规则。
|
||||
|
||||
**文本编码(UTF-8 without BOM)**
|
||||
|
||||
- 所有提交到 skill 的源码、文档、配置文本文件必须是 **UTF-8 without BOM**。
|
||||
- Python 文件**绝对不能**带 UTF-8 BOM;否则 PyArmor/CI 可能报:`invalid non-printable character U+FEFF`。
|
||||
- Windows 编辑器容易保存成 UTF-8 BOM,提交前须确认文件开头无 `U+FEFF`。
|
||||
- 不要只写「UTF-8」,必须明确 **UTF-8 without BOM**。
|
||||
|
||||
本地可用 `python tests/run_tests.py release_packaging_constraints -v` 自检;默认必跑套件中的 `test_release_packaging_constraints.py` 会守护上述规则。
|
||||
|
||||
## 4. 开发一个新 skill 的标准步骤
|
||||
|
||||
下面这套顺序建议严格按步骤做,不要一上来就直接写 `service`。
|
||||
@@ -666,6 +688,8 @@ uses: admin/jiangchang-platform-kit/.github/workflows/reusable-release-skill.yam
|
||||
- [ ] `release.ps1` 存在
|
||||
- [ ] `.github/workflows/release_skill.yaml` 存在
|
||||
- [ ] `python tests/run_tests.py -v` 全部通过
|
||||
- [ ] `scripts/**/*.py` 单文件 **< 1000 行**(PyArmor 试用/免费模式限制;推荐 < 900 行)
|
||||
- [ ] 源码/文档/配置为 **UTF-8 without BOM**(Python 文件不得含 `U+FEFF`)
|
||||
- [ ] 没有新增默认必跑测试访问真实网络或浏览器
|
||||
- [ ] 如有 integration 测试需求,已写在 `tests/integration/` 下并保持 `.sample` 后缀
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
4. [`ADAPTER.md`](ADAPTER.md) — 涉及外部系统对接时
|
||||
5. [`RPA.md`](RPA.md) — 涉及浏览器 / 桌面 / 手机自动化时
|
||||
6. [`CONFIG.md`](CONFIG.md) — `.env` 规范与 bootstrap 机制
|
||||
7. [`RUNTIME.md`](RUNTIME.md) — 共享 runtime、数据路径与诊断约定
|
||||
7. [`RUNTIME.md`](RUNTIME.md) — 共享 runtime、数据路径、发布打包与编码约定
|
||||
|
||||
Agent 调用契约见 [`../references/CLI.md`](../references/CLI.md)、[`../references/SCHEMA.md`](../references/SCHEMA.md)。
|
||||
用户市场说明见根目录 [`README.md`](../README.md),不要写进本目录。
|
||||
|
||||
@@ -108,6 +108,17 @@ RPA 录屏成片(`RpaVideoSession`)、ffmpeg 路径、背景音乐探测均
|
||||
|
||||
## 编码与输出
|
||||
|
||||
- 所有提交到 skill 的源码、文档、配置文本文件必须是 **UTF-8 without BOM**(不要只写「UTF-8」)。
|
||||
- Python 文件**绝对不能**带 UTF-8 BOM;否则 release CI 的 PyArmor 阶段可能报:`invalid non-printable character U+FEFF`。
|
||||
- Windows 编辑器容易保存成 UTF-8 BOM;提交前确认文件开头无 `U+FEFF`。
|
||||
- Windows 终端建议在 `scripts/main.py` 里做 UTF-8 stdout/stderr 包装
|
||||
- 机读输出优先单行 JSON
|
||||
- 错误前缀建议统一 `ERROR:`
|
||||
|
||||
## 发布打包约束(`scripts/**/*.py`)
|
||||
|
||||
release workflow 加密 `scripts/` 源码时,可能运行在 PyArmor 免费/试用模式:
|
||||
|
||||
- `scripts/**/*.py` 单文件必须 **< 1000 行**(`>= 1000` 可能在 CI 失败);推荐 **< 900 行**。
|
||||
- 接近上限时拆分到 `cli/`、`service/`、`db/`、`util/`,不要把 RPA 主流程、选择器、DB、CLI 堆进单文件。
|
||||
- 此行数限制**仅适用于** `scripts/**/*.py`;不限制 README 等文档行数,也不把 examples 里的 HTML/README 纳入 PyArmor 行数约束。
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
- `SKILL.md` YAML slug vs [`constants.SKILL_SLUG`](../scripts/util/constants.py);
|
||||
- SQLite 骨架:`task_logs` 创建幂等与仓储读写;
|
||||
- **adapter profile**:[`tests/test_adapter_profile_policy.py`](../tests/test_adapter_profile_policy.py) + [`adapter_test_utils`](../tests/adapter_test_utils.py) ——验证在未授权情况下绝不误判开启真实网络/RPA。
|
||||
- **发布打包守护**:[`tests/test_release_packaging_constraints.py`](../tests/test_release_packaging_constraints.py) ——`scripts/**/*.py` 单文件 < 1000 行、文本文件 UTF-8 without BOM。
|
||||
|
||||
**原则:`tests/test_*.py` 不允许隐形访问外网、不允许拉起真实浏览器、不允许读写开发者机器的真实数据根**。如果需要仿真服务器,也应仅在 integration(并由档位变量放行)。
|
||||
|
||||
@@ -224,6 +225,8 @@ Golden fixture 流程同理([`tests/samples/test_golden_cases.py.sample`](../t
|
||||
- [ ] `pytest.ini` 存在且 `python_files` 只收集 `test_*.py` / `*_test.py`
|
||||
- [ ] pytest **不会**误收集 `tests/` 下的 `.txt` / 日志 / 结果文件
|
||||
- [ ] `python tests/run_tests.py -v` 全部通过
|
||||
- [ ] `scripts/**/*.py` 单文件 < 1000 行(`test_release_packaging_constraints.py`)
|
||||
- [ ] 文本文件为 UTF-8 without BOM,无 `U+FEFF`(同上)
|
||||
|
||||
---
|
||||
|
||||
|
||||
165
tests/test_release_packaging_constraints.py
Normal file
165
tests/test_release_packaging_constraints.py
Normal file
@@ -0,0 +1,165 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""发布打包守护:PyArmor 单文件行数与 UTF-8 without BOM。"""
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import unittest
|
||||
|
||||
from _support import get_skill_root
|
||||
|
||||
SCRIPTS_MAX_LINES = 1000
|
||||
SCRIPTS_RECOMMENDED_MAX_LINES = 900
|
||||
|
||||
TEXT_EXTENSIONS = frozenset(
|
||||
{
|
||||
".py",
|
||||
".md",
|
||||
".yml",
|
||||
".yaml",
|
||||
".json",
|
||||
".toml",
|
||||
".ini",
|
||||
".txt",
|
||||
".ps1",
|
||||
".sh",
|
||||
}
|
||||
)
|
||||
|
||||
SPECIAL_TEXT_FILES = frozenset({".env.example", ".gitignore"})
|
||||
|
||||
SKIP_DIR_NAMES = frozenset(
|
||||
{
|
||||
".git",
|
||||
"__pycache__",
|
||||
".pytest_cache",
|
||||
"rpa-artifacts",
|
||||
"videos",
|
||||
}
|
||||
)
|
||||
|
||||
BINARY_EXTENSIONS = frozenset(
|
||||
{
|
||||
".db",
|
||||
".sqlite",
|
||||
".png",
|
||||
".jpg",
|
||||
".jpeg",
|
||||
".gif",
|
||||
".webp",
|
||||
".mp4",
|
||||
".zip",
|
||||
".tar",
|
||||
".gz",
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def _is_text_candidate(path: str) -> bool:
|
||||
basename = os.path.basename(path)
|
||||
if basename in SPECIAL_TEXT_FILES:
|
||||
return True
|
||||
ext = os.path.splitext(basename)[1].lower()
|
||||
if ext in BINARY_EXTENSIONS:
|
||||
return False
|
||||
return ext in TEXT_EXTENSIONS
|
||||
|
||||
|
||||
def _iter_text_files(skill_root: str) -> list[str]:
|
||||
found: list[str] = []
|
||||
for root, dirnames, filenames in os.walk(skill_root):
|
||||
dirnames[:] = [d for d in dirnames if d not in SKIP_DIR_NAMES]
|
||||
for name in filenames:
|
||||
full = os.path.join(root, name)
|
||||
if _is_text_candidate(full):
|
||||
found.append(full)
|
||||
return sorted(found)
|
||||
|
||||
|
||||
def _iter_scripts_py_files(skill_root: str) -> list[str]:
|
||||
scripts_dir = os.path.join(skill_root, "scripts")
|
||||
found: list[str] = []
|
||||
if not os.path.isdir(scripts_dir):
|
||||
return found
|
||||
for root, dirnames, filenames in os.walk(scripts_dir):
|
||||
dirnames[:] = [d for d in dirnames if d not in SKIP_DIR_NAMES]
|
||||
for name in filenames:
|
||||
if name.endswith(".py"):
|
||||
found.append(os.path.join(root, name))
|
||||
return sorted(found)
|
||||
|
||||
|
||||
class TestReleasePackagingConstraints(unittest.TestCase):
|
||||
def test_scripts_py_files_under_pyarmor_line_limit(self) -> None:
|
||||
skill_root = get_skill_root()
|
||||
offenders: list[str] = []
|
||||
for path in _iter_scripts_py_files(skill_root):
|
||||
with open(path, encoding="utf-8") as f:
|
||||
line_count = sum(1 for _ in f)
|
||||
if line_count >= SCRIPTS_MAX_LINES:
|
||||
rel = os.path.relpath(path, skill_root).replace("\\", "/")
|
||||
offenders.append(
|
||||
f"{rel}: {line_count} lines (limit < {SCRIPTS_MAX_LINES})"
|
||||
)
|
||||
self.assertEqual(
|
||||
offenders,
|
||||
[],
|
||||
msg=(
|
||||
"PyArmor trial/free mode may fail when a single Python file reaches "
|
||||
f"{SCRIPTS_MAX_LINES} lines; split this file into smaller modules. "
|
||||
f"Recommended keep each file < {SCRIPTS_RECOMMENDED_MAX_LINES} lines. "
|
||||
f"Offenders: {offenders}"
|
||||
),
|
||||
)
|
||||
|
||||
def test_text_files_are_utf8_without_bom(self) -> None:
|
||||
skill_root = get_skill_root()
|
||||
bom_failures: list[str] = []
|
||||
decode_failures: list[str] = []
|
||||
feff_failures: list[str] = []
|
||||
|
||||
for path in _iter_text_files(skill_root):
|
||||
rel = os.path.relpath(path, skill_root).replace("\\", "/")
|
||||
with open(path, "rb") as f:
|
||||
raw = f.read()
|
||||
|
||||
if raw.startswith(b"\xef\xbb\xbf"):
|
||||
bom_failures.append(rel)
|
||||
continue
|
||||
|
||||
try:
|
||||
text = raw.decode("utf-8")
|
||||
except UnicodeDecodeError as exc:
|
||||
decode_failures.append(f"{rel}: {exc}")
|
||||
continue
|
||||
|
||||
if "\ufeff" in text:
|
||||
feff_failures.append(rel)
|
||||
|
||||
self.assertEqual(
|
||||
bom_failures,
|
||||
[],
|
||||
msg=(
|
||||
"Files must be saved as UTF-8 without BOM (remove UTF-8 BOM prefix). "
|
||||
f"Offenders: {bom_failures}"
|
||||
),
|
||||
)
|
||||
self.assertEqual(
|
||||
decode_failures,
|
||||
[],
|
||||
msg=(
|
||||
"Files must be valid UTF-8 without BOM. "
|
||||
f"Offenders: {decode_failures}"
|
||||
),
|
||||
)
|
||||
self.assertEqual(
|
||||
feff_failures,
|
||||
[],
|
||||
msg=(
|
||||
"Files must not contain U+FEFF; save as UTF-8 without BOM. "
|
||||
f"Offenders: {feff_failures}"
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user