名称变更:2026-10-21 我核对时,windsurf.com 跳转到 devin.ai/desktop,docs.windsurf.com 跳转到 docs.devin.ai/desktop,产品文档里叫 Devin Desktop。Cascade 智能体还在,但新标签页默认用的是 Devin Local 智能体;文档说 Workflows 和 Memories 只适用于 Cascade(旧版智能体),Devin Local 用 Skills。配置目录优先用
.devin/,旧的.windsurf/仍会被读取。本文按当前文档写,界面细节以你安装的版本为准。
这篇把“发版前检查”做成可复用的流程:一个检查脚本
release_check.py(版本号三处一致、CHANGELOG 有当前版本、git 工作区干净、测试通过),一个 Skill(Devin Local 和 Cascade 都能用,模型可自动调用),一个 Workflow(Cascade 中用/release-check手动调用)。两者都要求 AI 只检查和起草,不打 tag、不推送。另写lint_custom.py检查技能和工作流文件。
先看清成品
- 检查脚本在示例项目上真实运行:干净仓库全部通过;改版本号后正确报出 3 项失败。
- 发现并修复:测试运行时生成的
__pycache__让 git 工作区变“脏”,第二次检查就失败。- lint_custom.py 在故意写错的目录里找出 5 个问题。4 项 pytest 通过。
合规提醒:用你自己的账号和授权的套餐,不共享账号。规则、工作流和 MCP 配置会把指令和工具交给 AI,不要写进密钥;公司代码先确认组织政策是否允许使用。AI 运行命令前要看清楚,不要开启“自动执行所有命令”。
运行说明:box 是 Linux 服务器,没有图形界面,我没有安装或运行 Devin Desktop / Windsurf 编辑器,所以没有验证编辑器实际加载这些文件的效果。真实运行过的是:本文的检查脚本、示例项目和 MCP 服务器(2026-10-21,UTC+8)。Workflow 文件的具体格式文档里没有给出示例(只说包含标题、描述和步骤),我用 frontmatter 写了 description;如果编辑器用“+ Workspace”生成的格式不同,以它为准。
做完之后你会得到什么?
| 成果 | 说明 |
|---|---|
demo/ |
示例项目 tinycalc |
demo/.devin/skills/release-check/ |
SKILL.md + release_check.py |
demo/.devin/workflows/release-check.md |
工作流 |
lint_custom.py |
检查技能和工作流 |
broken/ |
故意写错的例子 |
test_89b.py |
4 项测试 |
需要准备哪些环境?
| 项目 | 说明 |
|---|---|
| 编辑器 | Devin Desktop(原 Windsurf),从 https://windsurf.com 下载 |
| Python | 3.11 以上(用到 tomllib),pip install pytest |
| git | 检查工作区是否干净 |
关键事实
| 事实 | 来源 |
|---|---|
Workflows:.devin/workflows/*.md(或旧 .windsurf/workflows/),每个最多 12000 字符;只能手动用 /名称 调用,可以在一个工作流里调用另一个 |
官方 Workflows |
| Workflows 只适用于 Cascade;Devin Local 智能体不支持,可用 “Devin: Open Cascade Migration Wizard” 迁移成 Skills | 同上 |
Skills:.devin/skills/<名称>/SKILL.md,frontmatter 必填 name 和 description;默认只把 name 和 description 给模型,调用时才读全文和附带文件;也可以 @名称 手动调用 |
官方 Skills |
| name:1–64 字符,小写字母、数字、连字符,不能以连字符开头或结尾、不能连续两个连字符,必须与目录名相同;description 最多 1024 字符 | Agent Skills 规范 |
文件结构
89-windsurf-B-src/
├── demo/
│ ├── pyproject.toml
│ ├── CHANGELOG.md
│ ├── .gitignore
│ ├── src/tinycalc/__init__.py
│ ├── tests/test_calc.py
│ └── .devin/
│ ├── skills/release-check/
│ │ ├── SKILL.md
│ │ └── release_check.py
│ └── workflows/release-check.md
├── broken/
├── lint_custom.py
└── test_89b.py
第 1 步:Skill
---
name: release-check
description: 发版前检查 tinycalc:版本号是否一致、CHANGELOG 是否更新、测试是否通过。用户提到“发版”“发布新版本”“打 tag 前检查”时使用。
---
## 步骤
1. 在项目根目录运行 `python .devin/skills/release-check/release_check.py .`,把输出的 JSON 原样给我看。
2. 如果 `ok` 为 false:逐项说明哪一项没通过、可能的原因,**不要自己改版本号或 CHANGELOG**,等我确认。
3. 如果 `ok` 为 true:根据 CHANGELOG 中当前版本的条目,起草一段发布说明(新增 / 修复 / 变更)。
4. 到此为止。**不要**执行 `git tag`、`git push` 或任何发布命令,这些由我手动执行。
## 说明
- `git_clean` 为 null 表示当前目录不是 git 仓库,不算失败,但要提醒我。
第 2 步:检查脚本
"""发版前的机械检查:版本号三处一致、CHANGELOG 有当前版本、工作区干净、测试通过。只检查,不打 tag、不推送。"""
import json, os, re, subprocess, sys, tomllib
from pathlib import Path
def run(root):
root = Path(root); res = {}
ver = tomllib.loads((root / "pyproject.toml").read_text(encoding="utf-8"))["project"]["version"]
init = next((root / "src").glob("*/__init__.py"))
m = re.search(r'__version__\s*=\s*"([^"]+)"', init.read_text(encoding="utf-8"))
res["version"] = ver
res["version_match"] = bool(m and m.group(1) == ver)
res["changelog"] = f"## {ver}" in (root / "CHANGELOG.md").read_text(encoding="utf-8")
git = subprocess.run(["git", "status", "--porcelain"], cwd=root, capture_output=True, text=True)
res["git_clean"] = None if git.returncode else (git.stdout.strip() == "")
# 不写 .pyc:否则测试本身会让工作区变“脏”(实测踩过)
env = {**os.environ, "PYTHONDONTWRITEBYTECODE": "1"}
t = subprocess.run([sys.executable, "-m", "pytest", "-q", "-p", "no:cacheprovider"], cwd=root, capture_output=True, text=True, env=env)
res["tests"] = t.returncode == 0
res["tests_tail"] = (t.stdout.strip().splitlines() or [""])[-1]
res["ok"] = all(res[k] for k in ("version_match", "changelog", "tests")) and res["git_clean"] is not False
return res
if __name__ == "__main__":
r = run(sys.argv[1] if len(sys.argv) > 1 else ".")
print(json.dumps(r, ensure_ascii=False, indent=2))
sys.exit(0 if r["ok"] else 1)
第 3 步:Workflow
---
description: 发版前检查(手动用 /release-check 调用)
---
1. 运行 `python .devin/skills/release-check/release_check.py .`,把 JSON 结果原样贴出来。
2. 有任何一项没通过:列出失败项和建议的修改,停下来等我确认,不要自己修改。
3. 全部通过:根据 CHANGELOG 当前版本条目起草发布说明。
4. 不要执行 git tag、git push 或任何发布命令。
第 4 步:.gitignore
__pycache__/
*.pyc
.pytest_cache/
第 5 步:lint 脚本
"""检查工作流和技能文件。
工作流(docs.devin.ai/desktop/cascade/workflows):.devin/workflows/*.md(旧 .windsurf/workflows/),每个文件最多 12000 字符,只能手动 /名称 调用。
技能(.../skills 与 agentskills.io 规范):.devin/skills/<名称>/SKILL.md,frontmatter 必填 name 和 description;
name 1–64 个字符,只能小写字母、数字、连字符,不能以连字符开头/结尾、不能连续两个连字符,并且要和所在目录同名;description 1–1024 字符。"""
import re, sys
from pathlib import Path
NAME_RE = re.compile(r"^[a-z0-9]+(-[a-z0-9]+)*$")
def fm(text):
if not text.startswith("---\n") or "\n---" not in text[4:]:
return {}
end = text.index("\n---", 4)
return {k.strip(): v.strip() for k, v in (l.split(":", 1) for l in text[4:end].splitlines() if ":" in l)}
def lint(root):
root = Path(root); out = []
for f in sorted(list(root.glob(".devin/workflows/*.md")) + list(root.glob(".windsurf/workflows/*.md"))):
t = f.read_text(encoding="utf-8"); e = []
if len(t) > 12000: e.append(f"{len(t)} 字符,超过 12000")
if not NAME_RE.match(f.stem): e.append("文件名建议只用小写字母、数字和连字符")
if not re.search(r"^\s*\d+\.", t, re.M): e.append("没有编号步骤")
out.append((f.relative_to(root).as_posix(), "workflow", e))
for d in sorted(p for p in list(root.glob(".devin/skills/*")) + list(root.glob(".windsurf/skills/*")) if p.is_dir()):
e = []; s = d / "SKILL.md"
if not s.exists():
out.append((d.relative_to(root).as_posix(), "skill", ["缺少 SKILL.md"])); continue
m = fm(s.read_text(encoding="utf-8")); name = m.get("name", ""); desc = m.get("description", "")
if not name: e.append("缺少 name")
elif not (1 <= len(name) <= 64 and NAME_RE.match(name)): e.append(f"name={name!r} 不合规(小写字母/数字/单个连字符,1–64)")
elif name != d.name: e.append(f"name={name!r} 与目录名 {d.name!r} 不一致")
if not desc: e.append("缺少 description(模型靠它决定何时调用)")
elif len(desc) > 1024: e.append("description 超过 1024 字符")
out.append((s.relative_to(root).as_posix(), "skill", e))
return out
def main(argv=None):
root = (argv if argv is not None else sys.argv[1:] or ["."])[0]
res = lint(root); bad = 0
for p, kind, e in res:
print(f"{'✗' if e else '✓'} [{kind}] {p}")
for x in e: print(" ", x)
bad += len(e)
print(f"{len(res)} 项,{bad} 个问题"); return 1 if bad else 0
if __name__ == "__main__":
sys.exit(main())
第 6 步:测试
import importlib.util, subprocess, sys, shutil
from pathlib import Path
import lint_custom as L
spec = importlib.util.spec_from_file_location("rc", "demo/.devin/skills/release-check/release_check.py")
rc = importlib.util.module_from_spec(spec); spec.loader.exec_module(rc)
def test_lint_demo_clean():
assert all(not e for _, _, e in L.lint("demo"))
def test_lint_broken():
errs = {p: e for p, _, e in L.lint("broken")}
assert len(errs) == 4 and sum(len(e) for e in errs.values()) == 5
def git(cwd, *a):
subprocess.run(["git", "-c", "user.email=t@example.com", "-c", "user.name=t", *a], cwd=cwd, check=True, capture_output=True)
def test_release_check_clean_repo_stays_clean(tmp_path):
d = tmp_path / "p"; shutil.copytree("demo", d)
git(d, "init", "-q"); git(d, "add", "-A"); git(d, "commit", "-qm", "init")
assert rc.run(d)["ok"] and rc.run(d)["git_clean"] # 连续两次:测试不能把工作区弄脏
def test_release_check_version_bump_detected(tmp_path):
d = tmp_path / "p"; shutil.copytree("demo", d)
p = d / "pyproject.toml"; p.write_text(p.read_text().replace("0.3.0", "0.4.0"))
r = rc.run(d)
assert not r["ok"] and not r["version_match"] and not r["changelog"] and r["git_clean"] is None
第 7 步:运行
# 2026-10-21 22:01 UTC+8,示例项目(未初始化 git)
$ python .devin/skills/release-check/release_check.py .
{
"version": "0.3.0",
"version_match": true,
"changelog": true,
"git_clean": null,
"tests": true,
"tests_tail": "2 passed in 0.01s",
"ok": true
}
# 初始化 git 并提交后,连续检查两次(修复后)
"git_clean": true,
"ok": true
"git_clean": true,
"ok": true
# 把 pyproject.toml 的版本改成 0.4.0
$ python .devin/skills/release-check/release_check.py .
{
"version": "0.4.0",
"version_match": false,
"changelog": false,
"git_clean": false,
"tests": true,
"tests_tail": "2 passed in 0.00s",
"ok": false
}
$ echo $?
1
$ python lint_custom.py demo
✓ [workflow] .devin/workflows/release-check.md
✓ [skill] .devin/skills/release-check/SKILL.md
2 项,0 个问题
$ python lint_custom.py broken
✗ [workflow] .devin/workflows/Run Tests.md
文件名建议只用小写字母、数字和连字符
没有编号步骤
✗ [skill] .devin/skills/Deploy/SKILL.md
name='Deploy' 不合规(小写字母/数字/单个连字符,1–64)
✗ [skill] .devin/skills/empty
缺少 SKILL.md
✗ [skill] .devin/skills/review/SKILL.md
name='code-review' 与目录名 'review' 不一致
4 项,5 个问题
$ python -m pytest -q test_89b.py
4 passed
怎么检查结果?
- 在编辑器的 Customizations 面板里,Skills 下应出现 release-check,Workflows 下应出现 release-check。
- 对 AI 说“准备发 0.3.0”,看它是否自动调用 skill、贴出 JSON,并在最后停下而不是打 tag。
- 用 Cascade 时输入
/release-check调用工作流。
调试示例
测试把 git 工作区弄“脏”了。 第一次在初始化好的 git 仓库里测试时,连续运行两次检查,第二次 git_clean 变成 false:
$ git ls-files | rg pycache
src/tinycalc/__pycache__/__init__.cpython-313.pyc
tests/__pycache__/test_calc.cpython-313-pytest-9.1.1.pyc
$ git status --porcelain
M src/tinycalc/__pycache__/__init__.cpython-313.pyc
M tests/__pycache__/test_calc.cpython-313-pytest-9.1.1.pyc
原因有两层:我先在示例目录里跑过测试,__pycache__ 被 git add -A 提交了;之后每次检查都会运行测试,重新生成 .pyc,工作区就变了。修复:
- 示例项目加
.gitignore(__pycache__/、*.pyc、.pytest_cache/); - 检查脚本运行 pytest 时设置
PYTHONDONTWRITEBYTECODE=1,即使别的项目没有 .gitignore 也不会写 .pyc。
测试 test_release_check_clean_repo_stays_clean 连续调用两次,确认第二次仍然干净。这类问题如果让 AI 自己处理,它很可能“顺手”把 .pyc 提交或者删掉,所以 SKILL.md 里写明了:检查不通过时只报告,不自己修改。
常见问题
| 现象 | 原因 | 处理 |
|---|---|---|
| /release-check 不出现 | 用的是 Devin Local 智能体 | Workflows 只适用于 Cascade,改用 Skill |
| Skill 不被自动调用 | description 太笼统 | 写清楚做什么、什么时候用 |
| Skill 不显示 | name 与目录名不一致或含大写 | 用 lint_custom.py 检查 |
| git_clean 一直 false | 构建产物被提交 | 加 .gitignore,清理已提交的产物 |
模板规则
- 机械检查写成脚本,AI 只负责运行和解读。
- 明确写出不能做的事(tag、push)。
- Skill 的 description 要具体。
- 检查本身不能改变仓库状态。
项目资源
| 用途 | 地址 |
|---|---|
| Devin Desktop 文档(原 docs.windsurf.com) | https://docs.windsurf.com |
| Workflows(官方) | https://docs.devin.ai/desktop/cascade/workflows |
| Skills(官方) | https://docs.devin.ai/desktop/cascade/skills |
| Agent Skills 规范 | https://agentskills.io/specification |
| Devin Local 智能体(官方) | https://docs.devin.ai/desktop/devin-local |
把“怎么做”写成脚本,把“什么时候停”写进说明。