WordPress 插件发布的几条经验:版本号、门禁、SVN 与 CloudFront 缓存

AI Chat for Amazon Bedrock 的发布流程:三处版本号一致性检查、跑不了就报 skip 的门禁、SVN 发布,以及同版本重新上传后 CloudFront 按 ?ver= 继续给旧 CSS 的教训。

本站运行的聊天插件 AI Chat for Amazon Bedrock 是一个开源 WordPress 插件,源码在 GitHub,发布渠道是 wordpress.org 的 SVN。它的发布流程是一个脚本化的“门禁”,外加几步手工的 SVN 操作。本文整理这套流程里可以复用的部分,以及一个和 CloudFront 有关的教训:同一个版本号重新上传安装包,站点会继续提供旧的 CSS 和 JS。

版本号要在三处同时改

一个 WordPress 插件的版本至少出现在三个地方,任何一处漏改都会出问题:

  • 主文件头部的 Version:,WordPress 用它判断已安装的版本;
  • 代码里的版本常量(这个插件是 AI_CHAT_BEDROCK_VERSION),入队脚本和样式时作为 ?ver= 使用;
  • readme.txt 的 Stable tag,wordpress.org 用 trunk 里这一行决定向用户分发哪个 tag。

此外还要在 readme.txt 里加一条 changelog 和一条 Upgrade Notice,并重新生成翻译模板 .pot。项目的 bin/prerelease.sh 会读出这三个版本号,要求完全一致,并检查 changelog 里有没有 = x.y.z = 这一节,缺了就判失败。

发布前的门禁脚本

bin/prerelease.sh 按“最快失败”的顺序跑以下检查,本地和 CI 跑的是同一个脚本:

  • 每个 PHP 文件都能通过 php -l;
  • 上面说的版本一致性和 changelog;
  • readme 的长度限制:简短描述不超过 150 字符,每条升级提示不超过 300 字符,描述加隐私政策少于 2500 词;
  • tests/ 下的全部测试套件;
  • 用固定版本的 phpcs 跑 WordPress 编码规范;
  • 提供 WP-CLI 命令时,跑官方 Plugin Check;
  • 从 .distignore 算出将要发布的文件列表,并扫描其中形似凭证的内容。

README 特别说明了两条设计:

  • 跑不了的检查报 skip,不报 ok。开发期间门禁出过两次“假通过”:phpcs 没能启动却被当成没有问题;凭证扫描在门禁实际调用的模式下根本没执行。之后专门写了 tests/tooling.php:把项目复制到临时目录,每次故意破坏一处,断言对应的检查在 FAIL 行报错。
  • .distignore 是唯一的排除清单。打包脚本 bin/build-package.py 读它,门禁也问打包脚本“会发什么”。原因是以前有三份手写清单互相不一致,一个新加的工具目录被算进了审计范围。

打包脚本拒绝打包含 AWS 访问密钥、SVN 令牌或私钥特征的文件,打完输出版本、文件数、字节数和 SHA-256。CI(GitHub Actions)在 PHP 7.4 和 8.3 上各跑一遍门禁,再单独跑工具测试、构建并校验安装包,并在真实的 WordPress 上跑 Plugin Check。7.4 是插件头部声明的最低版本。

wordpress.org 的 SVN 发布

门禁通过、安装包构建好之后,发布是一步单独的手工操作,脚本不碰 wordpress.org。README 里的步骤是:

  1. 把安装包的内容复制到 SVN 工作副本的 trunk/,提交;
  2. 从 trunk 复制出新 tag 并提交,即 WordPress 手册推荐的 svn cp trunk tags/x.y.z;
  3. 确认插件目录页显示了新版本,并且 downloads.wordpress.org 上的包和你构建的内容一致。

WordPress 手册提醒:每次向 SVN 推送,都会重建所有版本的 zip,所以更新有时要过最多 6 小时才会出现。截图也要跟着界面走:assets/ 里的截图必须是当前界面的真实截图,界面变了就重拍。

同一版本号重新上传:CloudFront 继续给旧文件

WordPress 入队脚本和样式时,把版本号作为查询参数拼到 URL 上,例如 …/admin/css/ai-chat-bedrock-admin.css?ver=1.47.6。这是 wp_enqueue_style() 的 $ver 参数,本来就是为了让缓存失效。本站的 CloudFront 也缓存插件的脚本和样式,缓存键包含查询字符串。实测同一个静态资源用两个不同的 ver 值请求,第一次都是 Miss;同一个 ver 值再请求一次就是 Hit。源站响应里没有 Cache-Control,缓存多久由分发上该缓存行为的默认 TTL 决定。

正常升级不受影响,因为版本号变了,URL 也就变了。问题出在 1.47.6 上。本站通过上传 zip 的方式升级插件,先上传了一次 1.47.6。之后仓库里又合入一个只改 CSS 的修复:表格行按钮里给屏幕阅读器用的文字是绝对定位的,没有被外层滚动容器裁剪,在 390px 宽的手机上把 MCP 页面撑到了 548px。这次修复没有改版本号。重新上传后,PHP 已经是新的,但 URL 还是 ?ver=1.47.6,CloudFront 按缓存键继续返回第一次缓存的旧 CSS,要等缓存过期才会换成新的。wordpress.org 上的 tags/1.47.6 里已经包含这个修复,这个问题只出在本站“同版本重新上传”的操作上。

避免的办法按推荐顺序:

  1. 改了任何发布出去的文件,就升一个补丁版本号。这和 AWS 文档的建议一致:频繁更新的文件优先用版本化的文件名,而不是靠失效。
  2. 非要复用版本号时,对插件路径做一次 CloudFront 失效:
    aws cloudfront create-invalidation --distribution-id <分发 ID> \
      --paths "/wp-content/plugins/<插件目录>/*"
  3. 检查缓存策略是否真的把查询字符串放进了缓存键。托管策略 CachingOptimized 不包含查询字符串,默认 TTL 是 24 小时。用它缓存 wp-content 时,连正常的版本升级都会拿到旧文件。

局限 / 注意

  • 浏览器自己的缓存与 CloudFront 无关。清掉 CDN 缓存之后,个别访客可能仍会看到旧文件,版本号变更同样能解决这一层。
  • 无障碍检查(bin/audit-accessibility.py、bin/audit-frontend.py)需要驱动真实浏览器,是手工步骤,不在门禁里。按 README 的记录,它们发现过静态分析发现不了的问题,例如一次找出了二十个没有可访问名称的控件。

链接