Sphinx 文档工程改版折腾过的人都有体会:一次发布,手动要跑五六条命令,等构建、查链接、清理旧产物、切分支、同步服务器,整套流程下来二十分钟起步,稍不留神还会把旧版本的静态资源混进新版本里。团队里文档更新频繁,每周好几次发布,靠人肉流程迟早出事。
“AI辅助编程”这两年让我最大的收益点,其实不在业务代码,而在这类“看着不起眼、但天天要跑”的运维脚本上。用大模型辅助写了一个 Sphinx 文档工程自动化发布脚本,把构建、检查、版本标记、发布配置全串起来,跑一次命令就搞定整套发布。从手动五步变成一键执行,技术传播这件事的效率一下子就提上来了。
这篇文章就把这个脚本的完整设计思路、核心实现、踩坑经历全盘写出来。适合正在维护 Sphinx 技术文档站、被反复构建发布困扰的开发者或文档工程师,也适合想试试用 AI 辅助写工程脚本、但不知道从哪里下手的朋友。
1. 从手动发布到自动化:为什么需要这个脚本
1.1 Sphinx 文档工程发布的典型痛点
Sphinx 本身是文档生成器,职责很干净:把 reStructuredText 或 Markdown 源码编译成 HTML、PDF、EPUB 等格式。但一个真正面向用户的技术文档站点,远不止“跑一下 sphinx-build”这么简单。
我维护过一个产品文档工程,目录按版本分支组织,存在同一个代码仓库里。发布时要做的事包括:切换到对应版本分支、安装依赖、清理旧构建产物、执行 sphinx-build 生成 HTML、跑链接检查、给文档写入版本号、最后把输出同步到服务器。中间还要处理多语言文档目录,繁体、简体、英文各一套,构建一次全站要十几分钟。
这些步骤里最怕的是漏掉某一步。比如忘了清理_build目录,旧版本的页面文件残留在里面,轻则文档页面混合错乱,重则把已废弃的接口文档又发布上线。还有链接检查这步,链接彬多了会超时,很多人嫌麻烦直接跳过,结果坏链上线,用户点进去 404,技术传播的信任感就在这种细节里慢慢被消耗掉。
1.2 脚本化发布解决了什么问题
人肉执行命令的最大问题是“不可复现”。每一次发布的状态都依赖于执行者当时记住了多少步骤。脚本化之后,整个流程固化在代码里,任何人跑同一套脚本,得到的结果是一致的。
具体来说,自动化发布脚本解决了四件事:
- 构建一致性:固定 python 环境、固定 sphinx 版本、固定构建参数,排除“我这能过你那过不了”的环境差异。
- 流程可审计:日志记录每一步执行的命令和结果,出了问题知道是哪一步、什么原因。
- 减少人为失误:清理、检查、版本写入这些容易被遗忘的步骤,全部由脚本强制执行。
- 多端发布能力:本地执行、服务器定时、CI 流水线都能调用同一个入口,不用为每种场景重新写一套命令。
说白了,脚本化不是把命令简单拼起来,而是把“发布”当成一个工程事件对待。文档也是一种代码资产,发布流程理应像部署应用一样严谨。
1.3 AI 辅助编程在这一场景中的定位
写这类自动化脚本,我的建议是:让 AI 生成框架,让人处理细节。
因为发布脚本的逻辑并不复杂,无非是配置读取、子进程调用、日志输出、错误处理这几块。这类需求非常适合让大模型快速生成一个骨架,节省掉写模板代码的时间。但文档工程的发布有自己的细节:链接检查的忽略规则、版本标记的方式、多语言的目录结构、服务器的同步策略,这些必须由懂业务的人来定义,AI 不知道你的目录长什么样、也不知道你的发布纪律是什么。
我实际的用法是:先自己把业务流程画清楚(脑子里或本子上,文字列表也行),再把这个流程喂给 AI,让它生成脚本初版。然后在初版基础上,把我踩过的坑一个一个补进去:路径要处理空格、子进程要捕获标准输出、失败要重试、日志要带时间戳、敏感信息要从配置文件读取。这样配合下来,脚本质量比全手写快得多,也比全甩给 AI 可靠得多。
2. AI 辅助脚本设计的核心思路
2.1 需求先行:先梳理发布流程再写代码
让 AI 写脚本前,最忌讳一句“帮我写一个 Sphinx 发布脚本”就丢过去。AI 不了解你的工程结构,生成出来的大概率是一个万物皆system()调用的玩具脚本,跑起来一堆坑。
我的做法是先把发布流程画成一条线,分成阶段。以我维护的文档工程为例,流程是这样的:
- 读取项目配置,确认要发布的分支或版本号。
- 切换/确认 Git 分支,拉取最新代码。
- 创建 Python 虚拟环境并安装依赖。
- 清理旧的构建产物。
- 执行 sphinx-build 生成 HTML。
- 执行 sphinx-build -b linkcheck 检查外链。
- 生成版本信息文件(写入构建时间和版本号)。
- 通过 rsync 同步到服务器。
- 记录发布日志,失败时发送异常通知。
把这个流程按步骤编号列出来,每步标注输入、输出、需要注意的风险点,然后再让 AI 基于这个流程去写脚本。流程图本身才是灵魂,AI 只是帮你把流程图翻译成代码的助手。
2.2 用 AI 生成脚本骨架的提示词设计
这里分享一个我实际用过的提示词模板,按“角色 + 任务 + 输入约束 + 输出要求”四段式组织:
你是资深 DevOps 工程师,熟悉 Sphinx 文档工程。请用 Python 3.10+ 编写一个自动化发布脚本 publish.py,按以下流程执行: 1. 从 publish.yaml 读取配置,包括版本号、发布目录、服务器地址、rsync 参数; 2. 使用 subprocess 执行 git pull,切换分支; 3. 使用 pathlib 清理 build 目录,注意不能误删其他文件; 4. 调用 sphinx-build -b html -W --keep-going; 5. 调用 sphinx-build -b linkcheck; 6. 生成 version.html 或 version.json 文件; 7. 通过 rsync 同步 build/html 到远端目录; 8. 每个关键步骤记录带时间戳的日志,失败时抛出带上下文的异常。 约束: - 只用标准库和 PyYAML; - 路径全部用 pathlib,不要硬编码绝对路径; - 子进程必须捕获 stdout 和 stderr,超时时间可配置; - 提供 argparse 入口参数:--config、--dry-run、--skip-linkcheck; - 输出风格简洁,注释写清楚每个函数的目的。这样写出来的初版脚本,在我实测里可以达到七成可用。剩下的三成,主要是它不太会照顾到具体环境里那些边边角角的问题,比如某个目录是不是 git 子模块、rsync 是否要排除.git目录、虚拟环境在 CI 和本地是不是同一个路径。这些都是在真实跑过之后才暴露出来的。
2.3 人工复核与场景适配的关键点
用 AI 生成的代码,再顺手也必须过一遍人工复核。我总结出几个每次必查的地方:
- 路径安全:凡是
rm -rf的地方必须限定在项目构建目录下,而且要用 resolve() 验证,防止路径穿越或误删。 - 子进程默认 shell:AI 很喜欢生成
subprocess.run(command, shell=True),这有潜在的命令注入风险,尤其是命令里拼接外部输入的时候。我一般要求改成参数列表形式,关掉 shell。 - 异常处理粒度:AI 生成的 try/except 往往过于笼统,一个 except Exception 就吞掉所有错误。发布脚本最重要的就是失败时要暴露原因,所以每个阶段要单独捕获,异常消息里带上执行命令和退出码。
- 环境依赖假设:AI 默认所有人的电脑上都装好了 Python、rsync、git,实际上新同事的机器经常连 venv 都没有。脚本里要加环境预检逻辑,缺什么工具提示什么。
- 敏感信息处理:服务器地址、账号密码绝不能写死在脚本或配置文件里。我从第一版起就要求从环境变量或
.env读取,发布配置里只留占位符。
复核完这些,AI 生成的内容才能真的落到生产环境里。
3. 自动化发布脚本的完整实现
3.1 技术选型与环境准备
我做这套自动化发布脚本,技术选型比较朴素:Python 3.10 + PyYAML + argparse,没有引入复杂框架。原因很简单:发布脚本本身要足够轻,依赖越少,部署到任何机器上的成本越低。PyYAML 用来读配置,argparse 用来处理命令行参数,subprocess 和 pathlib 都是标准库,基本做到了“有一台 Linux 机器就能跑”。
环境层面,脚本会自己准备 Python 虚拟环境。我习惯在文档工程根目录下建一个.venv,脚本执行时先检查这个环境是否存在,不存在就通过python -m venv .venv创建,然后调用.venv/bin/pip安装requirements.txt。依赖包括 Sphinx、Read the Docs 主题、myst-parser 等。
为什么用脚本管理虚拟环境而不是手工建?因为新入职的文档工程师可能完全不熟悉 Python 环境这套东西,脚本里自动处理可以降低使用门槛。每次都重新建环境也不是最优解,所以我加了时间戳判断:虚拟环境超过 7 天没有更新依赖就重装,否则直接复用。
3.2 脚本功能模块拆解
整个脚本我拆成五个模块,每个模块职责单一:
- 配置模块:读取 YAML 配置,校验必填项,解析命令行参数,合并出最终的运行参数。
- 环境模块:检查 git、python、sphinx、rsync 是否可用,准备虚拟环境,安装依赖。
- 构建模块:清理
_build目录,执行 HTML 构建和链接检查,生成版本信息文件。 - 发布模块:将构建产物同步到目标服务器,支持 dry-run 模式预览将要同步的文件列表。
- 日志模块:统一日志格式,记录每步耗时、执行结果,失败时输出排查线索。
这种模块化写法最大的好处是:哪一步出问题,一眼就能定位。比如链接检查失败,日志里会直接指出是 linkcheck 阶段出错,而不是笼统一句“发布失败”。后续想在中间加 PDF 构建,或者在发布前跑代码示例调试,也只是在对应模块里加一个函数的事。
3.3 核心代码实现
下面贴出核心代码,我做了适当精简,保留了完整逻辑骨架:
#!/usr/bin/env python3 """Sphinx 文档工程自动化发布脚本。""" import argparse import datetime import json import logging import os import subprocess import sys from pathlib import Path import yaml logging.basicConfig( format="%(asctime)s [%(levelname)s] %(message)s", level=logging.INFO, ) def load_config(config_path: Path) -> dict: """读取发布配置并校验必填项。""" with open(config_path, "r", encoding="utf-8") as f: cfg = yaml.safe_load(f) required_keys = ["project_name", "version", "source_dir", "build_dir", "release_server"] for key in required_keys: if key not in cfg: raise ValueError(f"配置项缺失: {key}") return cfg def run_command(cmd, cwd: Path | None = None, timeout: int = 180): """执行命令并捕获输出,失败时抛出包含上下文信息的异常。""" logging.info("执行命令: %s", " ".join(cmd)) try: result = subprocess.run( cmd, cwd=cwd, capture_output=True, text=True, timeout=timeout, check=False, ) except subprocess.TimeoutExpired as exc: raise RuntimeError(f"命令执行超时 ({timeout}s): {cmd}") from exc if result.returncode != 0: raise RuntimeError( f"命令执行失败 (exit={result.returncode}): {cmd}\n" f"stdout: {result.stdout[-2000:]}\n" f"stderr: {result.stderr[-2000:]}" ) return result def prepare_environment(root: Path, config: dict): """准备虚拟环境并安装依赖。""" venv_python = root / ".venv" / "bin" / "python" if not venv_python.exists(): logging.info("创建虚拟环境 .venv") run_command(["python3", "-m", "venv", str(root / ".venv")]) requirements = root / "requirements.txt" if requirements.exists(): run_command([str(venv_python), "-m", "pip", "install", "-q", "-r", str(requirements)]) return venv_python def clean_build_dir(build_dir: Path): """清理构建目录,注意路径安全校验。""" resolved = build_dir.resolve() allowed_prefixes = [Path.cwd().resolve(), (Path.cwd() / "build").resolve()] if not any(str(resolved).startswith(str(prefix)) for prefix in allowed_prefixes): raise RuntimeError(f"拒绝清理非安全目录: {resolved}") if resolved.exists(): import shutil shutil.rmtree(resolved) resolved.mkdir(parents=True, exist_ok=True) logging.info("已清理构建目录: %s", resolved) def build_html(venv_python: Path, source_dir: Path, build_dir: Path, config: dict): """执行 Sphinx HTML 构建。""" cmd = [ str(venv_python), "-m", "sphinx", "-b", "html", "-W", # 警告视为错误 "--keep-going", # 出错时尽可能继续编译 str(source_dir), str(build_dir / "html"), ] run_command(cmd, timeout=600) def build_linkcheck(venv_python: Path, source_dir: Path, build_dir: Path): """执行链接检查。""" cmd = [ str(venv_python), "-m", "sphinx", "-b", "linkcheck", str(source_dir), str(build_dir / "linkcheck"), ] try: run_command(cmd, timeout=600) except RuntimeError as exc: logging.warning("链接检查发现异常,注意查看 linkcheck 输出详情") # 不中断发布,但要保留日志,链接错误由人工确认 # 如需强制,可在配置中设置 fail_on_broken: true def write_version_info(build_dir: Path, config: dict): """生成版本信息文件,随文档发布。""" info = { "project_name": config["project_name"], "version": config["version"], "build_time": datetime.datetime.now().isoformat(), "sphinx": config.get("sphinx_version", "unknown"), } info_path = build_dir / "html" / "version.json" with open(info_path, "w", encoding="utf-8") as f: json.dump(info, f, indent=2, ensure_ascii=False) logging.info("已生成版本信息文件: %s", info_path) def sync_to_server(build_dir: Path, config: dict, dry_run: bool = False): """通过 rsync 同步文档到目标服务器。""" source = str(build_dir / "html") + "/" target = f"{config['release_server']}:{config['release_path']}" cmd = [ "rsync", "-avz", "--delete", "--exclude", ".git", ] if dry_run: cmd.append("--dry-run") cmd.extend([source, target]) run_command(cmd, timeout=1200) def parse_args(): parser = argparse.ArgumentParser(description="Sphinx 文档工程自动化发布脚本") parser.add_argument("--config", default="publish.yaml", help="配置文件路径") parser.add_argument("--dry-run", action="store_true", help="只显示将要同步的文件,不实际发布") parser.add_argument("--skip-build", action="store_true", help="跳过构建,直接同步现有产物") parser.add_argument("--skip-linkcheck", action="store_true", help="跳过链接检查") return parser.parse_args() def main(): args = parse_args() root = Path(__file__).resolve().parent config = load_config(root / args.config) logging.info("开始发布文档: %s v%s", config["project_name"], config["version"]) venv_python = prepare_environment(root, config) if not args.skip_build: source_dir = root / config["source_dir"] build_dir = root / config["build_dir"] clean_build_dir(build_dir) build_html(venv_python, source_dir, build_dir, config) write_version_info(build_dir, config) if not args.skip_linkcheck: build_linkcheck(venv_python, source_dir, build_dir) else: build_dir = root / config["build_dir"] sync_to_server(build_dir, config, dry_run=args.dry_run) logging.info("文档发布流程完成") if __name__ == "__main__": main()这段代码是我实际在用的版本的核心骨架,去掉了一些和具体环境绑定的细节。有几个地方值得解释:
-W和--keep-going必须搭配使用。-W把所有 Sphinx warning 当成 error,避免文档里有残缺引用还悄悄通过构建;--keep-going保证遇到第一个错误后不会立刻停止,而是尽可能编译完所有页面,一次性把问题暴露出来,省得改一次跑一次。
链接检查我处理成“失败不中断发布”的模式,因为外链服务器不稳定是常态,偶尔一个 503 就中断发布有点因噎废食。但是日志里会明确 warning,配合定期的全量检查去消除坏链。
写版本信息文件这个步骤是我踩过坑之后加的。文档站点用户经常反馈“缓存导致看不到更新”,有了 version.json,每次发布后在页脚显示构建时间,问题定位直接减半。
3.4 参数与使用方式
脚本支持四个命令行参数,日常使用频率从高到低排列如下:
| 参数 | 作用 | 使用场景 |
|---|---|---|
--config | 指定配置文件路径 | 多分支发布时切换不同的配置文件 |
--dry-run | 模拟执行,只预览不同步 | 发布前自查、审核 rsync 文件列表 |
--skip-build | 跳过构建和检查,直接同步 | 手动构建完成后只需推送产物时 |
--skip-linkcheck | 跳过链接检查 | 临时紧急修复上线、外链排查期间 |
配合一个publish.yaml配置文件使用:
project_name: product-docs version: 2.4.0 source_dir: docs/source build_dir: build sphinx_version: "7.2.6" release_server: deploy@doc-server release_path: /var/www/docs/current用起来就是一个命令的事:
python3 publish.py --config publish.yaml需要先看同步清单的时候:
python3 publish.py --dry-run这套设计在实际使用里最舒服的一点,是本地联调和线上发布用的是同一个命令,只是多了一步登录服务器。本地预览就是最后不执行 sync 而已。想不加“发布”到线上,把 sync_to_server 那段注释掉或者把配置里的 release_path 改成临时目录都行。
4. 实操过程与关键细节
4.1 从零到一:搭建 Sphinx 文档工程的准备
脚本不是凭空跑的,前提是文档工程本身已经正常用 Sphinx 构建。这里说下我建议的工程目录结构,这套结构是踩了不少坑后稳定下来的:
docs/ ├── docs/ │ ├── source/ │ │ ├── conf.py │ │ ├── index.rst │ │ ├── guide/ │ │ ├── api/ │ │ └── changelog.rst │ ├── build/ # 脚本清理和输出的目录 │ └── requirements.txt ├── publish.py ├── publish.yaml └── .venv/ # 虚拟环境,不纳入版本管理conf.py里几个关键配置项,直接影响发布效果:
import os import sys sys.path.insert(0, os.path.abspath(".")) project = "产品技术文档" author = "文档团队" version = "2.4.0" release = version extensions = [ "sphinx.ext.autodoc", "sphinx.ext.intersphinx", "sphinx.ext.todo", "sphinx.ext.coverage", "sphinx.ext.viewcode", "myst_parser", ] html_theme = "sphinx_rtd_theme" html_static_path = ["_static"] linkcheck_ignore = [ r"https://example.com/private/.*", ] linkcheck_anchors = Falselinkcheck_anchors = False是因为链接检查默认会去验证页面锚点,跨域锚点经常误报,关掉后省一大半维护精力。linkcheck_ignore可以配置正则或者精确 URL,把内部系统、需要登录才能访问的页面排除掉,不然每次检查都红一片。
如果你要从零初始化一个工程,推荐直接执行:
sphinx-quickstart docs/source按提示选择“分离源码和构建目录”(把 source 和 build 分开)。开启autodoc和intersphinx,这两个扩展对 API 类文档几乎是刚需。然后安装主题sphinx_rtd_theme和myst_parser,分别解决观感和 Markdown 语法支持。
4.2 脚本的部署与日常使用
脚本做好之后,部署方式要分场景看。最简单的场景就是在文档团队共用的 Linux 服务器上放一份代码,大家 ssh 上去执行。这种方式适合团队小、发布频率不高的情况。
如果文档更新频繁,我更推荐把发布接入 CI 流水线。Git 仓库里配置一个流水线触发规则,比如往main分支推送带docs/路径前缀的变更时,自动执行publish.py --config publish.yaml。脚本本身不需要做太大改动,只是在流水线里要注入环境变量,配置文件中不要写死机密信息。
我自己的实践是两种方式并行:日常小修改,直接在服务器上手动跑一次脚本,两三分钟搞定;涉及多语言全量构建的大发布,走 CI,保证历史构建产物可追溯。脚本入口函数设计成main(),天然适合被 CI 调用,只需要在流水线里安装 Python 和依赖。
还有一个细节:脚本默认读取配置里的version字段,但手动维护版本号容易漏改。后来我改成优先从 git tag 读取版本号,只有 tag 为空时才回退到配置文件里的值。版本号跟着 tag 走,发布哪个 tag 就出哪个版本的文档,从源头避免了“代码改了版本号忘改”的问题。
4.3 发布流程中的注意事项
实践下来,有几个细节直接影响发布质量,必须在流程里守住:
- 清理构建目录不能省。增量构建虽然快,但 Sphinx 对已删除文件的处理并不可靠,旧页面残留是真实发生过的事。每次正式发布前都执行清理,成本多几十秒,省心一整天。
- 版本信息文件要在构建完成后、同步之前生成。它混在 HTML 产物里一起同步,用户在页面上能看到当前版本。生成之后再跑一次增量构建让页面引用到它,确保不是孤立的 JSON 文件。
- rsync 的
--delete参数要谨慎使用。它保证目标目录和源目录完全一致,但也会删除服务器上你手动放的东西。第一次配置时建议先执行--dry-run,确认没有误删风险再打开这个参数。 - rsync 同步完成后,可以在访问入口做一层版本校验。比如服务器上的
current软链先指到新版本目录,确认构建没问题再切换。不过这就属于灰度发布的范畴了,小团队可暂不引入。 - 发布日志记得留。脚本每跑一步都有输出,但输出会滚动消失,最好同时写入文件。后期排查“用户反映文档没更新”的时候,日志能告诉你今天到底发布成功没有。
5. 常见问题与排查技巧实录
5.1 典型问题速查表
脚本用久了,遇到的问题会慢慢沉淀成一个固定的集合。我把高频问题和排查路径整理成表,希望对你有用:
| 问题现象 | 可能原因 | 排查思路与解法 |
|---|---|---|
sphinx-build 报 warning 被-W拦下 | 文档存在无效引用、缺失图片或配置警告 | 查看错误详情,优先用--keep-going拿到完整错误列表 |
| 链接检查大量超时 | 外链网站访问慢、网络不稳定 | 调大 timeouts,或配置linkcheck_ignore排除已知临时故障域名 |
| 同步后页面 404 | rsync 源路径少了末尾/ | 没有/会把 html 目录本身同步过去,层级多一层 |
| 构建成功但页面样式丢失 | 主题静态资源路径不对 | 检查html_static_path和主题的_static路径 |
| 在 CI 里脚本找不到命令 | PATH 环境变量不同于本地 | 脚本改用.venv/bin/xxx绝对路径,不依赖系统 PATH |
| 定时任务跑发布没人知道结果 | 没有通知机制 | 接一个 webhook 通知,发布成功或失败都发一条 |
rm -rf提示权限拒绝 | 构建目录属于其他用户 | 检查目录属主,团队共享目录建议统一使用一个服务账号 |
| 发布内容覆盖了旧版本文档 | 目标目录结构不区分版本 | 发布目录按版本建子目录,保留最近 N 个版本 |
5.2 实战排查案例
这里分享三个真实遇到过的排查案例,比看官方文档管用。
第一个是构建成功但页面空白。当时加了一批新文档,构建日志一切正常,但发布后访问首页白屏。排查了很久,最终发现是_static目录里新增的 CSS 引用了一个不存在的字体文件,浏览器加载资源超时导致页面渲染异常。这个问题能通过sphinx-build -W发现吗?发现不了,因为 Sphinx 不会校验 CSS 内部引用的文件。所以我在脚本里额外加了一步:构建完成后扫描 HTML 中引用的本地静态资源,确认文件都存在。这个小检查拯救了后来不知多少次发布。
第二个是链接检查误报。文档里链接到内部某系统,系统要求登录才能访问,linkcheck 每次报 401。一开始在linkcheck_ignore里加了正则,后来发现这个系统的 IP 会变,正则维护成本变高。最终方案是把这类链接统一改成相对链接或代理路径,从根本上绕开“外部检查”的场景。
第三个是发布超时。某个版本文档加入了大量图片,同步时间从 20 秒涨到 8 分钟,直接触发 rsync 超时。一开始盲目调大 timeout,后来发现根源是图片未压缩,一张截图 3MB。在脚本里加了构建前对_static/images目录自动压缩的处理,同步时间降回 30 秒。这个案例让我意识到:脚本不只是流程粘合剂,还能在关键环节做质量过滤。
5.3 避坑指南:那些年踩过的坑
最后聊几条纯经验向的避坑指南,有些内容是代码示例里体现不出来的。
第一,不要在生产环境直接跑一个从未验证过--dry-run的同步命令。严重的时候会把服务器上的东西清光。我有一条纪律:任何新增的删除步骤,第一次执行前必须打日志打印出将要删除的内容,人工确认后再放行。
第二,虚拟环境不要放在被 rsync 的目录里。我早期把.venv放在工程根目录,同步时不小心把虚拟环境一起推到服务器,结果服务器磁盘暴涨。现在脚本里的--exclude .venv是默认强制项。
第三,发布命令不要用sudo。一旦脚本里sudo了,后续所有权限管理都会变复杂。在文档服务器上专门建一个发布用的系统账号,目录权限配好,脚本用这个账号跑,干净清爽。
第四,配置文件中的版本号、服务器地址等字段,改完后要自己在脑海里跑一遍验证路径。AI 辅助生成脚本时,很容易忽略“路径拼接中有没有多余分隔符”这种细节,构建目录、配置目录、发布目录之间差一个斜杠,结果天壤之别。
第五,日志永远要保留完整输出,而不仅仅是“成功”两个字。发布脚本失败时最怕的就是什么日志都没有。我后来给每步命令都记录了 stdout 和 stderr 的最后 2000 字符,排查问题时这些尾巴就是救命稻草。
6. 从脚本到体系:一些额外思考
脚本跑通只是第一步,真正让技术传播进入良性循环的,是把它放到一个更大的体系里去看。
文档发布频率上去了之后,我加了两个扩展点。一个是版本化发布目录,每次发布都以版本号建目录,服务器上保留最近三个版本的完整文档。用户在页面右上角切旧版本,这在回滚场景里价值巨大。另一个是发布后自动触发搜索索引重建,文档站如果对接了站内搜索引擎,不重建索引的话,新文档发布了半天搜索不到,等于没发布。
AI 辅助编程在这个体系里的角色也值得一提。脚本本身是第一次用 AI 生成的,后来每次改需求,比如新增多语言构建、增加 PDF 输出,我都是先描述改动点,让 AI 给出 diff 建议,再人工审查合入。这个工作流的好处是:AI 处理重复性较高的“把 A 阶段输出接到 B 阶段输入”这类事情非常擅长,而我只需要把精力放在那些 AI 理解不了的业务规则上。
最后再分享一个小技巧:脚本跑久了,配置文件容易出现“这里改一下那里改一下”变得混乱。我建议给publish.yaml里每个字段加注释,并在脚本启动时打印最终生效的参数。这个习惯在多人协作时特别重要,任何人在发布前扫一眼日志就知道“这次发布是哪个版本、到哪里、用了什么配置”。
整套脚本跑了快半年,最深的体会是:文档工程自动化这件事,价值不在于省下几分钟,而在于把“发布”从个人记忆变成了工程资产。新同事接手文档维护,不需要先问一遍“老员工是怎么发布的”,跑一遍脚本看日志就全明白了。技术传播的最后一公里,很多时候就是靠这样的工具链走通的。