diff --git a/CHANGELOG.md b/CHANGELOG.md index f3cd9bc..9ae01b7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,11 @@ - 小节正文写用户能看懂的变化(新能力、修复、注意事项),避免堆砌内部实现细节 - 打 tag 前**必须**为即将发布的版本新增一节;找不到匹配小节时,本次发布不会更新 changelog 字段 +## 1.0.104 + +- 新增发布契约 POLICY-RELEASE-001:`release.ps1` 打 tag 前强制校验 Next tag = `SKILL.md` version = `SKILL_VERSION`,避免市场一直显示「更新」 +- 日常测试增加 `SKILL.md` 与 `SKILL_VERSION` 对齐;更新 DEVELOPMENT / POLICY_MATRIX / CHECKLIST A9-4 + ## 1.0.103 - 用户市场文档:说明/教程正文禁止与宿主页头、Tab 重复的文首一级标题;主分节统一用 `##` diff --git a/SKILL.md b/SKILL.md index 35b8f6e..2b7a159 100644 --- a/SKILL.md +++ b/SKILL.md @@ -1,7 +1,7 @@ --- name: 技能开发模板(通用业务版) description: "OpenClaw 通用业务技能开发模板,供复制后定制新业务 skill。定制步骤见 development/DEVELOPMENT.md。" -version: 1.0.103 +version: 1.0.104 author: 深圳匠厂科技有限公司 metadata: openclaw: diff --git a/development/CHECKLIST.md b/development/CHECKLIST.md index 5062f59..c6b2e07 100644 --- a/development/CHECKLIST.md +++ b/development/CHECKLIST.md @@ -187,7 +187,7 @@ checklist_version: 1.0 - [ ] A9-1 **P0** 仓库默认测试套件通过(执行技能约定的一键测试命令,如 `python tests/run_tests.py -v`) - [ ] A9-2 **P0** 不以「仅模拟档通过」作为交活依据(有真实档要求时必须真实跑通或书面阶段性豁免) - [ ] A9-3 **P0** **NEED_MANUAL** 入口定位表中定为「挂」的宿主入口(详情/Agent/工具栏/定时等)按声明抽测可通过;未在定位表中规划且未声明的入口可标 N/A(须引用定位表理由),**禁止**用「未声明」掩盖「从未做入口定位」 -- [ ] A9-4 **P1** 对外版本一致:详情、version、更新日志、发布标签能对上 +- [ ] A9-4 **P0** 对外版本一致(POLICY-RELEASE-001):`SKILL.md` version = `SKILL_VERSION` = 发布 git tag(无前缀);`release.ps1` 打 tag 前会硬校验,漏改不得发布;CHANGELOG 有对应小节 - [ ] A9-5 **P1** 开发者按平台规则能安装并测到该技能(若平台有开发者可见性要求) --- diff --git a/development/DEVELOPMENT.md b/development/DEVELOPMENT.md index 3be971a..f6bfd09 100644 --- a/development/DEVELOPMENT.md +++ b/development/DEVELOPMENT.md @@ -720,9 +720,15 @@ uses: client-jiangchang/jiangchang-platform-kit/.github/workflows/reusable-relea 在执行 `release.ps1` 之前,必须先在本地确认 `python tests/run_tests.py -v` 通过。测试不通过不得发起正式发布。 -**发布前必须**将 `SKILL.md` 的 `version:` 与 `scripts/util/constants.py` 的 `SKILL_VERSION` 调整为将打出的版本(例如下一 patch),再执行 `release.ps1`。`release.ps1` **不会**自动改这两个字段;漏改会造成 git tag 与技能版本号不一致。 +**POLICY-RELEASE-001(硬门禁)**:发布前必须将 `SKILL.md` 的 `version:`、`scripts/util/constants.py` 的 `SKILL_VERSION`、以及 `CHANGELOG.md` 对应小节,调整为**即将打出的**版本(与 `.\release.ps1 -DryRun` 打印的 `Next tag` 去掉 `v` 前缀一致)。`release.ps1` **不会**自动改这些字段;若三者与 Next tag 不一致,脚本在跑测试/打 tag **之前**直接失败,避免市场「最新版本」(git tag) 与安装包「当前版本」(本地 `SKILL.md`) 长期错位、技能市场一直显示「更新」。 -`release.ps1` 会在 **auto commit 之前**、推送 tag 之前,对**当前工作区**自动执行默认测试(`python tests/run_tests.py -v`);失败则中止 release,不会留下失败状态的自动提交。仅紧急排障可使用 `-SkipTests` 跳过(会打印黄色警告)。`-DryRun` 预览发布时**仍会实际运行**默认测试,便于在不打 tag 的情况下验证门禁。 +建议流程: + +1. `.\release.ps1 -DryRun` 查看 `Next tag`(例如 `v1.0.104`) +2. 将 `SKILL.md` / `SKILL_VERSION` / `CHANGELOG` 改为 `1.0.104` +3. 再执行正式 `.\release.ps1`(可用 `-AutoCommit`) + +`release.ps1` 会在 **auto commit 之前**、推送 tag 之前,对**当前工作区**自动执行默认测试(`python tests/run_tests.py -v`);失败则中止 release,不会留下失败状态的自动提交。仅紧急排障可使用 `-SkipTests` 跳过(会打印黄色警告)。`-DryRun` 预览发布时**仍会实际运行**版本对齐门禁与默认测试,便于在不打 tag 的情况下验证。 新增或调整开发规范时,须同步 [`development/POLICY_MATRIX.md`](POLICY_MATRIX.md) 与 [`tests/test_development_policy_guard.py`](../tests/test_development_policy_guard.py)(或注明由既有测试覆盖)。 diff --git a/development/POLICY_MATRIX.md b/development/POLICY_MATRIX.md index 84ed0d0..be0656f 100644 --- a/development/POLICY_MATRIX.md +++ b/development/POLICY_MATRIX.md @@ -24,6 +24,7 @@ | POLICY-RPA-006 | 浏览器 RPA 必须保留 account-manager 生命周期标准;所有样例账号选择路径都须返回非空 `profile_dir + lease_token`,指定数字账号 ID 不得退化为无租约 `account get` | development/ACCOUNT_MANAGER.md;development/ADAPTER.md §兄弟依赖;development/RPA.md §0.2 | hard | 检查标准文档关键标记;扫描真实/仿真 `account_client.py` 的指定 ID 带租约实现并禁止空租约赋值 | `tests/test_development_policy_guard.py::TestPolicyRpa006` | | POLICY-PACKAGING-001 | `scripts/**/*.py` 单文件 < 1000 行 | development/DEVELOPMENT.md §3.4 发布打包约束;development/RUNTIME.md §发布打包约束 | hard | 行数统计 | **已有测试覆盖**:`tests/test_release_packaging_constraints.py::test_scripts_py_files_under_pyarmor_line_limit` | | POLICY-PACKAGING-002 | 文本文件 UTF-8 without BOM,无 `U+FEFF` | development/DEVELOPMENT.md §3.4;development/RUNTIME.md §编码与输出 | hard | BOM / 解码检查 | **已有测试覆盖**:`tests/test_release_packaging_constraints.py::test_text_files_are_utf8_without_bom` | +| POLICY-RELEASE-001 | 发布 tag(去掉前缀)必须等于 `SKILL.md` 的 `version:` 且等于 `scripts/util/constants.py` 的 `SKILL_VERSION`;`release.ps1` 打 tag 前硬失败,不自动改写字段 | development/DEVELOPMENT.md §15 发布;宿主市场「最新版本」来自 tag、「当前版本」来自包内 `SKILL.md` | hard | `release.ps1` 门禁 + 日常版本字段对齐测试 | `release.ps1`(`Assert-SkillReleaseVersionAlignment`);`tests/test_development_policy_guard.py::TestPolicyRelease001`;`tests/test_skill_metadata.py::test_skill_version_alignment` | | POLICY-DOCS-001 | 本矩阵存在且包含上述全部 `policy_id` | 本次规范化约定 | hard | 解析 `development/POLICY_MATRIX.md` | `tests/test_development_policy_guard.py::TestPolicyDocs001` | | POLICY-LOGGING-001 | CLI 入口必须调用 `setup_skill_logging` | development/LOGGING.md;development/RUNTIME.md | hard | 扫描 `scripts/cli/app.py` 含 `setup_skill_logging` 与 logger info | `tests/test_development_policy_guard.py::TestPolicyLogging001` | | POLICY-LOGGING-002 | service 主任务入口必须使用 `get_skill_logger` | development/LOGGING.md;development/DEVELOPMENT.md | hard | 扫描 `scripts/service/task_service.py` | `tests/test_development_policy_guard.py::TestPolicyLogging002` | diff --git a/release.ps1 b/release.ps1 index 1923e57..db77b94 100644 --- a/release.ps1 +++ b/release.ps1 @@ -7,6 +7,10 @@ Encryption, ZIP packaging, and marketplace sync are done by CI (.github/workflows/release_skill.yaml → reusable-release-skill.yaml@main). + POLICY-RELEASE-001: before tagging, requires SKILL.md version and + scripts/util/constants.py SKILL_VERSION to equal the next tag (without prefix). + release.ps1 does not auto-edit those fields. + Requires: git, PowerShell 5+ After fetch, runs default tests against the current working tree (before auto-commit): @@ -162,6 +166,93 @@ function Get-NextTag { return "$TagPrefix$($latest.Major).$($latest.Minor).$([int]$latest.Patch + 1)" } +function Read-SkillMdVersion { + param([Parameter(Mandatory = $true)][string]$SkillRoot) + + $path = Join-Path $SkillRoot "SKILL.md" + if (-not (Test-Path -LiteralPath $path)) { + return $null + } + $text = Get-Content -LiteralPath $path -Raw -Encoding UTF8 -ErrorAction SilentlyContinue + if ([string]::IsNullOrWhiteSpace($text)) { + return $null + } + $m = [regex]::Match($text, '(?m)^version:\s*["'']?([0-9]+\.[0-9]+\.[0-9]+)["'']?\s*$') + if (-not $m.Success) { + return $null + } + return $m.Groups[1].Value.Trim() +} + +function Read-SkillConstantsVersion { + param([Parameter(Mandatory = $true)][string]$SkillRoot) + + $path = Join-Path $SkillRoot "scripts\util\constants.py" + if (-not (Test-Path -LiteralPath $path)) { + return $null + } + $text = Get-Content -LiteralPath $path -Raw -Encoding UTF8 -ErrorAction SilentlyContinue + if ([string]::IsNullOrWhiteSpace($text)) { + return $null + } + $m = [regex]::Match($text, '(?m)^SKILL_VERSION\s*=\s*["'']([0-9]+\.[0-9]+\.[0-9]+)["'']') + if (-not $m.Success) { + return $null + } + return $m.Groups[1].Value.Trim() +} + +# POLICY-RELEASE-001: marketplace latest (git tag) must equal package manifest versions. +function Assert-SkillReleaseVersionAlignment { + param( + [Parameter(Mandatory = $true)][string]$SkillRoot, + [Parameter(Mandatory = $true)][string]$NextTag, + [Parameter(Mandatory = $true)][string]$TagPrefix + ) + + $skillMdPath = Join-Path $SkillRoot "SKILL.md" + if (-not (Test-Path -LiteralPath $skillMdPath)) { + return + } + + $expected = $NextTag + if ($TagPrefix -and $expected.StartsWith($TagPrefix)) { + $expected = $expected.Substring($TagPrefix.Length) + } + $expected = $expected.Trim() + if ($expected -notmatch '^\d+\.\d+\.\d+$') { + throw "Release check failed (POLICY-RELEASE-001): cannot derive semver from Next tag '$NextTag'." + } + + $mdVersion = Read-SkillMdVersion -SkillRoot $SkillRoot + $constVersion = Read-SkillConstantsVersion -SkillRoot $SkillRoot + + $problems = @() + if ([string]::IsNullOrWhiteSpace($mdVersion)) { + $problems += "SKILL.md missing top-level version: X.Y.Z" + } + elseif ($mdVersion -ne $expected) { + $problems += "SKILL.md version=$mdVersion (want $expected)" + } + if ([string]::IsNullOrWhiteSpace($constVersion)) { + $problems += "scripts/util/constants.py missing SKILL_VERSION = `"X.Y.Z`"" + } + elseif ($constVersion -ne $expected) { + $problems += "SKILL_VERSION=$constVersion (want $expected)" + } + + if ($problems.Count -gt 0) { + throw ( + "Release aborted (POLICY-RELEASE-001): Next tag=$NextTag requires manifest version $expected, but " + + ($problems -join "; ") + + ". Bump SKILL.md version and scripts/util/constants.py SKILL_VERSION to $expected (and CHANGELOG), then re-run release.ps1. " + + "release.ps1 does not auto-edit these fields." + ) + } + + Write-Host "Version alignment OK: tag=$NextTag == SKILL.md=$mdVersion == SKILL_VERSION=$constVersion" -ForegroundColor DarkGray +} + function Assert-SkillReleasePackagingSources { param([Parameter(Mandatory = $true)][string]$SkillRoot) @@ -283,6 +374,15 @@ try { Assert-SkillReleasePackagingSources -SkillRoot $skillRoot } + $nextTag = Get-NextTag -TagPrefix $Prefix + Write-Host "Next tag: $nextTag" -ForegroundColor Green + Assert-SkillReleaseVersionAlignment -SkillRoot $skillRoot -NextTag $nextTag -TagPrefix $Prefix + + $existing = @(Get-GitOutput -GitArgs @("tag", "--list", $nextTag)) + if ($existing.Length -gt 0) { + throw "Tag already exists: $nextTag" + } + Invoke-DefaultTests -SkillRoot $skillRoot -Skip:$SkipTests Ensure-CleanOrAutoCommit -DoAutoCommit:$AutoCommit -NeedClean:$RequireClean -IsDryRun:$DryRun -Msg $CommitMessage @@ -304,14 +404,6 @@ try { } } - $nextTag = Get-NextTag -TagPrefix $Prefix - Write-Host "Next tag: $nextTag" -ForegroundColor Green - - $existing = @(Get-GitOutput -GitArgs @("tag", "--list", $nextTag)) - if ($existing.Length -gt 0) { - throw "Tag already exists: $nextTag" - } - if ($DryRun) { Write-Host "[DryRun] Would run: git tag -a $nextTag -F ($Message)" -ForegroundColor Yellow Write-Host "[DryRun] Would run: git push origin $nextTag" -ForegroundColor Yellow diff --git a/scripts/util/constants.py b/scripts/util/constants.py index 881a0ac..9a5f49b 100644 --- a/scripts/util/constants.py +++ b/scripts/util/constants.py @@ -1,6 +1,6 @@ """技能标识、版本与平台公共库约束(复制后请修改 slug/version/logger)。""" SKILL_SLUG = "your-skill-slug" -SKILL_VERSION = "1.0.103" +SKILL_VERSION = "1.0.104" LOG_LOGGER_NAME = "openclaw.skill.your_skill_slug" PLATFORM_KIT_MIN_VERSION = "1.2.11" diff --git a/tests/test_cli_smoke.py b/tests/test_cli_smoke.py index 61bb379..1a911db 100644 --- a/tests/test_cli_smoke.py +++ b/tests/test_cli_smoke.py @@ -14,7 +14,7 @@ from jiangchang_skill_core import config # scripts/ 已由 _support 注入 sys.path from cli.app import main -from util.constants import SKILL_SLUG +from util.constants import SKILL_SLUG, SKILL_VERSION class TestCliSmoke(unittest.TestCase): diff --git a/tests/test_development_policy_guard.py b/tests/test_development_policy_guard.py index e863fd3..1e0d4d1 100644 --- a/tests/test_development_policy_guard.py +++ b/tests/test_development_policy_guard.py @@ -40,6 +40,7 @@ POLICY_IDS = ( "POLICY-RPA-006", "POLICY-PACKAGING-001", "POLICY-PACKAGING-002", + "POLICY-RELEASE-001", "POLICY-DOCS-001", "POLICY-LOGGING-001", "POLICY-LOGGING-002", @@ -1530,6 +1531,44 @@ class TestPolicyDataSync001(unittest.TestCase): ) +class TestPolicyRelease001(unittest.TestCase): + def test_release_ps1_enforces_version_alignment(self) -> None: + skill_root = get_skill_root() + path = os.path.join(skill_root, "release.ps1") + self.assertTrue( + os.path.isfile(path), + msg=_policy_msg("POLICY-RELEASE-001", "release.ps1", "file missing"), + ) + text = _read_text(path) + self.assertIn( + "Assert-SkillReleaseVersionAlignment", + text, + msg=_policy_msg( + "POLICY-RELEASE-001", + "release.ps1", + "must define Assert-SkillReleaseVersionAlignment", + ), + ) + self.assertIn( + "POLICY-RELEASE-001", + text, + msg=_policy_msg( + "POLICY-RELEASE-001", + "release.ps1", + "must reference POLICY-RELEASE-001", + ), + ) + self.assertRegex( + text, + r"Assert-SkillReleaseVersionAlignment\s+-SkillRoot", + msg=_policy_msg( + "POLICY-RELEASE-001", + "release.ps1", + "must invoke Assert-SkillReleaseVersionAlignment before tagging", + ), + ) + + class TestPolicyDocs001(unittest.TestCase): def test_policy_matrix_exists_and_lists_policy_ids(self) -> None: skill_root = get_skill_root() diff --git a/tests/test_skill_metadata.py b/tests/test_skill_metadata.py index fa2d2ce..532b463 100644 --- a/tests/test_skill_metadata.py +++ b/tests/test_skill_metadata.py @@ -50,6 +50,26 @@ class TestSkillMetadata(unittest.TestCase): self.assertEqual(SKILL_SLUG, skill_dir) self.assertEqual(slug_md, skill_dir) + def test_skill_version_alignment(self) -> None: + """SKILL.md version 必须与 constants.SKILL_VERSION 一致(POLICY-RELEASE-001 日常侧)。""" + root = get_skill_root() + md_path = os.path.join(root, "SKILL.md") + with open(md_path, encoding="utf-8") as f: + md = f.read() + match = re.search(r"(?m)^version:\s*[\"']?([0-9]+\.[0-9]+\.[0-9]+)[\"']?\s*$", md) + self.assertIsNotNone(match, "SKILL.md must declare top-level semver version") + assert match is not None + md_version = match.group(1) + + from util.constants import SKILL_VERSION + + self.assertEqual( + md_version, + SKILL_VERSION, + "SKILL.md version 必须与 constants.SKILL_VERSION 一致(发布前再与 git tag 对齐)", + ) + self.assertRegex(SKILL_VERSION, r"^\d+\.\d+\.\d+$") + if __name__ == "__main__": unittest.main()