车东 Che, Dong

Wed 21 October, 2026

08:37 docs(plan): 订正 v3.7.9–3.7.12 的日期,并标注 11/12 是回填 tag » Recent Commits to phpman:master
docs(plan): 订正 v3.7.9–3.7.12 的日期,并标注 11/12 是回填 tag

`### v3.7.9–3.7.12` 的日期原写 2026-06-11 → **06-14**,但 tag 实际只到 06-13
(v3.7.9 打于 06-11 07:04,v3.7.12 打于 06-13 14:06)。

同时把这一段最容易绕人的地方写清楚:**v3.7.11 与 v3.7.12 是回填的 tag,
编号顺序与提交顺序相反。**

  v3.7.13–v3.7.19   2026-06-11 15:30–17:02   一口气打完
  v3.7.11 / v3.7.12 2026-06-13 01:56 / 14:06 两天后才补打

拓扑上 **v3.7.13 的提交是 v3.7.12 提交的祖先**,实际提交顺序是
`13 → 14 → 15 → 16 → 17 → 18 → 19 → 11 → 12`,中间还有一个
`Merge remote: resolve v3.7.11 version conflict` 的合并提交 —— 当时就撞过版本号。
所以这一段不能按 `git log` 的先后读版本先后,要按 tag 读。

起因是核对 v3.7.12 这个 tag 的来历时发现的:tag 本身是人打的
(tagger=chedong,2026-06-13 14:06:34),它指向的提交 4ab3aea
(man page fallback cheat.sh → man7.org)署名是 Claude Opus 4.7。

纯文档改动,不动代码,不涉及渲染输出。

Co-Authored-By: Claude code 2.1.304 with deepseek-flash <noreply@taotoken.net>
01:38 fix(cache): 超时写短 TTL 负行,同名不再被无限重复触发 20s 扫描 (#248) » Recent Commits to phpman:master
fix(cache): 超时写短 TTL 负行,同名不再被无限重复触发 20s 扫描 (#248)

`cacheOrSearchFallback()` 的两条路径不对称:

  兜底有正文  → 写 not_found 行(TTL 24h)→ 下次命中负缓存,不重跑 ✓
  兜底超时    → **什么都不写**              → 每次全量重跑 20 秒 ✗

而且 `cacheOrExecute()` 只在 `trim($content) !== ""` 时写行,所以超时之后库里
**一行都没有** —— 这就是「同名可被无限重复触发」的来源。生产实测同一个名字在
2 秒到 42 秒内被反复问(NullProvider 在 01:55:50 与 01:55:52 各超时一次,IPv…
一天 10 次),每次都付满 20 秒;几个并发就把 FastCGI 池打满 —— 2026-10-20
01:54–01:55 那 36 个 503 就是这么来的。

原注释的理由(24h 会把瞬时失败冻成一天)是对的,但「完全不写」是另一个极端。
短 TTL 落在中间:

- 新增 `PHPMAN_CACHE_TTL_TIMEOUT`(默认 600s)
- `PageCache::set()` 加可选 `?int $ttl = null`,null = 维持现有推导,调用点不受影响
- 超时路径写一条空的短 TTL 负行,**但只在没有现存行时写**

最后那个「只在没有现存行时写」是关键:`set()` 的 UPSERT 会刷新 updated_at 与 ttl,
每次都写的话持续爬取下 TTL 被不断续期,等于永不重试 —— 又回到「瞬时失败被冻住」。
写一次、到期再写,TTL 才是「距第一次超时 10 分钟」而不是「距上次请求 10 分钟」。

不复用新的 `status` 值:CHECK 是 `status IN ('found','not_found')`,SQLite 不能
ALTER 一个 CHECK,要改就得重建表。复用 not_found + 不同 TTL 行为完全一样且零迁移。
代价是超时派生的行与真正的「不存在」在库里分不出来,需要时再加 reason 列。

测试跟着改:原来那两条断言钉的正是旧行为(不写行、下次重试),按设计变更重写,
并新增三条 —— 短 TTL 确实更短、命中不重写行(把行手工做旧后验证 updated_at 未变)、
过期后确实重跑。720 passed / 0 failed。

Co-Authored-By: Claude code 2.1.287 with deepseek-flash <noreply@taotoken.net>
00:25 feat(logs): 埋点加 bytes 触发条件,大响应不再对诊断隐形(#249) » Recent Commits to phpman:master
feat(logs): 埋点加 bytes 触发条件,大响应不再对诊断隐形(#249)

#249 的 500 丢正文那批请求是**大**,不是**慢**:实测生产上被截断的正文 77-81 KB、
完整 157-646 KB,而抽样的 6 个 500 **全是热缓存命中、渲染 ~150ms** —— 一个 150ms
的渲染永远跨不过 500ms 的耗时阈值,所以它们对诊断**结构性不可见**:

    mf 行 133 条 / 机器格式请求 3,102 次 = 4.3%

新增 `PHPMAN_MF_LOG_BYTES`(默认 65536),与 `PHPMAN_MF_LOG_MS` **取或** ——
「慢 **或** 大」都记一行。于是每个大响应都留下带 `bytes=` 的记录,拿它和 access log
的 `%b` 比对即可离线判定投递是否完整。

判据的边界写在了 config 注释里:`%b` 是**发出**的字节,所以 gzip 过的响应(JSON 超过
PHPMAN_GZIP_MIN_BYTES 且客户端带 Accept-Encoding: gzip)合法地更小 —— 这个比对只对
**未压缩**的响应有结论性。

顺带把 `phpman.config.php.example` 里缺失的遥测配置补齐(MS / BYTES / FILE 三项)。

实现时改了一处**位置参数**:`phpManMachineRenderLine()` 原来 `$now` 在第 8 位,把
`$thresholdBytes` 插到它前面会静默改掉 11 处测试调用的语义。改成具名参数 `now:` ——
对以后的重排免疫。

新增 8 条断言覆盖:大而快必须记、小而快不记、小而慢仍按耗时记(OR 不是 AND)、
两个阈值都为 0 时全关、size 阈值 0 = 退回只看耗时、`bytes=` 是未压缩长度、
正好等于阈值也记。677 passed / 0 failed。

Co-Authored-By: Claude code 2.1.287 with deepseek-flash <noreply@taotoken.net>

Tue 20 October, 2026

22:06 feat(logcheck): 5xx 按族分开计数,并把窗口范围写进输出 (#249 路线 1) » Recent Commits to phpman:master
feat(logcheck): 5xx 按族分开计数,并把窗口范围写进输出 (#249 路线 1)

原来这一节只有 `grep -c '" 5[0-9][0-9] '` 一个数字。一行 5xx 说不出自己是哪种
5xx —— 「客户端放弃大响应」(EPIPE) 和「进程被杀」从 access log 看完全一样,
缺的那一半在 server error log 里(一行 `Broken pipe` 带着客户端 IP 和分钟)。
而且那个数字不说自己看了多大范围:2026-10-20 它在有 38 个 503 的日志上报了
"none",因为 503 全在 `LOGCHECK_RECENT_LINES` 窗口之外。

改成一次 awk 双文件遍历(error log 用 BEGIN + getline 读,access log 走 stdin):

    window: last 40000 lines, 20/Oct/2026:03:37:52 → 20/Oct/2026:22:03:18
    5xx in window: 9
      + body, Broken pipe in server log (EPIPE family): 0
      + body, no Broken pipe match:                     4
      zero-byte body:                                   5
      pairing source: no Broken pipe lines in .../error.log — EPIPE=0 proves nothing here

两族相加等于总数(不会漏掉既非 EPIPE 又带 body 的那类)。两个日志时间戳格式
不同(`Tue Oct 20 15:02:19 2026` vs `20/Oct/2026:15:02:19 -0700`),awk 里统一
成 MMDD HH:MM 再按 (MMDD HH:MM, IP) 配对。

`pairing source` 那行不是装饰:两个日志各按各的节奏轮转,error log 已经轮转出
窗口时,EPIPE=0 说的是「证据过期了」而不是「没有客户端放弃」。这次在生产上
跑出来就是这一种 —— 没有它,读者会把 0 读成结论。

写测试时踩到两个只会静默出错的坑,都在注释里留了记录:
  - `mon[p[1]]` 用了星期("Tue")去查月份表 ⇒ 月份恒为空、key 全变成 "00 <day>";
    正确是 p[2]=月 / p[3]=日 / p[4]=HH:MM。配错时计数不报错,只是永远配不上。
  - error log 写的是 `IP:port`,access log 是裸地址 ⇒ 不减掉端口号则 key 永不命中。

`(70007)` APR_TIMEUP 是不同 errno、不同族,故意不收进关联集合,测试里钉住了
这一点(否则那个 503 会被算进 EPIPE 族)。

顺带修 Makefile:注释一直写着 `LOGCHECK_DAYS / LOGCHECK_RECENT_LINES` 可调窗口,
但两个变量从没传进 ssh,实际永远是默认值 —— 加窗口范围这件事本身就需要能调它。

测试:新增 test/unit/test_logcheck_families.php(21 条,已注册进 run_all.php),
用构造日志跑真实脚本,覆盖两族分界、IP 配对(同分钟不同 IP 不能算命中)、
70007 排除、窗口收窄不谎报 none、空/缺失 error log 不崩、始终 exit 0。

验证:php test/run_all.php 707 passed, 0 failed;make test 通过;
对生产实跑,窗口与分族计数均正确(20-Oct 全天 9 个 5xx:带 body 4 / 0 字节 5,
且全部是 HTTP/2.0)。

Co-Authored-By: Claude code 2.1.304 with deepseek-flash <noreply@taotoken.net>
08:20 fix(ri): 未命中不再跑第二遍相同命令;空兜底页也说人话 (#252) » Recent Commits to phpman:master
fix(ri): 未命中不再跑第二遍相同命令;空兜底页也说人话 (#252)

一、去掉重复的子进程

getRiPage() 和 getRiSearchPage() 跑的是**同一条命令**(`ri <参数>`),空值
判据也**完全一样**:

  getRiPage:       if ($return_code !== 0 || count($lines) === 0) return "";
  getRiSearchPage: if ($return_code !== 0 || count($lines) === 0) return "";

所以能走到兜底,就说明这条命令刚刚已经返回过空 —— 再跑一遍只可能还是空,
代价是又一个 ~2 秒。线上实测:ri 404 花 2.2–2.7 秒产出一个 31 字节的页面。

miss 分支现在传一个返回空串的生成器(负缓存行照写,下一次请求仍是瞬时),
不再 shell 出去。getRiSearchPage() 本身保留 —— ?mode=search 的级联在三处
调它,那里不是重复。

二、空兜底页给说明

和 pydoc 同样的问题:空兜底以前渲染成一个光秃秃的标题。

共用的信封逻辑抽到 src/util.php 的 noMatchNotice(),pydocNoMatchNotice()
和 riNoMatchNotice() 是两个只负责措辞的薄包装。

不 bump RENDERER_VERSION:和 pydoc 那半一样,说明是在写缓存**之后**加的,
负缓存行里仍然是空正文。

测试:test_exec_timeout.php 加 6 条(ri 的 markdown/html 形状、json 信封的
mode 字段是 ri 而不是 pydoc、results 空数组、parameter、note)。全套
686 passed / 0 failed。

Refs: #252
Co-Authored-By: Claude code 2.1.292 with deepseek-flash <noreply@taotoken.net>
04:52 fix(pydoc): 兜底无匹配时给出说明,不再只剩一个标题 (#252) » Recent Commits to phpman:master
fix(pydoc): 兜底无匹配时给出说明,不再只剩一个标题 (#252)

空兜底以前渲染成一个光秃秃的标题(markdown 45 字节),json 更是**完全空的
响应体**(0 字节)—— 两者都读作「这页坏了」,而不是「没匹配到」。而且这页
现在来自一枚 24 小时的负缓存行,所以会一直那样。

pydocNoMatchNotice() 按格式补上:

- markdown:引用块
- html:段落
- json / mcp:仍是真信封 {mode, parameter, results: [], note} —— 保留机器
  可读的空 results,不把结构换成散文

说明里还点出**为什么类名/函数名搜不到** —— 这正是 #252 里被误判成回归的
那一点:`pydoc3 -k` 拿搜索词去比模块名与**模块的一行摘要**,不比完整
docstring。`BaseHTTPRequestHandler` 是 http.server 里的类,而那个模块的摘要
就是 "HTTP server classes.",所以它从来就搜不到。

放在 phpMan.php 的 pydoc miss 分支里,而不是 getPydocSearchPage() 里面 ——
?mode=search 的级联用它的空返回值当「没有 pydoc 段落」的信号,不能受影响。

刻意不 bump RENDERER_VERSION:说明是在写缓存**之后**加的,负缓存行里仍然
是空正文,措辞每次请求现算 —— 以后改文案不需要作废缓存。

测试:test_exec_timeout.php 加 11 条(四种格式的形状、json/mcp 信封字段、
说明里点出摘要匹配这件事)。全套 680 passed / 0 failed。

Refs: #252
Co-Authored-By: Claude code 2.1.292 with deepseek-flash <noreply@taotoken.net>

Mon 19 October, 2026

23:06 fix(pydoc): 兜底不再扫 dotted 名字;超时不再被缓存成 24 小时 404 (#247) » Recent Commits to phpman:master
fix(pydoc): 兜底不再扫 dotted 名字;超时不再被缓存成 24 小时 404 (#247)

三件事,都出自同一次排查。

一、dotted 名字不再扫描

`pydoc3 -k` 无论匹配与否都要 10–14 秒 —— 它为了拿摘要把路径上每个模块都
import 一遍,所以「找不到」和「19 条结果」一样贵。2026-10-20 实测:
database 14.11s / 19 条,json 13.00s / 91 条,zzz_nonexistent_module_xyz
12.47s / **0 条**。

线上 1,600 个 pydoc 404 里 **86.9% 是 dotted 全路径**(sos.report.plugins.kafka
这类),其中 1,336 个返回空正文、72 个有列表 —— 而 dotted 名字真正存在时是
以页面形式服务的(sos.report.plugins.kafka 返回 200),根本走不到这个兜底。
所以那次扫描几乎全是白付。

pydocSearchWorthScanning() 现在对含点的一律跳过、立刻返回。裸关键词
(datab、DataBase)照旧扫描 —— 那正是扫描擅长的事:它连模块**摘要**一起匹配,
所以 database 能找到 grp / pwd / spwd。

二、扫描有了自己的超时(PHPMAN_PYDOC_K_TIMEOUT,默认 20s)

全局 15s 正好压在 10–14s 上,同一个查询会因服务器负载给出不同结果:
/pydoc/DataBase 那次跑进了(缓存下来),/pydoc/database 在 15.27s 被杀、
落到空页。phpManExec() 现在接受按调用点的超时;放宽**全局**会为一条命令
削弱所有渲染命令的槽保证(#248),所以只覆盖这一个调用点。

三、超时不再被当成「不存在」缓存 24 小时(复审指出)

timeout(1) 的退出码 124 意思是「没能确定」,不是「不存在」。但它以空正文的
形式流到 cacheOrSearchFallback(),被写成一枚 TTL 24 小时的负缓存行 ——
/pydoc/BaseHTTPRequestHandler(真实类,pydoc3 -k 本应解出 http.server)
就此稳定 404。兜底生成器现在可以返回 null 表示「没能确定」:请求照常应答,
但不写负缓存行,下一次请求还有机会重试。

RENDERER_VERSION 2 → 3:dotted 名字的旧负缓存行里存着旧正文,而负缓存行
是「无论带什么都复用」,不 bump 就会在整个 24h TTL 里继续吐旧列表。

顺带修了复审提到的一处措辞:PageCache::fetchRow() 里「Check TTL expiry for
not_found entries」读起来像 found 行不会过期,实际两者都按 ttl 过期。

测试:test_exec_timeout.php 加 7 条(pydocSearchWorthScanning 的 dotted/裸词/
大小写/空白),test_negative_cache_body.php 加 5 条(null 兜底不写负缓存行、
下一次重试、真答案照常缓存)。全套 669 passed / 0 failed。

Refs: #247
Co-Authored-By: Claude code 2.1.292 with deepseek-flash <noreply@taotoken.net>
18:29 fix(logs): mf 遥测改用独立文件,错误日志恢复成错误日志 (#251) » Recent Commits to phpman:master
fix(logs): mf 遥测改用独立文件,错误日志恢复成错误日志 (#251)

#244 的渲染遥测走 error_log(),而 error_log() 的目标就是 phpman_error.log,
于是那个文件不再是错误日志:报告时 8,786 行、8,786 行是 mf、错误 0 行。

代价不是「不整洁」。make logcheck 是 make release 末尾自动跑的部署后检查,
它报的就是这个文件,所以无论站点在干什么它都显示「每天一两千条 entries、
fatals 全 0」;而它的 top message types 只读最后 2000 行,一次真实的错误
爆发会被 mf 行挤出视图。2026-10-19 正是如此:一个 TypeError 回归让每次缓存
未命中都 500、持续九小时、写了 3,071 条 fatal,而 logcheck 的 fatals 列
全程是 0。

改动:

- 新增 phpManLogMachine(),写 PHPMAN_MF_LOG_FILE
  (~/.phpman/logs/phpman_mf.log),LOCK_EX 追加;沿用 error_log() 自己的
  [日期] 框架,两个文件仍可直接对比;写失败静默吞掉 —— 遥测绝不该有能力
  弄坏一个请求。phpManLogMachineRender() 改调它。
- PHPMAN_MF_LOG_FILE 用 !defined() 包起来,测试才能指向临时文件。
- cli/log-rotate.sh 轮转并裁剪两个日志,各自只匹配自己的轮转名模式
  (坏 glob 仍然删不到别的东西);「other files」一节两个都排除。
- cli/logcheck.sh 单列一节报遥测文件,量级可见但不再被当成错误读。
- test/unit/test_mf_render_log.php 钉住这次拆分:遥测行不得进入
  error_log()(新增 3 条断言)。

不 bump RENDERER_VERSION:渲染输出一个字节没变。

验证:make test 通过;php test/run_all.php 657 passed, 0 failed;
log-rotate.sh 用两个临时日志跑过 dry-run 与实跑,各自轮转/裁剪正确,
无关文件未被动。

Refs: #251
Co-Authored-By: Claude code 2.1.292 with deepseek-flash <noreply@taotoken.net>
01:37 fix(render): phpManExec 的 $rc 收窄成必填 int,12 个调用点全线 500 (#248) » Recent Commits to phpman:master
fix(render): phpManExec 的 $rc 收窄成必填 int,12 个调用点全线 500 (#248)

#248 把裸 exec() 换成 phpManExec() 时,第三个参数从 exec() 的无类型/可选/可空
收成了必填的 `int`。exec() 那边本来就允许省略、允许 null,所以调用点不初始化
`$return_code` 一直是合法的 —— source_perldoc / source_man / source_pydoc /
source_ri 共 12 处就是这么写的。收窄之后它们全部抛:

  TypeError: phpManExec(): Argument #3 ($rc) must be of type int, null given
    thrown in src/exec.php on line 71

当时只有 source_info.php($exitCode = 0)和 source_search.php($aproposRc = 0)
跟着补了初始化,另外 4 个文件漏了。

生产表现是**每次页面缓存未命中都 500**:cacheOrExecute() 命中缓存时直接返回、
根本不进渲染函数,所以只有长尾页面(clock_nanosleep、threads::joinable、
DocumentatiMoose::...)挂掉 —— 它们正是没被缓存的那批。访问日志最近 2000 行
305 个 5xx,与「长尾未命中」吻合。

修法是把 exec() 的行为原样接过来:

  function phpManExec(string $cmd, array &$lines, ?int &$rc = null): bool

函数内部第 73 行立刻就 `$rc = 0`,所以调用点拿到的永远是 int,`$rc === 0`
之类的比较不受影响。一处改动覆盖全部 12 个调用点,且以后新调用点不会再踩。
(函数 docblock 一直声称「同样的签名与语义」—— 这次才算真的成立。)

本地对照验证(同一份探针,四个真实函数):

  修前  getPerldocPage / getManPage / getPydocPage / getRiPage
        → 四者全部 TypeError,分别指向 source_*.php 的第一处调用
  修后  → 0 / 29265 / 0 / 2403 字节,全部正常返回

回归测试补在 test/unit/test_exec_timeout.php:新用例**故意不初始化 $rc**,
断言不抛 TypeError。原有的 execInFreshProcess() 自己写了 `$rc=0`,所以整个套件
在 bug 期间是绿的 —— 这正是它溜到生产的原因。已验证该用例在旧签名下失败
(13 passed, 1 failed)、在新签名下通过(17 passed, 0 failed)。

不需要善后:cacheOrExecute() 的 $generator() 没有 try/catch,抛异常时什么都没写,
这些 500 从未被缓存过,修好后会正常渲染并落盘。也没有改渲染输出,故不动
RENDERER_VERSION。

验证:make test 通过;php test/run_all.php 654 passed, 0 failed。

Co-Authored-By: Claude code 2.1.292 with deepseek-flash <noreply@taotoken.net>

Sun 18 October, 2026

20:24 fix(render): 给所有文档渲染子进程加上界(PHPMAN_CMD_TIMEOUT,默认 15s)(#248 #247) » Recent Commits to phpman:master
fix(render): 给所有文档渲染子进程加上界(PHPMAN_CMD_TIMEOUT,默认 15s)(#248 #247)

生产上 `/man/DCB-DCBX/markdown` 跑 **36.4 秒**、`/pydoc/local/markdown` 29.6 秒、
`/pydoc/FTP/markdown` 30.5 秒,而响应体分别只有 34 / 415 / 756 字节。在 mod_fcgid 的
固定进程池下,一次这样的请求就把一个槽占住半分钟 —— 几个并发就能让池饱和,其余所有人
一起吃 503。13 处 exec() 原来**一个超时都没有**。

- 新增 `src/exec.php`:`phpManExec()` / `phpManExecString()`,用 `timeout(1)` 包住命令;
  探测一次并缓存(只看 /usr/bin 与 /bin 的固定路径,不为它再 fork 一次 `command -v`),
  探测不到就退回裸 exec —— macOS 没有 timeout(1),本地开发不受影响
- `PHPMAN_CMD_TIMEOUT` 默认 **15s**,不是 10s:实测 `pydoc3 -k json` 本身就要 9.5 秒,
  10s 会把正常路径也切掉。mf 遥测显示 p50=799ms / p99=4.09s,上界切的是病态尾部
- 14 处调用点改用它(man 2 / pydoc 3 / ri 3 / perldoc 4 / info 2 + 1 shell_exec / apropos 1)

**实现时踩到一个真 bug,值得记下来**:`timeout N <命令>` 只约束**第一个简单命令**。
`timeout 1 sleep 30; echo never` 会被 shell 解析成 `(timeout 1 sleep 30); echo never`
—— sleep 被杀掉后 `echo never` 照跑。而 source_perldoc.php 的命令本身就是管道
(`perldoc -l … | head -1 | tr … | xargs …`),不包起来等于没上界。第一版在 macOS 上
测不出来(没有 timeout(1),全部走裸 exec 分支),拿到服务器上一跑 8 个断言全红。
修法:经 `/bin/sh -c` 包一层(这也正是 PHP 的 exec() 在 Unix 上用的形式)。

#247 的降级路径:`getPydocSearchPage()` 在 `pydoc3 -k` 超时或无结果时退到 FTS
(`pydocSearchLinesFromFts()` 复用 searchFtsBySource 的 json 形态再映射回行)。
这是**降级不是替代** —— 索引里的描述是常量 `'Python 3 module'`,替代会丢真描述;
但有名字好过挂 20 秒然后报错。

`test/unit/test_info_valid_files.php` 跟着修了一处:它原来只 require 一个 source 文件,
而 source 文件本来就依赖加载顺序(h()、formatManPerlDoc() 等),之前能过是偶然。

测试 `test/unit/test_exec_timeout.php`(18 断言)在**两端都跑**:macOS 上 13 passed
(切断类断言跳过并显式说明),服务器上 18 passed —— 切断、退出码 124、无 timeout(1)
时退回裸 exec、`PHPMAN_CMD_TIMEOUT=0` 不限制,都覆盖到了。全套 650 passed / 0 failed。

Co-Authored-By: Claude code 2.1.287 with deepseek-flash <noreply@taotoken.net>
04:14 fix(cache): 负缓存行一律复用 —— 空正文是「搜过了、没结果」,不是「没缓存」(#247) » Recent Commits to phpman:master
03:22 fix(cache): 负缓存带上兜底正文,pydoc 未命中不再每次重跑 10s 扫描 (#247) » Recent Commits to phpman:master

Fri 16 October, 2026

03:49 refactor(cli): log-analyze.sh 参数化,项目里不再有站点专属配置 » Recent Commits to phpman:master
03:08 fix(release): RELEASE_ID 恢复净化 —— 它会被远端 shell 重新解析 (#250) » Recent Commits to phpman:master
02:52 docs: 发布流程文档同步到新的版本号方案,并归档实施计划 (#250) » Recent Commits to phpman:master
02:31 feat(release): 版本号以 origin 的 tag 为准,打 tag 并入 make release (#250) » Recent Commits to phpman:master
02:05 feat(staging): 版本串改为 staging-<sha>[-dirty],不再冒充发布版本 (#250) » Recent Commits to phpman:master
02:02 feat(deploy): release-precheck.sh — 发布前的三条守卫 (#250) » Recent Commits to phpman:master
01:58 feat(deploy): next-version.sh — 版本号以远端 tag 为权威来源 (#250) » Recent Commits to phpman:master
01:39 docs: 发布版本号完整性设计(#250) » Recent Commits to phpman:master