CI:移除本仓非主分支构建能力 —— 分支 push 会推 runtime-base:latest 并污染正式镜像(主 issue xm/yuvu#10) #1

Open
opened 2026-10-01 16:09:29 +08:00 by xm · 3 comments
Collaborator

关联

主 issue:xm/yuvu#10(P1)——本 issue 是该根因在本仓的落地项,优先级降为 P2,是否可关闭以主 issue 的验收口径为准。

仓名注记:本仓在 Forgejo 上曾用名 xm/matchsubtitles,现改名为 xm/matchsubtitles-old(改名残留、无 Woodpecker 条目);活仓是 zx/matchsubtitles(即本 issue 所在仓)。两仓 .woodpecker.yml 字节数相同(各 1947B),缺陷同型。

现象

.woodpecker.yml 根 when: 是裸 trigger(不含 branch:):

when:
  - event: [push, manual]

发布逻辑内联在 yml 里,无任何分支判定,且两个镜像都打 :latest:

BASE_IMAGE_REPO="docker.lib.zuixue.site/matchsubtitles/runtime-base"
IMAGE_REPO="docker.lib.zuixue.site/matchsubtitles/matchsubtitles"
...
  -t "$${BASE_IMAGE_REPO}:$${TAG}" \
  -t "$${BASE_IMAGE_REPO}:latest" \
...
  --build-arg MATCHSUBTITLES_RUNTIME_BASE_IMAGE="$${BASE_IMAGE_REPO}:$${TAG}" \
  -t "$${IMAGE_REPO}:$${TAG}" \
  -t "$${IMAGE_REPO}:latest" \
  --push

注意第二个构建把 runtime-base:$${TAG} 作为 build-arg 输入 → 非主分支 push 时,被同时污染的 base 会被当次正式镜像构建直接消费。

实测证据

本仓共 7 条流水线(Woodpecker repo id=8),全部是 main:

# 状态 时间(UTC) 分支
7 success 2026-06-05 08:26 main
6 success 2026-06-05 07:06 main
5 success 2026-06-04 15:44 main
4 success 2026-06-03 13:29 main
3 failure 2026-06-03 00:46 main
2 success 2026-06-02 17:07 main
1 error 2026-06-02 17:07 main

即非主分支构建 0 条 —— 属潜伏缺陷(能力已具备,尚未被触发)。对照 yuvuNode 今天已经真实发生三次(见 xm/yuvu#10)。本仓实例部署在节点 5b(受管适配器 matchsubtitles-5b)。

推测根因

Woodpecker 3.18.1 的流水线配置是从被推送的那个 ref 上读取的(本仓 repo 设置 config_file 为空 → 走默认路径 .woodpecker/*.{yaml,yml} / .woodpecker.yaml / .woodpecker.yml);服务器侧 repo 对象没有分支白名单字段(仅 pr_enabled / allow_pr / allow_deploy / require_approval / timeout / config_file)。因此 when: 未声明 branch 时,push 事件对任意分支成立;发布侧又没有分支判定。

期望(完成定义)

when:
  - event: [push, manual]
    branch: main
  - event: manual        # 任意 ref 手动触发,供分支验证使用;不要删除,见 xm/yuvu#10「与 yuvuAdapter#30 的冲突」

发布侧二选一:

  • 步骤级:给发布步骤加 when: { branch: main }(照 yuvuAdapter)
  • 脚本级:加 should_publish_latest 判定,只在 main 上附加 :latest(照 yuvuCore/scripts/ci/publish_image.sh)

最小复现

  1. 造一次无害提交推到非主分支(如 fix/ci-scope-probe);
  2. 观察:Woodpecker 是否产生流水线、发布步骤是否执行、registry 上 matchsubtitles:latest 与 runtime-base:latest 的 digest 是否变化。
    (期望:无流水线;或发布步骤 skipped 且 digest 不变。)

验收

见 xm/yuvu#10「验收口径」1–4。

## 关联 主 issue:**xm/yuvu#10**(P1)——本 issue 是该根因在本仓的落地项,优先级降为 **P2**,是否可关闭以主 issue 的验收口径为准。 > 仓名注记:本仓在 Forgejo 上曾用名 `xm/matchsubtitles`,现改名为 `xm/matchsubtitles-old`(改名残留、无 Woodpecker 条目);**活仓是 `zx/matchsubtitles`**(即本 issue 所在仓)。两仓 `.woodpecker.yml` 字节数相同(各 1947B),缺陷同型。 ## 现象 `.woodpecker.yml` 根 `when:` 是裸 trigger(**不含 `branch:`**): ```yaml when: - event: [push, manual] ``` 发布逻辑**内联在 yml 里**,无任何分支判定,且**两个镜像都打 `:latest`**: ``` BASE_IMAGE_REPO="docker.lib.zuixue.site/matchsubtitles/runtime-base" IMAGE_REPO="docker.lib.zuixue.site/matchsubtitles/matchsubtitles" ... -t "$${BASE_IMAGE_REPO}:$${TAG}" \ -t "$${BASE_IMAGE_REPO}:latest" \ ... --build-arg MATCHSUBTITLES_RUNTIME_BASE_IMAGE="$${BASE_IMAGE_REPO}:$${TAG}" \ -t "$${IMAGE_REPO}:$${TAG}" \ -t "$${IMAGE_REPO}:latest" \ --push ``` 注意第二个构建把 `runtime-base:$${TAG}` 作为 build-arg 输入 → 非主分支 push 时,**被同时污染的 base 会被当次正式镜像构建直接消费**。 ## 实测证据 本仓共 7 条流水线(Woodpecker repo id=8),**全部是 `main`**: | # | 状态 | 时间(UTC) | 分支 | |---|---|---|---| | 7 | success | 2026-06-05 08:26 | main | | 6 | success | 2026-06-05 07:06 | main | | 5 | success | 2026-06-04 15:44 | main | | 4 | success | 2026-06-03 13:29 | main | | 3 | failure | 2026-06-03 00:46 | main | | 2 | success | 2026-06-02 17:07 | main | | 1 | error | 2026-06-02 17:07 | main | 即**非主分支构建 0 条** —— 属**潜伏缺陷**(能力已具备,尚未被触发)。对照 `yuvuNode` 今天已经真实发生三次(见 xm/yuvu#10)。本仓实例部署在节点 `5b`(受管适配器 `matchsubtitles-5b`)。 ## 推测根因 Woodpecker `3.18.1` 的流水线配置是**从被推送的那个 ref 上读取**的(本仓 repo 设置 `config_file` 为空 → 走默认路径 `.woodpecker/*.{yaml,yml}` / `.woodpecker.yaml` / `.woodpecker.yml`);服务器侧 repo 对象**没有分支白名单字段**(仅 `pr_enabled / allow_pr / allow_deploy / require_approval / timeout / config_file`)。因此 `when:` 未声明 `branch` 时,push 事件对**任意分支**成立;发布侧又没有分支判定。 ## 期望(完成定义) ```yaml when: - event: [push, manual] branch: main - event: manual # 任意 ref 手动触发,供分支验证使用;不要删除,见 xm/yuvu#10「与 yuvuAdapter#30 的冲突」 ``` 发布侧二选一: - **步骤级**:给发布步骤加 `when: { branch: main }`(照 `yuvuAdapter`) - **脚本级**:加 `should_publish_latest` 判定,只在 main 上附加 `:latest`(照 `yuvuCore/scripts/ci/publish_image.sh`) ## 最小复现 1. 造一次无害提交推到非主分支(如 `fix/ci-scope-probe`); 2. 观察:Woodpecker 是否产生流水线、发布步骤是否执行、registry 上 `matchsubtitles:latest` 与 `runtime-base:latest` 的 digest 是否变化。 (期望:无流水线;或发布步骤 `skipped` 且 digest 不变。) ## 验收 见 **xm/yuvu#10**「验收口径」1–4。
Author
Collaborator

本轮复核(2026-10-01)

main 的 .woodpecker.yml 仍是裸 - event: [push, manual](发布脚本无 branch 守卫),自 3~7 月起无修改 ⇒ 未修。

对照:yuvuCore、yuvuAdapter 两仓已完成同样收口。修复归属本仓 coding agent。

## 本轮复核(2026-10-01) main 的 `.woodpecker.yml` **仍是裸 `- event: [push, manual]`**(发布脚本无 branch 守卫),自 3~7 月起无修改 ⇒ **未修**。 对照:`yuvuCore`、`yuvuAdapter` 两仓已完成同样收口。修复归属本仓 coding agent。
Author
Collaborator

总控复验并合入(2026-10-01 21:2x CST)

分支 fix/ms-1-ci-publish-scope(worktree wt/wms-01,4 提交,tip 588c9a7)→ 已 --no-ff 合入 main 1761e69 并 push。

改动(仅 3 文件;未动 Dockerfile 构建内容与缓存参数、未部署、未推删任何 tag):

  • .woodpecker.yml:顶层 when 收窄为 push: branch=main + manual(任意 ref,保留分支真机构建);docker-publish 里 runtime-base 与 matchsubtitles 的 -t …:latest 改为由 scripts/ci/publish_scope.sh --tag-args <repo> 产出;新增 test 步骤真跑测试脚本。
  • 新增 scripts/ci/publish_scope.sh(纯函数 should_publish_latest / publish_latest_decision / latest_tag_args + CLI 入口;缺省/不可判定一律 fail-closed 判 no)。
  • 新增 scripts/ci/test_publish_scope.sh。

门禁(原始输出):分支与合并后 main 各跑一次,均为

$ bash scripts/ci/test_publish_scope.sh
publish scope checks passed        # rc=0

真跑判定(两例 + CLI):

$ bash scripts/ci/publish_scope.sh refs/heads/main                  -> yes   rc=0
$ bash scripts/ci/publish_scope.sh refs/heads/fix/ci-scope-probe    -> no    rc=1
$ bash scripts/ci/publish_scope.sh ""                               -> no    rc=1
$ bash scripts/ci/publish_scope.sh --tag-args docker.lib.zuixue.site/matchsubtitles/matchsubtitles refs/heads/main
  -> -t docker.lib.zuixue.site/matchsubtitles/matchsubtitles:latest rc=0
$ bash scripts/ci/publish_scope.sh --tag-args docker.lib.zuixue.site/matchsubtitles/matchsubtitles refs/heads/foo
  -> (空输出) rc=0

反向验证(变异必红 -> 还原逐字节一致 -> 复绿)

  • M1 守卫恒真(should_publish_latest 直接 return 0):test_publish_scope.sh rc=1,FAIL: should_publish_latest('refs/heads/fix/ms-1-ci-publish-scope')=yes want no;真跑分支 ref 得到 yes(错)。
  • M2 latest_tag_args 无条件输出 :latest:rc=1,FAIL: latest_tag_args(...,'refs/heads/fix/ms-1-ci-publish-scope')='-t ...:latest' want ''。
  • 还原后 sha256sum scripts/ci/publish_scope.sh = cf5c317e20d04e66784dd4e9342c4223fec6d44fa47dae6efccb51f88fc403ee(与变异前逐位相同),测试复绿 rc=0。

未通过项(如实标注):go build ./... / go test ./... 本机无法全绿,但与本轮改动无关且 main 同样失败——仓内无 vendor/ 而环境默认 GOFLAGS=-mod=vendor;改 -mod=mod 后 internal/httpapi 与 cmd/matchsubtitles 因缺 github.com/go-chi/chi/v5@v5.2.5、github.com/danielgtaylor/huma/v2@v2.37.2 且 GOPROXY=off 而 [setup failed](在 main b9f0141 复现同样两条 FAIL)。本轮改动不含任何 Go 代码。

请验证这几点

  1. 造一次无害提交推到非主分支 -> 期望不产生流水线,且 registry matchsubtitles:latest 与 matchsubtitles/runtime-base:latest 的 digest 不变;
  2. main push -> 仍出 :latest,且 CI 日志中 test 步骤可见 publish scope checks passed;
  3. manual 触发任意分支 -> 只出 <UTC 日>-<commit7> 标签、不带动 :latest。

issue 保持 open,由归属方/真机验收会话关闭。

## 总控复验并合入(2026-10-01 21:2x CST) **分支** `fix/ms-1-ci-publish-scope`(worktree `wt/wms-01`,4 提交,tip `588c9a7`)→ **已 `--no-ff` 合入 main `1761e69` 并 push**。 **改动**(仅 3 文件;未动 Dockerfile 构建内容与缓存参数、未部署、未推删任何 tag): - `.woodpecker.yml`:顶层 `when` 收窄为 `push: branch=main` + `manual`(任意 ref,保留分支真机构建);`docker-publish` 里 `runtime-base` 与 `matchsubtitles` 的 `-t …:latest` 改为由 `scripts/ci/publish_scope.sh --tag-args <repo>` 产出;新增 `test` 步骤真跑测试脚本。 - 新增 `scripts/ci/publish_scope.sh`(纯函数 `should_publish_latest` / `publish_latest_decision` / `latest_tag_args` + CLI 入口;**缺省/不可判定一律 fail-closed 判 no**)。 - 新增 `scripts/ci/test_publish_scope.sh`。 **门禁(原始输出)**:分支与合并后 main 各跑一次,均为 ``` $ bash scripts/ci/test_publish_scope.sh publish scope checks passed # rc=0 ``` 真跑判定(两例 + CLI): ``` $ bash scripts/ci/publish_scope.sh refs/heads/main -> yes rc=0 $ bash scripts/ci/publish_scope.sh refs/heads/fix/ci-scope-probe -> no rc=1 $ bash scripts/ci/publish_scope.sh "" -> no rc=1 $ bash scripts/ci/publish_scope.sh --tag-args docker.lib.zuixue.site/matchsubtitles/matchsubtitles refs/heads/main -> -t docker.lib.zuixue.site/matchsubtitles/matchsubtitles:latest rc=0 $ bash scripts/ci/publish_scope.sh --tag-args docker.lib.zuixue.site/matchsubtitles/matchsubtitles refs/heads/foo -> (空输出) rc=0 ``` **反向验证(变异必红 -> 还原逐字节一致 -> 复绿)** - M1 守卫恒真(`should_publish_latest` 直接 `return 0`):`test_publish_scope.sh` rc=1,`FAIL: should_publish_latest('refs/heads/fix/ms-1-ci-publish-scope')=yes want no`;真跑分支 ref 得到 `yes`(错)。 - M2 `latest_tag_args` 无条件输出 `:latest`:rc=1,`FAIL: latest_tag_args(...,'refs/heads/fix/ms-1-ci-publish-scope')='-t ...:latest' want ''`。 - 还原后 `sha256sum scripts/ci/publish_scope.sh = cf5c317e20d04e66784dd4e9342c4223fec6d44fa47dae6efccb51f88fc403ee`(与变异前逐位相同),测试复绿 rc=0。 **未通过项(如实标注)**:`go build ./...` / `go test ./...` 本机无法全绿,但**与本轮改动无关且 main 同样失败**——仓内无 `vendor/` 而环境默认 `GOFLAGS=-mod=vendor`;改 `-mod=mod` 后 `internal/httpapi` 与 `cmd/matchsubtitles` 因缺 `github.com/go-chi/chi/v5@v5.2.5`、`github.com/danielgtaylor/huma/v2@v2.37.2` 且 `GOPROXY=off` 而 `[setup failed]`(在 main `b9f0141` 复现同样两条 FAIL)。本轮改动不含任何 Go 代码。 **请验证这几点** 1. 造一次无害提交推到非主分支 -> 期望**不产生流水线**,且 registry `matchsubtitles:latest` 与 `matchsubtitles/runtime-base:latest` 的 digest **不变**; 2. main push -> 仍出 `:latest`,且 CI 日志中 `test` 步骤可见 `publish scope checks passed`; 3. manual 触发任意分支 -> 只出 `<UTC 日>-<commit7>` 标签、不带动 `:latest`。 issue 保持 open,由归属方/真机验收会话关闭。
Author
Collaborator

补充验证通过:CI 发布面收口已合入 main(245d2e6)

  • 合并:245d2e6(--no-ff 合并 fix/ms-1-ci-publish-scope;含新提交 29e86cc「test: 覆盖 publish_scope --tag-args 的 CI_COMMIT_REF 缺省路径」= 29 行新增测试),已 push 到 zx/matchsubtitles main。此前 1761e69 的收口改动(顶层 - event: [push, manual] 已移除、docker-publish 走 publish_latest_decision、base image 不再无条件推 latest)保持不变。
  • 门禁原文:env -u GOFLAGS go build ./... → build OK;go test ./... → 全 ok;bash scripts/ci/test_publish_scope.sh → publish scope checks passed(rc=0)。
  • 反向验证(变异必红):① 把库函数 latest_tag_args 的 ref="${2-${CI_COMMIT_REF:-}}" 改成 ref="${2:-}" → FAIL: latest_tag_args with CI_COMMIT_REF=main must emit :latest,rc=1;② 把 CLI 分支 --tag-args 的同一缺省去掉 → FAIL: publish_scope.sh --tag-args ... (CI_COMMIT_REF='refs/heads/main') => '' want '-t ...:latest',rc=1。两次还原后 sha256=cf5c317e20d04e66784dd4e9342c4223fec6d44fa47dae6efccb51f88fc403ee 与 git show HEAD:scripts/ci/publish_scope.sh 逐位一致、git status 干净、复绿 rc=0 ⇒ 新测试确实承重。
  • 请验证这几点(本 issue 保持 open,等你真机确认后关闭):① 向非 main 分支 push,确认生产 matchsubtitles:latest 与共享 base 镜像 tag 未被改写(可对比 push 前后 registry digest);② main 分支 push 仍能正常产出 :latest;③ Woodpecker 本仓流水线 docker-publish 步骤:main 带 -t ...:latest,非 main 不带(这正是 publish_latest_decision 的判定口径:main→yes、非 main→no、空 ref→no)。
补充验证通过:CI 发布面收口已合入 main(`245d2e6`) - **合并**:`245d2e6`(`--no-ff` 合并 `fix/ms-1-ci-publish-scope`;含新提交 `29e86cc`「test: 覆盖 publish_scope --tag-args 的 CI_COMMIT_REF 缺省路径」= 29 行新增测试),已 push 到 `zx/matchsubtitles` main。此前 1761e69 的收口改动(顶层 `- event: [push, manual]` 已移除、docker-publish 走 `publish_latest_decision`、base image 不再无条件推 latest)保持不变。 - **门禁原文**:`env -u GOFLAGS go build ./...` → build OK;`go test ./...` → 全 ok;`bash scripts/ci/test_publish_scope.sh` → `publish scope checks passed`(rc=0)。 - **反向验证(变异必红)**:① 把库函数 `latest_tag_args` 的 `ref="${2-${CI_COMMIT_REF:-}}"` 改成 `ref="${2:-}"` → `FAIL: latest_tag_args with CI_COMMIT_REF=main must emit :latest`,rc=1;② 把 CLI 分支 `--tag-args` 的同一缺省去掉 → `FAIL: publish_scope.sh --tag-args ... (CI_COMMIT_REF='refs/heads/main') => '' want '-t ...:latest'`,rc=1。两次还原后 `sha256=cf5c317e20d04e66784dd4e9342c4223fec6d44fa47dae6efccb51f88fc403ee` 与 `git show HEAD:scripts/ci/publish_scope.sh` 逐位一致、`git status` 干净、复绿 rc=0 ⇒ 新测试确实承重。 - **请验证这几点**(本 issue 保持 open,等你真机确认后关闭):① 向非 main 分支 push,确认生产 `matchsubtitles:latest` 与共享 base 镜像 tag **未被改写**(可对比 push 前后 registry digest);② main 分支 push 仍能正常产出 `:latest`;③ Woodpecker 本仓流水线 `docker-publish` 步骤:main 带 `-t ...:latest`,非 main 不带(这正是 `publish_latest_decision` 的判定口径:main→yes、非 main→no、空 ref→no)。
Sign in to join this conversation.
No description provided.