chore: auto release commit (2026-06-17 09:48:59)
All checks were successful
技能自动化发布 / release (push) Successful in 5s

This commit is contained in:
2026-06-17 09:49:00 +08:00
parent 79098dad5c
commit f3f7a9b9e5
5 changed files with 204 additions and 1 deletions

View File

@@ -215,6 +215,28 @@ scripts/
`health` 应委托 `collect_runtime_diagnostics`,不要在 `scripts/util/` 自建 `runtime_diagnostics.py`。 `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 的标准步骤 ## 4. 开发一个新 skill 的标准步骤
下面这套顺序建议严格按步骤做,不要一上来就直接写 `service`。 下面这套顺序建议严格按步骤做,不要一上来就直接写 `service`。
@@ -666,6 +688,8 @@ uses: admin/jiangchang-platform-kit/.github/workflows/reusable-release-skill.yam
- [ ] `release.ps1` 存在 - [ ] `release.ps1` 存在
- [ ] `.github/workflows/release_skill.yaml` 存在 - [ ] `.github/workflows/release_skill.yaml` 存在
- [ ] `python tests/run_tests.py -v` 全部通过 - [ ] `python tests/run_tests.py -v` 全部通过
- [ ] `scripts/**/*.py` 单文件 **< 1000 行**PyArmor 试用/免费模式限制;推荐 < 900 行)
- [ ] 源码/文档/配置为 **UTF-8 without BOM**Python 文件不得含 `U+FEFF`
- [ ] 没有新增默认必跑测试访问真实网络或浏览器 - [ ] 没有新增默认必跑测试访问真实网络或浏览器
- [ ] 如有 integration 测试需求,已写在 `tests/integration/` 下并保持 `.sample` 后缀 - [ ] 如有 integration 测试需求,已写在 `tests/integration/` 下并保持 `.sample` 后缀

View File

@@ -8,7 +8,7 @@
4. [`ADAPTER.md`](ADAPTER.md) — 涉及外部系统对接时 4. [`ADAPTER.md`](ADAPTER.md) — 涉及外部系统对接时
5. [`RPA.md`](RPA.md) — 涉及浏览器 / 桌面 / 手机自动化时 5. [`RPA.md`](RPA.md) — 涉及浏览器 / 桌面 / 手机自动化时
6. [`CONFIG.md`](CONFIG.md) — `.env` 规范与 bootstrap 机制 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)。 Agent 调用契约见 [`../references/CLI.md`](../references/CLI.md)、[`../references/SCHEMA.md`](../references/SCHEMA.md)。
用户市场说明见根目录 [`README.md`](../README.md),不要写进本目录。 用户市场说明见根目录 [`README.md`](../README.md),不要写进本目录。

View File

@@ -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 包装 - Windows 终端建议在 `scripts/main.py` 里做 UTF-8 stdout/stderr 包装
- 机读输出优先单行 JSON - 机读输出优先单行 JSON
- 错误前缀建议统一 `ERROR:` - 错误前缀建议统一 `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 行数约束。

View File

@@ -29,6 +29,7 @@
- `SKILL.md` YAML slug vs [`constants.SKILL_SLUG`](../scripts/util/constants.py) - `SKILL.md` YAML slug vs [`constants.SKILL_SLUG`](../scripts/util/constants.py)
- SQLite 骨架:`task_logs` 创建幂等与仓储读写; - 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。 - **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并由档位变量放行 **原则:`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.ini` 存在且 `python_files` 只收集 `test_*.py` / `*_test.py`
- [ ] pytest **不会**误收集 `tests/` 下的 `.txt` / 日志 / 结果文件 - [ ] pytest **不会**误收集 `tests/` 下的 `.txt` / 日志 / 结果文件
- [ ] `python tests/run_tests.py -v` 全部通过 - [ ] `python tests/run_tests.py -v` 全部通过
- [ ] `scripts/**/*.py` 单文件 < 1000 行(`test_release_packaging_constraints.py`
- [ ] 文本文件为 UTF-8 without BOM`U+FEFF`(同上)
--- ---

View 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()