From 087714342a09f1cc2318bee9d570c2b6ed028044 Mon Sep 17 00:00:00 2001 From: "Xin.Zh" Date: Sat, 1 Aug 2026 14:20:32 +0800 Subject: [PATCH 01/24] Handle WSS shutdown and UDP buffer sizing Treat expected WSS server shutdown as a normal event-loop exit and compute UDP receive buffers from configured message limits instead of unread data. Add deterministic regressions for Issue #97 items 5 and 6. Constraint: Preserve public APIs and reconnect semantics, and keep the change limited to the two confirmed remaining runtime defects. Confidence: High; both regressions failed for the expected reasons before the production fixes and pass after them, including repeated race execution. Scope-risk: Limited to WSS Serve error handling and UDP receive-buffer allocation in the transport package. Tested: WSL Go 1.25.1 targeted red-green tests; targeted race count=20; go test -race ./transport; go vet ./...; go test ./...; gofmt; git diff --check. Not-tested: GitHub CI and external review checks are pending on the pushed commit. Co-authored-by: OmX --- ...-08-01-issue-97-remaining-runtime-fixes.md | 310 ++++++++++++++++++ ...issue-97-remaining-runtime-fixes-design.md | 115 +++++++ transport/server.go | 4 +- transport/server_test.go | 46 +++ transport/session.go | 35 +- transport/session_test.go | 23 ++ 6 files changed, 515 insertions(+), 18 deletions(-) create mode 100644 doc/superpowers/plans/2026-08-01-issue-97-remaining-runtime-fixes.md create mode 100644 doc/superpowers/specs/2026-08-01-issue-97-remaining-runtime-fixes-design.md diff --git a/doc/superpowers/plans/2026-08-01-issue-97-remaining-runtime-fixes.md b/doc/superpowers/plans/2026-08-01-issue-97-remaining-runtime-fixes.md new file mode 100644 index 00000000..b25a1516 --- /dev/null +++ b/doc/superpowers/plans/2026-08-01-issue-97-remaining-runtime-fixes.md @@ -0,0 +1,310 @@ +# Issue #97 剩余确定性运行时问题实现计划 + +> **面向 AI 代理的工作者:** 必需子技能:使用 superpowers:executing-plans 逐任务实现此计划。步骤使用复选框(`- [ ]`)语法跟踪进度。用户未授权 commit,因此每个任务以 diff/status 检查代替提交。 + +**目标:** 修复 WSS 正常关闭 panic 和 UDP 接收缓冲区计算死分支,并用先失败、后通过的回归测试锁定行为。 + +**架构:** WSS event loop 在 `Serve` 返回处区分预期关闭与非预期错误,正常关闭安静退出、其他错误记录后退出。UDP buffer 规则提取为包内私有纯函数,由 `handleUDPPackage` 调用并通过表驱动边界测试验证。 + +**技术栈:** Go 1.25.1、标准库 `net/http`/`crypto/tls`、Getty transport 包、Go test/race detector、WSL/Linux。 + +--- + +## 文件结构 + +- 修改 `transport/server_test.go`:增加 WSS 启动后正常关闭的集成回归测试。 +- 修改 `transport/server.go`:将 WSS `Serve` 返回分类为预期关闭或需记录的运行错误。 +- 修改 `transport/session_test.go`:增加 UDP buffer 大小的表驱动边界测试。 +- 修改 `transport/session.go`:增加包内私有 `udpReadBufferSize` 并在 UDP 接收路径使用。 +- 保留 `doc/superpowers/specs/2026-08-01-issue-97-remaining-runtime-fixes-design.md`:批准后的设计依据。 +- 新增本计划文件:记录 TDD、验证和范围边界。 + +### 任务 1:WSS 正常关闭回归测试与最小修复 + +**文件:** +- 修改:`transport/server_test.go:301-318` +- 修改:`transport/server.go:20-31` +- 修改:`transport/server.go:481-537` + +- [x] **步骤 1:编写失败的 WSS 正常关闭测试** + +在 `transport/server_test.go` 的 `TestServer` 后加入: + +```go +func TestWSSServerCloseDoesNotPanic(t *testing.T) { + certPath, err := filepath.Abs("../examples/profiles/wss/server_cert/server.crt") + if err != nil { + t.Fatal(err) + } + keyPath, err := filepath.Abs("../examples/profiles/wss/server_cert/server.key") + if err != nil { + t.Fatal(err) + } + + server := newServer( + WSS_SERVER, + WithLocalAddress("127.0.0.1:0"), + WithWebsocketServerPath("/ws"), + WithWebsocketServerCert(certPath), + WithWebsocketServerPrivateKey(keyPath), + ) + server.RunEventLoop(func(Session) error { return nil }) + + deadline := time.Now().Add(time.Second) + for { + server.lock.RLock() + serving := server.server != nil + server.lock.RUnlock() + if serving { + break + } + if time.Now().After(deadline) { + t.Fatal("WSS event loop did not publish its HTTP server") + } + time.Sleep(time.Millisecond) + } + + closed := make(chan struct{}) + go func() { + server.Close() + close(closed) + }() + select { + case <-closed: + case <-time.After(2 * time.Second): + t.Fatal("WSS server Close did not return") + } +} +``` + +该测试使用真实 listener、真实 TLS 证书和真实 `http.Server`,不 mock Getty 内部实现;它专门回归 Issue #97 的正常关闭 panic。 + +- [x] **步骤 2:运行测试,确认红灯来自当前 WSS panic** + +运行: + +```bash +wsl.exe --cd /mnt/d/test/github/review/getty-issue-97-fix -- bash -lc \ + "go test ./transport -run '^TestWSSServerCloseDoesNotPanic$' -count=1" +``` + +预期:FAIL,进程输出包含 `panic: http: Server closed`,证明测试命中了当前 `runWSSEventLoop` 的无条件 panic;如果失败来自证书、端口或启动超时,先修正测试夹具并重新取得正确红灯。 + +- [x] **步骤 3:实现最小 WSS 错误分类** + +在 `transport/server.go` 标准库 import 组增加: + +```go +"errors" +``` + +将 WSS `Serve` 返回处理替换为: + +```go + err = server.Serve(tls.NewListener(s.streamListener, config)) + if err != nil && !errors.Is(err, http.ErrServerClosed) && !s.IsClosed() { + log.Errorf("http.server.Serve(addr{%s}) = err:%+v", s.addr, perrors.WithStack(err)) + } +``` + +删除 `panic(err)`。不修改证书加载错误,因为它们发生在启动配置阶段,不属于正常关闭问题。 + +- [x] **步骤 4:运行 WSS 测试,确认绿灯** + +运行: + +```bash +wsl.exe --cd /mnt/d/test/github/review/getty-issue-97-fix -- bash -lc \ + "go test ./transport -run '^TestWSSServerCloseDoesNotPanic$' -count=1" +``` + +预期:PASS,且没有 panic 或错误日志。 + +- [x] **步骤 5:检查任务 1 的变更边界** + +运行: + +```powershell +git diff --check +git diff -- transport/server.go transport/server_test.go +git status --short +``` + +预期:只出现 WSS 测试和错误分类所需变更,以及已批准的规格/计划文件;不 commit。 + +### 任务 2:UDP buffer 边界测试与最小修复 + +**文件:** +- 修改:`transport/session_test.go:29-33` +- 修改:`transport/session.go:48-64` +- 修改:`transport/session.go:914-937` + +- [x] **步骤 1:编写失败的 UDP buffer 表驱动测试** + +在 `transport/session_test.go` 的包级测试辅助类型之前加入: + +```go +func TestUDPReadBufferSize(t *testing.T) { + tests := []struct { + name string + maxMsgLen int32 + want int + }{ + {name: "tiny message", maxMsgLen: 1, want: 2}, + {name: "below crossover", maxMsgLen: maxReadBufLen - 1, want: 2 * (maxReadBufLen - 1)}, + {name: "at crossover", maxMsgLen: maxReadBufLen, want: 2 * maxReadBufLen}, + {name: "above crossover", maxMsgLen: maxReadBufLen + 1, want: 2*maxReadBufLen + 1}, + {name: "large message", maxMsgLen: 128 * 1024, want: 128*1024 + maxReadBufLen}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := udpReadBufferSize(tt.maxMsgLen); got != tt.want { + t.Fatalf("udpReadBufferSize(%d) = %d, want %d", tt.maxMsgLen, got, tt.want) + } + }) + } +} +``` + +一个表驱动测试覆盖同一计算规则的五个输入变体,避免重复测试体。 + +- [x] **步骤 2:运行测试,确认红灯来自 helper 缺失** + +运行: + +```bash +wsl.exe --cd /mnt/d/test/github/review/getty-issue-97-fix -- bash -lc \ + "go test ./transport -run '^TestUDPReadBufferSize$' -count=1" +``` + +预期:FAIL,编译错误包含 `undefined: udpReadBufferSize`。这证明生产 helper 尚不存在。 + +- [x] **步骤 3:实现最小 UDP buffer 计算并替换错误分支** + +在 `transport/session.go` 常量块后加入: + +```go +func udpReadBufferSize(maxMsgLen int32) int { + maxBufLen := int(maxMsgLen + maxReadBufLen) + if doubledMaxMsgLen := int(maxMsgLen << 1); doubledMaxMsgLen < maxBufLen { + return doubledMaxMsgLen + } + return maxBufLen +} +``` + +在 `handleUDPPackage` 中删除局部变量 `maxBufLen`,并将: + +```go + maxBufLen = int(s.maxMsgLen + maxReadBufLen) + if int(s.maxMsgLen<<1) < bufLen { + maxBufLen = int(s.maxMsgLen << 1) + } + bufp = gxbytes.AcquireBytes(maxBufLen) +``` + +替换为: + +```go + bufp = gxbytes.AcquireBytes(udpReadBufferSize(s.maxMsgLen)) +``` + +- [x] **步骤 4:运行 UDP 测试,确认绿灯** + +运行: + +```bash +wsl.exe --cd /mnt/d/test/github/review/getty-issue-97-fix -- bash -lc \ + "go test ./transport -run '^TestUDPReadBufferSize$' -count=1" +``` + +预期:PASS,五个子测试全部通过。 + +- [x] **步骤 5:运行两个回归测试的普通与 race 版本** + +运行: + +```bash +wsl.exe --cd /mnt/d/test/github/review/getty-issue-97-fix -- bash -lc \ + "go test ./transport -run '^(TestWSSServerCloseDoesNotPanic|TestUDPReadBufferSize)$' -count=1 && go test -race ./transport -run '^(TestWSSServerCloseDoesNotPanic|TestUDPReadBufferSize)$' -count=1" +``` + +预期:两个命令均 PASS,race detector 不报告竞态。 + +- [x] **步骤 6:检查任务 2 的变更边界** + +运行: + +```powershell +git diff --check +git diff -- transport/session.go transport/session_test.go +git status --short +``` + +预期:只出现 UDP helper、调用替换和表驱动测试;不 commit。 + +### 任务 3:测试质量门禁与完整验证 + +**文件:** +- 审查:`transport/server_test.go` +- 审查:`transport/session_test.go` +- 验证:全部已修改文件 + +- [x] **步骤 1:按 test-guard 审查新测试** + +逐项确认: + +- WSS 测试断言真实可观察行为,没有 mock 内部 helper。 +- WSS 测试只覆盖正常关闭场景,并明确对应 Issue #97。 +- UDP 的五个输入变体合并在一个表驱动测试中。 +- 测试名称描述场景和期望,不测试 Go/http 框架自身保证。 +- 没有仅为测试向生产类型添加公开方法。 + +若发现违反规则,先修改测试并重新运行对应红绿验证。 + +- [x] **步骤 2:运行 transport race 测试** + +运行: + +```bash +wsl.exe --cd /mnt/d/test/github/review/getty-issue-97-fix -- bash -lc \ + "go test -race ./transport -count=1" +``` + +预期:PASS,无 data race。 + +- [x] **步骤 3:运行静态检查** + +运行: + +```bash +wsl.exe --cd /mnt/d/test/github/review/getty-issue-97-fix -- bash -lc \ + "go vet ./..." +``` + +预期:退出码 0,无 vet 诊断。 + +- [x] **步骤 4:运行全仓测试** + +运行: + +```bash +wsl.exe --cd /mnt/d/test/github/review/getty-issue-97-fix -- bash -lc \ + "go test ./... -count=1" +``` + +预期:所有有测试的包 PASS,无失败包。 + +- [x] **步骤 5:最终 diff、格式和状态检查** + +运行: + +```powershell +git diff --check +git status --short --branch +git diff --stat +git diff -- transport/server.go transport/server_test.go transport/session.go transport/session_test.go +``` + +预期:无空白错误;生产代码和测试只覆盖方案 A;规格和计划文件未超出批准范围;不 commit、不 push。 diff --git a/doc/superpowers/specs/2026-08-01-issue-97-remaining-runtime-fixes-design.md b/doc/superpowers/specs/2026-08-01-issue-97-remaining-runtime-fixes-design.md new file mode 100644 index 00000000..b091bcbf --- /dev/null +++ b/doc/superpowers/specs/2026-08-01-issue-97-remaining-runtime-fixes-design.md @@ -0,0 +1,115 @@ +# Issue #97 剩余确定性运行时问题修复设计 + +## 目标 + +修复 Issue #97 在当前 `master@cc9909dc9e0aab1307f553bc2a3d8400161be4e2` 上仍可由源码直接确认的两个确定性问题: + +1. WSS 服务在正常关闭时,将 `http.Server.Serve` 返回的预期关闭错误升级为进程级 panic。 +2. UDP 接收缓冲区大小计算使用尚未由 `recv` 填充的 `bufLen`,导致分支恒定按零值判断。 + +变更必须保持现有公开接口不变,并通过 Linux race 测试、静态检查和仓库测试验证。 + +## 范围 + +### 本批包含 + +- 修改 `transport/server.go` 的 WSS `Serve` 返回处理。 +- 修改 `transport/session.go` 的 UDP 接收缓冲区大小计算。 +- 在 `transport/server_test.go` 增加 WSS 正常启动和关闭的回归测试。 +- 在 `transport/session_test.go` 增加 UDP 缓冲区大小边界测试。 + +### 本批不包含 + +- 不修改 `WithReconnectAttempts` 的语义。当前公开文档将其描述为最大重连尝试次数,现有测试也验证总尝试次数;把它改为连续失败次数需要单独设计和兼容性决策。 +- 不修改 Issue #93 的 Bug 模板、自动回复或 Release Note workflow。 +- 不重构 WS/WSS 服务生命周期之外的代码。 +- 不 commit、不 push、不创建 PR,也不修改或关闭 GitHub Issue。 + +## 设计 + +### 1. WSS 正常关闭不再 panic + +当前 WSS event loop 对 `server.Serve(tls.NewListener(...))` 的任意非空错误执行 `panic(err)`。`http.Server.Serve` 在调用 `Shutdown` 或 `Close` 后会返回 `http.ErrServerClosed`,这是服务生命周期的正常结束信号。 + +修改后的行为: + +- `errors.Is(err, http.ErrServerClosed)` 时直接退出 goroutine,不记录错误,不 panic。 +- Server 已进入 Getty 自身关闭状态时,listener close 产生的返回同样作为预期退出处理。 +- 其他 `Serve` 错误沿用非 TLS WS event loop 的容错方式:记录带地址和错误上下文的错误日志,然后退出 goroutine,不在后台服务 goroutine 中 panic 整个进程。 +- 保留 `defer s.wg.Done()`,确保 `Server.Close()` 能完成等待。 + +不引入新的公开 API。错误分类逻辑优先保持在 `runWSSEventLoop` 附近,除非测试表明抽取小型私有 helper 能显著降低重复。 + +### 2. UDP 接收缓冲区大小使用目标变量计算 + +当前意图等价于在两个上限中取较小值: + +```text +min(maxMsgLen + maxReadBufLen, 2 * maxMsgLen) +``` + +现有实现错误地将尚未赋值的 `bufLen` 与 `2 * maxMsgLen` 比较。修复将计算提取为包内私有函数: + +```go +func udpReadBufferSize(maxMsgLen int32) int +``` + +函数规则: + +- 输入采用 Session 已经保存的正数 `maxMsgLen`。 +- 返回 `maxMsgLen + maxReadBufLen` 与 `2 * maxMsgLen` 中较小者。 +- `handleUDPPackage` 只负责使用返回值申请和释放 buffer,不再保留尚未接收数据就读取 `bufLen` 的分支。 + +提取函数的目的是让边界规则可以直接测试,而不是暴露新的产品接口。 + +## 测试设计 + +### WSS 生命周期测试 + +新增集成回归测试,使用仓库已有 TLS 测试证书或测试内临时证书夹具: + +1. 创建监听随机本地端口的 WSS Server。 +2. 在 goroutine 中启动 `RunEventLoop`。 +3. 等待 listener 和 HTTP server 已发布,避免把异步启动竞态误当成关闭行为。 +4. 调用 `Close()`。 +5. 断言 `Close()` 和 event loop 在有界时间内返回。 + +在修复前,测试应因服务 goroutine 执行 `panic(http.ErrServerClosed)` 而失败;修复后应正常通过。测试不得通过 sleep 猜测启动状态,应轮询可观察的 listener/server 状态并设置总超时。 + +### UDP 边界测试 + +对 `udpReadBufferSize` 使用表驱动测试,至少覆盖: + +| `maxMsgLen` | 预期结果 | 说明 | +|---:|---:|---| +| `1` | `2` | 小消息由 `2 * maxMsgLen` 限制 | +| `4095` | `8190` | 低于交叉点一字节 | +| `4096` | `8192` | 两个公式在交叉点相等 | +| `4097` | `8193` | 高于交叉点后由 `maxMsgLen + 4096` 限制 | +| `128 * 1024` | `128 * 1024 + 4096` | 常见大消息配置 | + +测试先在未修改生产代码的状态下运行并确认失败,失败原因必须是 helper 尚不存在或旧逻辑不满足断言;随后只实现使测试通过所需的最小代码。 + +## 验证 + +实现完成后在 WSL/Linux、Go 1.25.1 下依次运行: + +```bash +go test ./transport -run '^(TestWSSServerCloseDoesNotPanic|TestUDPReadBufferSize)$' -count=1 +go test -race ./transport -run '^(TestWSSServerCloseDoesNotPanic|TestUDPReadBufferSize)$' -count=1 +go test -race ./transport -count=1 +go vet ./... +go test ./... -count=1 +``` + +若仓库级命令因为既有基线、环境或超时失败,必须区分本次新增失败和环境/基线失败,不得通过修改或跳过测试来制造通过结果。 + +## 完成标准 + +- WSS 正常关闭路径不产生 panic,并能完成 WaitGroup 等待。 +- 非预期 WSS `Serve` 错误仍被记录,不静默吞掉。 +- UDP buffer 计算不再读取接收前的 `bufLen`。 +- UDP buffer 边界规则由表驱动测试锁定。 +- 新测试经过明确的红灯和绿灯阶段。 +- WSL/Linux race 测试、静态检查和适用的仓库测试获得新鲜验证结果。 +- 用户原始 checkout 和其中的未跟踪内容保持不变。 diff --git a/transport/server.go b/transport/server.go index f5f60808..9f4ca227 100644 --- a/transport/server.go +++ b/transport/server.go @@ -21,6 +21,7 @@ import ( "context" "crypto/tls" "crypto/x509" + "errors" "fmt" "net" "net/http" @@ -530,9 +531,8 @@ func (s *server) runWSSEventLoop(newSession NewSessionCallback) { s.server = server s.lock.Unlock() err = server.Serve(tls.NewListener(s.streamListener, config)) - if err != nil { + if err != nil && !errors.Is(err, http.ErrServerClosed) && !s.IsClosed() { log.Errorf("http.server.Serve(addr{%s}) = err:%+v", s.addr, perrors.WithStack(err)) - panic(err) } }() } diff --git a/transport/server_test.go b/transport/server_test.go index 605981a5..1521bf1f 100644 --- a/transport/server_test.go +++ b/transport/server_test.go @@ -315,6 +315,52 @@ func TestServer(t *testing.T) { testTCPTlsServer(t, addr) } +// Regression test for #97: normal WSS shutdown must not panic on http.ErrServerClosed. +func TestWSSServerCloseDoesNotPanic(t *testing.T) { + certPath, err := filepath.Abs("../examples/profiles/wss/server_cert/server.crt") + if err != nil { + t.Fatal(err) + } + keyPath, err := filepath.Abs("../examples/profiles/wss/server_cert/server.key") + if err != nil { + t.Fatal(err) + } + + server := newServer( + WSS_SERVER, + WithLocalAddress("127.0.0.1:0"), + WithWebsocketServerPath("/ws"), + WithWebsocketServerCert(certPath), + WithWebsocketServerPrivateKey(keyPath), + ) + server.RunEventLoop(func(Session) error { return nil }) + + deadline := time.Now().Add(time.Second) + for { + server.lock.RLock() + serving := server.server != nil + server.lock.RUnlock() + if serving { + break + } + if time.Now().After(deadline) { + t.Fatal("WSS event loop did not publish its HTTP server") + } + time.Sleep(time.Millisecond) + } + + closed := make(chan struct{}) + go func() { + server.Close() + close(closed) + }() + select { + case <-closed: + case <-time.After(2 * time.Second): + t.Fatal("WSS server Close did not return") + } +} + func TestWSServeWSRequestClosesSelfConnectConn(t *testing.T) { server := newServer(WS_SERVER) newSessionCalled := false diff --git a/transport/session.go b/transport/session.go index ff1634f0..a2fa407e 100644 --- a/transport/session.go +++ b/transport/session.go @@ -64,6 +64,14 @@ const ( outputFormat = "session %s, Read Bytes: %d, Write Bytes: %d, Read Pkgs: %d, Write Pkgs: %d" ) +func udpReadBufferSize(maxMsgLen int32) int { + maxBufLen := int(maxMsgLen + maxReadBufLen) + if doubledMaxMsgLen := int(maxMsgLen << 1); doubledMaxMsgLen < maxBufLen { + return doubledMaxMsgLen + } + return maxBufLen +} + var defaultTimerWheel *gxtime.TimerWheel func init() { @@ -914,25 +922,20 @@ func (s *session) handleTCPPackage() error { // get package from udp packet func (s *session) handleUDPPackage() error { var ( - ok bool - err error - netError net.Error - conn *gettyUDPConn - bufLen int - maxBufLen int - bufp *[]byte - buf []byte - addr *net.UDPAddr - pkgLen int - pkg any + ok bool + err error + netError net.Error + conn *gettyUDPConn + bufLen int + bufp *[]byte + buf []byte + addr *net.UDPAddr + pkgLen int + pkg any ) conn = s.Connection.(*gettyUDPConn) - maxBufLen = int(s.maxMsgLen + maxReadBufLen) - if int(s.maxMsgLen<<1) < bufLen { - maxBufLen = int(s.maxMsgLen << 1) - } - bufp = gxbytes.AcquireBytes(maxBufLen) + bufp = gxbytes.AcquireBytes(udpReadBufferSize(s.maxMsgLen)) defer gxbytes.ReleaseBytes(bufp) buf = *bufp for !s.IsClosed() { diff --git a/transport/session_test.go b/transport/session_test.go index c7e5e3f8..ec9452f9 100644 --- a/transport/session_test.go +++ b/transport/session_test.go @@ -31,6 +31,29 @@ var ( errUnexpectedSecondRead = errors.New("unexpected second read") ) +// Regression test for #97: size the UDP read buffer from configured limits, not unread data. +func TestUDPReadBufferSize(t *testing.T) { + tests := []struct { + name string + maxMsgLen int32 + want int + }{ + {name: "tiny message", maxMsgLen: 1, want: 2}, + {name: "below crossover", maxMsgLen: maxReadBufLen - 1, want: 2 * (maxReadBufLen - 1)}, + {name: "at crossover", maxMsgLen: maxReadBufLen, want: 2 * maxReadBufLen}, + {name: "above crossover", maxMsgLen: maxReadBufLen + 1, want: 2*maxReadBufLen + 1}, + {name: "large message", maxMsgLen: 128 * 1024, want: 128*1024 + maxReadBufLen}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := udpReadBufferSize(tt.maxMsgLen); got != tt.want { + t.Fatalf("udpReadBufferSize(%d) = %d, want %d", tt.maxMsgLen, got, tt.want) + } + }) + } +} + type errorReader struct{} func (errorReader) Read(Session, []byte) (any, int, error) { From 7539afef7d495ed43f94e0ae488d8da010b9a7f5 Mon Sep 17 00:00:00 2001 From: "Xin.Zh" Date: Sat, 1 Aug 2026 15:21:48 +0800 Subject: [PATCH 02/24] docs: design comprehensive GitHub CI hardening --- .../2026-08-01-github-ci-hardening-design.md | 344 ++++++++++++++++++ 1 file changed, 344 insertions(+) create mode 100644 doc/superpowers/specs/2026-08-01-github-ci-hardening-design.md diff --git a/doc/superpowers/specs/2026-08-01-github-ci-hardening-design.md b/doc/superpowers/specs/2026-08-01-github-ci-hardening-design.md new file mode 100644 index 00000000..57518d81 --- /dev/null +++ b/doc/superpowers/specs/2026-08-01-github-ci-hardening-design.md @@ -0,0 +1,344 @@ +# GitHub CI 全面加固设计 + +## 背景与证据 + +本设计以 `AlexStocks/getty` PR #108 的实时快照为基准: + +- Base:`master@cc9909dc9e0aab1307f553bc2a3d8400161be4e2` +- Head:`codex/fix-issue-97-remaining@087714342a09f1cc2318bee9d570c2b6ed028044` +- 现有 workflow:`.github/workflows/github-actions.yml` +- 对照仓库:`apache/dubbo-go@53d81d17c0f658b7151fb8c44f0d80371ef047e7` + +PR #108 的 CI 日志确认了以下问题: + +1. `actions/setup-go@v5` 在 checkout 之前执行,内置缓存找不到 `go.sum`;随后 workflow 又使用 `actions/cache@v4` 恢复相同的 Go module/build cache。 +2. Coverage step 执行远程脚本 `bash <(curl -s https://codecov.io/bash)`,Codecov 返回 HTTP 400 和 `Token required - not valid tokenless upload`,但 step 和 job 仍显示成功。 +3. CI 不执行 race detector,无法持续覆盖 Getty 的并发和生命周期风险。 +4. workflow 未显式设置最小权限、并发取消或 job 超时。 +5. `make test` 使用 `go env -w` 修改 runner 用户级 Go 配置;测试也未使用 `-count=1`。 +6. `imports-formatter@latest` 是浮动工具依赖;GitHub Actions 也使用可变 major tag 或 `@main`。 +7. README 仍展示 Travis CI badge,仓库仍保留已不参与当前 PR 检查的 `.travis.yml`;该文件还包含明文 Codecov upload token 和第三方 webhook access token。 +8. `master` 分支当前没有 required status checks,也没有 repository ruleset。 + +## 目标 + +本次改造采用完整方案,目标是: + +1. 让测试、race、格式、静态检查、coverage 上传失败能够真实反映到 GitHub check 结果。 +2. 消除重复缓存、全局 Go 配置写入和浮动工具版本。 +3. 将 workflow 权限限制在每个 job 实际需要的最小集合。 +4. 增加跨平台构建、CodeQL 和 Dependabot,覆盖 Go 源码、GitHub Actions 与依赖维护。 +5. 固定第三方 Action 到核验过的完整 commit SHA,并保留版本注释,兼顾供应链可审计性和后续升级。 +6. 清理已经被 GitHub Actions 取代的 Travis CI 展示与配置。 +7. 保持 Getty 公开 Go API 和运行时行为不变。 + +## 非目标与权限边界 + +- 不在本次 CI 改造中修复 PR #108 已审查出的 UDP 运行时问题或测试缺口;这些问题继续由现有 Files changed 线程跟踪。 +- 不增加 Getty 专属外部集成服务、数据库、消息队列或部署流程。 +- 不在 workflow 中自动发布、创建 release、写回源码或提交生成文件。 +- 不直接修改 GitHub branch protection 或 ruleset。required checks 属于 PR 文件之外的仓库设置;只有新 job 名称和实际运行结果稳定后,才提交精确配置建议,并在获得单独确认后修改。 +- 不调用或验证 `.travis.yml` 中暴露的第三方 token。删除文件不能从 Git 历史撤销凭据;轮换或吊销 Codecov/DingTalk 凭据属于需要账号权限的外部安全收尾。 +- 不把 Dubbo-Go 的 RPC integration test、RISC-V 工具子模块或 samples 流程机械复制到 Getty。 + +## 变更文件 + +### 修改 + +- `.github/workflows/github-actions.yml` +- `Makefile` +- `README.md` +- `README_CN.md` + +### 新增 + +- `.github/workflows/codeql.yml` +- `.github/dependabot.yml` + +### 删除 + +- `.travis.yml` + +删除 `.travis.yml` 的前提是最终复核仍满足:PR status rollup 中没有 Travis check,GitHub Actions 已覆盖其有效命令,README badge 同步改为 GitHub Actions。 + +## 设计 + +### 1. 主 CI workflow + +保留 `.github/workflows/github-actions.yml` 作为主 workflow,名称继续使用 `CI`,触发范围为: + +- push 到 `master` +- 以 `master` 为 base 的 pull request + +workflow 顶层设置: + +```yaml +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true +``` + +`concurrency` 用于取消同一 PR 或同一 ref 的旧运行,避免过期 Head 继续占用 runner。所有 job 都设置显式 `timeout-minutes`,防止网络测试、工具下载或 race 测试无限挂起。 + +### 2. Action 固定策略 + +所有第三方 Action 使用完整 commit SHA,并在同行注释来源版本,例如: + +```yaml +uses: actions/checkout@ # v7 +``` + +实现前重新查询并固定: + +- `actions/checkout@v7` +- `actions/setup-go@v6` +- `apache/skywalking-eyes/header` 当前核验提交 +- `codecov/codecov-action@v7` +- `github/codeql-action@v3` + +Dependabot 的 `github-actions` ecosystem 负责后续 Action 更新。不得使用 `@main`,也不得在同一 workflow 中同时保留 major tag 与完整 SHA 两套引用方式。 + +### 3. License job + +License job: + +- `permissions: contents: read` +- checkout 固定到完整 SHA +- SkyWalking Eyes 固定到完整 SHA +- `timeout-minutes: 10` +- 保持 `.licenserc.yaml` 和 `mode: check` + +License job 不获得 `id-token`、`security-events` 或写入仓库内容的权限。 + +### 4. Test and Lint job + +主验证 job 使用稳定名称 `Test and Lint`,步骤顺序固定为: + +1. Checkout +2. Setup Go +3. Verify modules +4. Check format +5. Unit tests and coverage +6. Lint +7. Upload coverage + +Setup Go 使用: + +```yaml +with: + go-version-file: go.mod + cache-dependency-path: go.sum +``` + +删除独立 `actions/cache` step,让 `setup-go` 成为 Go module/build cache 的唯一 owner。 + +模块验证执行 `go mod verify`。格式检查执行 `make check-fmt`。测试执行 `make test`,生成 `coverage.txt`。Lint 执行 `make lint`。 + +### 5. Codecov OIDC + +Coverage 上传使用固定到完整 SHA 的 `codecov/codecov-action`,不再下载并执行 Codecov bash uploader。 + +`Test and Lint` job 的权限为: + +```yaml +permissions: + contents: read + id-token: write +``` + +Codecov 参数至少包含: + +```yaml +with: + use_oidc: true + fail_ci_if_error: true + files: ./coverage.txt + disable_search: true +``` + +设计要求: + +- 上传失败必须使 job 失败。 +- 不依赖 `CODECOV_TOKEN` 仓库 secret。 +- 只上传明确生成的 `coverage.txt`,不扫描工作区中的其他 coverage 文件。 +- push 后必须核对日志中没有 HTTP 400、tokenless upload 错误或被吞掉的非零状态。 + +### 6. Race job + +新增独立 job `Race`: + +- Ubuntu runner +- checkout + setup-go 内置缓存 +- `timeout-minutes: 15` +- 执行 `make test-race` + +`make test-race` 固定执行: + +```bash +GOTOOLCHAIN=go1.25.0+auto go test -race ./transport -count=1 +``` + +Race job 与普通单测并行,单独展示结果,便于后续配置 required check。 + +### 7. 跨平台构建 job + +新增 job `Build`,使用真实 GitHub-hosted runner matrix: + +- `ubuntu-latest` +- `windows-latest` +- `macos-latest` + +每个平台执行 checkout、setup-go、`go mod verify` 和 `go build ./...`。该 job 只验证编译兼容性,不在本次范围内把全量网络测试扩展到 Windows/macOS,避免把既有平台测试差异和 CI 架构改造混为一体。 + +Matrix 设置 `fail-fast: false`,保证一个平台失败时仍能取得另外两个平台的完整证据。job 名称包含 runner OS,便于将来精确配置 required checks。 + +### 8. Makefile + +Makefile 调整为可由本地和 CI 复用的显式门禁: + +- `.PHONY` 补全 `check-fmt`、`test-race` 和安装目标。 +- `test` 不再执行 `go env -w`,改为命令级 `GOTOOLCHAIN`。 +- `test` 增加 `-count=1`,同时保留 atomic coverage 输出。 +- 新增 `test-race`,只运行 `./transport` 的 race 测试。 +- 新增 `check-fmt`:执行项目格式化命令后,用 `git diff --exit-code --quiet` 检测是否产生差异,并输出差异文件。 +- `imports-formatter` 从 `@latest` 固定到本次已验证的 `v1.0.10`。 +- `golangci-lint` 暂时保持当前已验证的 `v2.4.0`,避免在 CI 架构改造中混入新 lint 规则导致的源码修复;升级到 Dubbo-Go 使用的更高版本应单独处理。 + +`check-fmt` 会在一次性 CI checkout 中运行写入式 formatter,但只用于验证差异;本地验证时必须在独立 probe 副本执行,不能改写 PR 主证据副本后再恢复。 + +### 9. CodeQL + +新增 `.github/workflows/codeql.yml`: + +- push 到 `master` +- 以 `master` 为 base 的 pull request +- 每周一次定时扫描 +- `concurrency` 取消同一 PR 的旧扫描 + +权限限定为: + +```yaml +permissions: + contents: read + +jobs: + analyze: + permissions: + actions: read + contents: read + security-events: write +``` + +CodeQL 显式指定 `languages: go`,使用固定 commit SHA 的 `init` 和 `analyze`,构建模式采用适合 Go 的自动构建。不得复制 Dubbo-Go workflow 中手工 checkout PR merge commit 父节点的历史逻辑;使用 GitHub 当前标准 pull request checkout 语义。 + +### 10. Dependabot + +新增 `.github/dependabot.yml`: + +- `gomod`:根目录,每周更新,目标分支 `master` +- `github-actions`:根目录,每月更新,目标分支 `master` +- `gomod` 的 `open-pull-requests-limit` 设为 `5`,commit message 前缀设为 `deps` +- `github-actions` 的 `open-pull-requests-limit` 设为 `3`,commit message 前缀设为 `ci` + +配置只负责创建依赖更新 PR,不自动 approve、merge 或修改 branch protection。 + +### 11. README 与 Travis 清理 + +`README.md` 和 `README_CN.md` 的 Travis badge 替换为 GitHub Actions `CI` workflow badge,并继续保留 Codecov、Go reference、Go Report Card 和 license badge。 + +确认当前 GitHub status rollup 没有 Travis check 后删除 `.travis.yml`。删除前对照其命令与新 workflow,确保 Go 测试、race、格式、lint、coverage 和构建范围不存在仅由 Travis 承担的路径。 + +旧 Travis 文件中的明文 Codecov token 和第三方 webhook token 已经进入 Git 历史。PR 负责从当前树删除这些值,并在收尾报告中列出必须由仓库维护者完成的轮换/吊销动作;不得在评论、日志、设计文档或 commit message 中复制 token 内容。 + +### 12. Branch protection 后续配置 + +本 PR 只提交可审查的仓库文件。新 workflow 在 PR #108 当前 Head 上全部稳定通过后,输出建议 required checks 列表,预计包含: + +- `Check License Header` +- `Test and Lint` +- `Race` +- 三个平台的 `Build` matrix checks +- `CodeQL` 分析 check + +实际 check 名称以 GitHub 新运行返回值为准。修改 branch protection/ruleset 前必须再次获取现有配置,使用增量更新,保留 force-push、review、conversation resolution 等与本任务无关的设置,并获得单独确认。 + +## 验证策略 + +### 静态与语法验证 + +- `git diff --check` +- 使用 `actionlint v1.7.12` 检查全部 `.github/workflows/*.yml` +- 解析 `.github/dependabot.yml`,确认 YAML 语法和必需字段 +- 检查所有 `uses:` 都固定为完整 40 字符 SHA +- 检查不存在 `@main`、`@latest`、`curl | bash` 或 process substitution 远程执行 + +### Makefile 验证 + +在独立 probe 副本运行: + +- `make check-fmt` +- `make test` +- `make test-race` +- `make lint` +- `git status --porcelain=v2 --branch --untracked-files=all` + +确认 `make test` 不改写用户级 `go env`,工具版本与设计一致,格式检查能在故意制造格式差异时失败。 + +### Go 验证 + +- `go mod verify` +- `go vet ./...` +- `go test ./... -count=1` +- `go test -race ./transport -count=1` +- `GOOS=windows GOARCH=amd64 go build ./...` +- `GOOS=darwin GOARCH=amd64 go build ./...` +- `GOOS=linux GOARCH=arm64 CGO_ENABLED=0 go build ./...` +- `GOOS=linux GOARCH=riscv64 CGO_ENABLED=0 go build ./...` + +本地交叉编译不能替代真实 GitHub Windows/macOS runner;最终结论必须等待远端 matrix job。 + +### GitHub 实时验证 + +push 前: + +1. 重新获取 PR state、Base、Head、checks 和远端 head branch SHA。 +2. 确认远端 Head 仍等于本轮实现基准,不使用 force push。 +3. commit 后再次核对本地 branch 只领先预期提交。 + +push 后: + +1. 等待全部新 workflow run 完成。 +2. 核对每个 job 的真实命令、平台、结论和日志。 +3. 确认 Codecov 上传成功且失败可传播。 +4. 确认 setup-go 在 checkout 后找到 `go.sum`,不存在第二套 Go cache。 +5. 确认 CodeQL 上传 security result 成功。 +6. 确认 Dependabot 配置被 GitHub 接受。 +7. 重新获取 PR Base/Head、mergeable、mergeStateStatus、reviewDecision、required checks 和 review threads。 + +## 失败处理与回滚 + +- actionlint/YAML 失败:只修 workflow 语法,不绕过检查。 +- Windows/macOS build 暴露既有源码不兼容:记录为独立产品问题;不为了让 CI 变绿而跳过失败包。若修复明显超出 CI 范围,保留失败证据并由用户决定拆分或扩大授权。 +- Codecov OIDC 不被当前仓库接受:先核对 job 权限和 Codecov 官方日志;不得恢复旧 bash uploader。若需要 Codecov 侧启用设置,报告精确外部前置条件。 +- CodeQL 因仓库安全设置不可用:保留 workflow 和失败证据,说明所需 GitHub 设置;不把环境/权限失败归责为 Go 源码问题。 +- 远端 Head 漂移:停止 push,重新审查新增远端提交并适配;禁止 force push 覆盖。 +- 回滚通过新增普通 commit 完成,不改写 PR 历史。 + +## 完成标准 + +只有同时满足以下条件,CI 改造才算完成: + +1. 本设计列出的仓库文件完成修改,且没有越过非目标边界。 +2. 本地 workflow、YAML、Makefile、Go 测试、race、lint 和交叉编译验证获得新鲜证据。 +3. 新提交以普通 push 进入 PR #108,不覆盖远端新增提交。 +4. GitHub 上 License、Test and Lint、Race、Build matrix、CodeQL 全部产生可识别的 checks。 +5. Codecov 上传成功,日志不再出现 HTTP 400 或被吞掉的失败。 +6. setup-go 缓存由唯一 action 管理,并在 checkout 后读取 `go.sum`。 +7. README badge 指向 GitHub Actions,旧 Travis 配置已在覆盖核对后删除。 +8. Dependabot 配置被 GitHub 接受。 +9. 最终 Head 与验证基准一致。 +10. PR #108 的 UDP P1 finding 仍单独对账,不因 CI 改造而被误报为已修复。 +11. 收尾报告明确要求轮换或吊销旧 Travis 文件中暴露的 Codecov 和第三方 webhook 凭据,并确认 PR 没有再次复制其值。 From 21f84b85f66fa41fce703f5b7c3b3352aeaf8217 Mon Sep 17 00:00:00 2001 From: "Xin.Zh" Date: Sat, 1 Aug 2026 15:36:43 +0800 Subject: [PATCH 03/24] docs: plan comprehensive GitHub CI hardening --- .../plans/2026-08-01-github-ci-hardening.md | 975 ++++++++++++++++++ 1 file changed, 975 insertions(+) create mode 100644 doc/superpowers/plans/2026-08-01-github-ci-hardening.md diff --git a/doc/superpowers/plans/2026-08-01-github-ci-hardening.md b/doc/superpowers/plans/2026-08-01-github-ci-hardening.md new file mode 100644 index 00000000..ab2fceac --- /dev/null +++ b/doc/superpowers/plans/2026-08-01-github-ci-hardening.md @@ -0,0 +1,975 @@ +# GitHub CI 全面加固实现计划 + +> **面向 AI 代理的工作者:** 必需子技能:使用 `superpowers:subagent-driven-development`(推荐)或 `superpowers:executing-plans` 逐任务实现此计划。使用复选框(`- [ ]`)跟踪步骤;每个任务都要先取得失败或缺口证据,再实施最小改动并验证。不得修改 PR #108 的 Getty 运行时源码,不得修改 branch protection,不得 force push。 + +**目标:** 在 PR #108 中把 Getty 的 GitHub CI 改造成可复现、失败可传播、最小权限、供应链可审计的门禁,并增加 race、真实跨平台构建、CodeQL 与 Dependabot;同时删除已失效且暴露明文凭据的 Travis 配置。 + +**架构:** 主 `CI` workflow 负责 license、格式、单测/coverage、lint、race 和三平台构建;独立 `CodeQL` workflow 负责安全分析;Dependabot 负责 Go module 与 GitHub Actions 更新。Makefile 提供本地与 CI 共用的确定性入口。所有 Action 固定到核验过的完整 commit SHA,Go 缓存只由 `setup-go` 管理,Codecov 使用 OIDC 并在上传失败时使 job 失败。 + +**技术栈:** GitHub Actions、Go 1.25、GNU Make/Bash、`actionlint v1.7.12`、Codecov Action v7 OIDC、GitHub CodeQL Action v3、Dependabot、WSL/Linux 与 GitHub-hosted Ubuntu/Windows/macOS runner。 + +--- + +## 文件结构与职责 + +- 修改 `.github/workflows/github-actions.yml`:主 CI 门禁、最小权限、并发取消、超时、唯一缓存、OIDC coverage、race 与三平台构建。 +- 新增 `.github/workflows/codeql.yml`:Go CodeQL pull request、push 和每周扫描。 +- 新增 `.github/dependabot.yml`:Go module 与 GitHub Actions 的受控自动更新。 +- 修改 `Makefile`:确定性 `test`、只读结果门禁 `check-fmt`、独立 `test-race` 和固定工具版本。 +- 修改 `README.md`:将 Travis badge 替换为 GitHub Actions `CI` badge。 +- 修改 `README_CN.md`:同步英文 README 的 CI badge。 +- 删除 `.travis.yml`:从当前树移除失效 Travis 配置及其中的明文凭据;不复述、不调用凭据。 +- 保留 `doc/superpowers/specs/2026-08-01-github-ci-hardening-design.md`:用户已批准的设计边界和验收依据。 +- 新增本计划:记录逐步实现、验证、提交、push 和 GitHub 实时复检流程。 + +## 固定基准 + +实现开始前重新查询;只有结果仍匹配时才能继续: + +```text +PR: AlexStocks/getty#108 +Base: master@cc9909dc9e0aab1307f553bc2a3d8400161be4e2 +Remote Head branch: codex/fix-issue-97-remaining +Remote Head SHA: 087714342a09f1cc2318bee9d570c2b6ed028044 +Approved design commit: 7539afef7d495ed43f94e0ae488d8da010b9a7f5 +``` + +本次核验的 Action 提交: + +```text +actions/checkout@v7: 3d3c42e5aac5ba805825da76410c181273ba90b1 +actions/setup-go@v6: 924ae3a1cded613372ab5595356fb5720e22ba16 +apache/skywalking-eyes@main: 315732dd4b8d3a015d8d9b91936b935a0b854817 +codecov/codecov-action@v7: fb8b3582c8e4def4969c97caa2f19720cb33a72f +github/codeql-action@v3: a2983b8bed1923f44751c5c43237f479442827b3 +``` + +即使计划中记录了 SHA,实施时也必须再次通过 GitHub API 查询对应版本引用;若上游引用移动,记录新旧值、核验 release/tag 后再更新计划内实际使用值,不得静默使用过期或未知提交。 + +### 任务 1:实时租约与旧门禁缺口基线 + +**文件:** +- 读取:PR #108 GitHub 实时状态 +- 写入证据:`D:\test\github\review\AlexStocks-getty-pr-108\evidence\ci-preflight.json` +- 写入证据:`D:\test\github\review\AlexStocks-getty-pr-108\evidence\ci-old-policy-gaps.txt` + +- [ ] **步骤 1:确认 PR 仍可实施且远端 Head 未漂移** + +在 PowerShell 中运行: + +```powershell +gh pr view 108 --repo AlexStocks/getty ` + --json number,state,headRefName,headRefOid,baseRefName,mergeable,mergeStateStatus,reviewDecision,statusCheckRollup ` + > D:\test\github\review\AlexStocks-getty-pr-108\evidence\ci-preflight.json + +gh pr view 108 --repo AlexStocks/getty ` + --json state,headRefName,headRefOid,baseRefName ` + --jq 'select(.state == "OPEN" and .headRefName == "codex/fix-issue-97-remaining" and .headRefOid == "087714342a09f1cc2318bee9d570c2b6ed028044" and .baseRefName == "master") | .headRefOid' +``` + +预期:第二条命令只输出 `087714342a09f1cc2318bee9d570c2b6ed028044`。没有输出或 SHA 不同即停止,不得 push;先 fetch 并增量审查远端新增提交。 + +- [ ] **步骤 2:确认本地提交链只建立在远端 Head 上** + +```powershell +git fetch origin codex/fix-issue-97-remaining +git merge-base --is-ancestor origin/codex/fix-issue-97-remaining HEAD +git log --oneline origin/codex/fix-issue-97-remaining..HEAD +git status --short --branch +``` + +预期:ancestor 检查退出码为 0;日志仅包含批准设计和本计划提交;工作树干净。 + +- [ ] **步骤 3:保存旧配置缺口的可复验基线** + +```powershell +@( + '--- workflow gaps ---' + (rg -n 'setup-go@|actions/cache@|codecov\.io/bash|@main|permissions:|concurrency:|timeout-minutes:|-race' .github\workflows\github-actions.yml) + '--- makefile gaps ---' + (rg -n 'go env -w|go test|imports-formatter@|check-fmt|test-race' Makefile) + '--- travis references ---' + (rg -n 'travis-ci' README.md README_CN.md) +) | Set-Content -Encoding utf8 D:\test\github\review\AlexStocks-getty-pr-108\evidence\ci-old-policy-gaps.txt +``` + +预期:证据能定位 setup-go 在 checkout 前、第二套 cache、远程 Codecov bash uploader、`@main`、`go env -w`、`@latest` 和 Travis badge。不得把 `.travis.yml` 中的凭据值写入证据。 + +### 任务 2:Makefile 确定性门禁 + +**文件:** +- 修改:`Makefile:24-55` +- 验证副本:`D:\test\github\review\AlexStocks-getty-pr-108\probes\ci-makefile-gates` + +- [ ] **步骤 1:证明当前 Makefile 缺少新入口且会写用户级 Go 配置** + +```bash +wsl.exe --cd /mnt/d/test/github/review/AlexStocks-getty-pr-108/source -- bash -lc \ + "make -n test | tee ../evidence/make-test-before.txt; ! make -n check-fmt; ! make -n test-race" +``` + +预期:`make -n test` 输出包含 `go env -w GOTOOLCHAIN=...`;`check-fmt` 与 `test-race` 报 `No rule to make target`,两个否定命令因此成功。 + +- [ ] **步骤 2:补全 phony、help 与确定性目标** + +将 Makefile 中目标声明和命令调整为: + +```make +.PHONY: help test test-race fmt check-fmt clean lint install-golangci-lint install-imports-formatter + +help: + @echo "Available commands:" + @echo " test - Run unit tests with coverage" + @echo " test-race - Run transport tests with the race detector" + @echo " fmt - Format code" + @echo " check-fmt - Verify that formatting produces no diff" + @echo " lint - Run go vet and golangci-lint" + @echo " clean - Clean generated test files" + +# Run unit tests with a command-scoped toolchain selection. +test: clean + GOTOOLCHAIN=go1.25.0+auto go test ./... -count=1 -coverprofile=coverage.txt -covermode=atomic + +# Run the concurrency-sensitive transport package under the race detector. +test-race: + GOTOOLCHAIN=go1.25.0+auto go test -race ./transport -count=1 + +fmt: install-imports-formatter + go fmt ./... && GOROOT=$(shell go env GOROOT) imports-formatter + +check-fmt: fmt + @git diff --exit-code -- . ':!coverage.txt' || { \ + echo "Formatting changes are required. Run 'make fmt'."; \ + exit 1; \ + } + +# Clean generated test files. +clean: + rm -rf coverage.txt + +# Run golangci-lint. +lint: install-golangci-lint + go vet ./... + golangci-lint run ./... --timeout=10m + +install-golangci-lint: + go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.4.0 + +install-imports-formatter: + go install github.com/dubbogo/tools/cmd/imports-formatter@v1.0.10 +``` + +不要修改 `.DEFAULT_GOAL`、`.SHELLFLAGS` 或当前清理文件范围。`test` 不得调用 `go env -w`。 + +- [ ] **步骤 3:验证命令展开没有全局写入且版本固定** + +```bash +wsl.exe --cd /mnt/d/test/github/review/AlexStocks-getty-pr-108/source -- bash -lc \ + "make -n test test-race install-imports-formatter | tee ../evidence/make-targets-after.txt; ! rg -n 'go env -w|imports-formatter@latest' Makefile" +``` + +预期:输出包含命令级 `GOTOOLCHAIN=go1.25.0+auto`、两个 `-count=1` 和 `imports-formatter@v1.0.10`,反向检索无匹配。 + +- [ ] **步骤 4:提交 Makefile 改动** + +```powershell +git diff --check -- Makefile +git add Makefile +git commit -m "build: make CI checks deterministic" +``` + +预期:只提交 `Makefile`。 + +### 任务 3:重写主 CI workflow + +**文件:** +- 修改:`.github/workflows/github-actions.yml` + +- [ ] **步骤 1:建立会使旧 workflow 失败的政策检查** + +```bash +wsl.exe --cd /mnt/d/test/github/review/AlexStocks-getty-pr-108/source -- bash -lc ' + set -eu + workflow=.github/workflows/github-actions.yml + ! rg -q "actions/cache@|codecov.io/bash|@(main|latest)([[:space:]#]|$)" "$workflow" + first_checkout=$(rg -n "uses: actions/checkout@" "$workflow" | head -1 | cut -d: -f1) + first_setup=$(rg -n "uses: actions/setup-go@" "$workflow" | head -1 | cut -d: -f1) + test "$first_checkout" -lt "$first_setup" +' +``` + +预期:在修改前失败,原因至少包括旧 `actions/cache`、远程 uploader、`@main` 或 setup-go 排在 checkout 前。 + +- [ ] **步骤 2:用完整内容替换主 workflow** + +`.github/workflows/github-actions.yml` 应为: + +```yaml +name: CI + +on: + push: + branches: + - master + pull_request: + branches: + - master + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +jobs: + license: + name: Check License Header + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - name: Check out repository + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 + + - name: Check license headers + uses: apache/skywalking-eyes/header@315732dd4b8d3a015d8d9b91936b935a0b854817 # main, verified 2026-08-01 + with: + config: .licenserc.yaml + mode: check + + test-and-lint: + name: Test and Lint + runs-on: ubuntu-latest + timeout-minutes: 20 + permissions: + contents: read + id-token: write + steps: + - name: Check out repository + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 + + - name: Set up Go + uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6 + with: + go-version-file: go.mod + cache-dependency-path: go.sum + + - name: Verify modules + run: go mod verify + + - name: Check format + run: make check-fmt + + - name: Run unit tests with coverage + run: make test + + - name: Run lint + run: make lint + + - name: Upload coverage + uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7 + with: + use_oidc: true + fail_ci_if_error: true + files: ./coverage.txt + disable_search: true + + race: + name: Race + runs-on: ubuntu-latest + timeout-minutes: 15 + steps: + - name: Check out repository + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 + + - name: Set up Go + uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6 + with: + go-version-file: go.mod + cache-dependency-path: go.sum + + - name: Verify modules + run: go mod verify + + - name: Run race detector + run: make test-race + + build: + name: Build (${{ matrix.os }}) + runs-on: ${{ matrix.os }} + timeout-minutes: 15 + strategy: + fail-fast: false + matrix: + os: + - ubuntu-latest + - windows-latest + - macos-latest + steps: + - name: Check out repository + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 + + - name: Set up Go + uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6 + with: + go-version-file: go.mod + cache-dependency-path: go.sum + + - name: Verify modules + run: go mod verify + + - name: Build packages + run: go build ./... +``` + +不得恢复独立 `actions/cache`、CodeCov bash uploader 或任何可变 Action 引用。License job 不需要显式 `GITHUB_TOKEN` 环境变量;GitHub 会为 Action 提供最小权限 token 上下文。 + +- [ ] **步骤 3:运行政策检查并验证全部 Action 引用** + +```bash +wsl.exe --cd /mnt/d/test/github/review/AlexStocks-getty-pr-108/source -- bash -lc ' + set -eu + workflow=.github/workflows/github-actions.yml + ! rg -n "actions/cache@|codecov.io/bash|@(main|latest)([[:space:]#]|$)" "$workflow" + first_checkout=$(rg -n "uses: actions/checkout@" "$workflow" | head -1 | cut -d: -f1) + first_setup=$(rg -n "uses: actions/setup-go@" "$workflow" | head -1 | cut -d: -f1) + test "$first_checkout" -lt "$first_setup" + python3 - <<"PY" +import pathlib +import re + +for path in pathlib.Path(".github/workflows").glob("*.yml"): + for number, line in enumerate(path.read_text(encoding="utf-8").splitlines(), 1): + match = re.search(r"\buses:\s+\S+@([^\s#]+)", line) + if match and not re.fullmatch(r"[0-9a-f]{40}", match.group(1)): + raise SystemExit(f"{path}:{number}: mutable action ref {match.group(1)}") +PY +' +``` + +预期:退出码 0;无 mutable ref、重复 cache 或远程 shell uploader。 + +- [ ] **步骤 4:提交主 workflow** + +```powershell +git diff --check -- .github/workflows/github-actions.yml +git add .github/workflows/github-actions.yml +git commit -m "ci: harden tests race and platform builds" +``` + +预期:只提交主 workflow。 + +### 任务 4:新增 CodeQL workflow + +**文件:** +- 新增:`.github/workflows/codeql.yml` + +- [ ] **步骤 1:证明当前仓库没有 CodeQL workflow** + +```powershell +Test-Path .github\workflows\codeql.yml +rg -n 'github/codeql-action' .github\workflows +``` + +预期:`Test-Path` 输出 `False`,`rg` 无匹配并返回 1。 + +- [ ] **步骤 2:新增固定 SHA、最小权限的 Go CodeQL workflow** + +```yaml +name: CodeQL + +on: + push: + branches: + - master + pull_request: + branches: + - master + schedule: + - cron: '30 1 * * 1' + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +jobs: + analyze: + name: Analyze (Go) + runs-on: ubuntu-latest + timeout-minutes: 20 + permissions: + actions: read + contents: read + security-events: write + steps: + - name: Check out repository + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 + + - name: Initialize CodeQL + uses: github/codeql-action/init@a2983b8bed1923f44751c5c43237f479442827b3 # v3 + with: + languages: go + build-mode: autobuild + + - name: Build + uses: github/codeql-action/autobuild@a2983b8bed1923f44751c5c43237f479442827b3 # v3 + + - name: Analyze + uses: github/codeql-action/analyze@a2983b8bed1923f44751c5c43237f479442827b3 # v3 +``` + +- [ ] **步骤 3:用 actionlint 验证两个 workflow** + +```bash +wsl.exe --cd /mnt/d/test/github/review/AlexStocks-getty-pr-108/source -- bash -lc \ + "GOTOOLCHAIN=go1.25.0+auto go run github.com/rhysd/actionlint/cmd/actionlint@v1.7.12 -color" +``` + +预期:退出码 0,无 workflow 语法、表达式、shell 或 action 输入错误。若 actionlint 对 action metadata 的远程可见性有限,不能把该限制当成 GitHub 运行成功证据;仍须等待远端 check。 + +- [ ] **步骤 4:提交 CodeQL workflow** + +```powershell +git add .github/workflows/codeql.yml +git commit -m "ci: add CodeQL analysis" +``` + +预期:只提交 `codeql.yml`。 + +### 任务 5:新增 Dependabot 配置并严格解析 YAML + +**文件:** +- 新增:`.github/dependabot.yml` +- 新增临时验证器:`D:\test\github\review\AlexStocks-getty-pr-108\probes\validate-dependabot-yaml.go` + +- [ ] **步骤 1:证明当前仓库没有 Dependabot 配置** + +```powershell +Test-Path .github\dependabot.yml +``` + +预期:输出 `False`。 + +- [ ] **步骤 2:新增受控更新配置** + +```yaml +version: 2 +updates: + - package-ecosystem: gomod + directory: / + target-branch: master + schedule: + interval: weekly + open-pull-requests-limit: 5 + commit-message: + prefix: deps + + - package-ecosystem: github-actions + directory: / + target-branch: master + schedule: + interval: monthly + open-pull-requests-limit: 3 + commit-message: + prefix: ci +``` + +- [ ] **步骤 3:用仓库已有 YAML 依赖执行严格解析和结构断言** + +在镜像 `probes` 目录通过 `apply_patch` 创建以下一次性验证器,不提交到 PR: + +```go +package main + +import ( + "fmt" + "os" + + "gopkg.in/yaml.v2" +) + +type config struct { + Version int `yaml:"version"` + Updates []struct { + Ecosystem string `yaml:"package-ecosystem"` + Directory string `yaml:"directory"` + Target string `yaml:"target-branch"` + Schedule struct { + Interval string `yaml:"interval"` + } `yaml:"schedule"` + Limit int `yaml:"open-pull-requests-limit"` + CommitMessage struct { + Prefix string `yaml:"prefix"` + } `yaml:"commit-message"` + } `yaml:"updates"` +} + +func main() { + data, err := os.ReadFile(".github/dependabot.yml") + if err != nil { + panic(err) + } + var cfg config + if err := yaml.UnmarshalStrict(data, &cfg); err != nil { + panic(err) + } + if cfg.Version != 2 || len(cfg.Updates) != 2 { + panic(fmt.Sprintf("unexpected Dependabot structure: %+v", cfg)) + } + want := map[string]struct { + interval string + limit int + prefix string + }{ + "gomod": {interval: "weekly", limit: 5, prefix: "deps"}, + "github-actions": {interval: "monthly", limit: 3, prefix: "ci"}, + } + for _, update := range cfg.Updates { + expected, ok := want[update.Ecosystem] + if !ok || update.Directory != "/" || update.Target != "master" || + update.Schedule.Interval != expected.interval || update.Limit != expected.limit || + update.CommitMessage.Prefix != expected.prefix { + panic(fmt.Sprintf("unexpected update entry: %+v", update)) + } + delete(want, update.Ecosystem) + } + if len(want) != 0 { + panic(fmt.Sprintf("missing ecosystems: %+v", want)) + } +} +``` + +运行: + +```bash +wsl.exe --cd /mnt/d/test/github/review/AlexStocks-getty-pr-108/source -- bash -lc \ + "GOTOOLCHAIN=go1.25.0+auto go run ../probes/validate-dependabot-yaml.go" +``` + +预期:退出码 0,无输出;严格解析会拒绝未知字段。 + +- [ ] **步骤 4:提交 Dependabot 配置** + +```powershell +git add .github/dependabot.yml +git commit -m "ci: add Dependabot updates" +``` + +预期:只提交 `.github/dependabot.yml`;临时验证器留在镜像 `probes`,不进入 Git index。 + +### 任务 6:替换 badge 并删除 Travis 当前树配置 + +**文件:** +- 修改:`README.md:5` +- 修改:`README_CN.md:5` +- 删除:`.travis.yml` + +- [ ] **步骤 1:再次确认 Travis 不在当前 PR checks 中** + +```powershell +$checks = gh pr view 108 --repo AlexStocks/getty --json statusCheckRollup --jq '.statusCheckRollup[].name' +$checks +if ($checks -match '(?i)travis') { throw 'Travis check is still active; stop deletion' } +``` + +预期:现有 check 名称中没有 Travis。若出现 Travis,停止删除并重新评估迁移覆盖。 + +- [ ] **步骤 2:只比较 Travis 命令范围,不输出敏感值** + +```powershell +Select-String -Path .travis.yml -Pattern '^language:|^os:|^go:|^install:|^script:|^after_success:|^\s*-\s+(go|make)\s' | + ForEach-Object { '{0}:{1}' -f $_.LineNumber,$_.Line.Trim() } +``` + +预期:有效范围为格式、测试/coverage 和 race;新主 workflow/Makefile 已覆盖这些门禁,并额外增加 lint、模块验证与跨平台构建。不得运行、复制或打印 uploader/webhook 行。 + +- [ ] **步骤 3:替换两个 README badge** + +把两个 README 的 Travis badge 行替换为: + +```markdown +[![CI](https://github.com/AlexStocks/getty/actions/workflows/github-actions.yml/badge.svg?branch=master)](https://github.com/AlexStocks/getty/actions/workflows/github-actions.yml) +``` + +- [ ] **步骤 4:删除 `.travis.yml`** + +使用 `apply_patch` 删除整个文件。删除仅从当前树移除凭据,不能清除 Git 历史;不得在 commit message 或 PR 评论中复制任何 token。 + +- [ ] **步骤 5:验证当前树没有 Travis 引用和已知敏感配置键** + +```powershell +if (Test-Path .travis.yml) { throw '.travis.yml still exists' } +if (rg -n 'travis-ci' README.md README_CN.md) { throw 'Travis badge remains' } +rg -n 'actions/workflows/github-actions\.yml/badge\.svg' README.md README_CN.md +``` + +预期:前两个检查通过;最后一条在两个 README 各匹配一次。 + +- [ ] **步骤 6:提交 README 与 Travis 清理** + +```powershell +git add README.md README_CN.md .travis.yml +git commit -m "docs: replace Travis CI references" +``` + +预期:提交包含两个 badge 替换和 `.travis.yml` 删除,不包含其他文件。 + +### 任务 7:本地静态、变异和 Go 验证 + +**文件:** +- 读取:全部实施文件 +- 验证副本:`D:\test\github\review\AlexStocks-getty-pr-108\probes\ci-check-fmt-clean` +- 验证副本:`D:\test\github\review\AlexStocks-getty-pr-108\probes\ci-check-fmt-mutation` +- 输出:`D:\test\github\review\AlexStocks-getty-pr-108\evidence\ci-local-validation-*.txt` + +- [ ] **步骤 1:对全部 workflow 运行 actionlint** + +```bash +wsl.exe --cd /mnt/d/test/github/review/AlexStocks-getty-pr-108/source -- bash -lc \ + "GOTOOLCHAIN=go1.25.0+auto go run github.com/rhysd/actionlint/cmd/actionlint@v1.7.12 -color |& tee ../evidence/ci-local-validation-actionlint.txt" +``` + +预期:退出码 0,无诊断。 + +- [ ] **步骤 2:执行 workflow 供应链政策检查** + +```bash +wsl.exe --cd /mnt/d/test/github/review/AlexStocks-getty-pr-108/source -- bash -lc ' + set -eu + ! rg -n "actions/cache@|codecov.io/bash|curl[[:space:]].*\|[[:space:]]*(ba)?sh|@(main|master|latest)([[:space:]#]|$)" .github/workflows Makefile + python3 - <<"PY" +import pathlib +import re + +for path in pathlib.Path(".github/workflows").glob("*.yml"): + for number, line in enumerate(path.read_text(encoding="utf-8").splitlines(), 1): + match = re.search(r"\buses:\s+\S+@([^\s#]+)", line) + if match and not re.fullmatch(r"[0-9a-f]{40}", match.group(1)): + raise SystemExit(f"{path}:{number}: mutable action ref {match.group(1)}") +PY +' +``` + +预期:退出码 0。注释中的版本标签允许存在,但 `uses:` 的实际 ref 必须是完整 SHA。 + +- [ ] **步骤 3:在干净独立 worktree 证明 `check-fmt` 绿灯** + +```powershell +git worktree add --detach D:\test\github\review\AlexStocks-getty-pr-108\probes\ci-check-fmt-clean HEAD +wsl.exe --cd /mnt/d/test/github/review/AlexStocks-getty-pr-108/probes/ci-check-fmt-clean -- bash -lc ` + 'PATH=/home/alex/bin/go1.25/bin:$PATH make check-fmt |& tee ../../evidence/ci-local-validation-check-fmt-clean.txt' +``` + +预期:退出码 0,probe worktree 的 `git status --porcelain` 为空。主 source worktree 不运行写入式 formatter。 + +- [ ] **步骤 4:用格式变异证明 `check-fmt` 会阻断** + +```powershell +git worktree add --detach D:\test\github\review\AlexStocks-getty-pr-108\probes\ci-check-fmt-mutation HEAD +``` + +在 mutation worktree 中通过 `apply_patch` 将一个已跟踪 Go 函数签名改成 gofmt 会修复的格式,例如: + +```diff +-func udpReadBufferSize(maxMsgLen int32) int { ++func udpReadBufferSize( maxMsgLen int32 ) int { +``` + +然后运行: + +```powershell +wsl.exe --cd /mnt/d/test/github/review/AlexStocks-getty-pr-108/probes/ci-check-fmt-mutation -- bash -lc ` + 'PATH=/home/alex/bin/go1.25/bin:$PATH make check-fmt |& tee ../../evidence/ci-local-validation-check-fmt-mutation.txt; test ${PIPESTATUS[0]} -ne 0' +``` + +预期:formatter 修复变异后,`git diff --exit-code` 使 `make check-fmt` 非零退出;日志输出具体 diff 和修复提示。该 probe 不提交、不 push。 + +- [ ] **步骤 5:运行模块、测试、race 与 lint** + +```bash +wsl.exe --cd /mnt/d/test/github/review/AlexStocks-getty-pr-108/source -- bash -lc ' + set -o pipefail + export PATH=/home/alex/bin/go1.25/bin:$PATH + go version + go mod verify + make test + make test-race + make lint +' |& tee /mnt/d/test/github/review/AlexStocks-getty-pr-108/evidence/ci-local-validation-go.txt +``` + +预期:Go 为 `go1.25.1 linux/amd64`;所有命令退出码 0;`coverage.txt` 是唯一预期生成文件。若失败,先按 `superpowers:systematic-debugging` 区分 PR 新增、Base 既有和环境问题,不得跳过失败。 + +- [ ] **步骤 6:执行跨编译补充验证** + +```bash +wsl.exe --cd /mnt/d/test/github/review/AlexStocks-getty-pr-108/source -- bash -lc ' + set -eu -o pipefail + export PATH=/home/alex/bin/go1.25/bin:$PATH + GOOS=windows GOARCH=amd64 go build ./... + GOOS=darwin GOARCH=amd64 go build ./... + CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build ./... + CGO_ENABLED=0 GOOS=linux GOARCH=riscv64 go build ./... +' |& tee /mnt/d/test/github/review/AlexStocks-getty-pr-108/evidence/ci-local-validation-cross-build.txt +``` + +预期:退出码 0。交叉编译只是补充证据,不能替代远端 Windows/macOS runner。 + +- [ ] **步骤 7:检查 diff、index、意外文件和敏感值回流** + +```powershell +git diff --check origin/codex/fix-issue-97-remaining...HEAD +git status --short --branch +git diff --name-status origin/codex/fix-issue-97-remaining...HEAD +git diff --stat origin/codex/fix-issue-97-remaining...HEAD +git grep -n -I -E 'travis-ci|codecov\.io/bash|go env -w GOTOOLCHAIN|imports-formatter@latest' -- . ':!doc/superpowers/specs/2026-08-01-github-ci-hardening-design.md' ':!doc/superpowers/plans/2026-08-01-github-ci-hardening.md' +``` + +预期:除 `coverage.txt` 外 source worktree无非预期生成文件;实际实施文件严格匹配设计。最后一条仅允许历史说明文档中的证据性文字,不允许生产配置出现旧模式。检查输出时不得复述任何已删除凭据。 + +### 任务 8:实现复核与必要修正 + +**文件:** +- 复核:本计划列出的全部变更文件 +- 可能修改:只限 CI、Makefile、README 与设计/计划中已授权的文件 + +- [ ] **步骤 1:逐项对照批准设计的完成标准** + +核对: + +```text +[ ] 唯一 Go cache owner 是 setup-go v6 +[ ] checkout 位于 setup-go 前 +[ ] Test and Lint 具有 contents:read + id-token:write,其他 job 无多余权限 +[ ] Codecov 使用 OIDC、显式 coverage 文件、fail_ci_if_error +[ ] Race 独立执行 transport race +[ ] Build matrix 使用真实 ubuntu/windows/macos runner +[ ] CodeQL 是独立 workflow,权限最小且 Action 固定 SHA +[ ] Dependabot 只有 gomod 与 github-actions 两个受控入口 +[ ] Makefile 无 go env -w、无浮动工具版本,测试禁用缓存 +[ ] 两个 README 使用 GitHub Actions badge +[ ] .travis.yml 从当前树删除 +[ ] 未修改运行时 Go 源码、branch protection 或 GitHub ruleset +``` + +- [ ] **步骤 2:审阅提交边界和提交消息** + +```powershell +git log --reverse --stat --oneline origin/codex/fix-issue-97-remaining..HEAD +git show --check --stat HEAD +``` + +预期:每个提交单一目的;没有凭据、生成二进制、`coverage.txt`、probe 或 evidence 进入提交。 + +- [ ] **步骤 3:如果复核发现 CI 配置问题,先取得失败证据再修正** + +只允许修正本计划范围内文件。每个修正运行直接相关的 actionlint、YAML、Makefile 或 Go 验证后,以普通提交记录: + +例如 actionlint 发现 CodeQL build mode 配置错误时,只暂存该 workflow 并使用具体消息: + +```powershell +git add .github/workflows/codeql.yml +git commit -m "ci: fix CodeQL build configuration" +``` + +不得 amend 已提交历史,不得用 force push。 + +### 任务 9:push 前最终实时复检与普通 push + +**文件:** +- 读取:PR #108 实时状态、远端分支 SHA、本地提交链 +- 不修改:branch protection、ruleset、review threads + +- [ ] **步骤 1:执行 `verification-before-completion` 新鲜验证** + +至少重新运行: + +```bash +wsl.exe --cd /mnt/d/test/github/review/AlexStocks-getty-pr-108/source -- bash -lc ' + set -eu -o pipefail + export PATH=/home/alex/bin/go1.25/bin:$PATH + GOTOOLCHAIN=go1.25.0+auto go run github.com/rhysd/actionlint/cmd/actionlint@v1.7.12 + go mod verify + go test ./... -count=1 + go test -race ./transport -count=1 + go vet ./... +' +``` + +以及: + +```powershell +git diff --check origin/codex/fix-issue-97-remaining...HEAD +git status --short --branch +``` + +预期:全部退出码 0;不得用较早日志替代该步骤的新鲜结果。 + +- [ ] **步骤 2:再次获取远端 Head 并执行显式 lease** + +```powershell +git fetch origin codex/fix-issue-97-remaining +$remoteHead = git rev-parse origin/codex/fix-issue-97-remaining +$liveHead = gh pr view 108 --repo AlexStocks/getty --json state,headRefOid --jq 'select(.state == "OPEN") | .headRefOid' +if ($remoteHead -ne '087714342a09f1cc2318bee9d570c2b6ed028044' -or $liveHead -ne $remoteHead) { + throw "Remote PR Head drifted; stop before push" +} +git merge-base --is-ancestor $remoteHead HEAD +``` + +预期:远端 Git ref、GitHub PR Head 和实施基准三者相同;ancestor 检查成功。 + +- [ ] **步骤 3:普通 push 当前分支** + +```powershell +git push origin HEAD:codex/fix-issue-97-remaining +``` + +预期:普通 fast-forward push 成功;不得添加 `--force` 或 `--force-with-lease`。 + +### 任务 10:等待并核验 GitHub 新 checks + +**文件:** +- 写入证据:`D:\test\github\review\AlexStocks-getty-pr-108\evidence\ci-post-push-pr.json` +- 写入证据:`D:\test\github\review\AlexStocks-getty-pr-108\evidence\ci-post-push-checks.txt` +- 写入证据:各 workflow/job 的日志片段,仅保存无敏感值的诊断 + +- [ ] **步骤 1:获取 push 后新 Head 和 workflow runs** + +```powershell +$newHead = gh pr view 108 --repo AlexStocks/getty --json headRefOid --jq .headRefOid +gh run list --repo AlexStocks/getty --branch codex/fix-issue-97-remaining --limit 20 ` + --json databaseId,workflowName,headSha,status,conclusion,url,createdAt ` + --jq ".[] | select(.headSha == \"$newHead\")" +``` + +预期:至少出现 `CI` 和 `CodeQL` 的新运行,Head 等于刚 push 的本地 `HEAD`。 + +- [ ] **步骤 2:等待当前 Head 的所有新运行完成** + +通过 API 逐个等待上一步返回的 run ID: + +```powershell +$headSha = gh pr view 108 --repo AlexStocks/getty --json headRefOid --jq .headRefOid +$runIds = gh run list --repo AlexStocks/getty --branch codex/fix-issue-97-remaining --limit 20 ` + --json databaseId,headSha --jq ".[] | select(.headSha == \"$headSha\") | .databaseId" +foreach ($runId in $runIds) { + gh run watch $runId --repo AlexStocks/getty --exit-status + if ($LASTEXITCODE -ne 0) { throw "GitHub Actions run $runId failed" } +} +``` + +预期:全部成功。若失败,下载精确失败 job 日志,按系统化调试区分配置、源码基线和外部服务问题;不得为了变绿而跳过门禁。 + +- [ ] **步骤 3:核对主 CI job 和真实 runner** + +```powershell +$headSha = gh pr view 108 --repo AlexStocks/getty --json headRefOid --jq .headRefOid +$ciRunId = gh run list --repo AlexStocks/getty --branch codex/fix-issue-97-remaining --workflow CI --limit 20 ` + --json databaseId,headSha --jq ".[] | select(.headSha == \"$headSha\") | .databaseId" | Select-Object -First 1 +if (-not $ciRunId) { throw 'CI run for current Head not found' } +gh run view $ciRunId --repo AlexStocks/getty --json headSha,status,conclusion,jobs,url ` + > D:\test\github\review\AlexStocks-getty-pr-108\evidence\ci-post-push-checks.txt +``` + +必须确认实际 job 包含并成功: + +```text +Check License Header +Test and Lint +Race +Build (ubuntu-latest) +Build (windows-latest) +Build (macos-latest) +``` + +同时从 `Test and Lint` 日志确认:setup-go 在 checkout 后读取 `go.mod`/`go.sum`,没有第二个 `actions/cache`,coverage 上传没有 HTTP 400、tokenless upload 错误或被吞掉的失败。 + +- [ ] **步骤 4:核对 CodeQL result 上传** + +```powershell +$headSha = gh pr view 108 --repo AlexStocks/getty --json headRefOid --jq .headRefOid +$codeqlRunId = gh run list --repo AlexStocks/getty --branch codex/fix-issue-97-remaining --workflow CodeQL --limit 20 ` + --json databaseId,headSha --jq ".[] | select(.headSha == \"$headSha\") | .databaseId" | Select-Object -First 1 +if (-not $codeqlRunId) { throw 'CodeQL run for current Head not found' } +gh run view $codeqlRunId --repo AlexStocks/getty --log-failed +gh run view $codeqlRunId --repo AlexStocks/getty --json headSha,status,conclusion,jobs,url +``` + +预期:`Analyze (Go)` 成功,Head 与 PR 最新 Head 一致。若 GitHub 安全设置阻止上传,记录准确错误和所需外部设置,不把它伪装成源码缺陷。 + +- [ ] **步骤 5:保存最终 PR 状态并复核 Head 未变化** + +```powershell +gh pr view 108 --repo AlexStocks/getty ` + --json number,state,headRefName,headRefOid,baseRefName,mergeable,mergeStateStatus,reviewDecision,statusCheckRollup ` + > D:\test\github\review\AlexStocks-getty-pr-108\evidence\ci-post-push-pr.json +git rev-parse HEAD +gh pr view 108 --repo AlexStocks/getty --json headRefOid --jq .headRefOid +``` + +预期:本地 HEAD 与 GitHub PR Head 完全一致。 + +### 任务 11:最终 Review、required checks 建议与收尾对账 + +**文件:** +- 读取:当前 PR 完整 files/diff、review comments、checks、branch protection/rulesets +- 可能更新:`D:\test\github\arch-practice\alg\openclaw\review-experience.md` 或 `review-AlexStocks-getty.md`,仅当本轮产生经过验证的新经验 + +- [ ] **步骤 1:重新保存完整 PR 文件列表和 Diff** + +```powershell +gh api repos/AlexStocks/getty/pulls/108/files --paginate ` + > D:\test\github\review\AlexStocks-getty-pr-108\files.json +gh pr diff 108 --repo AlexStocks/getty ` + > D:\test\github\review\AlexStocks-getty-pr-108\pr.diff +``` + +逐文件增量审查 CI 改动,确认没有运行时源码漂移。若发现本轮 CI 变更引入的可定位问题,先本地修复、重新验证、普通 push,再重复任务 10;不要给自己的 CI 改动留下明知的 P0/P1。 + +- [ ] **步骤 2:检索 review threads 并保留 UDP P1 独立状态** + +重新获取所有 review comments/threads,确认已存在的 UDP invalid-input 和生产调用路径测试缺口未因 CI 改造被误报为解决。除非用户另行授权,不修改 `transport/session.go`、`transport/session_test.go`,也不 resolve 对应线程。 + +- [ ] **步骤 3:读取而不修改 branch protection/rulesets** + +根据 push 后真实 check 名称输出建议 required checks;预计为: + +```text +Check License Header +Test and Lint +Race +Build (ubuntu-latest) +Build (windows-latest) +Build (macos-latest) +Analyze (Go) +``` + +实际名称以 GitHub API 返回为准。本任务禁止调用 branch protection/ruleset 写 API;需要用户单独授权。 + +- [ ] **步骤 4:明确外部凭据收尾** + +收尾必须要求仓库维护者在外部系统轮换或吊销旧 `.travis.yml` 中暴露的 Codecov upload token 和第三方 webhook access tokens。只说明凭据类型和风险,不复述值。删除当前文件不等于清除 Git 历史。 + +- [ ] **步骤 5:按工业 Review 协议输出最终对账** + +最终报告必须包含: + +```text +结论:PR #108 因现存 UDP P1 仍为 🚫 不可 Merge;CI 改造本身的 checks 结果单独列明。 +PR、镜像路径、WSL 路径、最终 Head、Base、分类。 +gh 与 rg/fd/grep/ls 调用次数。 +actionlint、YAML、Makefile、测试、race、lint、跨平台构建结果。 +远端 CI/CodeQL/Codecov 结果和 URL。 +已提交行内评论及去重说明。 +所有本轮 commit 和普通 push 结果。 +未修改 branch protection;给出建议 required checks 并请求单独授权。 +必须轮换/吊销的凭据类型。 +所有 evidence/probe/worktree 路径及是否可删除。 +Review 经验复利:实际记录内容,或“无新增可复用经验”。 +``` + +只有全部 CI 改造验证通过时,才能声称“CI 改造完成”;不得把这一结论扩展成 PR #108 可 Merge,因为 UDP P1 仍未修复。 From 70dc8a15fb7e5959f8703069604c7053074fbeb0 Mon Sep 17 00:00:00 2001 From: "Xin.Zh" Date: Sat, 1 Aug 2026 15:37:38 +0800 Subject: [PATCH 04/24] docs: fix CI plan whitespace --- doc/superpowers/plans/2026-08-01-github-ci-hardening.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/doc/superpowers/plans/2026-08-01-github-ci-hardening.md b/doc/superpowers/plans/2026-08-01-github-ci-hardening.md index ab2fceac..fe7e96f9 100644 --- a/doc/superpowers/plans/2026-08-01-github-ci-hardening.md +++ b/doc/superpowers/plans/2026-08-01-github-ci-hardening.md @@ -195,7 +195,7 @@ wsl.exe --cd /mnt/d/test/github/review/AlexStocks-getty-pr-108/source -- bash -l first_checkout=$(rg -n "uses: actions/checkout@" "$workflow" | head -1 | cut -d: -f1) first_setup=$(rg -n "uses: actions/setup-go@" "$workflow" | head -1 | cut -d: -f1) test "$first_checkout" -lt "$first_setup" -' +' ``` 预期:在修改前失败,原因至少包括旧 `actions/cache`、远程 uploader、`@main` 或 setup-go 排在 checkout 前。 @@ -344,7 +344,7 @@ for path in pathlib.Path(".github/workflows").glob("*.yml"): if match and not re.fullmatch(r"[0-9a-f]{40}", match.group(1)): raise SystemExit(f"{path}:{number}: mutable action ref {match.group(1)}") PY -' +' ``` 预期:退出码 0;无 mutable ref、重复 cache 或远程 shell uploader。 @@ -650,7 +650,7 @@ for path in pathlib.Path(".github/workflows").glob("*.yml"): if match and not re.fullmatch(r"[0-9a-f]{40}", match.group(1)): raise SystemExit(f"{path}:{number}: mutable action ref {match.group(1)}") PY -' +' ``` 预期:退出码 0。注释中的版本标签允许存在,但 `uses:` 的实际 ref 必须是完整 SHA。 @@ -796,7 +796,7 @@ wsl.exe --cd /mnt/d/test/github/review/AlexStocks-getty-pr-108/source -- bash -l go test ./... -count=1 go test -race ./transport -count=1 go vet ./... -' +' ``` 以及: From 84c155761336a01dd45163cb6df26eb14080351c Mon Sep 17 00:00:00 2001 From: "Xin.Zh" Date: Sat, 1 Aug 2026 16:47:32 +0800 Subject: [PATCH 05/24] build: make CI checks deterministic Keep test execution reproducible without mutating the user's persistent Go environment, and expose race and formatting verification as explicit Make targets. Constraint: Preserve the existing default goal, shell flags, formatting command, lint versions, and coverage-only clean scope. Confidence: High; the old targets failed for the expected reasons, dry-run expansion matches the requested commands, and both WSL test targets pass with an isolated GOENV. Scope-risk: Limited to local and CI Makefile entry points. Tested: make dry runs; WSL Go 1.25.1 make test; WSL Go 1.25.1 make test-race; isolated GOENV absence check; git diff --check. Not-tested: The write-capable make check-fmt target was intentionally not executed in the source worktree; only its dry-run expansion was verified. Co-authored-by: OmX Signed-off-by: Xin.Zh --- Makefile | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/Makefile b/Makefile index e56de5c9..4a4bed9b 100644 --- a/Makefile +++ b/Makefile @@ -21,24 +21,33 @@ MAKEFLAGS += --warn-undefined-variables MAKEFLAGS += --no-builtin-rules MAKEFLAGS += --no-print-directory -.PHONY: help test fmt clean lint +.PHONY: help test test-race fmt check-fmt clean lint install-golangci-lint install-imports-formatter help: @echo "Available commands:" - @echo " test - Run unit tests" - @echo " clean - Clean test generate files" + @echo " test - Run unit tests with coverage" + @echo " test-race - Run transport race tests" @echo " fmt - Format code" + @echo " check-fmt - Verify code formatting" @echo " lint - Run golangci-lint" + @echo " clean - Clean test generate files" # Run unit tests test: clean - # For go 1.25.0 - go env -w GOTOOLCHAIN=go1.25.0+auto - go test ./... -coverprofile=coverage.txt -covermode=atomic + GOTOOLCHAIN=go1.25.0+auto go test ./... -count=1 -coverprofile=coverage.txt -covermode=atomic + +test-race: + GOTOOLCHAIN=go1.25.0+auto go test -race ./transport -count=1 fmt: install-imports-formatter go fmt ./... && GOROOT=$(shell go env GOROOT) imports-formatter +check-fmt: fmt + @git diff --exit-code -- . ':!coverage.txt' || { \ + echo "Formatting changes are required. Run 'make fmt'."; \ + exit 1; \ + } + # Clean test generate files clean: rm -rf coverage.txt @@ -52,4 +61,4 @@ install-golangci-lint: go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.4.0 install-imports-formatter: - go install github.com/dubbogo/tools/cmd/imports-formatter@latest + go install github.com/dubbogo/tools/cmd/imports-formatter@v1.0.10 From 41726941eaa4b94e280e6c6337d60dbfcbe9a9fd Mon Sep 17 00:00:00 2001 From: "Xin.Zh" Date: Sat, 1 Aug 2026 17:28:24 +0800 Subject: [PATCH 06/24] ci: harden tests race and platform builds Replace mutable and redundant action setup with least-privilege, cancellation-aware jobs for license checks, tests and lint, race detection, and cross-platform builds. Constraint: Keep triggers limited to master, pin every action to a verified full commit SHA, grant OIDC only to the Codecov job, and do not change source or build scripts. Confidence: High; the old workflow failed the policy probe for nine expected gaps, while the rewritten workflow passes the same policy, exact-SHA Codecov input validation, and actionlint. Scope-risk: Limited to .github/workflows/github-actions.yml and the CI job topology it defines. Tested: WSL Go 1.25.1 actionlint v1.7.12; policy red/green checks; Codecov action.yml and README input verification; git diff --check; cached diff check. Not-tested: The GitHub-hosted jobs were not dispatched because this task does not authorize push. Co-authored-by: OmX Signed-off-by: Xin.Zh --- .github/workflows/github-actions.yml | 134 ++++++++++++++++++--------- 1 file changed, 91 insertions(+), 43 deletions(-) diff --git a/.github/workflows/github-actions.yml b/.github/workflows/github-actions.yml index 8e53cb22..cfa04113 100644 --- a/.github/workflows/github-actions.yml +++ b/.github/workflows/github-actions.yml @@ -2,66 +2,114 @@ name: CI on: push: - branches: [ master ] + branches: + - master pull_request: - branches: "*" + branches: + - master + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true jobs: license: name: Check License Header runs-on: ubuntu-latest + timeout-minutes: 10 steps: - - uses: actions/checkout@v4 + - name: Checkout + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 - name: Check License Header - uses: apache/skywalking-eyes/header@main #NOSONAR - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + uses: apache/skywalking-eyes/header@315732dd4b8d3a015d8d9b91936b935a0b854817 with: config: .licenserc.yaml mode: check - CI: - name: CI + test-and-lint: + name: Test and Lint + runs-on: ubuntu-latest + timeout-minutes: 20 + permissions: + contents: read + id-token: write + steps: + - name: Checkout + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 + + - name: Set up Go + uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 + with: + go-version-file: go.mod + cache-dependency-path: go.sum + + - name: Verify Go Modules + run: go mod verify + + - name: Check Code Format + run: make check-fmt + + - name: Unit Test + run: make test + + - name: Lint + run: make lint + + - name: Upload Coverage + uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f + with: + use_oidc: true + fail_ci_if_error: true + files: ./coverage.txt + disable_search: true + + race: + name: Race + runs-on: ubuntu-latest + timeout-minutes: 15 + steps: + - name: Checkout + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 + + - name: Set up Go + uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 + with: + go-version-file: go.mod + cache-dependency-path: go.sum + + - name: Verify Go Modules + run: go mod verify + + - name: Race Test + run: make test-race + + build: + name: Build (${{ matrix.os }}) runs-on: ${{ matrix.os }} + timeout-minutes: 15 strategy: - # If you want to matrix build , you can append the following list. + fail-fast: false matrix: - go_version: - - '1.25' os: - ubuntu-latest - + - windows-latest + - macos-latest steps: + - name: Checkout + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 + + - name: Set up Go + uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 + with: + go-version-file: go.mod + cache-dependency-path: go.sum + + - name: Verify Go Modules + run: go mod verify - - name: Set up Go ${{ matrix.go_version }} - uses: actions/setup-go@v5 - with: - go-version: ${{ matrix.go_version }} - - - name: Check out code into the Go module directory - uses: actions/checkout@v4 - - - name: Cache Go Dependence - # ref: https://github.com/actions/cache/blob/main/examples.md#go---module - uses: actions/cache@v4 - with: - # Cache, works only on Linux - path: | - ~/.cache/go-build - ~/go/pkg/mod - key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} - # An ordered list of keys to use for restoring the cache if no cache hit occurred for key - restore-keys: ${{ runner.os }}-go- - - - name: Check Code Format - run: make fmt && git status && [[ -z `git status -s` ]] - - - name: Unit Test - run: make test - - - name: Lint - run: make lint - - - name: Coverage - run: bash <(curl -s https://codecov.io/bash) + - name: Build + run: go build ./... From 67bb4e235a431183af3a80b6a17fec71c6e1bd0d Mon Sep 17 00:00:00 2001 From: "Xin.Zh" Date: Sat, 1 Aug 2026 17:58:37 +0800 Subject: [PATCH 07/24] ci: isolate Codecov OIDC upload Move coverage authentication into a dedicated job that only downloads the coverage artifact and invokes the pinned Codecov action, keeping PR-controlled Go and Make execution outside the OIDC boundary. Constraint: Preserve the test, lint, race, and build commands; grant id-token write only to the coverage job; pin artifact actions and the Codecov CLI; do not change source or planning files. Confidence: High; the prior workflow fails the targeted security policy, while the isolated workflow passes the OIDC boundary, action pinning, timeout, and actionlint checks. Scope-risk: Limited to the CI coverage handoff and adds a one-day coverage artifact between jobs. Tested: targeted security policy red and green checks; official Action and Codecov release metadata; WSL Go 1.25.1 actionlint v1.7.12; git diff and cached diff checks. Not-tested: GitHub-hosted artifact transfer and Codecov upload will run only after an authorized push. Co-authored-by: OmX Signed-off-by: Xin.Zh --- .github/workflows/github-actions.yml | 26 +++++++++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/.github/workflows/github-actions.yml b/.github/workflows/github-actions.yml index cfa04113..4fc4fdba 100644 --- a/.github/workflows/github-actions.yml +++ b/.github/workflows/github-actions.yml @@ -34,9 +34,6 @@ jobs: name: Test and Lint runs-on: ubuntu-latest timeout-minutes: 20 - permissions: - contents: read - id-token: write steps: - name: Checkout uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 @@ -59,9 +56,32 @@ jobs: - name: Lint run: make lint + - name: Upload Coverage Artifact + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a + with: + name: coverage + path: coverage.txt + if-no-files-found: error + retention-days: 1 + + coverage: + name: Upload Coverage + needs: test-and-lint + runs-on: ubuntu-latest + timeout-minutes: 10 + permissions: + contents: read + id-token: write + steps: + - name: Download Coverage Artifact + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c + with: + name: coverage + - name: Upload Coverage uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f with: + version: v11.3.1 use_oidc: true fail_ci_if_error: true files: ./coverage.txt From 19994850e1b376f61ec456e25923b7b1b5407d7d Mon Sep 17 00:00:00 2001 From: "Xin.Zh" Date: Sat, 1 Aug 2026 18:09:29 +0800 Subject: [PATCH 08/24] ci: minimize coverage permissions Remove the unnecessary contents read grant from the isolated coverage upload job so its explicit permissions contain only the OIDC capability required by Codecov. Constraint: Do not change any job, step, action version, trigger, or command beyond the coverage permissions map. Confidence: High; the baseline fails the exact-permission policy, while the one-line change passes that policy and actionlint. Scope-risk: Limited to removing repository contents access from a job that does not checkout or call the GitHub contents API. Tested: minimum-permission policy red and green checks; WSL Go 1.25.1 actionlint v1.7.12; git diff and cached diff checks. Not-tested: GitHub-hosted execution remains assigned to the later workflow-run validation task. Co-authored-by: OmX Signed-off-by: Xin.Zh --- .github/workflows/github-actions.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/github-actions.yml b/.github/workflows/github-actions.yml index 4fc4fdba..79ddaf87 100644 --- a/.github/workflows/github-actions.yml +++ b/.github/workflows/github-actions.yml @@ -70,7 +70,6 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 10 permissions: - contents: read id-token: write steps: - name: Download Coverage Artifact From 709cdb85939ca20d6478ba24a38a733d4bcfbf8a Mon Sep 17 00:00:00 2001 From: "Xin.Zh" Date: Sat, 1 Aug 2026 18:28:50 +0800 Subject: [PATCH 09/24] ci: add CodeQL analysis Add branch and scheduled Go CodeQL analysis using the verified v3 action commit, minimal permissions, and the current official autobuild mode. Constraint: Limit the commit to .github/workflows/codeql.yml and do not push. Confidence: High; the policy checks and actionlint v1.7.12 pass in WSL. Scope-risk: Low; this adds one isolated CI workflow. Tested: WSL go1.25.1 actionlint v1.7.12 .github/workflows/*.yml; CodeQL policy checks; git diff --check. Not-tested: GitHub-hosted CodeQL execution has not run because this commit is not pushed. Co-authored-by: OmX Signed-off-by: Xin.Zh --- .github/workflows/codeql.yml | 40 ++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 .github/workflows/codeql.yml diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 00000000..899deef4 --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,40 @@ +name: CodeQL + +on: + push: + branches: + - master + pull_request: + branches: + - master + schedule: + - cron: '30 1 * * 1' + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +jobs: + analyze: + name: Analyze (Go) + runs-on: ubuntu-latest + timeout-minutes: 20 + permissions: + actions: read + contents: read + security-events: write + steps: + - name: Check out repository + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 + + - name: Initialize CodeQL + uses: github/codeql-action/init@a2983b8bed1923f44751c5c43237f479442827b3 # v3 + with: + languages: go + build-mode: autobuild + + - name: Analyze + uses: github/codeql-action/analyze@a2983b8bed1923f44751c5c43237f479442827b3 # v3 From c1dd4d91ce00d2f5cddfed90a4a6e5a90e63f27a Mon Sep 17 00:00:00 2001 From: "Xin.Zh" Date: Sat, 1 Aug 2026 18:56:00 +0800 Subject: [PATCH 10/24] ci: add Dependabot updates Configure weekly Go module updates and monthly GitHub Actions updates with bounded pull request limits and scoped commit prefixes. Constraint: Limit the commit to .github/dependabot.yml and do not enable auto-merge, approvals, registries, ignore rules, groups, or additional ecosystems. Confidence: High; strict YAML parsing and exact policy assertions pass under fixed WSL Go 1.25.0. Scope-risk: Low; this adds only Dependabot scheduling metadata targeting master. Tested: WSL Go 1.25.0 yaml.UnmarshalStrict validator; forbidden-key and exact-ecosystem policy; git diff --check. Not-tested: GitHub-hosted Dependabot scheduling will begin only after an authorized push. Co-authored-by: OmX Signed-off-by: Xin.Zh --- .github/dependabot.yml | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 .github/dependabot.yml diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 00000000..85d0bb59 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,19 @@ +version: 2 +updates: + - package-ecosystem: gomod + directory: / + target-branch: master + schedule: + interval: weekly + open-pull-requests-limit: 5 + commit-message: + prefix: deps + + - package-ecosystem: github-actions + directory: / + target-branch: master + schedule: + interval: monthly + open-pull-requests-limit: 3 + commit-message: + prefix: ci From 6a17b6efec87a903bc28572ae2cb4303a0c4e98b Mon Sep 17 00:00:00 2001 From: "Xin.Zh" Date: Sat, 1 Aug 2026 19:24:34 +0800 Subject: [PATCH 11/24] docs: replace Travis CI references Replace the obsolete Travis badges with the repository GitHub Actions CI badge and remove the superseded Travis configuration now that formatting, tests, coverage, and race checks are provided by the maintained workflow. Constraint: Limit the commit to README.md, README_CN.md, and .travis.yml; do not change other badges or CI behavior. Confidence: High; the red/green policy, current-tree credential-shape scan, staged-scope gate, and diff checks pass. Scope-risk: Low; this removes the current-tree legacy configuration only. Git history retains prior content, so historical credentials still require external rotation or revocation. Tested: exact badge policy red and green checks; final workflow and Makefile command coverage; current-tree credential-shape filename and content scan; git diff --check; cached scope and diff checks. Not-tested: GitHub-hosted badge rendering and workflow execution require an authorized push; external credential rotation is outside this commit. Co-authored-by: OmX Signed-off-by: Xin.Zh --- .travis.yml | 41 ----------------------------------------- README.md | 2 +- README_CN.md | 4 ++-- 3 files changed, 3 insertions(+), 44 deletions(-) delete mode 100644 .travis.yml diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 362c1969..00000000 --- a/.travis.yml +++ /dev/null @@ -1,41 +0,0 @@ -# Licensed to the Apache Software Foundation (ASF) under one or more -# contributor license agreements. See the NOTICE file distributed with -# this work for additional information regarding copyright ownership. -# The ASF licenses this file to You under the Apache License, Version 2.0 -# (the "License"); you may not use this file except in compliance with -# the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -language: go - -os: - - linux - -go: - - "1.13" - -env: - - GO111MODULE=on - -install: true -script: - - echo 'start license check' - - sh before_validate_license.sh - - chmod u+x /tmp/tools/license/license-header-checker - - /tmp/tools/license/license-header-checker -v -a -r -i vendor /tmp/tools/license/license.txt . go && [[ -z `git status -s` ]] - - go fmt ./... && [[ -z `git status -s` ]] - - go mod vendor && go test $(go list ./... | grep -v vendor | grep -v examples) -coverprofile=coverage.txt -covermode=atomic - -after_success: - - bash <(curl -s https://codecov.io/bash) -t "26520766-2aa8-4b82-8e44-f778d718b4d9" - -notifications: - webhooks: https://oapi.dingtalk.com/robot/send?access_token=75f4f1ec3868508aa89e5a5d6f9d342216809df3ebc8a78c8ae8722848e06166 - webhooks: https://oapi.dingtalk.com/robot/send?access_token=072b74afbf3e746adeac1edecd5823cd24625a97eac42862476046e3057fb5ab \ No newline at end of file diff --git a/README.md b/README.md index 265833a1..55c37558 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ *a netty like asynchronous network I/O library* -[![Build Status](https://travis-ci.org/AlexStocks/getty.svg?branch=master)](https://travis-ci.org/AlexStocks/getty) +[![CI](https://github.com/AlexStocks/getty/actions/workflows/github-actions.yml/badge.svg?branch=master)](https://github.com/AlexStocks/getty/actions/workflows/github-actions.yml) [![codecov](https://codecov.io/gh/AlexStocks/getty/branch/master/graph/badge.svg)](https://codecov.io/gh/AlexStocks/getty) [![go.dev reference](https://img.shields.io/badge/go.dev-reference-007d9c?logo=go&logoColor=white&style=flat-square)](https://pkg.go.dev/github.com/AlexStocks/getty?tab=doc) [![Go Report Card](https://goreportcard.com/badge/github.com/AlexStocks/getty)](https://goreportcard.com/report/github.com/AlexStocks/getty) diff --git a/README_CN.md b/README_CN.md index 2732a767..b2b6f366 100644 --- a/README_CN.md +++ b/README_CN.md @@ -2,7 +2,7 @@ *一个类似 Netty 的异步网络 I/O 库* -[![Build Status](https://travis-ci.org/AlexStocks/getty.svg?branch=master)](https://travis-ci.org/AlexStocks/getty) +[![CI](https://github.com/AlexStocks/getty/actions/workflows/github-actions.yml/badge.svg?branch=master)](https://github.com/AlexStocks/getty/actions/workflows/github-actions.yml) [![codecov](https://codecov.io/gh/AlexStocks/getty/branch/master/graph/badge.svg)](https://codecov.io/gh/AlexStocks/getty) [![go.dev reference](https://img.shields.io/badge/go.dev-reference-007d9c?logo=go&logoColor=white&style=flat-square)](https://pkg.go.dev/github.com/AlexStocks/getty?tab=doc) [![Go Report Card](https://goreportcard.com/badge/github.com/AlexStocks/getty)](https://goreportcard.com/report/github.com/AlexStocks/getty) @@ -554,4 +554,4 @@ session.AddCloseCallback([]int{1, 2, 3}, "key", callback) // 记录日 ## 许可证 -Apache 许可证 2.0 \ No newline at end of file +Apache 许可证 2.0 From 7c02e88c403c94ac6ad902753cf0177fdac56c8b Mon Sep 17 00:00:00 2001 From: "Xin.Zh" Date: Sat, 1 Aug 2026 21:45:29 +0800 Subject: [PATCH 12/24] docs: align CI design with reviewed implementation Align the approved CI design and reproducible implementation plan with the formally reviewed five-job workflow, isolated coverage artifact handoff, current CodeQL shape, and final validation policy. Constraint: Limit the commit to the two CI design and plan documents; do not modify workflows, Makefile, Dependabot, README files, Go source, branch protection, or the independent UDP P1. Confidence: High; workflow snippets exactly match the final files, policy counts and OIDC assertions match the reviewed implementation, and document consistency checks pass. Scope-risk: Low; this changes documentation only and preserves the approved goals, non-goals, and no-push boundary. Tested: git diff --check; balanced Markdown fences; placeholder and stale-conflict scans; sensitive-value pattern scan; exact main CI and CodeQL snippet comparison; WSL actionlint v1.7.12 on all current workflows. Not-tested: GitHub-hosted artifact transfer, Codecov upload, CodeQL execution, and branch-protection checks require a later authorized push and remote run. Co-authored-by: OmX Signed-off-by: Xin.Zh --- .../plans/2026-08-01-github-ci-hardening.md | 195 ++++++++++++------ .../2026-08-01-github-ci-hardening-design.md | 53 +++-- 2 files changed, 171 insertions(+), 77 deletions(-) diff --git a/doc/superpowers/plans/2026-08-01-github-ci-hardening.md b/doc/superpowers/plans/2026-08-01-github-ci-hardening.md index fe7e96f9..0baa9614 100644 --- a/doc/superpowers/plans/2026-08-01-github-ci-hardening.md +++ b/doc/superpowers/plans/2026-08-01-github-ci-hardening.md @@ -4,15 +4,15 @@ **目标:** 在 PR #108 中把 Getty 的 GitHub CI 改造成可复现、失败可传播、最小权限、供应链可审计的门禁,并增加 race、真实跨平台构建、CodeQL 与 Dependabot;同时删除已失效且暴露明文凭据的 Travis 配置。 -**架构:** 主 `CI` workflow 负责 license、格式、单测/coverage、lint、race 和三平台构建;独立 `CodeQL` workflow 负责安全分析;Dependabot 负责 Go module 与 GitHub Actions 更新。Makefile 提供本地与 CI 共用的确定性入口。所有 Action 固定到核验过的完整 commit SHA,Go 缓存只由 `setup-go` 管理,Codecov 使用 OIDC 并在上传失败时使 job 失败。 +**架构:** 主 `CI` workflow 包含 license、Test and Lint、隔离的 Upload Coverage、race 和三平台构建 5 个逻辑 job;独立 `CodeQL` workflow 负责安全分析;Dependabot 负责 Go module 与 GitHub Actions 更新。Makefile 提供本地与 CI 共用的确定性入口。所有 Action 固定到核验过的完整 commit SHA,Go 缓存只由 `setup-go` 管理,coverage 先通过 artifact 在 job 间传递,再由只拥有 OIDC 权限的上传 job 调用 Codecov,并在任一阶段失败时使 check 失败。 -**技术栈:** GitHub Actions、Go 1.25、GNU Make/Bash、`actionlint v1.7.12`、Codecov Action v7 OIDC、GitHub CodeQL Action v3、Dependabot、WSL/Linux 与 GitHub-hosted Ubuntu/Windows/macOS runner。 +**技术栈:** GitHub Actions、Go 1.25、GNU Make/Bash、`actionlint v1.7.12`、Codecov Action v7 OIDC(CLI 固定 `v11.3.1`)、GitHub CodeQL Action v3、Dependabot、WSL/Linux 与 GitHub-hosted Ubuntu/Windows/macOS runner。 --- ## 文件结构与职责 -- 修改 `.github/workflows/github-actions.yml`:主 CI 门禁、最小权限、并发取消、超时、唯一缓存、OIDC coverage、race 与三平台构建。 +- 修改 `.github/workflows/github-actions.yml`:主 CI 门禁、最小权限、并发取消、超时、唯一缓存、coverage artifact 与隔离 OIDC 上传、race 与三平台构建。 - 新增 `.github/workflows/codeql.yml`:Go CodeQL pull request、push 和每周扫描。 - 新增 `.github/dependabot.yml`:Go module 与 GitHub Actions 的受控自动更新。 - 修改 `Makefile`:确定性 `test`、只读结果门禁 `check-fmt`、独立 `test-race` 和固定工具版本。 @@ -40,12 +40,16 @@ Approved design commit: 7539afef7d495ed43f94e0ae488d8da010b9a7f5 actions/checkout@v7: 3d3c42e5aac5ba805825da76410c181273ba90b1 actions/setup-go@v6: 924ae3a1cded613372ab5595356fb5720e22ba16 apache/skywalking-eyes@main: 315732dd4b8d3a015d8d9b91936b935a0b854817 +actions/upload-artifact@v7.0.1: 043fb46d1a93c77aae656e7c1c64a875d1fc6a0a +actions/download-artifact@v8.0.1: 3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c codecov/codecov-action@v7: fb8b3582c8e4def4969c97caa2f19720cb33a72f github/codeql-action@v3: a2983b8bed1923f44751c5c43237f479442827b3 ``` 即使计划中记录了 SHA,实施时也必须再次通过 GitHub API 查询对应版本引用;若上游引用移动,记录新旧值、核验 release/tag 后再更新计划内实际使用值,不得静默使用过期或未知提交。 +正式质量审查已同时核对 `upload-artifact` v7.0.1 和 `download-artifact` v8.0.1 的 GitHub release 元数据与精确 tag ref,上述 SHA 均双向一致。`download-artifact` v8.0.1 tag 下 README 第 48 行仍保留一处 `@v7` 示例,但同一 release/ref 明确指向 v8.0.1 的上述提交;该示例按文档滞后处理,不覆盖 release/ref 证据。 + ### 任务 1:实时租约与旧门禁缺口基线 **文件:** @@ -228,11 +232,11 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 10 steps: - - name: Check out repository - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 + - name: Checkout + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 - - name: Check license headers - uses: apache/skywalking-eyes/header@315732dd4b8d3a015d8d9b91936b935a0b854817 # main, verified 2026-08-01 + - name: Check License Header + uses: apache/skywalking-eyes/header@315732dd4b8d3a015d8d9b91936b935a0b854817 with: config: .licenserc.yaml mode: check @@ -241,34 +245,53 @@ jobs: name: Test and Lint runs-on: ubuntu-latest timeout-minutes: 20 - permissions: - contents: read - id-token: write steps: - - name: Check out repository - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 + - name: Checkout + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 - name: Set up Go - uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6 + uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 with: go-version-file: go.mod cache-dependency-path: go.sum - - name: Verify modules + - name: Verify Go Modules run: go mod verify - - name: Check format + - name: Check Code Format run: make check-fmt - - name: Run unit tests with coverage + - name: Unit Test run: make test - - name: Run lint + - name: Lint run: make lint - - name: Upload coverage - uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7 + - name: Upload Coverage Artifact + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a + with: + name: coverage + path: coverage.txt + if-no-files-found: error + retention-days: 1 + + coverage: + name: Upload Coverage + needs: test-and-lint + runs-on: ubuntu-latest + timeout-minutes: 10 + permissions: + id-token: write + steps: + - name: Download Coverage Artifact + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c + with: + name: coverage + + - name: Upload Coverage + uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f with: + version: v11.3.1 use_oidc: true fail_ci_if_error: true files: ./coverage.txt @@ -279,19 +302,19 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 15 steps: - - name: Check out repository - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 + - name: Checkout + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 - name: Set up Go - uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6 + uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 with: go-version-file: go.mod cache-dependency-path: go.sum - - name: Verify modules + - name: Verify Go Modules run: go mod verify - - name: Run race detector + - name: Race Test run: make test-race build: @@ -306,19 +329,19 @@ jobs: - windows-latest - macos-latest steps: - - name: Check out repository - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 + - name: Checkout + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 - name: Set up Go - uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6 + uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 with: go-version-file: go.mod cache-dependency-path: go.sum - - name: Verify modules + - name: Verify Go Modules run: go mod verify - - name: Build packages + - name: Build run: go build ./... ``` @@ -338,7 +361,9 @@ wsl.exe --cd /mnt/d/test/github/review/AlexStocks-getty-pr-108/source -- bash -l import pathlib import re -for path in pathlib.Path(".github/workflows").glob("*.yml"): +paths = list(pathlib.Path(".github/workflows").glob("*.yml")) +paths += list(pathlib.Path(".github/workflows").glob("*.yaml")) +for path in paths: for number, line in enumerate(path.read_text(encoding="utf-8").splitlines(), 1): match = re.search(r"\buses:\s+\S+@([^\s#]+)", line) if match and not re.fullmatch(r"[0-9a-f]{40}", match.group(1)): @@ -347,7 +372,7 @@ PY ' ``` -预期:退出码 0;无 mutable ref、重复 cache 或远程 shell uploader。 +预期:退出码 0;无 mutable ref、重复 cache 或远程 shell uploader。主 CI 应恰好包含 5 个逻辑 job 和 11 个 `uses:`;`id-token: write` 只位于 `Upload Coverage`,Codecov `version` 为 `v11.3.1`。 - [ ] **步骤 4:提交主 workflow** @@ -414,13 +439,12 @@ jobs: languages: go build-mode: autobuild - - name: Build - uses: github/codeql-action/autobuild@a2983b8bed1923f44751c5c43237f479442827b3 # v3 - - name: Analyze uses: github/codeql-action/analyze@a2983b8bed1923f44751c5c43237f479442827b3 # v3 ``` +当前官方形状为 `init` 中声明 `build-mode: autobuild` 后直接执行 `analyze`。旧的 `init(build-mode: autobuild) -> autobuild -> analyze` 三步写法仍兼容,但显式 `autobuild` 与 init 的 build mode 重复,属于冗余,不应保留在可复现计划中。 + - [ ] **步骤 3:用 actionlint 验证两个 workflow** ```bash @@ -628,8 +652,12 @@ git commit -m "docs: replace Travis CI references" - [ ] **步骤 1:对全部 workflow 运行 actionlint** ```bash -wsl.exe --cd /mnt/d/test/github/review/AlexStocks-getty-pr-108/source -- bash -lc \ - "GOTOOLCHAIN=go1.25.0+auto go run github.com/rhysd/actionlint/cmd/actionlint@v1.7.12 -color |& tee ../evidence/ci-local-validation-actionlint.txt" +wsl.exe --cd /mnt/d/test/github/review/AlexStocks-getty-pr-108/source -- bash -lc ' + shopt -s nullglob + workflows=(.github/workflows/*.yml .github/workflows/*.yaml) + test "${#workflows[@]}" -gt 0 + GOTOOLCHAIN=go1.25.0+auto go run github.com/rhysd/actionlint/cmd/actionlint@v1.7.12 -color "${workflows[@]}" +' ``` 预期:退出码 0,无诊断。 @@ -644,7 +672,9 @@ wsl.exe --cd /mnt/d/test/github/review/AlexStocks-getty-pr-108/source -- bash -l import pathlib import re -for path in pathlib.Path(".github/workflows").glob("*.yml"): +paths = list(pathlib.Path(".github/workflows").glob("*.yml")) +paths += list(pathlib.Path(".github/workflows").glob("*.yaml")) +for path in paths: for number, line in enumerate(path.read_text(encoding="utf-8").splitlines(), 1): match = re.search(r"\buses:\s+\S+@([^\s#]+)", line) if match and not re.fullmatch(r"[0-9a-f]{40}", match.group(1)): @@ -653,7 +683,7 @@ PY ' ``` -预期:退出码 0。注释中的版本标签允许存在,但 `uses:` 的实际 ref 必须是完整 SHA。 +预期:退出码 0。注释中的版本标签允许存在,但 `uses:` 的实际 ref 必须是完整 SHA。当前最终文件应为 6 个逻辑 job、14 个 `uses:`;主 CI 应为 5 个逻辑 job;`id-token: write` 只出现于 `Upload Coverage`;Codecov `version` 必须为 `v11.3.1`。 - [ ] **步骤 3:在干净独立 worktree 证明 `check-fmt` 绿灯** @@ -734,7 +764,8 @@ git grep -n -I -E 'travis-ci|codecov\.io/bash|go env -w GOTOOLCHAIN|imports-form **文件:** - 复核:本计划列出的全部变更文件 -- 可能修改:只限 CI、Makefile、README 与设计/计划中已授权的文件 +- 可能修改:只限设计和计划文档,用于同步已通过正式质量审查的实现偏差 +- 阻塞边界:若 CI、Makefile、Dependabot、README 或 Go 源码仍有 Critical/Important 问题,报告 `BLOCKED`,不得在本任务中自行修改实现文件 - [ ] **步骤 1:逐项对照批准设计的完成标准** @@ -743,16 +774,19 @@ git grep -n -I -E 'travis-ci|codecov\.io/bash|go env -w GOTOOLCHAIN|imports-form ```text [ ] 唯一 Go cache owner 是 setup-go v6 [ ] checkout 位于 setup-go 前 -[ ] Test and Lint 具有 contents:read + id-token:write,其他 job 无多余权限 -[ ] Codecov 使用 OIDC、显式 coverage 文件、fail_ci_if_error +[ ] 主 CI 恰好 5 个逻辑 job:License、Test and Lint、Upload Coverage、Race、Build matrix +[ ] Test and Lint 无 OIDC,使用 upload-artifact v7.0.1 上传 coverage(missing=error、retention=1) +[ ] Upload Coverage needs test-and-lint,权限只有 id-token:write,不 checkout/setup-go/run +[ ] Upload Coverage 使用 download-artifact v8.0.1 后调用 Codecov;CLI 固定 v11.3.1,显式 coverage 文件并启用 fail_ci_if_error [ ] Race 独立执行 transport race [ ] Build matrix 使用真实 ubuntu/windows/macos runner -[ ] CodeQL 是独立 workflow,权限最小且 Action 固定 SHA +[ ] CodeQL 是独立 workflow,形状为 init(build-mode: autobuild) -> analyze,权限最小且 Action 固定 SHA [ ] Dependabot 只有 gomod 与 github-actions 两个受控入口 [ ] Makefile 无 go env -w、无浮动工具版本,测试禁用缓存 [ ] 两个 README 使用 GitHub Actions badge [ ] .travis.yml 从当前树删除 [ ] 未修改运行时 Go 源码、branch protection 或 GitHub ruleset +[ ] 全部 workflow 合计 6 个逻辑 job、14 个 uses;id-token:write 只出现 1 次 ``` - [ ] **步骤 2:审阅提交边界和提交消息** @@ -766,13 +800,14 @@ git show --check --stat HEAD - [ ] **步骤 3:如果复核发现 CI 配置问题,先取得失败证据再修正** -只允许修正本计划范围内文件。每个修正运行直接相关的 actionlint、YAML、Makefile 或 Go 验证后,以普通提交记录: +只允许修正设计和计划文档,使代码/YAML 片段、job/uses 数量、权限断言、Codecov 版本和 required checks 建议与最终实现一致。每次修正后运行文档直接相关的 `git diff --check`、代码围栏/占位符检查和旧冲突模式扫描,并对全部 workflow 重跑 actionlint。 -例如 actionlint 发现 CodeQL build mode 配置错误时,只暂存该 workflow 并使用具体消息: +只暂存两份文档并使用具体消息: ```powershell -git add .github/workflows/codeql.yml -git commit -m "ci: fix CodeQL build configuration" +git add doc/superpowers/specs/2026-08-01-github-ci-hardening-design.md ` + doc/superpowers/plans/2026-08-01-github-ci-hardening.md +git commit -m "docs: align CI design with reviewed implementation" ``` 不得 amend 已提交历史,不得用 force push。 @@ -781,32 +816,68 @@ git commit -m "ci: fix CodeQL build configuration" **文件:** - 读取:PR #108 实时状态、远端分支 SHA、本地提交链 +- 写入证据:`D:\test\github\review\AlexStocks-getty-pr-108\evidence\ci-pre-push-validation-raw.txt`,只能用 `apply_patch` 保存完整原始输出 - 不修改:branch protection、ruleset、review threads - [ ] **步骤 1:执行 `verification-before-completion` 新鲜验证** -至少重新运行: +必须重新运行 actionlint、Go 门禁和四个 cross-build,并让每条命令的边界、当前 HEAD、Go 版本、开始/结束时间和退出码出现在原始输出中。actionlint 必须同时覆盖 `.yml` 与 `.yaml`: ```bash wsl.exe --cd /mnt/d/test/github/review/AlexStocks-getty-pr-108/source -- bash -lc ' - set -eu -o pipefail + set -u -o pipefail export PATH=/home/alex/bin/go1.25/bin:$PATH - GOTOOLCHAIN=go1.25.0+auto go run github.com/rhysd/actionlint/cmd/actionlint@v1.7.12 - go mod verify - go test ./... -count=1 - go test -race ./transport -count=1 - go vet ./... + + overall=0 + run_check() { + label=$1 + shift + printf "=== BEGIN %s ===\n" "$label" + printf "STARTED_AT=%s\n" "$(date --iso-8601=seconds)" + printf "HEAD=%s\n" "$(git rev-parse HEAD)" + printf "GO_VERSION=%s\n" "$(go version)" + printf "COMMAND=" + printf "%q " "$@" + printf "\n" + "$@" + rc=$? + printf "EXIT=%d\n" "$rc" + printf "ENDED_AT=%s\n" "$(date --iso-8601=seconds)" + printf "=== END %s ===\n" "$label" + if [ "$rc" -ne 0 ]; then overall=1; fi + } + + run_actionlint() { + shopt -s nullglob + workflows=(.github/workflows/*.yml .github/workflows/*.yaml) + test "${#workflows[@]}" -gt 0 || return 1 + GOTOOLCHAIN=go1.25.0+auto go run github.com/rhysd/actionlint/cmd/actionlint@v1.7.12 "${workflows[@]}" + } + + printf "VALIDATION_STARTED_AT=%s\n" "$(date --iso-8601=seconds)" + printf "VALIDATION_HEAD=%s\n" "$(git rev-parse HEAD)" + printf "VALIDATION_GO_VERSION=%s\n" "$(go version)" + run_check actionlint run_actionlint + run_check go-mod-verify go mod verify + run_check go-test go test ./... -count=1 + run_check go-test-race go test -race ./transport -count=1 + run_check go-vet go vet ./... + run_check make-lint make lint + run_check build-windows-amd64 env GOOS=windows GOARCH=amd64 go build ./... + run_check build-darwin-amd64 env GOOS=darwin GOARCH=amd64 go build ./... + run_check build-linux-arm64 env CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build ./... + run_check build-linux-riscv64 env CGO_ENABLED=0 GOOS=linux GOARCH=riscv64 go build ./... + run_check git-diff-check git diff --check origin/codex/fix-issue-97-remaining...HEAD + run_check git-status git status --short --branch + printf "VALIDATION_ENDED_AT=%s\n" "$(date --iso-8601=seconds)" + printf "VALIDATION_EXIT=%d\n" "$overall" + exit "$overall" ' ``` -以及: - -```powershell -git diff --check origin/codex/fix-issue-97-remaining...HEAD -git status --short --branch -``` +保留上述完整终端原始输出,包括测试、race、vet、lint 和 build 的所有正文。随后用 `apply_patch` 新增或完整替换 `ci-pre-push-validation-raw.txt`;不得用 `>`、`tee`、脚本摘要或人工改写后的“pass”列表代替 raw。若原始输出包含意外敏感值,先停止并报告,不得把该值写入证据。 -预期:全部退出码 0;不得用较早日志替代该步骤的新鲜结果。 +预期:每个边界的 `EXIT=0` 且最终 `VALIDATION_EXIT=0`;记录的 `VALIDATION_HEAD` 与待 push HEAD 一致。不得用较早日志替代该步骤的新鲜结果。 - [ ] **步骤 2:再次获取远端 Head 并执行显式 lease** @@ -880,13 +951,14 @@ gh run view $ciRunId --repo AlexStocks/getty --json headSha,status,conclusion,jo ```text Check License Header Test and Lint +Upload Coverage Race Build (ubuntu-latest) Build (windows-latest) Build (macos-latest) ``` -同时从 `Test and Lint` 日志确认:setup-go 在 checkout 后读取 `go.mod`/`go.sum`,没有第二个 `actions/cache`,coverage 上传没有 HTTP 400、tokenless upload 错误或被吞掉的失败。 +同时从 `Test and Lint` 日志确认:setup-go 在 checkout 后读取 `go.mod`/`go.sum`,没有第二个 `actions/cache`,并成功上传名为 `coverage` 的 artifact。再从 `Upload Coverage` 日志确认:只下载该 artifact,Codecov CLI 为 `v11.3.1`,没有 HTTP 400、tokenless upload 错误或被吞掉的失败。 - [ ] **步骤 4:核对 CodeQL result 上传** @@ -941,6 +1013,7 @@ gh pr diff 108 --repo AlexStocks/getty ` ```text Check License Header Test and Lint +Upload Coverage Race Build (ubuntu-latest) Build (windows-latest) diff --git a/doc/superpowers/specs/2026-08-01-github-ci-hardening-design.md b/doc/superpowers/specs/2026-08-01-github-ci-hardening-design.md index 57518d81..680e4639 100644 --- a/doc/superpowers/specs/2026-08-01-github-ci-hardening-design.md +++ b/doc/superpowers/specs/2026-08-01-github-ci-hardening-design.md @@ -28,7 +28,7 @@ PR #108 的 CI 日志确认了以下问题: 2. 消除重复缓存、全局 Go 配置写入和浮动工具版本。 3. 将 workflow 权限限制在每个 job 实际需要的最小集合。 4. 增加跨平台构建、CodeQL 和 Dependabot,覆盖 Go 源码、GitHub Actions 与依赖维护。 -5. 固定第三方 Action 到核验过的完整 commit SHA,并保留版本注释,兼顾供应链可审计性和后续升级。 +5. 固定第三方 Action 到核验过的完整 commit SHA,并在受审查文档中保留版本与 SHA 映射,兼顾供应链可审计性和后续升级。 6. 清理已经被 GitHub Actions 取代的 Travis CI 展示与配置。 7. 保持 Getty 公开 Go API 和运行时行为不变。 @@ -81,11 +81,11 @@ concurrency: cancel-in-progress: true ``` -`concurrency` 用于取消同一 PR 或同一 ref 的旧运行,避免过期 Head 继续占用 runner。所有 job 都设置显式 `timeout-minutes`,防止网络测试、工具下载或 race 测试无限挂起。 +`concurrency` 用于取消同一 PR 或同一 ref 的旧运行,避免过期 Head 继续占用 runner。主 CI 最终包含 5 个逻辑 job:`Check License Header`、`Test and Lint`、`Upload Coverage`、`Race` 和 `Build` matrix。所有 job 都设置显式 `timeout-minutes`,防止网络测试、工具下载或 race 测试无限挂起。 ### 2. Action 固定策略 -所有第三方 Action 使用完整 commit SHA,并在同行注释来源版本,例如: +所有第三方 Action 使用完整 commit SHA。版本与 SHA 的对应关系必须在设计或计划中集中记录;workflow 同行可以保留版本注释,但不得用可变 tag 替代 SHA,例如: ```yaml uses: actions/checkout@ # v7 @@ -96,11 +96,15 @@ uses: actions/checkout@ # v7 - `actions/checkout@v7` - `actions/setup-go@v6` - `apache/skywalking-eyes/header` 当前核验提交 +- `actions/upload-artifact@v7.0.1` +- `actions/download-artifact@v8.0.1` - `codecov/codecov-action@v7` - `github/codeql-action@v3` Dependabot 的 `github-actions` ecosystem 负责后续 Action 更新。不得使用 `@main`,也不得在同一 workflow 中同时保留 major tag 与完整 SHA 两套引用方式。 +本轮正式质量审查确认:`actions/upload-artifact@v7.0.1` 的 release 与 tag ref 都指向 `043fb46d1a93c77aae656e7c1c64a875d1fc6a0a`;`actions/download-artifact@v8.0.1` 的 release 与 tag ref 都指向 `3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c`。`download-artifact` v8.0.1 tag 下 README 仍有一处 `@v7` 示例,属于示例文本滞后;release 元数据和精确 tag ref 一致,因此实现以 release/ref 指向的完整 SHA 为准,不因 README 的单处旧示例降级到 v7。 + ### 3. License job License job: @@ -123,7 +127,7 @@ License job 不获得 `id-token`、`security-events` 或写入仓库内容的权 4. Check format 5. Unit tests and coverage 6. Lint -7. Upload coverage +7. Upload coverage artifact Setup Go 使用: @@ -135,24 +139,35 @@ with: 删除独立 `actions/cache` step,让 `setup-go` 成为 Go module/build cache 的唯一 owner。 -模块验证执行 `go mod verify`。格式检查执行 `make check-fmt`。测试执行 `make test`,生成 `coverage.txt`。Lint 执行 `make lint`。 +模块验证执行 `go mod verify`。格式检查执行 `make check-fmt`。测试执行 `make test`,生成 `coverage.txt`。Lint 执行 `make lint`。随后使用固定 SHA 的 `actions/upload-artifact@v7.0.1` 上传 artifact:名称为 `coverage`,路径为 `coverage.txt`,文件缺失时报错,保留 1 天。 + +`Test and Lint` 继承 workflow 顶层的 `contents: read`,不声明也不获得 `id-token: write`。OIDC 权限只授予后续隔离的 `Upload Coverage` job。 -### 5. Codecov OIDC +### 5. Coverage artifact 与隔离的 Codecov OIDC -Coverage 上传使用固定到完整 SHA 的 `codecov/codecov-action`,不再下载并执行 Codecov bash uploader。 +新增 `Upload Coverage` job,`needs: test-and-lint`。它不 checkout 源码、不 setup Go,也不执行 shell 命令;只下载前一 job 产生的 `coverage` artifact,再调用固定到完整 SHA 的 `codecov/codecov-action`。这样只有 coverage 上传边界获得 OIDC 权限,不再下载并执行 Codecov bash uploader。 -`Test and Lint` job 的权限为: +`Upload Coverage` job 的权限只有: ```yaml permissions: - contents: read id-token: write ``` +artifact 下载固定为: + +```yaml +- name: Download Coverage Artifact + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c + with: + name: coverage +``` + Codecov 参数至少包含: ```yaml with: + version: v11.3.1 use_oidc: true fail_ci_if_error: true files: ./coverage.txt @@ -164,6 +179,7 @@ with: - 上传失败必须使 job 失败。 - 不依赖 `CODECOV_TOKEN` 仓库 secret。 - 只上传明确生成的 `coverage.txt`,不扫描工作区中的其他 coverage 文件。 +- `id-token: write` 只能出现在 `Upload Coverage` job;`Test and Lint`、License、Race 和 Build 均不得获得 OIDC。 - push 后必须核对日志中没有 HTTP 400、tokenless upload 错误或被吞掉的非零状态。 ### 6. Race job @@ -203,7 +219,7 @@ Makefile 调整为可由本地和 CI 复用的显式门禁: - `test` 不再执行 `go env -w`,改为命令级 `GOTOOLCHAIN`。 - `test` 增加 `-count=1`,同时保留 atomic coverage 输出。 - 新增 `test-race`,只运行 `./transport` 的 race 测试。 -- 新增 `check-fmt`:执行项目格式化命令后,用 `git diff --exit-code --quiet` 检测是否产生差异,并输出差异文件。 +- 新增 `check-fmt`:执行项目格式化命令后,用 `git diff --exit-code -- . ':!coverage.txt'` 检测并输出格式化差异,同时排除测试生成的 coverage 文件。 - `imports-formatter` 从 `@latest` 固定到本次已验证的 `v1.0.10`。 - `golangci-lint` 暂时保持当前已验证的 `v2.4.0`,避免在 CI 架构改造中混入新 lint 规则导致的源码修复;升级到 Dubbo-Go 使用的更高版本应单独处理。 @@ -232,7 +248,7 @@ jobs: security-events: write ``` -CodeQL 显式指定 `languages: go`,使用固定 commit SHA 的 `init` 和 `analyze`,构建模式采用适合 Go 的自动构建。不得复制 Dubbo-Go workflow 中手工 checkout PR merge commit 父节点的历史逻辑;使用 GitHub 当前标准 pull request checkout 语义。 +CodeQL 显式指定 `languages: go`,使用固定 commit SHA 的 `init` 和 `analyze`。当前官方形状是在 `init` 中设置 `build-mode: autobuild`,随后直接执行 `analyze`,不再增加显式 `github/codeql-action/autobuild` step。旧的 `init(build-mode: autobuild) -> autobuild -> analyze` 三步形状仍可兼容运行,但显式 `autobuild` 与 init 的 build mode 重复,属于冗余。不得复制 Dubbo-Go workflow 中手工 checkout PR merge commit 父节点的历史逻辑;使用 GitHub 当前标准 pull request checkout 语义。 ### 10. Dependabot @@ -259,9 +275,10 @@ CodeQL 显式指定 `languages: go`,使用固定 commit SHA 的 `init` 和 `an - `Check License Header` - `Test and Lint` +- `Upload Coverage` - `Race` - 三个平台的 `Build` matrix checks -- `CodeQL` 分析 check +- `Analyze (Go)`(CodeQL workflow 的真实 check 名预计值) 实际 check 名称以 GitHub 新运行返回值为准。修改 branch protection/ruleset 前必须再次获取现有配置,使用增量更新,保留 force-push、review、conversation resolution 等与本任务无关的设置,并获得单独确认。 @@ -270,9 +287,12 @@ CodeQL 显式指定 `languages: go`,使用固定 commit SHA 的 `init` 和 `an ### 静态与语法验证 - `git diff --check` -- 使用 `actionlint v1.7.12` 检查全部 `.github/workflows/*.yml` +- 使用 `actionlint v1.7.12` 检查全部 `.github/workflows/*.yml` 与 `*.yaml` - 解析 `.github/dependabot.yml`,确认 YAML 语法和必需字段 - 检查所有 `uses:` 都固定为完整 40 字符 SHA +- 确认当前两个 workflow 共 6 个逻辑 job、14 个 `uses:`;其中主 CI 为 5 个逻辑 job +- 确认 `id-token: write` 只出现 1 次且位于 `Upload Coverage`,`Test and Lint` 无 OIDC +- 确认 Codecov `version` 固定为 `v11.3.1` - 检查不存在 `@main`、`@latest`、`curl | bash` 或 process substitution 远程执行 ### Makefile 验证 @@ -312,7 +332,7 @@ push 后: 1. 等待全部新 workflow run 完成。 2. 核对每个 job 的真实命令、平台、结论和日志。 -3. 确认 Codecov 上传成功且失败可传播。 +3. 确认 `Test and Lint` 成功上传 `coverage` artifact,`Upload Coverage` 下载同名 artifact 后完成 Codecov 上传,且两段失败都可传播。 4. 确认 setup-go 在 checkout 后找到 `go.sum`,不存在第二套 Go cache。 5. 确认 CodeQL 上传 security result 成功。 6. 确认 Dependabot 配置被 GitHub 接受。 @@ -334,11 +354,12 @@ push 后: 1. 本设计列出的仓库文件完成修改,且没有越过非目标边界。 2. 本地 workflow、YAML、Makefile、Go 测试、race、lint 和交叉编译验证获得新鲜证据。 3. 新提交以普通 push 进入 PR #108,不覆盖远端新增提交。 -4. GitHub 上 License、Test and Lint、Race、Build matrix、CodeQL 全部产生可识别的 checks。 -5. Codecov 上传成功,日志不再出现 HTTP 400 或被吞掉的失败。 +4. GitHub 上 License、Test and Lint、Upload Coverage、Race、Build matrix、CodeQL 全部产生可识别的 checks。 +5. Codecov Action 使用 CLI `v11.3.1`,上传成功,日志不再出现 HTTP 400 或被吞掉的失败。 6. setup-go 缓存由唯一 action 管理,并在 checkout 后读取 `go.sum`。 7. README badge 指向 GitHub Actions,旧 Travis 配置已在覆盖核对后删除。 8. Dependabot 配置被 GitHub 接受。 9. 最终 Head 与验证基准一致。 10. PR #108 的 UDP P1 finding 仍单独对账,不因 CI 改造而被误报为已修复。 11. 收尾报告明确要求轮换或吊销旧 Travis 文件中暴露的 Codecov 和第三方 webhook 凭据,并确认 PR 没有再次复制其值。 +12. 静态政策断言与最终文件一致:主 CI 5 个逻辑 job、全部 workflow 合计 6 个逻辑 job 与 14 个 `uses:`,OIDC 只授予 `Upload Coverage`。 From d075d46e8469eb61d703c15dabdb123451cfb215 Mon Sep 17 00:00:00 2001 From: "Xin.Zh" Date: Sat, 1 Aug 2026 22:20:40 +0800 Subject: [PATCH 13/24] docs: make CI verification plan executable Replace fragile PowerShell-to-WSL inline validation commands with auditable UTF-8 Base64 transport, preserve per-command raw evidence and exit propagation, and correct the Dependabot verification boundary. Constraint: Limit the commit to the CI design and implementation plan; do not modify workflows, Makefile, Dependabot, README files, Go source, remote state, or the independent UDP P1. Confidence: High; the harmless Base64 transport probe preserved Bash variables and exit 7, and all specified documentation consistency scans pass. Scope-risk: Low; this changes documentation and future verification instructions only. Tested: PowerShell-to-WSL UTF-8 Base64 harmless probe; git diff --check; Markdown fence balance; placeholder, forbidden evidence-write action, Dependabot boundary, and sensitive-value scans; cached scope and diff checks. Not-tested: The full Go, race, lint, cross-build, GitHub Actions, Codecov, and CodeQL validation sequence remains a later execution step; Dependabot platform acceptance requires merge to the default branch. Co-authored-by: OmX Signed-off-by: Xin.Zh --- .../plans/2026-08-01-github-ci-hardening.md | 302 +++++++++++------- .../2026-08-01-github-ci-hardening-design.md | 6 +- 2 files changed, 192 insertions(+), 116 deletions(-) diff --git a/doc/superpowers/plans/2026-08-01-github-ci-hardening.md b/doc/superpowers/plans/2026-08-01-github-ci-hardening.md index 0baa9614..f4045ee3 100644 --- a/doc/superpowers/plans/2026-08-01-github-ci-hardening.md +++ b/doc/superpowers/plans/2026-08-01-github-ci-hardening.md @@ -63,15 +63,14 @@ github/codeql-action@v3: a2983b8bed1923f44751c5c43237f479442827b3 ```powershell gh pr view 108 --repo AlexStocks/getty ` - --json number,state,headRefName,headRefOid,baseRefName,mergeable,mergeStateStatus,reviewDecision,statusCheckRollup ` - > D:\test\github\review\AlexStocks-getty-pr-108\evidence\ci-preflight.json + --json number,state,headRefName,headRefOid,baseRefName,mergeable,mergeStateStatus,reviewDecision,statusCheckRollup gh pr view 108 --repo AlexStocks/getty ` --json state,headRefName,headRefOid,baseRefName ` --jq 'select(.state == "OPEN" and .headRefName == "codex/fix-issue-97-remaining" and .headRefOid == "087714342a09f1cc2318bee9d570c2b6ed028044" and .baseRefName == "master") | .headRefOid' ``` -预期:第二条命令只输出 `087714342a09f1cc2318bee9d570c2b6ed028044`。没有输出或 SHA 不同即停止,不得 push;先 fetch 并增量审查远端新增提交。 +完整保留第一条命令的 stdout/stderr,由 agent 使用 `apply_patch` 写入 `evidence/ci-preflight.json`;命令本身不得重定向或创建证据文件。预期:第二条命令只输出 `087714342a09f1cc2318bee9d570c2b6ed028044`。没有输出或 SHA 不同即停止,不得 push;先 fetch 并增量审查远端新增提交。 - [ ] **步骤 2:确认本地提交链只建立在远端 Head 上** @@ -87,17 +86,15 @@ git status --short --branch - [ ] **步骤 3:保存旧配置缺口的可复验基线** ```powershell -@( - '--- workflow gaps ---' - (rg -n 'setup-go@|actions/cache@|codecov\.io/bash|@main|permissions:|concurrency:|timeout-minutes:|-race' .github\workflows\github-actions.yml) - '--- makefile gaps ---' - (rg -n 'go env -w|go test|imports-formatter@|check-fmt|test-race' Makefile) - '--- travis references ---' - (rg -n 'travis-ci' README.md README_CN.md) -) | Set-Content -Encoding utf8 D:\test\github\review\AlexStocks-getty-pr-108\evidence\ci-old-policy-gaps.txt +'--- workflow gaps ---' +rg -n 'setup-go@|actions/cache@|codecov\.io/bash|@main|permissions:|concurrency:|timeout-minutes:|-race' .github\workflows\github-actions.yml +'--- makefile gaps ---' +rg -n 'go env -w|go test|imports-formatter@|check-fmt|test-race' Makefile +'--- travis references ---' +rg -n 'travis-ci' README.md README_CN.md ``` -预期:证据能定位 setup-go 在 checkout 前、第二套 cache、远程 Codecov bash uploader、`@main`、`go env -w`、`@latest` 和 Travis badge。不得把 `.travis.yml` 中的凭据值写入证据。 +完整保留命令 stdout/stderr,再由 agent 使用 `apply_patch` 写入 `evidence/ci-old-policy-gaps.txt`。预期:证据能定位 setup-go 在 checkout 前、第二套 cache、远程 Codecov bash uploader、`@main`、`go env -w`、`@latest` 和 Travis badge。不得把 `.travis.yml` 中的凭据值写入证据。 ### 任务 2:Makefile 确定性门禁 @@ -107,12 +104,27 @@ git status --short --branch - [ ] **步骤 1:证明当前 Makefile 缺少新入口且会写用户级 Go 配置** -```bash -wsl.exe --cd /mnt/d/test/github/review/AlexStocks-getty-pr-108/source -- bash -lc \ - "make -n test | tee ../evidence/make-test-before.txt; ! make -n check-fmt; ! make -n test-race" +```powershell +$bash = @' +set -eu -o pipefail +export PATH=/home/alex/bin/go1.25/bin:/home/alex/go/bin:/usr/local/bin:/usr/bin:/bin +export GOTOOLCHAIN=go1.25.0+auto +make -n test +check_fmt_exit=0 +make -n check-fmt || check_fmt_exit=$? +test_race_exit=0 +make -n test-race || test_race_exit=$? +printf 'CHECK_FMT_EXIT=%d\n' "$check_fmt_exit" +printf 'TEST_RACE_EXIT=%d\n' "$test_race_exit" +test "$check_fmt_exit" -ne 0 +test "$test_race_exit" -ne 0 +'@ +$encoded = [Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes($bash)) +wsl.exe --cd /mnt/d/test/github/review/AlexStocks-getty-pr-108/source -- /bin/bash -c "printf '%s' '$encoded' | base64 -d | /bin/bash" +if ($LASTEXITCODE -ne 0) { throw 'Makefile baseline probe failed' } ``` -预期:`make -n test` 输出包含 `go env -w GOTOOLCHAIN=...`;`check-fmt` 与 `test-race` 报 `No rule to make target`,两个否定命令因此成功。 +完整保留 stdout/stderr,由 agent 使用 `apply_patch` 写入 `evidence/make-test-before.txt`。预期:`make -n test` 输出包含 `go env -w GOTOOLCHAIN=...`;`check-fmt` 与 `test-race` 报 `No rule to make target`,且两个记录的退出码均非零。 - [ ] **步骤 2:补全 phony、help 与确定性目标** @@ -167,12 +179,23 @@ install-imports-formatter: - [ ] **步骤 3:验证命令展开没有全局写入且版本固定** -```bash -wsl.exe --cd /mnt/d/test/github/review/AlexStocks-getty-pr-108/source -- bash -lc \ - "make -n test test-race install-imports-formatter | tee ../evidence/make-targets-after.txt; ! rg -n 'go env -w|imports-formatter@latest' Makefile" +```powershell +$bash = @' +set -eu -o pipefail +export PATH=/home/alex/bin/go1.25/bin:/home/alex/go/bin:/usr/local/bin:/usr/bin:/bin +export GOTOOLCHAIN=go1.25.0+auto +make -n test test-race install-imports-formatter +if rg -n 'go env -w|imports-formatter@latest' Makefile; then + printf 'forbidden Makefile pattern found\n' >&2 + exit 1 +fi +'@ +$encoded = [Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes($bash)) +wsl.exe --cd /mnt/d/test/github/review/AlexStocks-getty-pr-108/source -- /bin/bash -c "printf '%s' '$encoded' | base64 -d | /bin/bash" +if ($LASTEXITCODE -ne 0) { throw 'Makefile policy validation failed' } ``` -预期:输出包含命令级 `GOTOOLCHAIN=go1.25.0+auto`、两个 `-count=1` 和 `imports-formatter@v1.0.10`,反向检索无匹配。 +完整保留 stdout/stderr,由 agent 使用 `apply_patch` 写入 `evidence/make-targets-after.txt`。预期:输出包含命令级 `GOTOOLCHAIN=go1.25.0+auto`、两个 `-count=1` 和 `imports-formatter@v1.0.10`,反向检索无匹配。 - [ ] **步骤 4:提交 Makefile 改动** @@ -647,7 +670,7 @@ git commit -m "docs: replace Travis CI references" - 读取:全部实施文件 - 验证副本:`D:\test\github\review\AlexStocks-getty-pr-108\probes\ci-check-fmt-clean` - 验证副本:`D:\test\github\review\AlexStocks-getty-pr-108\probes\ci-check-fmt-mutation` -- 输出:`D:\test\github\review\AlexStocks-getty-pr-108\evidence\ci-local-validation-*.txt` +- 输出:agent 仅使用 `apply_patch` 写入 `D:\test\github\review\AlexStocks-getty-pr-108\evidence\ci-local-validation-*.txt`;验证命令只向调用端返回完整 stdout/stderr - [ ] **步骤 1:对全部 workflow 运行 actionlint** @@ -689,11 +712,13 @@ PY ```powershell git worktree add --detach D:\test\github\review\AlexStocks-getty-pr-108\probes\ci-check-fmt-clean HEAD -wsl.exe --cd /mnt/d/test/github/review/AlexStocks-getty-pr-108/probes/ci-check-fmt-clean -- bash -lc ` - 'PATH=/home/alex/bin/go1.25/bin:$PATH make check-fmt |& tee ../../evidence/ci-local-validation-check-fmt-clean.txt' +wsl.exe --cd /mnt/d/test/github/review/AlexStocks-getty-pr-108/probes/ci-check-fmt-clean -- ` + env PATH=/home/alex/bin/go1.25/bin:/home/alex/go/bin:/usr/local/bin:/usr/bin:/bin ` + GOTOOLCHAIN=go1.25.0+auto make check-fmt +if ($LASTEXITCODE -ne 0) { throw 'clean check-fmt probe failed' } ``` -预期:退出码 0,probe worktree 的 `git status --porcelain` 为空。主 source worktree 不运行写入式 formatter。 +完整保留 stdout/stderr,由 agent 使用 `apply_patch` 写入 `evidence/ci-local-validation-check-fmt-clean.txt`。预期:退出码 0,probe worktree 的 `git status --porcelain` 为空。主 source worktree 不运行写入式 formatter。 - [ ] **步骤 4:用格式变异证明 `check-fmt` 会阻断** @@ -711,42 +736,60 @@ git worktree add --detach D:\test\github\review\AlexStocks-getty-pr-108\probes\c 然后运行: ```powershell -wsl.exe --cd /mnt/d/test/github/review/AlexStocks-getty-pr-108/probes/ci-check-fmt-mutation -- bash -lc ` - 'PATH=/home/alex/bin/go1.25/bin:$PATH make check-fmt |& tee ../../evidence/ci-local-validation-check-fmt-mutation.txt; test ${PIPESTATUS[0]} -ne 0' +$bash = @' +set -eu -o pipefail +export PATH=/home/alex/bin/go1.25/bin:/home/alex/go/bin:/usr/local/bin:/usr/bin:/bin +export GOTOOLCHAIN=go1.25.0+auto +check_fmt_exit=0 +make check-fmt || check_fmt_exit=$? +printf 'CHECK_FMT_EXIT=%d\n' "$check_fmt_exit" +test "$check_fmt_exit" -ne 0 +'@ +$encoded = [Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes($bash)) +wsl.exe --cd /mnt/d/test/github/review/AlexStocks-getty-pr-108/probes/ci-check-fmt-mutation -- /bin/bash -c "printf '%s' '$encoded' | base64 -d | /bin/bash" +if ($LASTEXITCODE -ne 0) { throw 'check-fmt mutation did not fail as required' } ``` -预期:formatter 修复变异后,`git diff --exit-code` 使 `make check-fmt` 非零退出;日志输出具体 diff 和修复提示。该 probe 不提交、不 push。 +完整保留 stdout/stderr,由 agent 使用 `apply_patch` 写入 `evidence/ci-local-validation-check-fmt-mutation.txt`。预期:formatter 修复变异后,`git diff --exit-code` 使 `make check-fmt` 非零退出;`CHECK_FMT_EXIT` 明确为非零,输出包含具体 diff 和修复提示。该 probe 不提交、不 push。 - [ ] **步骤 5:运行模块、测试、race 与 lint** -```bash -wsl.exe --cd /mnt/d/test/github/review/AlexStocks-getty-pr-108/source -- bash -lc ' - set -o pipefail - export PATH=/home/alex/bin/go1.25/bin:$PATH - go version - go mod verify - make test - make test-race - make lint -' |& tee /mnt/d/test/github/review/AlexStocks-getty-pr-108/evidence/ci-local-validation-go.txt +```powershell +$bash = @' +set -eu -o pipefail +export PATH=/home/alex/bin/go1.25/bin:/home/alex/go/bin:/usr/local/bin:/usr/bin:/bin +export GOTOOLCHAIN=go1.25.0+auto +go version +go mod verify +make test +make test-race +make lint +'@ +$encoded = [Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes($bash)) +wsl.exe --cd /mnt/d/test/github/review/AlexStocks-getty-pr-108/source -- /bin/bash -c "printf '%s' '$encoded' | base64 -d | /bin/bash" +if ($LASTEXITCODE -ne 0) { throw 'Go validation failed' } ``` -预期:Go 为 `go1.25.1 linux/amd64`;所有命令退出码 0;`coverage.txt` 是唯一预期生成文件。若失败,先按 `superpowers:systematic-debugging` 区分 PR 新增、Base 既有和环境问题,不得跳过失败。 +完整保留 stdout/stderr,由 agent 使用 `apply_patch` 写入 `evidence/ci-local-validation-go.txt`。预期:Go 为 `go1.25.1 linux/amd64`;所有命令退出码 0;`coverage.txt` 是唯一预期生成文件。若失败,先按 `superpowers:systematic-debugging` 区分 PR 新增、Base 既有和环境问题,不得跳过失败。 - [ ] **步骤 6:执行跨编译补充验证** -```bash -wsl.exe --cd /mnt/d/test/github/review/AlexStocks-getty-pr-108/source -- bash -lc ' - set -eu -o pipefail - export PATH=/home/alex/bin/go1.25/bin:$PATH - GOOS=windows GOARCH=amd64 go build ./... - GOOS=darwin GOARCH=amd64 go build ./... - CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build ./... - CGO_ENABLED=0 GOOS=linux GOARCH=riscv64 go build ./... -' |& tee /mnt/d/test/github/review/AlexStocks-getty-pr-108/evidence/ci-local-validation-cross-build.txt +```powershell +$bash = @' +set -eu -o pipefail +export PATH=/home/alex/bin/go1.25/bin:/home/alex/go/bin:/usr/local/bin:/usr/bin:/bin +export GOTOOLCHAIN=go1.25.0+auto +GOOS=windows GOARCH=amd64 go build ./... +GOOS=darwin GOARCH=amd64 go build ./... +CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build ./... +CGO_ENABLED=0 GOOS=linux GOARCH=riscv64 go build ./... +'@ +$encoded = [Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes($bash)) +wsl.exe --cd /mnt/d/test/github/review/AlexStocks-getty-pr-108/source -- /bin/bash -c "printf '%s' '$encoded' | base64 -d | /bin/bash" +if ($LASTEXITCODE -ne 0) { throw 'cross-build validation failed' } ``` -预期:退出码 0。交叉编译只是补充证据,不能替代远端 Windows/macOS runner。 +完整保留 stdout/stderr,由 agent 使用 `apply_patch` 写入 `evidence/ci-local-validation-cross-build.txt`。预期:退出码 0。交叉编译只是补充证据,不能替代远端 Windows/macOS runner。 - [ ] **步骤 7:检查 diff、index、意外文件和敏感值回流** @@ -821,63 +864,92 @@ git commit -m "docs: align CI design with reviewed implementation" - [ ] **步骤 1:执行 `verification-before-completion` 新鲜验证** -必须重新运行 actionlint、Go 门禁和四个 cross-build,并让每条命令的边界、当前 HEAD、Go 版本、开始/结束时间和退出码出现在原始输出中。actionlint 必须同时覆盖 `.yml` 与 `.yaml`: +必须重新运行 actionlint、Go 门禁和四个 cross-build,并让每条命令的边界、当前 HEAD、Go 版本、UTC 开始/结束时间和退出码出现在原始输出中。不得把多行 Bash 直接嵌入 `wsl.exe ... bash -lc` 参数;PowerShell 必须把完整 Bash wrapper 编码为 UTF-8 Base64,WSL 内再无损解码并交给 `/bin/bash`。 -```bash -wsl.exe --cd /mnt/d/test/github/review/AlexStocks-getty-pr-108/source -- bash -lc ' - set -u -o pipefail - export PATH=/home/alex/bin/go1.25/bin:$PATH - - overall=0 - run_check() { - label=$1 - shift - printf "=== BEGIN %s ===\n" "$label" - printf "STARTED_AT=%s\n" "$(date --iso-8601=seconds)" - printf "HEAD=%s\n" "$(git rev-parse HEAD)" - printf "GO_VERSION=%s\n" "$(go version)" - printf "COMMAND=" - printf "%q " "$@" - printf "\n" - "$@" +先运行 harmless probe,证明 Bash 变量和值以及预期非零退出码能完整穿过 PowerShell、WSL 和 Base64 解码边界: + +```powershell +$probeScript = @' +set -eu -o pipefail +printf 'PROBE_VALUE=%s\n' "$PROBE_VALUE" +printf 'PROBE_EXIT=%s\n' "$PROBE_EXIT" +exit "$PROBE_EXIT" +'@ +$probeEncoded = [Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes($probeScript)) +wsl.exe --cd /mnt/d/test/github/review/AlexStocks-getty-pr-108/source -- ` + env PROBE_VALUE=base64-transport-ok PROBE_EXIT=7 ` + /bin/bash -c "printf '%s' '$probeEncoded' | base64 -d | /bin/bash" +if ($LASTEXITCODE -ne 7) { throw 'PowerShell-to-WSL Base64 probe did not preserve exit 7' } +``` + +预期原样输出 `PROBE_VALUE=base64-transport-ok` 和 `PROBE_EXIT=7`,PowerShell 观察到退出码 7。probe 不创建文件;任一值或退出码不一致都必须停止,不能继续正式验证。 + +probe 通过后运行正式 wrapper。actionlint 必须同时覆盖 `.yml` 与 `.yaml`: + +```powershell +$validationScript = @' +set -eu -o pipefail +export PATH=/home/alex/bin/go1.25/bin:/home/alex/go/bin:/usr/local/bin:/usr/bin:/bin +export GOTOOLCHAIN=go1.25.0+auto + +overall=0 +run_check() { + label=$1 + shift + printf '=== BEGIN %s ===\n' "$label" + printf 'BEGIN_UTC=%s\n' "$(date -u +%Y-%m-%dT%H:%M:%SZ)" + printf 'HEAD=%s\n' "$(git rev-parse HEAD)" + printf 'GO_VERSION=%s\n' "$(go version)" + printf 'COMMAND=' + printf '%q ' "$@" + printf '\n' + if "$@"; then + rc=0 + else rc=$? - printf "EXIT=%d\n" "$rc" - printf "ENDED_AT=%s\n" "$(date --iso-8601=seconds)" - printf "=== END %s ===\n" "$label" - if [ "$rc" -ne 0 ]; then overall=1; fi - } - - run_actionlint() { - shopt -s nullglob - workflows=(.github/workflows/*.yml .github/workflows/*.yaml) - test "${#workflows[@]}" -gt 0 || return 1 - GOTOOLCHAIN=go1.25.0+auto go run github.com/rhysd/actionlint/cmd/actionlint@v1.7.12 "${workflows[@]}" - } - - printf "VALIDATION_STARTED_AT=%s\n" "$(date --iso-8601=seconds)" - printf "VALIDATION_HEAD=%s\n" "$(git rev-parse HEAD)" - printf "VALIDATION_GO_VERSION=%s\n" "$(go version)" - run_check actionlint run_actionlint - run_check go-mod-verify go mod verify - run_check go-test go test ./... -count=1 - run_check go-test-race go test -race ./transport -count=1 - run_check go-vet go vet ./... - run_check make-lint make lint - run_check build-windows-amd64 env GOOS=windows GOARCH=amd64 go build ./... - run_check build-darwin-amd64 env GOOS=darwin GOARCH=amd64 go build ./... - run_check build-linux-arm64 env CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build ./... - run_check build-linux-riscv64 env CGO_ENABLED=0 GOOS=linux GOARCH=riscv64 go build ./... - run_check git-diff-check git diff --check origin/codex/fix-issue-97-remaining...HEAD - run_check git-status git status --short --branch - printf "VALIDATION_ENDED_AT=%s\n" "$(date --iso-8601=seconds)" - printf "VALIDATION_EXIT=%d\n" "$overall" - exit "$overall" -' + overall=1 + fi + printf 'EXIT=%d\n' "$rc" + printf 'END_UTC=%s\n' "$(date -u +%Y-%m-%dT%H:%M:%SZ)" + printf '=== END %s ===\n' "$label" +} + +run_actionlint() { + shopt -s nullglob + workflows=(.github/workflows/*.yml .github/workflows/*.yaml) + test "${#workflows[@]}" -gt 0 + go run github.com/rhysd/actionlint/cmd/actionlint@v1.7.12 "${workflows[@]}" +} + +printf 'VALIDATION_BEGIN_UTC=%s\n' "$(date -u +%Y-%m-%dT%H:%M:%SZ)" +printf 'VALIDATION_HEAD=%s\n' "$(git rev-parse HEAD)" +printf 'VALIDATION_GO_VERSION=%s\n' "$(go version)" +run_check actionlint run_actionlint +run_check go-mod-verify go mod verify +run_check go-test go test ./... -count=1 +run_check go-test-race go test -race ./transport -count=1 +run_check go-vet go vet ./... +run_check make-lint make lint +run_check build-windows-amd64 env GOOS=windows GOARCH=amd64 go build ./... +run_check build-darwin-amd64 env GOOS=darwin GOARCH=amd64 go build ./... +run_check build-linux-arm64 env CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build ./... +run_check build-linux-riscv64 env CGO_ENABLED=0 GOOS=linux GOARCH=riscv64 go build ./... +run_check git-diff-check git diff --check origin/codex/fix-issue-97-remaining...HEAD +run_check git-status git status --short --branch +printf 'VALIDATION_END_UTC=%s\n' "$(date -u +%Y-%m-%dT%H:%M:%SZ)" +printf 'VALIDATION_EXIT=%d\n' "$overall" +exit "$overall" +'@ +$validationEncoded = [Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes($validationScript)) +wsl.exe --cd /mnt/d/test/github/review/AlexStocks-getty-pr-108/source -- ` + /bin/bash -c "printf '%s' '$validationEncoded' | base64 -d | /bin/bash" +$validationExit = $LASTEXITCODE +if ($validationExit -ne 0) { throw "fresh CI validation failed with exit $validationExit" } ``` -保留上述完整终端原始输出,包括测试、race、vet、lint 和 build 的所有正文。随后用 `apply_patch` 新增或完整替换 `ci-pre-push-validation-raw.txt`;不得用 `>`、`tee`、脚本摘要或人工改写后的“pass”列表代替 raw。若原始输出包含意外敏感值,先停止并报告,不得把该值写入证据。 +保留上述完整终端 stdout/stderr,包括测试、race、vet、lint 和 build 的所有正文。随后由 agent 使用 `apply_patch` 新增或完整替换 `ci-pre-push-validation-raw.txt`;验证命令和 wrapper 不得创建、追加或修改 evidence 文件,也不得用脚本摘要或人工改写后的“pass”列表代替 raw。若原始输出包含意外敏感值,先停止并报告,不得把该值写入证据。 -预期:每个边界的 `EXIT=0` 且最终 `VALIDATION_EXIT=0`;记录的 `VALIDATION_HEAD` 与待 push HEAD 一致。不得用较早日志替代该步骤的新鲜结果。 +预期:harmless probe 证明传输边界无损;每个正式边界的 `EXIT=0` 且最终 `VALIDATION_EXIT=0`;记录的 `VALIDATION_HEAD` 与待 push HEAD 一致。任一命令失败都会把 `overall` 置为非零并传播到 PowerShell。不得用较早日志替代该步骤的新鲜结果。 - [ ] **步骤 2:再次获取远端 Head 并执行显式 lease** @@ -904,9 +976,9 @@ git push origin HEAD:codex/fix-issue-97-remaining ### 任务 10:等待并核验 GitHub 新 checks **文件:** -- 写入证据:`D:\test\github\review\AlexStocks-getty-pr-108\evidence\ci-post-push-pr.json` -- 写入证据:`D:\test\github\review\AlexStocks-getty-pr-108\evidence\ci-post-push-checks.txt` -- 写入证据:各 workflow/job 的日志片段,仅保存无敏感值的诊断 +- 写入证据:agent 仅使用 `apply_patch` 更新 `D:\test\github\review\AlexStocks-getty-pr-108\evidence\ci-post-push-pr.json` +- 写入证据:agent 仅使用 `apply_patch` 更新 `D:\test\github\review\AlexStocks-getty-pr-108\evidence\ci-post-push-checks.txt` +- 写入证据:各 workflow/job 的完整无敏感值 stdout/stderr;GitHub 查询命令本身不得创建或修改文件 - [ ] **步骤 1:获取 push 后新 Head 和 workflow runs** @@ -942,11 +1014,10 @@ $headSha = gh pr view 108 --repo AlexStocks/getty --json headRefOid --jq .headRe $ciRunId = gh run list --repo AlexStocks/getty --branch codex/fix-issue-97-remaining --workflow CI --limit 20 ` --json databaseId,headSha --jq ".[] | select(.headSha == \"$headSha\") | .databaseId" | Select-Object -First 1 if (-not $ciRunId) { throw 'CI run for current Head not found' } -gh run view $ciRunId --repo AlexStocks/getty --json headSha,status,conclusion,jobs,url ` - > D:\test\github\review\AlexStocks-getty-pr-108\evidence\ci-post-push-checks.txt +gh run view $ciRunId --repo AlexStocks/getty --json headSha,status,conclusion,jobs,url ``` -必须确认实际 job 包含并成功: +完整保留命令 stdout/stderr,再由 agent 使用 `apply_patch` 写入 `evidence/ci-post-push-checks.txt`。必须确认实际 job 包含并成功: ```text Check License Header @@ -977,30 +1048,32 @@ gh run view $codeqlRunId --repo AlexStocks/getty --json headSha,status,conclusio ```powershell gh pr view 108 --repo AlexStocks/getty ` - --json number,state,headRefName,headRefOid,baseRefName,mergeable,mergeStateStatus,reviewDecision,statusCheckRollup ` - > D:\test\github\review\AlexStocks-getty-pr-108\evidence\ci-post-push-pr.json + --json number,state,headRefName,headRefOid,baseRefName,mergeable,mergeStateStatus,reviewDecision,statusCheckRollup git rev-parse HEAD gh pr view 108 --repo AlexStocks/getty --json headRefOid --jq .headRefOid ``` -预期:本地 HEAD 与 GitHub PR Head 完全一致。 +完整保留第一条命令 stdout/stderr,再由 agent 使用 `apply_patch` 写入 `evidence/ci-post-push-pr.json`。预期:本地 HEAD 与 GitHub PR Head 完全一致。 + +- [ ] **步骤 6:记录 Dependabot 的 PR 阶段验证边界** + +PR 分支上只复用任务 5 的严格 YAML 解析和精确结构断言;PR push、CI run 或 `gh pr view` 都不能证明 GitHub 已接受、启用或排程 Dependabot。将平台接受、启用状态以及是否按计划创建更新 PR 明确列为配置合并到默认分支 `master` 后的跟进验证,不得在任务 10 中写成已完成结果。 ### 任务 11:最终 Review、required checks 建议与收尾对账 **文件:** - 读取:当前 PR 完整 files/diff、review comments、checks、branch protection/rulesets +- 写入证据:agent 仅使用 `apply_patch` 更新 `D:\test\github\review\AlexStocks-getty-pr-108\evidence\files.json` 和 `evidence\pr.diff` - 可能更新:`D:\test\github\arch-practice\alg\openclaw\review-experience.md` 或 `review-AlexStocks-getty.md`,仅当本轮产生经过验证的新经验 - [ ] **步骤 1:重新保存完整 PR 文件列表和 Diff** ```powershell -gh api repos/AlexStocks/getty/pulls/108/files --paginate ` - > D:\test\github\review\AlexStocks-getty-pr-108\files.json -gh pr diff 108 --repo AlexStocks/getty ` - > D:\test\github\review\AlexStocks-getty-pr-108\pr.diff +gh api repos/AlexStocks/getty/pulls/108/files --paginate +gh pr diff 108 --repo AlexStocks/getty ``` -逐文件增量审查 CI 改动,确认没有运行时源码漂移。若发现本轮 CI 变更引入的可定位问题,先本地修复、重新验证、普通 push,再重复任务 10;不要给自己的 CI 改动留下明知的 P0/P1。 +分别完整保留两个命令的 stdout/stderr,由 agent 使用 `apply_patch` 写入 `evidence/files.json` 和 `evidence/pr.diff`。逐文件增量审查 CI 改动,确认没有运行时源码漂移。若发现本轮 CI 变更引入的可定位问题,先本地修复、重新验证、普通 push,再重复任务 10;不要给自己的 CI 改动留下明知的 P0/P1。 - [ ] **步骤 2:检索 review threads 并保留 UDP P1 独立状态** @@ -1037,6 +1110,7 @@ PR、镜像路径、WSL 路径、最终 Head、Base、分类。 gh 与 rg/fd/grep/ls 调用次数。 actionlint、YAML、Makefile、测试、race、lint、跨平台构建结果。 远端 CI/CodeQL/Codecov 结果和 URL。 +Dependabot 在 PR 内只验证 YAML/结构;实际接受、启用和排程待合并默认分支后确认。 已提交行内评论及去重说明。 所有本轮 commit 和普通 push 结果。 未修改 branch protection;给出建议 required checks 并请求单独授权。 diff --git a/doc/superpowers/specs/2026-08-01-github-ci-hardening-design.md b/doc/superpowers/specs/2026-08-01-github-ci-hardening-design.md index 680e4639..759ecf5b 100644 --- a/doc/superpowers/specs/2026-08-01-github-ci-hardening-design.md +++ b/doc/superpowers/specs/2026-08-01-github-ci-hardening-design.md @@ -261,6 +261,8 @@ CodeQL 显式指定 `languages: go`,使用固定 commit SHA 的 `init` 和 `an 配置只负责创建依赖更新 PR,不自动 approve、merge 或修改 branch protection。 +PR 分支阶段只能通过严格 YAML 解析和字段/结构断言验证该文件;GitHub 是否接受并启用 Dependabot、是否按计划创建更新 PR,只能在配置合并到默认分支 `master` 后确认。PR push 后没有可证明“已接受/已启用/已排程”的 GitHub 结果,不得把本地结构验证写成平台接受证据。 + ### 11. README 与 Travis 清理 `README.md` 和 `README_CN.md` 的 Travis badge 替换为 GitHub Actions `CI` workflow badge,并继续保留 Codecov、Go reference、Go Report Card 和 license badge。 @@ -335,7 +337,7 @@ push 后: 3. 确认 `Test and Lint` 成功上传 `coverage` artifact,`Upload Coverage` 下载同名 artifact 后完成 Codecov 上传,且两段失败都可传播。 4. 确认 setup-go 在 checkout 后找到 `go.sum`,不存在第二套 Go cache。 5. 确认 CodeQL 上传 security result 成功。 -6. 确认 Dependabot 配置被 GitHub 接受。 +6. 对 Dependabot 只确认 PR 内 YAML 和结构门禁通过;实际接受、启用和排程列为合并到默认分支后的验证项。 7. 重新获取 PR Base/Head、mergeable、mergeStateStatus、reviewDecision、required checks 和 review threads。 ## 失败处理与回滚 @@ -358,7 +360,7 @@ push 后: 5. Codecov Action 使用 CLI `v11.3.1`,上传成功,日志不再出现 HTTP 400 或被吞掉的失败。 6. setup-go 缓存由唯一 action 管理,并在 checkout 后读取 `go.sum`。 7. README badge 指向 GitHub Actions,旧 Travis 配置已在覆盖核对后删除。 -8. Dependabot 配置被 GitHub 接受。 +8. Dependabot 配置在 PR 内通过严格 YAML 和结构验证;合并到默认分支后另行确认 GitHub 接受、启用和排程,不把该外部结果作为 PR push 阶段可完成的条件。 9. 最终 Head 与验证基准一致。 10. PR #108 的 UDP P1 finding 仍单独对账,不因 CI 改造而被误报为已修复。 11. 收尾报告明确要求轮换或吊销旧 Travis 文件中暴露的 Codecov 和第三方 webhook 凭据,并确认 PR 没有再次复制其值。 From 6dff6fe98bc56239c3b39f9d48f6871d774ede4f Mon Sep 17 00:00:00 2001 From: "Xin.Zh" Date: Sat, 1 Aug 2026 22:39:22 +0800 Subject: [PATCH 14/24] docs: harden remaining WSL command transport Convert the remaining complex PowerShell-to-WSL workflow and policy examples to UTF-8 Base64 payload transport so Bash variables, arrays, command substitutions, heredocs, and exit codes reach /bin/bash intact. Constraint: Limit the commit to the CI implementation plan; do not modify the design, workflows, Makefile, Dependabot, README files, Go source, evidence, or remote state. Confidence: High; Windows PowerShell probes exercised command substitution, workflow-array actionlint enumeration, heredoc policy validation, and expected nonzero mutation exit capture through the documented transport. Scope-risk: Low; this changes future verification commands only and preserves the approved CI behavior and no-push boundary. Tested: four PowerShell UTF-8 Base64 to WSL probes; actionlint v1.7.12 over two enumerated workflows; pinned-action policy with 14 uses; mutation exit 9 capture; git diff --check; fence, placeholder, inline bash-lc, evidence-write, sensitive-value, and single-file scope scans. Not-tested: The full Go, race, lint, cross-build, GitHub Actions, Codecov, CodeQL, and post-merge Dependabot sequence remains a later execution step. Co-authored-by: OmX Signed-off-by: Xin.Zh --- .../plans/2026-08-01-github-ci-hardening.md | 98 ++++++++++++------- 1 file changed, 65 insertions(+), 33 deletions(-) diff --git a/doc/superpowers/plans/2026-08-01-github-ci-hardening.md b/doc/superpowers/plans/2026-08-01-github-ci-hardening.md index f4045ee3..749b69c3 100644 --- a/doc/superpowers/plans/2026-08-01-github-ci-hardening.md +++ b/doc/superpowers/plans/2026-08-01-github-ci-hardening.md @@ -185,7 +185,7 @@ set -eu -o pipefail export PATH=/home/alex/bin/go1.25/bin:/home/alex/go/bin:/usr/local/bin:/usr/bin:/bin export GOTOOLCHAIN=go1.25.0+auto make -n test test-race install-imports-formatter -if rg -n 'go env -w|imports-formatter@latest' Makefile; then +if grep -En 'go env -w|imports-formatter@latest' Makefile; then printf 'forbidden Makefile pattern found\n' >&2 exit 1 fi @@ -214,15 +214,26 @@ git commit -m "build: make CI checks deterministic" - [ ] **步骤 1:建立会使旧 workflow 失败的政策检查** -```bash -wsl.exe --cd /mnt/d/test/github/review/AlexStocks-getty-pr-108/source -- bash -lc ' - set -eu - workflow=.github/workflows/github-actions.yml - ! rg -q "actions/cache@|codecov.io/bash|@(main|latest)([[:space:]#]|$)" "$workflow" - first_checkout=$(rg -n "uses: actions/checkout@" "$workflow" | head -1 | cut -d: -f1) - first_setup=$(rg -n "uses: actions/setup-go@" "$workflow" | head -1 | cut -d: -f1) - test "$first_checkout" -lt "$first_setup" -' +```powershell +$bash = @' +set -eu -o pipefail +export PATH=/home/alex/bin/go1.25/bin:/home/alex/go/bin:/usr/local/bin:/usr/bin:/bin +export GOTOOLCHAIN=go1.25.0+auto +workflow=.github/workflows/github-actions.yml +policy_exit=0 +if grep -Eq "actions/cache@|codecov.io/bash|@(main|latest)([[:space:]#]|$)" "$workflow"; then + policy_exit=11 +else + first_checkout=$(grep -n "uses: actions/checkout@" "$workflow" | head -1 | cut -d: -f1) + first_setup=$(grep -n "uses: actions/setup-go@" "$workflow" | head -1 | cut -d: -f1) + if ! test "$first_checkout" -lt "$first_setup"; then policy_exit=12; fi +fi +printf 'POLICY_EXIT=%d\n' "$policy_exit" +test "$policy_exit" -ne 0 +'@ +$encoded = [Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes($bash)) +wsl.exe --cd /mnt/d/test/github/review/AlexStocks-getty-pr-108/source -- /bin/bash -c "printf '%s' '$encoded' | base64 -d | /bin/bash" +if ($LASTEXITCODE -ne 0) { throw 'old workflow policy probe did not observe the expected failure' } ``` 预期:在修改前失败,原因至少包括旧 `actions/cache`、远程 uploader、`@main` 或 setup-go 排在 checkout 前。 @@ -372,15 +383,19 @@ jobs: - [ ] **步骤 3:运行政策检查并验证全部 Action 引用** -```bash -wsl.exe --cd /mnt/d/test/github/review/AlexStocks-getty-pr-108/source -- bash -lc ' - set -eu - workflow=.github/workflows/github-actions.yml - ! rg -n "actions/cache@|codecov.io/bash|@(main|latest)([[:space:]#]|$)" "$workflow" - first_checkout=$(rg -n "uses: actions/checkout@" "$workflow" | head -1 | cut -d: -f1) - first_setup=$(rg -n "uses: actions/setup-go@" "$workflow" | head -1 | cut -d: -f1) - test "$first_checkout" -lt "$first_setup" - python3 - <<"PY" +```powershell +$bash = @' +set -eu -o pipefail +export PATH=/home/alex/bin/go1.25/bin:/home/alex/go/bin:/usr/local/bin:/usr/bin:/bin +export GOTOOLCHAIN=go1.25.0+auto +workflow=.github/workflows/github-actions.yml +if grep -En "actions/cache@|codecov.io/bash|@(main|latest)([[:space:]#]|$)" "$workflow"; then + exit 1 +fi +first_checkout=$(grep -n "uses: actions/checkout@" "$workflow" | head -1 | cut -d: -f1) +first_setup=$(grep -n "uses: actions/setup-go@" "$workflow" | head -1 | cut -d: -f1) +test "$first_checkout" -lt "$first_setup" +python3 - <<"PY" import pathlib import re @@ -392,7 +407,10 @@ for path in paths: if match and not re.fullmatch(r"[0-9a-f]{40}", match.group(1)): raise SystemExit(f"{path}:{number}: mutable action ref {match.group(1)}") PY -' +'@ +$encoded = [Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes($bash)) +wsl.exe --cd /mnt/d/test/github/review/AlexStocks-getty-pr-108/source -- /bin/bash -c "printf '%s' '$encoded' | base64 -d | /bin/bash" +if ($LASTEXITCODE -ne 0) { throw 'main workflow policy validation failed' } ``` 预期:退出码 0;无 mutable ref、重复 cache 或远程 shell uploader。主 CI 应恰好包含 5 个逻辑 job 和 11 个 `uses:`;`id-token: write` 只位于 `Upload Coverage`,Codecov `version` 为 `v11.3.1`。 @@ -674,24 +692,35 @@ git commit -m "docs: replace Travis CI references" - [ ] **步骤 1:对全部 workflow 运行 actionlint** -```bash -wsl.exe --cd /mnt/d/test/github/review/AlexStocks-getty-pr-108/source -- bash -lc ' - shopt -s nullglob - workflows=(.github/workflows/*.yml .github/workflows/*.yaml) - test "${#workflows[@]}" -gt 0 - GOTOOLCHAIN=go1.25.0+auto go run github.com/rhysd/actionlint/cmd/actionlint@v1.7.12 -color "${workflows[@]}" -' +```powershell +$bash = @' +set -eu -o pipefail +export PATH=/home/alex/bin/go1.25/bin:/home/alex/go/bin:/usr/local/bin:/usr/bin:/bin +export GOTOOLCHAIN=go1.25.0+auto +shopt -s nullglob +workflows=(.github/workflows/*.yml .github/workflows/*.yaml) +printf 'ACTIONLINT_FILE_COUNT=%d\n' "${#workflows[@]}" +test "${#workflows[@]}" -gt 0 +go run github.com/rhysd/actionlint/cmd/actionlint@v1.7.12 -color "${workflows[@]}" +'@ +$encoded = [Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes($bash)) +wsl.exe --cd /mnt/d/test/github/review/AlexStocks-getty-pr-108/source -- /bin/bash -c "printf '%s' '$encoded' | base64 -d | /bin/bash" +if ($LASTEXITCODE -ne 0) { throw 'actionlint validation failed' } ``` 预期:退出码 0,无诊断。 - [ ] **步骤 2:执行 workflow 供应链政策检查** -```bash -wsl.exe --cd /mnt/d/test/github/review/AlexStocks-getty-pr-108/source -- bash -lc ' - set -eu - ! rg -n "actions/cache@|codecov.io/bash|curl[[:space:]].*\|[[:space:]]*(ba)?sh|@(main|master|latest)([[:space:]#]|$)" .github/workflows Makefile - python3 - <<"PY" +```powershell +$bash = @' +set -eu -o pipefail +export PATH=/home/alex/bin/go1.25/bin:/home/alex/go/bin:/usr/local/bin:/usr/bin:/bin +export GOTOOLCHAIN=go1.25.0+auto +if grep -ERn "actions/cache@|codecov.io/bash|curl[[:space:]].*\|[[:space:]]*(ba)?sh|@(main|master|latest)([[:space:]#]|$)" .github/workflows Makefile; then + exit 1 +fi +python3 - <<"PY" import pathlib import re @@ -703,7 +732,10 @@ for path in paths: if match and not re.fullmatch(r"[0-9a-f]{40}", match.group(1)): raise SystemExit(f"{path}:{number}: mutable action ref {match.group(1)}") PY -' +'@ +$encoded = [Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes($bash)) +wsl.exe --cd /mnt/d/test/github/review/AlexStocks-getty-pr-108/source -- /bin/bash -c "printf '%s' '$encoded' | base64 -d | /bin/bash" +if ($LASTEXITCODE -ne 0) { throw 'workflow supply-chain policy validation failed' } ``` 预期:退出码 0。注释中的版本标签允许存在,但 `uses:` 的实际 ref 必须是完整 SHA。当前最终文件应为 6 个逻辑 job、14 个 `uses:`;主 CI 应为 5 个逻辑 job;`id-token: write` 只出现于 `Upload Coverage`;Codecov `version` 必须为 `v11.3.1`。 From 25bd029e637bfa694373c3db0e27c05412ce8d1b Mon Sep 17 00:00:00 2001 From: "Xin.Zh" Date: Sat, 1 Aug 2026 23:27:24 +0800 Subject: [PATCH 15/24] ci: update setup-go to v7 Pin every direct setup-go use to the official v7.0.0 commit and align the approved CI design and implementation plan with that exact ref. Retain the verified post-v0.8.0 SkyWalking Eyes commit because moving to the older release would undo action pinning and shell-input hardening. Constraint: Limit this commit to the main workflow and the two CI design documents; do not push. Confidence: High; actionlint and the workflow, documentation, scope, and diff policy gates pass. Scope-risk: Low; setup-go v7 preserves the existing action interface and Node 24 runtime requirement. Tested: actionlint v1.7.12 over all workflows; 6-job and 14-use policy; setup-go exact-SHA gate; Markdown fence, placeholder, secret, scope, and git diff checks. Not-tested: GitHub-hosted workflow execution was not run because this commit is not pushed. Co-authored-by: OmX Signed-off-by: Xin.Zh --- .github/workflows/github-actions.yml | 8 ++++---- .../plans/2026-08-01-github-ci-hardening.md | 14 ++++++++------ .../specs/2026-08-01-github-ci-hardening-design.md | 6 ++++-- 3 files changed, 16 insertions(+), 12 deletions(-) diff --git a/.github/workflows/github-actions.yml b/.github/workflows/github-actions.yml index 79ddaf87..0a35b1e0 100644 --- a/.github/workflows/github-actions.yml +++ b/.github/workflows/github-actions.yml @@ -25,7 +25,7 @@ jobs: uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 - name: Check License Header - uses: apache/skywalking-eyes/header@315732dd4b8d3a015d8d9b91936b935a0b854817 + uses: apache/skywalking-eyes/header@315732dd4b8d3a015d8d9b91936b935a0b854817 # verified main, post-v0.8.0 with: config: .licenserc.yaml mode: check @@ -39,7 +39,7 @@ jobs: uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 - name: Set up Go - uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 + uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0 with: go-version-file: go.mod cache-dependency-path: go.sum @@ -95,7 +95,7 @@ jobs: uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 - name: Set up Go - uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 + uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0 with: go-version-file: go.mod cache-dependency-path: go.sum @@ -122,7 +122,7 @@ jobs: uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 - name: Set up Go - uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 + uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0 with: go-version-file: go.mod cache-dependency-path: go.sum diff --git a/doc/superpowers/plans/2026-08-01-github-ci-hardening.md b/doc/superpowers/plans/2026-08-01-github-ci-hardening.md index 749b69c3..cb51c710 100644 --- a/doc/superpowers/plans/2026-08-01-github-ci-hardening.md +++ b/doc/superpowers/plans/2026-08-01-github-ci-hardening.md @@ -38,8 +38,8 @@ Approved design commit: 7539afef7d495ed43f94e0ae488d8da010b9a7f5 ```text actions/checkout@v7: 3d3c42e5aac5ba805825da76410c181273ba90b1 -actions/setup-go@v6: 924ae3a1cded613372ab5595356fb5720e22ba16 -apache/skywalking-eyes@main: 315732dd4b8d3a015d8d9b91936b935a0b854817 +actions/setup-go@v7.0.0: b7ad1dad31e06c5925ef5d2fc7ad053ef454303e +apache/skywalking-eyes official main verified commit (post-v0.8.0): 315732dd4b8d3a015d8d9b91936b935a0b854817 actions/upload-artifact@v7.0.1: 043fb46d1a93c77aae656e7c1c64a875d1fc6a0a actions/download-artifact@v8.0.1: 3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c codecov/codecov-action@v7: fb8b3582c8e4def4969c97caa2f19720cb33a72f @@ -50,6 +50,8 @@ github/codeql-action@v3: a2983b8bed1923f44751c5c43237f479442827b3 正式质量审查已同时核对 `upload-artifact` v7.0.1 和 `download-artifact` v8.0.1 的 GitHub release 元数据与精确 tag ref,上述 SHA 均双向一致。`download-artifact` v8.0.1 tag 下 README 第 48 行仍保留一处 `@v7` 示例,但同一 release/ref 明确指向 v8.0.1 的上述提交;该示例按文档滞后处理,不覆盖 release/ref 证据。 +Action 固定默认要求官方稳定 release/tag 与完整 SHA 对应。窄例外是:official main 的 verified commit 明确晚于最新 release,并且回退 release 会撤销安全或可复现性加固;此时必须记录 ancestry、差异和完整 SHA,仍禁止 `@main` 等可变 ref。本轮 `setup-go` v7.0.0/v7 均解析为 `b7ad1dad31e06c5925ef5d2fc7ad053ef454303e`,action.yml 与 v6.5.0 的输入、输出和 Node 24 runtime 不变,所以只替换 SHA。SkyWalking Eyes v0.8.0 解析为 `61275cc80d0798a405cb070f7d3a8aaf7cf2c2c1`,而保留的 `315732dd4b8d3a015d8d9b91936b935a0b854817` 是 official main verified commit,位于其后 27 个提交,已固定内部 `setup-go` 并硬化 shell 输入;不得为了满足 release 标签而降级。 + ### 任务 1:实时租约与旧门禁缺口基线 **文件:** @@ -284,7 +286,7 @@ jobs: uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 - name: Set up Go - uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 + uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0 with: go-version-file: go.mod cache-dependency-path: go.sum @@ -340,7 +342,7 @@ jobs: uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 - name: Set up Go - uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 + uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0 with: go-version-file: go.mod cache-dependency-path: go.sum @@ -367,7 +369,7 @@ jobs: uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 - name: Set up Go - uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 + uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0 with: go-version-file: go.mod cache-dependency-path: go.sum @@ -847,7 +849,7 @@ git grep -n -I -E 'travis-ci|codecov\.io/bash|go env -w GOTOOLCHAIN|imports-form 核对: ```text -[ ] 唯一 Go cache owner 是 setup-go v6 +[ ] 唯一 Go cache owner 是 setup-go v7.0.0 [ ] checkout 位于 setup-go 前 [ ] 主 CI 恰好 5 个逻辑 job:License、Test and Lint、Upload Coverage、Race、Build matrix [ ] Test and Lint 无 OIDC,使用 upload-artifact v7.0.1 上传 coverage(missing=error、retention=1) diff --git a/doc/superpowers/specs/2026-08-01-github-ci-hardening-design.md b/doc/superpowers/specs/2026-08-01-github-ci-hardening-design.md index 759ecf5b..cb951903 100644 --- a/doc/superpowers/specs/2026-08-01-github-ci-hardening-design.md +++ b/doc/superpowers/specs/2026-08-01-github-ci-hardening-design.md @@ -94,8 +94,8 @@ uses: actions/checkout@ # v7 实现前重新查询并固定: - `actions/checkout@v7` -- `actions/setup-go@v6` -- `apache/skywalking-eyes/header` 当前核验提交 +- `actions/setup-go@v7.0.0` +- `apache/skywalking-eyes/header` 当前核验的 official main verified commit - `actions/upload-artifact@v7.0.1` - `actions/download-artifact@v8.0.1` - `codecov/codecov-action@v7` @@ -105,6 +105,8 @@ Dependabot 的 `github-actions` ecosystem 负责后续 Action 更新。不得使 本轮正式质量审查确认:`actions/upload-artifact@v7.0.1` 的 release 与 tag ref 都指向 `043fb46d1a93c77aae656e7c1c64a875d1fc6a0a`;`actions/download-artifact@v8.0.1` 的 release 与 tag ref 都指向 `3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c`。`download-artifact` v8.0.1 tag 下 README 仍有一处 `@v7` 示例,属于示例文本滞后;release 元数据和精确 tag ref 一致,因此实现以 release/ref 指向的完整 SHA 为准,不因 README 的单处旧示例降级到 v7。 +Action 固定默认采用官方稳定 release/tag 对应的完整 SHA。仅当官方 main 上的 verified commit 明确晚于最新 release,且退回该 release 会撤销安全加固或可复现性改进时,才允许在设计中记录 provenance 后固定该 verified commit;这个例外不允许使用 `@main` 等可变引用。本轮 `actions/setup-go@v7.0.0` 的 release、`v7.0.0` 与 `v7` tag 均指向 `b7ad1dad31e06c5925ef5d2fc7ad053ef454303e`,可直接替换 v6.5.0 SHA,现有输入和 Node 24 runner 要求不变。`apache/skywalking-eyes/header@315732dd4b8d3a015d8d9b91936b935a0b854817` 是 official main 上经 GitHub 验证、比 v0.8.0 release commit 多 27 个提交的固定提交;它已将内部 `setup-go` 固定到完整 SHA,并对 shell 输入进行环境变量和引用加固,因此保留该提交,避免降级到 v0.8.0。 + ### 3. License job License job: From 90d9ec5a869ace57bd6d87411a1a2df68d2896e0 Mon Sep 17 00:00:00 2001 From: "Xin.Zh" Date: Sun, 2 Aug 2026 00:25:38 +0800 Subject: [PATCH 16/24] build: make format check read-only Run gofmt and imports-formatter only inside a tracked-file temporary mirror, then compare clean-filter-normalized Git object hashes so the format gate never rewrites the checked-out source. Keep CRLF working trees compatible with core.autocrlf=true while still detecting independent gofmt and Getty import-order violations. Constraint: Limit this commit to Makefile and the two CI design documents; do not modify workflows, Go source, other tracked files, or remote state. Confidence: High; clean LF and CRLF probes pass without byte changes, both formatting mutations fail without source writes, and formatter failure propagates with temporary-directory cleanup. Scope-risk: Low; the explicit make fmt target and imports-formatter v1.0.10 behavior remain unchanged, while check-fmt moves their writes to an isolated temporary mirror. Tested: make check-fmt; CRLF clean-filter probe; gofmt-only mutation; import-order-only mutation; formatter failure/trap probe; actionlint v1.7.12; workflow and Makefile policy; go mod verify; Markdown fence, placeholder, secret, scope, and git diff checks. Not-tested: GitHub-hosted workflow execution is not run because this commit is not pushed. Co-authored-by: OmX Signed-off-by: Xin.Zh --- Makefile | 26 ++++-- .../plans/2026-08-01-github-ci-hardening.md | 80 ++++++++----------- .../2026-08-01-github-ci-hardening-design.md | 8 +- 3 files changed, 60 insertions(+), 54 deletions(-) diff --git a/Makefile b/Makefile index 4a4bed9b..16ab07a4 100644 --- a/Makefile +++ b/Makefile @@ -42,11 +42,27 @@ test-race: fmt: install-imports-formatter go fmt ./... && GOROOT=$(shell go env GOROOT) imports-formatter -check-fmt: fmt - @git diff --exit-code -- . ':!coverage.txt' || { \ - echo "Formatting changes are required. Run 'make fmt'."; \ - exit 1; \ - } +check-fmt: install-imports-formatter + @temp_dir=$$(mktemp -d /tmp/getty-check-fmt.XXXXXX); \ + trap 'case "$$temp_dir" in /tmp/getty-check-fmt.*) rm -rf -- "$$temp_dir" ;; esac' EXIT; \ + while IFS= read -r -d '' file; do \ + mkdir -p "$$temp_dir/$$(dirname "$$file")"; \ + cp -p -- "$$file" "$$temp_dir/$$file"; \ + done < <(git ls-files -z); \ + (cd "$$temp_dir" && \ + GOTOOLCHAIN=go1.25.0+auto go fmt ./... && \ + GOROOT="$$(GOTOOLCHAIN=go1.25.0+auto go env GOROOT)" \ + imports-formatter --path "$$temp_dir" --module github.com/AlexStocks/getty); \ + status=0; \ + while IFS= read -r -d '' file; do \ + current_hash=$$(git hash-object --path="$$file" "$$file"); \ + formatted_hash=$$(git hash-object --path="$$file" "$$temp_dir/$$file"); \ + if test "$$current_hash" != "$$formatted_hash"; then \ + printf 'Formatting changes are required: %s\n' "$$file"; \ + status=1; \ + fi; \ + done < <(git ls-files -z -- '*.go'); \ + exit "$$status" # Clean test generate files clean: diff --git a/doc/superpowers/plans/2026-08-01-github-ci-hardening.md b/doc/superpowers/plans/2026-08-01-github-ci-hardening.md index cb51c710..9a6f79e7 100644 --- a/doc/superpowers/plans/2026-08-01-github-ci-hardening.md +++ b/doc/superpowers/plans/2026-08-01-github-ci-hardening.md @@ -140,7 +140,7 @@ help: @echo " test - Run unit tests with coverage" @echo " test-race - Run transport tests with the race detector" @echo " fmt - Format code" - @echo " check-fmt - Verify that formatting produces no diff" + @echo " check-fmt - Verify formatting without modifying tracked files" @echo " lint - Run go vet and golangci-lint" @echo " clean - Clean generated test files" @@ -155,11 +155,27 @@ test-race: fmt: install-imports-formatter go fmt ./... && GOROOT=$(shell go env GOROOT) imports-formatter -check-fmt: fmt - @git diff --exit-code -- . ':!coverage.txt' || { \ - echo "Formatting changes are required. Run 'make fmt'."; \ - exit 1; \ - } +check-fmt: install-imports-formatter + @temp_dir=$$(mktemp -d /tmp/getty-check-fmt.XXXXXX); \ + trap 'case "$$temp_dir" in /tmp/getty-check-fmt.*) rm -rf -- "$$temp_dir" ;; esac' EXIT; \ + while IFS= read -r -d '' file; do \ + mkdir -p "$$temp_dir/$$(dirname "$$file")"; \ + cp -p -- "$$file" "$$temp_dir/$$file"; \ + done < <(git ls-files -z); \ + (cd "$$temp_dir" && \ + GOTOOLCHAIN=go1.25.0+auto go fmt ./... && \ + GOROOT="$$(GOTOOLCHAIN=go1.25.0+auto go env GOROOT)" \ + imports-formatter --path "$$temp_dir" --module github.com/AlexStocks/getty); \ + status=0; \ + while IFS= read -r -d '' file; do \ + current_hash=$$(git hash-object --path="$$file" "$$file"); \ + formatted_hash=$$(git hash-object --path="$$file" "$$temp_dir/$$file"); \ + if test "$$current_hash" != "$$formatted_hash"; then \ + printf 'Formatting changes are required: %s\n' "$$file"; \ + status=1; \ + fi; \ + done < <(git ls-files -z -- '*.go'); \ + exit "$$status" # Clean generated test files. clean: @@ -688,8 +704,10 @@ git commit -m "docs: replace Travis CI references" **文件:** - 读取:全部实施文件 -- 验证副本:`D:\test\github\review\AlexStocks-getty-pr-108\probes\ci-check-fmt-clean` -- 验证副本:`D:\test\github\review\AlexStocks-getty-pr-108\probes\ci-check-fmt-mutation` +- 验证副本:`D:\test\github\review\AlexStocks-getty-pr-108\probes\check-fmt-readonly-crlf` +- 验证副本:`D:\test\github\review\AlexStocks-getty-pr-108\probes\check-fmt-readonly-gofmt` +- 验证副本:`D:\test\github\review\AlexStocks-getty-pr-108\probes\check-fmt-readonly-imports` +- 验证副本:`D:\test\github\review\AlexStocks-getty-pr-108\probes\check-fmt-readonly-failure` - 输出:agent 仅使用 `apply_patch` 写入 `D:\test\github\review\AlexStocks-getty-pr-108\evidence\ci-local-validation-*.txt`;验证命令只向调用端返回完整 stdout/stderr - [ ] **步骤 1:对全部 workflow 运行 actionlint** @@ -742,49 +760,21 @@ if ($LASTEXITCODE -ne 0) { throw 'workflow supply-chain policy validation failed 预期:退出码 0。注释中的版本标签允许存在,但 `uses:` 的实际 ref 必须是完整 SHA。当前最终文件应为 6 个逻辑 job、14 个 `uses:`;主 CI 应为 5 个逻辑 job;`id-token: write` 只出现于 `Upload Coverage`;Codecov `version` 必须为 `v11.3.1`。 -- [ ] **步骤 3:在干净独立 worktree 证明 `check-fmt` 绿灯** - -```powershell -git worktree add --detach D:\test\github\review\AlexStocks-getty-pr-108\probes\ci-check-fmt-clean HEAD -wsl.exe --cd /mnt/d/test/github/review/AlexStocks-getty-pr-108/probes/ci-check-fmt-clean -- ` - env PATH=/home/alex/bin/go1.25/bin:/home/alex/go/bin:/usr/local/bin:/usr/bin:/bin ` - GOTOOLCHAIN=go1.25.0+auto make check-fmt -if ($LASTEXITCODE -ne 0) { throw 'clean check-fmt probe failed' } -``` - -完整保留 stdout/stderr,由 agent 使用 `apply_patch` 写入 `evidence/ci-local-validation-check-fmt-clean.txt`。预期:退出码 0,probe worktree 的 `git status --porcelain` 为空。主 source worktree 不运行写入式 formatter。 +- [ ] **步骤 3:证明 LF 与 CRLF clean 输入均为只读绿灯** -- [ ] **步骤 4:用格式变异证明 `check-fmt` 会阻断** +在 LF clean 的主验证副本运行 `make check-fmt`,并在命令前后分别记录 tracked Go 文件的聚合字节哈希、`git status --porcelain=v2 --branch --untracked-files=all` 和 `/tmp/getty-check-fmt.*` 列表。预期:退出码 0,三个值前后完全一致。 -```powershell -git worktree add --detach D:\test\github\review\AlexStocks-getty-pr-108\probes\ci-check-fmt-mutation HEAD -``` +再从固定 seed 建立 `core.autocrlf=true` 的 clean CRLF Git probe,确认 `git ls-files --eol -- '*.go'` 为 `i/lf w/crlf`。运行 `make check-fmt` 后再次核对原始字节哈希、Git clean 状态、物理 EOL 和临时目录列表。预期:退出码 0,文件仍为 CRLF,状态仍 clean,原始字节哈希不变且无临时目录残留。这一门禁证明 clean-filter hash 比较不会把 CRLF/LF 的工作树表示差异误报为格式错误。 -在 mutation worktree 中通过 `apply_patch` 将一个已跟踪 Go 函数签名改成 gofmt 会修复的格式,例如: +- [ ] **步骤 4:用两类格式变异和 formatter 故障证明非零传播** -```diff --func udpReadBufferSize(maxMsgLen int32) int { -+func udpReadBufferSize( maxMsgLen int32 ) int { -``` +建立三个独立 probe,所有变异只通过 `apply_patch` 写入 probe: -然后运行: - -```powershell -$bash = @' -set -eu -o pipefail -export PATH=/home/alex/bin/go1.25/bin:/home/alex/go/bin:/usr/local/bin:/usr/bin:/bin -export GOTOOLCHAIN=go1.25.0+auto -check_fmt_exit=0 -make check-fmt || check_fmt_exit=$? -printf 'CHECK_FMT_EXIT=%d\n' "$check_fmt_exit" -test "$check_fmt_exit" -ne 0 -'@ -$encoded = [Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes($bash)) -wsl.exe --cd /mnt/d/test/github/review/AlexStocks-getty-pr-108/probes/ci-check-fmt-mutation -- /bin/bash -c "printf '%s' '$encoded' | base64 -d | /bin/bash" -if ($LASTEXITCODE -ne 0) { throw 'check-fmt mutation did not fail as required' } -``` +1. `check-fmt-readonly-gofmt`:保持 import blocks 已符合项目规则,只制造 `gofmt -d` 可见的函数空格差异。先在临时副本单独执行 imports-formatter,证明 clean-filter hash 不变;再运行 `make check-fmt`,预期列出变异文件并非零退出。 +2. `check-fmt-readonly-imports`:构造 gofmt 已接受的单一 import block,但把标准库和项目内部 import 混在同一组。先确认 `gofmt -d` 输出为空,再在临时副本单独执行 imports-formatter,证明 clean-filter hash 改变;运行 `make check-fmt`,预期列出变异文件并非零退出。 +3. `check-fmt-readonly-failure`:通过导出的同名 Bash function 让 imports-formatter 明确返回 23。运行 `make check-fmt`,预期 formatter 错误向 make 非零传播,且 `/tmp/getty-check-fmt.*` 前后列表一致。 -完整保留 stdout/stderr,由 agent 使用 `apply_patch` 写入 `evidence/ci-local-validation-check-fmt-mutation.txt`。预期:formatter 修复变异后,`git diff --exit-code` 使 `make check-fmt` 非零退出;`CHECK_FMT_EXIT` 明确为非零,输出包含具体 diff 和修复提示。该 probe 不提交、不 push。 +每个 probe 都必须记录被测文件的原始字节哈希和 Git 状态,并证明运行前后完全一致;不能因为检查失败而允许 formatter 改写变异文件。完整 stdout/stderr 由 agent 使用 `apply_patch` 写入 `evidence/ci-local-validation-check-fmt-readonly.txt`。 - [ ] **步骤 5:运行模块、测试、race 与 lint** diff --git a/doc/superpowers/specs/2026-08-01-github-ci-hardening-design.md b/doc/superpowers/specs/2026-08-01-github-ci-hardening-design.md index cb951903..3ff474de 100644 --- a/doc/superpowers/specs/2026-08-01-github-ci-hardening-design.md +++ b/doc/superpowers/specs/2026-08-01-github-ci-hardening-design.md @@ -221,11 +221,11 @@ Makefile 调整为可由本地和 CI 复用的显式门禁: - `test` 不再执行 `go env -w`,改为命令级 `GOTOOLCHAIN`。 - `test` 增加 `-count=1`,同时保留 atomic coverage 输出。 - 新增 `test-race`,只运行 `./transport` 的 race 测试。 -- 新增 `check-fmt`:执行项目格式化命令后,用 `git diff --exit-code -- . ':!coverage.txt'` 检测并输出格式化差异,同时排除测试生成的 coverage 文件。 +- 新增 `check-fmt`:把当前 tracked 工作树内容复制到 `mktemp` 临时镜像,只在临时镜像内依次执行命令级 `GOTOOLCHAIN=go1.25.0+auto go fmt ./...` 和固定的 `imports-formatter v1.0.10`。随后逐个 tracked Go 文件使用 `git hash-object --path=<原路径>` 计算应用仓库 clean filter 后的对象哈希,比较当前文件与临时格式化结果;不一致时列出文件并非零退出。 - `imports-formatter` 从 `@latest` 固定到本次已验证的 `v1.0.10`。 - `golangci-lint` 暂时保持当前已验证的 `v2.4.0`,避免在 CI 架构改造中混入新 lint 规则导致的源码修复;升级到 Dubbo-Go 使用的更高版本应单独处理。 -`check-fmt` 会在一次性 CI checkout 中运行写入式 formatter,但只用于验证差异;本地验证时必须在独立 probe 副本执行,不能改写 PR 主证据副本后再恢复。 +`fmt` 保留为开发者明确调用的写入式格式化目标;`check-fmt` 不依赖 `fmt`,也不对当前 checkout 中的 tracked 文件运行写入式 formatter。临时目录通过 `trap` 清理,复制、格式化或 formatter 任一步失败都必须非零传播。比较使用带原路径的 Git clean-filter 哈希而不是原始字节比较,因此 `core.autocrlf=true` 下语义相同的 CRLF 工作树文件与 LF 格式化结果不会误报;无论检查成功还是失败,当前文件的物理换行和字节内容都保持不变。 ### 9. CodeQL @@ -301,7 +301,7 @@ PR 分支阶段只能通过严格 YAML 解析和字段/结构断言验证该文 ### Makefile 验证 -在独立 probe 副本运行: +在主验证副本和 `probes/` 下的独立变异副本运行: - `make check-fmt` - `make test` @@ -309,7 +309,7 @@ PR 分支阶段只能通过严格 YAML 解析和字段/结构断言验证该文 - `make lint` - `git status --porcelain=v2 --branch --untracked-files=all` -确认 `make test` 不改写用户级 `go env`,工具版本与设计一致,格式检查能在故意制造格式差异时失败。 +确认 `make test` 不改写用户级 `go env`,工具版本与设计一致。`check-fmt` 必须完成以下 TDD 门禁:LF clean 输入退出 0 且 tracked Go 文件聚合字节哈希和 Git 状态前后不变;`core.autocrlf=true` 的 clean CRLF 输入退出 0 且物理 EOL、字节哈希和 clean 状态不变;仅 gofmt 差异退出非零;仅项目 import order 差异在 `gofmt -d` 为空时仍退出非零;formatter 故障非零传播且临时目录被清理。所有失败探针都必须证明被测源文件的字节哈希和 Git 状态前后不变。 ### Go 验证 From 41e5057ec37ac4932c2f3426eb7b4d3bc0f0820f Mon Sep 17 00:00:00 2001 From: "Xin.Zh" Date: Sun, 2 Aug 2026 00:54:43 +0800 Subject: [PATCH 17/24] build: propagate format enumeration failures Write both NUL-delimited tracked-file manifests with ordinary commands before copy or formatter work so the recipe shell observes enumeration failures directly instead of accepting the consuming loop status. Keep the manifests inside the disposable metadata directory so they are neither copied nor compared, and document the injected producer-failure gates alongside the existing read-only formatting probes. Constraint: Limit this commit to Makefile and the two CI design documents; do not modify workflows, Go source, other tracked files, or remote state. Confidence: High; both enumeration failures now propagate before formatter execution, and all clean, mutation, failure, policy, module, and scope gates pass without source or temporary-directory changes. Scope-risk: Low; successful formatting behavior and the explicit write-capable fmt target remain unchanged while check-fmt now fails earlier on incomplete file discovery. Tested: make check-fmt; enumeration exit 38 and 37 red-green probes; clean LF and CRLF probes; gofmt-only, import-only, and formatter-exit-23 probes; actionlint v1.7.12; workflow and Makefile policy; go mod verify; Markdown fence, placeholder, secret, scope, and git diff checks. Not-tested: GitHub-hosted workflow execution was not run because this commit is not pushed. Co-authored-by: OmX Signed-off-by: Xin.Zh --- Makefile | 11 ++++++++--- .../plans/2026-08-01-github-ci-hardening.md | 11 +++++++++-- .../specs/2026-08-01-github-ci-hardening-design.md | 6 +++--- 3 files changed, 20 insertions(+), 8 deletions(-) diff --git a/Makefile b/Makefile index 16ab07a4..a0f24c5e 100644 --- a/Makefile +++ b/Makefile @@ -28,7 +28,7 @@ help: @echo " test - Run unit tests with coverage" @echo " test-race - Run transport race tests" @echo " fmt - Format code" - @echo " check-fmt - Verify code formatting" + @echo " check-fmt - Verify formatting without modifying tracked files" @echo " lint - Run golangci-lint" @echo " clean - Clean test generate files" @@ -45,10 +45,15 @@ fmt: install-imports-formatter check-fmt: install-imports-formatter @temp_dir=$$(mktemp -d /tmp/getty-check-fmt.XXXXXX); \ trap 'case "$$temp_dir" in /tmp/getty-check-fmt.*) rm -rf -- "$$temp_dir" ;; esac' EXIT; \ + mkdir -p "$$temp_dir/.git"; \ + tracked_files="$$temp_dir/.git/tracked-files.z"; \ + go_files="$$temp_dir/.git/go-files.z"; \ + git ls-files -z > "$$tracked_files"; \ + git ls-files -z -- '*.go' > "$$go_files"; \ while IFS= read -r -d '' file; do \ mkdir -p "$$temp_dir/$$(dirname "$$file")"; \ cp -p -- "$$file" "$$temp_dir/$$file"; \ - done < <(git ls-files -z); \ + done < "$$tracked_files"; \ (cd "$$temp_dir" && \ GOTOOLCHAIN=go1.25.0+auto go fmt ./... && \ GOROOT="$$(GOTOOLCHAIN=go1.25.0+auto go env GOROOT)" \ @@ -61,7 +66,7 @@ check-fmt: install-imports-formatter printf 'Formatting changes are required: %s\n' "$$file"; \ status=1; \ fi; \ - done < <(git ls-files -z -- '*.go'); \ + done < "$$go_files"; \ exit "$$status" # Clean test generate files diff --git a/doc/superpowers/plans/2026-08-01-github-ci-hardening.md b/doc/superpowers/plans/2026-08-01-github-ci-hardening.md index 9a6f79e7..8ae012cd 100644 --- a/doc/superpowers/plans/2026-08-01-github-ci-hardening.md +++ b/doc/superpowers/plans/2026-08-01-github-ci-hardening.md @@ -158,10 +158,15 @@ fmt: install-imports-formatter check-fmt: install-imports-formatter @temp_dir=$$(mktemp -d /tmp/getty-check-fmt.XXXXXX); \ trap 'case "$$temp_dir" in /tmp/getty-check-fmt.*) rm -rf -- "$$temp_dir" ;; esac' EXIT; \ + mkdir -p "$$temp_dir/.git"; \ + tracked_files="$$temp_dir/.git/tracked-files.z"; \ + go_files="$$temp_dir/.git/go-files.z"; \ + git ls-files -z > "$$tracked_files"; \ + git ls-files -z -- '*.go' > "$$go_files"; \ while IFS= read -r -d '' file; do \ mkdir -p "$$temp_dir/$$(dirname "$$file")"; \ cp -p -- "$$file" "$$temp_dir/$$file"; \ - done < <(git ls-files -z); \ + done < "$$tracked_files"; \ (cd "$$temp_dir" && \ GOTOOLCHAIN=go1.25.0+auto go fmt ./... && \ GOROOT="$$(GOTOOLCHAIN=go1.25.0+auto go env GOROOT)" \ @@ -174,7 +179,7 @@ check-fmt: install-imports-formatter printf 'Formatting changes are required: %s\n' "$$file"; \ status=1; \ fi; \ - done < <(git ls-files -z -- '*.go'); \ + done < "$$go_files"; \ exit "$$status" # Clean generated test files. @@ -774,6 +779,8 @@ if ($LASTEXITCODE -ne 0) { throw 'workflow supply-chain policy validation failed 2. `check-fmt-readonly-imports`:构造 gofmt 已接受的单一 import block,但把标准库和项目内部 import 混在同一组。先确认 `gofmt -d` 输出为空,再在临时副本单独执行 imports-formatter,证明 clean-filter hash 改变;运行 `make check-fmt`,预期列出变异文件并非零退出。 3. `check-fmt-readonly-failure`:通过导出的同名 Bash function 让 imports-formatter 明确返回 23。运行 `make check-fmt`,预期 formatter 错误向 make 非零传播,且 `/tmp/getty-check-fmt.*` 前后列表一致。 +另增加两个生产者故障注入门禁:让第一个 `git ls-files -z` 在输出部分或全部 tracked 路径后返回 38,确认 `make check-fmt` 非零且 formatter 未执行;让第二个 `git ls-files -z -- '*.go'` 零输出后返回 37,确认目标仍非零。两个枚举命令必须先把 NUL 清单写入临时镜像的 `.git/` 元数据目录,再由循环通过普通文件重定向读取;禁止使用会隐藏生产者退出码的 process substitution。两个故障探针都必须证明 source 字节哈希和 Git 状态不变,且 trap 没有留下临时目录。 + 每个 probe 都必须记录被测文件的原始字节哈希和 Git 状态,并证明运行前后完全一致;不能因为检查失败而允许 formatter 改写变异文件。完整 stdout/stderr 由 agent 使用 `apply_patch` 写入 `evidence/ci-local-validation-check-fmt-readonly.txt`。 - [ ] **步骤 5:运行模块、测试、race 与 lint** diff --git a/doc/superpowers/specs/2026-08-01-github-ci-hardening-design.md b/doc/superpowers/specs/2026-08-01-github-ci-hardening-design.md index 3ff474de..b88000f5 100644 --- a/doc/superpowers/specs/2026-08-01-github-ci-hardening-design.md +++ b/doc/superpowers/specs/2026-08-01-github-ci-hardening-design.md @@ -221,11 +221,11 @@ Makefile 调整为可由本地和 CI 复用的显式门禁: - `test` 不再执行 `go env -w`,改为命令级 `GOTOOLCHAIN`。 - `test` 增加 `-count=1`,同时保留 atomic coverage 输出。 - 新增 `test-race`,只运行 `./transport` 的 race 测试。 -- 新增 `check-fmt`:把当前 tracked 工作树内容复制到 `mktemp` 临时镜像,只在临时镜像内依次执行命令级 `GOTOOLCHAIN=go1.25.0+auto go fmt ./...` 和固定的 `imports-formatter v1.0.10`。随后逐个 tracked Go 文件使用 `git hash-object --path=<原路径>` 计算应用仓库 clean filter 后的对象哈希,比较当前文件与临时格式化结果;不一致时列出文件并非零退出。 +- 新增 `check-fmt`:先用普通命令把 `git ls-files -z` 和 `git ls-files -z -- '*.go'` 的结果分别写入 `mktemp` 临时镜像 `.git/` 目录下的 NUL 清单,确保 shell 的 `set -e` 能直接观察任一枚举失败;循环只从已成功生成的清单文件读取。随后把当前 tracked 工作树内容复制到临时镜像,只在临时镜像内依次执行命令级 `GOTOOLCHAIN=go1.25.0+auto go fmt ./...` 和固定的 `imports-formatter v1.0.10`。最后逐个 tracked Go 文件使用 `git hash-object --path=<原路径>` 计算应用仓库 clean filter 后的对象哈希,比较当前文件与临时格式化结果;不一致时列出文件并非零退出。 - `imports-formatter` 从 `@latest` 固定到本次已验证的 `v1.0.10`。 - `golangci-lint` 暂时保持当前已验证的 `v2.4.0`,避免在 CI 架构改造中混入新 lint 规则导致的源码修复;升级到 Dubbo-Go 使用的更高版本应单独处理。 -`fmt` 保留为开发者明确调用的写入式格式化目标;`check-fmt` 不依赖 `fmt`,也不对当前 checkout 中的 tracked 文件运行写入式 formatter。临时目录通过 `trap` 清理,复制、格式化或 formatter 任一步失败都必须非零传播。比较使用带原路径的 Git clean-filter 哈希而不是原始字节比较,因此 `core.autocrlf=true` 下语义相同的 CRLF 工作树文件与 LF 格式化结果不会误报;无论检查成功还是失败,当前文件的物理换行和字节内容都保持不变。 +`fmt` 保留为开发者明确调用的写入式格式化目标;`check-fmt` 不依赖 `fmt`,也不对当前 checkout 中的 tracked 文件运行写入式 formatter。临时目录通过 `trap` 清理,文件枚举、复制、格式化或 formatter 任一步失败都必须非零传播;不得把 `git ls-files` 放在 process substitution 中,因为消费循环的成功状态会隐藏生产者的非零退出。清单位于仓库外的临时 `.git/` 目录,不会被复制或参与格式比较。比较使用带原路径的 Git clean-filter 哈希而不是原始字节比较,因此 `core.autocrlf=true` 下语义相同的 CRLF 工作树文件与 LF 格式化结果不会误报;无论检查成功还是失败,当前文件的物理换行和字节内容都保持不变。 ### 9. CodeQL @@ -309,7 +309,7 @@ PR 分支阶段只能通过严格 YAML 解析和字段/结构断言验证该文 - `make lint` - `git status --porcelain=v2 --branch --untracked-files=all` -确认 `make test` 不改写用户级 `go env`,工具版本与设计一致。`check-fmt` 必须完成以下 TDD 门禁:LF clean 输入退出 0 且 tracked Go 文件聚合字节哈希和 Git 状态前后不变;`core.autocrlf=true` 的 clean CRLF 输入退出 0 且物理 EOL、字节哈希和 clean 状态不变;仅 gofmt 差异退出非零;仅项目 import order 差异在 `gofmt -d` 为空时仍退出非零;formatter 故障非零传播且临时目录被清理。所有失败探针都必须证明被测源文件的字节哈希和 Git 状态前后不变。 +确认 `make test` 不改写用户级 `go env`,工具版本与设计一致。`check-fmt` 必须完成以下 TDD 门禁:LF clean 输入退出 0 且 tracked Go 文件聚合字节哈希和 Git 状态前后不变;`core.autocrlf=true` 的 clean CRLF 输入退出 0 且物理 EOL、字节哈希和 clean 状态不变;仅 gofmt 差异退出非零;仅项目 import order 差异在 `gofmt -d` 为空时仍退出非零;formatter 故障非零传播且临时目录被清理;第一个 tracked-file 枚举在部分或全部输出后返回 38 时目标非零且 formatter 不执行;第二个 Go-file 枚举零输出后返回 37 时目标仍非零。所有失败探针都必须证明被测源文件的字节哈希和 Git 状态前后不变,并确认没有临时目录残留。 ### Go 验证 From 588997a1c1e0ade4585e4ab7dd0097b378e44d8c Mon Sep 17 00:00:00 2001 From: "Xin.Zh" Date: Sun, 2 Aug 2026 01:43:08 +0800 Subject: [PATCH 18/24] ci: disable checkout credential persistence Set persist-credentials to false on all five checkout steps because every downstream operation is local or authenticates through its own Action token rather than the repository's Git configuration. Keep the existing Action SHAs, inputs, permissions, OIDC isolation, Codecov flow, and CodeQL shape unchanged, and align the design, complete workflow examples, and staged and final policy gates with the credential boundary. Constraint: Limit this commit to the two workflow files and the two approved CI design documents; do not modify Makefile, Go source, Dependabot, README files, permissions, or remote state. Confidence: High; the policy failed with five missing settings before implementation and passes with five explicit false values afterward, while the downstream-step scan found no Git remote authentication requirement. Scope-risk: Low; checkout behavior changes only by removing persisted local Git credentials after source retrieval, while CodeQL continues to use its own built-in GitHub token input. Tested: checkout v7 and CodeQL official input provenance; TDD red and green credential policy; actionlint v1.7.12; workflow job, uses, timeout, permission, OIDC, cache, Codecov, and CodeQL policy; downstream remote-git scan; full YAML synchronization; git diff, Markdown fence, placeholder, secret, and four-file scope checks. Not-tested: GitHub-hosted workflow execution was not run because this commit is not pushed. Co-authored-by: OmX Signed-off-by: Xin.Zh --- .github/workflows/codeql.yml | 2 + .github/workflows/github-actions.yml | 8 ++++ .../plans/2026-08-01-github-ci-hardening.md | 46 ++++++++++++++++--- .../2026-08-01-github-ci-hardening-design.md | 13 ++++-- 4 files changed, 60 insertions(+), 9 deletions(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 899deef4..e731066e 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -29,6 +29,8 @@ jobs: steps: - name: Check out repository uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 + with: + persist-credentials: false - name: Initialize CodeQL uses: github/codeql-action/init@a2983b8bed1923f44751c5c43237f479442827b3 # v3 diff --git a/.github/workflows/github-actions.yml b/.github/workflows/github-actions.yml index 0a35b1e0..6e0765ca 100644 --- a/.github/workflows/github-actions.yml +++ b/.github/workflows/github-actions.yml @@ -23,6 +23,8 @@ jobs: steps: - name: Checkout uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 + with: + persist-credentials: false - name: Check License Header uses: apache/skywalking-eyes/header@315732dd4b8d3a015d8d9b91936b935a0b854817 # verified main, post-v0.8.0 @@ -37,6 +39,8 @@ jobs: steps: - name: Checkout uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 + with: + persist-credentials: false - name: Set up Go uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0 @@ -93,6 +97,8 @@ jobs: steps: - name: Checkout uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 + with: + persist-credentials: false - name: Set up Go uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0 @@ -120,6 +126,8 @@ jobs: steps: - name: Checkout uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 + with: + persist-credentials: false - name: Set up Go uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0 diff --git a/doc/superpowers/plans/2026-08-01-github-ci-hardening.md b/doc/superpowers/plans/2026-08-01-github-ci-hardening.md index 8ae012cd..10758924 100644 --- a/doc/superpowers/plans/2026-08-01-github-ci-hardening.md +++ b/doc/superpowers/plans/2026-08-01-github-ci-hardening.md @@ -52,6 +52,8 @@ github/codeql-action@v3: a2983b8bed1923f44751c5c43237f479442827b3 Action 固定默认要求官方稳定 release/tag 与完整 SHA 对应。窄例外是:official main 的 verified commit 明确晚于最新 release,并且回退 release 会撤销安全或可复现性加固;此时必须记录 ancestry、差异和完整 SHA,仍禁止 `@main` 等可变 ref。本轮 `setup-go` v7.0.0/v7 均解析为 `b7ad1dad31e06c5925ef5d2fc7ad053ef454303e`,action.yml 与 v6.5.0 的输入、输出和 Node 24 runtime 不变,所以只替换 SHA。SkyWalking Eyes v0.8.0 解析为 `61275cc80d0798a405cb070f7d3a8aaf7cf2c2c1`,而保留的 `315732dd4b8d3a015d8d9b91936b935a0b854817` 是 official main verified commit,位于其后 27 个提交,已固定内部 `setup-go` 并硬化 shell 输入;不得为了满足 release 标签而降级。 +`actions/checkout@v7` 官方 `action.yml` 支持 `persist-credentials`,默认值为 `true`。本计划的 5 个 checkout 后续只执行本地读取、构建、测试或使用各 Action 自身 token 的 API 上传,不需要 Git remote credential,因此全部显式设置 `persist-credentials: false`。CodeQL `init`/`analyze` 默认使用 `${{ github.token }}`,不依赖 checkout 持久化认证;不得为此扩大 workflow 或 job permissions。 + ### 任务 1:实时租约与旧门禁缺口基线 **文件:** @@ -291,6 +293,8 @@ jobs: steps: - name: Checkout uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 + with: + persist-credentials: false - name: Check License Header uses: apache/skywalking-eyes/header@315732dd4b8d3a015d8d9b91936b935a0b854817 @@ -305,6 +309,8 @@ jobs: steps: - name: Checkout uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 + with: + persist-credentials: false - name: Set up Go uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0 @@ -361,6 +367,8 @@ jobs: steps: - name: Checkout uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 + with: + persist-credentials: false - name: Set up Go uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0 @@ -388,6 +396,8 @@ jobs: steps: - name: Checkout uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 + with: + persist-credentials: false - name: Set up Go uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0 @@ -402,7 +412,7 @@ jobs: run: go build ./... ``` -不得恢复独立 `actions/cache`、CodeCov bash uploader 或任何可变 Action 引用。License job 不需要显式 `GITHUB_TOKEN` 环境变量;GitHub 会为 Action 提供最小权限 token 上下文。 +不得恢复独立 `actions/cache`、CodeCov bash uploader 或任何可变 Action 引用。4 个主 CI checkout 都显式设置 `persist-credentials: false`,因为后续 step 不需要 Git remote 认证。License job 不需要显式 `GITHUB_TOKEN` 环境变量;GitHub 会为 Action 提供最小权限 token 上下文。 - [ ] **步骤 3:运行政策检查并验证全部 Action 引用** @@ -422,13 +432,23 @@ python3 - <<"PY" import pathlib import re -paths = list(pathlib.Path(".github/workflows").glob("*.yml")) -paths += list(pathlib.Path(".github/workflows").glob("*.yaml")) +paths = [pathlib.Path(".github/workflows/github-actions.yml")] +workflow_text = "\n".join(path.read_text(encoding="utf-8") for path in paths) for path in paths: for number, line in enumerate(path.read_text(encoding="utf-8").splitlines(), 1): match = re.search(r"\buses:\s+\S+@([^\s#]+)", line) if match and not re.fullmatch(r"[0-9a-f]{40}", match.group(1)): raise SystemExit(f"{path}:{number}: mutable action ref {match.group(1)}") +checkout_pattern = r"(?m)^\s+uses:\s+actions/checkout@[0-9a-f]{40}(?:\s+#.*)?\s*$" +checkout_false_pattern = checkout_pattern + r"\n\s+with:\s*$\n\s+persist-credentials:\s+false\s*$" +checkout_count = len(re.findall(checkout_pattern, workflow_text)) +checkout_false_count = len(re.findall(checkout_false_pattern, workflow_text)) +checkout_true_count = len(re.findall(r"(?m)^\s+persist-credentials:\s+true\s*$", workflow_text)) +if checkout_count != 4 or checkout_false_count != 4 or checkout_true_count != 0: + raise SystemExit( + f"checkout credential policy mismatch: total={checkout_count}, " + f"false={checkout_false_count}, true={checkout_true_count}" + ) PY '@ $encoded = [Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes($bash)) @@ -436,7 +456,7 @@ wsl.exe --cd /mnt/d/test/github/review/AlexStocks-getty-pr-108/source -- /bin/ba if ($LASTEXITCODE -ne 0) { throw 'main workflow policy validation failed' } ``` -预期:退出码 0;无 mutable ref、重复 cache 或远程 shell uploader。主 CI 应恰好包含 5 个逻辑 job 和 11 个 `uses:`;`id-token: write` 只位于 `Upload Coverage`,Codecov `version` 为 `v11.3.1`。 +预期:退出码 0;无 mutable ref、重复 cache 或远程 shell uploader。主 CI 的 4 个 checkout 都必须显式设置 `persist-credentials: false`,不得出现 `true` 或缺失项;主 CI 应恰好包含 5 个逻辑 job 和 11 个 `uses:`;`id-token: write` 只位于 `Upload Coverage`,Codecov `version` 为 `v11.3.1`。 - [ ] **步骤 4:提交主 workflow** @@ -496,6 +516,8 @@ jobs: steps: - name: Check out repository uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 + with: + persist-credentials: false - name: Initialize CodeQL uses: github/codeql-action/init@a2983b8bed1923f44751c5c43237f479442827b3 # v3 @@ -507,7 +529,7 @@ jobs: uses: github/codeql-action/analyze@a2983b8bed1923f44751c5c43237f479442827b3 # v3 ``` -当前官方形状为 `init` 中声明 `build-mode: autobuild` 后直接执行 `analyze`。旧的 `init(build-mode: autobuild) -> autobuild -> analyze` 三步写法仍兼容,但显式 `autobuild` 与 init 的 build mode 重复,属于冗余,不应保留在可复现计划中。 +当前官方形状为 `init` 中声明 `build-mode: autobuild` 后直接执行 `analyze`。CodeQL checkout 显式设置 `persist-credentials: false`;后续 `init`/`analyze` 使用自身默认的 `${{ github.token }}`,不依赖本地 Git credential。旧的 `init(build-mode: autobuild) -> autobuild -> analyze` 三步写法仍兼容,但显式 `autobuild` 与 init 的 build mode 重复,属于冗余,不应保留在可复现计划中。 - [ ] **步骤 3:用 actionlint 验证两个 workflow** @@ -751,11 +773,22 @@ import re paths = list(pathlib.Path(".github/workflows").glob("*.yml")) paths += list(pathlib.Path(".github/workflows").glob("*.yaml")) +workflow_text = "\n".join(path.read_text(encoding="utf-8") for path in paths) for path in paths: for number, line in enumerate(path.read_text(encoding="utf-8").splitlines(), 1): match = re.search(r"\buses:\s+\S+@([^\s#]+)", line) if match and not re.fullmatch(r"[0-9a-f]{40}", match.group(1)): raise SystemExit(f"{path}:{number}: mutable action ref {match.group(1)}") +checkout_pattern = r"(?m)^\s+uses:\s+actions/checkout@[0-9a-f]{40}(?:\s+#.*)?\s*$" +checkout_false_pattern = checkout_pattern + r"\n\s+with:\s*$\n\s+persist-credentials:\s+false\s*$" +checkout_count = len(re.findall(checkout_pattern, workflow_text)) +checkout_false_count = len(re.findall(checkout_false_pattern, workflow_text)) +checkout_true_count = len(re.findall(r"(?m)^\s+persist-credentials:\s+true\s*$", workflow_text)) +if checkout_count != 5 or checkout_false_count != 5 or checkout_true_count != 0: + raise SystemExit( + f"checkout credential policy mismatch: total={checkout_count}, " + f"false={checkout_false_count}, true={checkout_true_count}" + ) PY '@ $encoded = [Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes($bash)) @@ -763,7 +796,7 @@ wsl.exe --cd /mnt/d/test/github/review/AlexStocks-getty-pr-108/source -- /bin/ba if ($LASTEXITCODE -ne 0) { throw 'workflow supply-chain policy validation failed' } ``` -预期:退出码 0。注释中的版本标签允许存在,但 `uses:` 的实际 ref 必须是完整 SHA。当前最终文件应为 6 个逻辑 job、14 个 `uses:`;主 CI 应为 5 个逻辑 job;`id-token: write` 只出现于 `Upload Coverage`;Codecov `version` 必须为 `v11.3.1`。 +预期:退出码 0。注释中的版本标签允许存在,但 `uses:` 的实际 ref 必须是完整 SHA。5 个 checkout 必须全部显式设置 `persist-credentials: false`,且后续 step 扫描不得发现 `git fetch`、`git push`、remote、submodule 等认证需求。当前最终文件应为 6 个逻辑 job、14 个 `uses:`;主 CI 应为 5 个逻辑 job;`id-token: write` 只出现于 `Upload Coverage`;Codecov `version` 必须为 `v11.3.1`。 - [ ] **步骤 3:证明 LF 与 CRLF clean 输入均为只读绿灯** @@ -848,6 +881,7 @@ git grep -n -I -E 'travis-ci|codecov\.io/bash|go env -w GOTOOLCHAIN|imports-form ```text [ ] 唯一 Go cache owner 是 setup-go v7.0.0 [ ] checkout 位于 setup-go 前 +[ ] 全部 5 个 checkout 显式设置 persist-credentials:false,后续 step 无 Git remote 认证需求 [ ] 主 CI 恰好 5 个逻辑 job:License、Test and Lint、Upload Coverage、Race、Build matrix [ ] Test and Lint 无 OIDC,使用 upload-artifact v7.0.1 上传 coverage(missing=error、retention=1) [ ] Upload Coverage needs test-and-lint,权限只有 id-token:write,不 checkout/setup-go/run diff --git a/doc/superpowers/specs/2026-08-01-github-ci-hardening-design.md b/doc/superpowers/specs/2026-08-01-github-ci-hardening-design.md index b88000f5..d69950ae 100644 --- a/doc/superpowers/specs/2026-08-01-github-ci-hardening-design.md +++ b/doc/superpowers/specs/2026-08-01-github-ci-hardening-design.md @@ -89,6 +89,8 @@ concurrency: ```yaml uses: actions/checkout@ # v7 +with: + persist-credentials: false ``` 实现前重新查询并固定: @@ -103,6 +105,8 @@ uses: actions/checkout@ # v7 Dependabot 的 `github-actions` ecosystem 负责后续 Action 更新。不得使用 `@main`,也不得在同一 workflow 中同时保留 major tag 与完整 SHA 两套引用方式。 +所有 5 个 checkout step 都显式设置 `persist-credentials: false`。这些 job 在 checkout 后只执行本地源码读取、Go 构建/测试、artifact 操作和固定 SHA 的 Action,不执行需要仓库认证的 `git fetch`、`git push`、remote 或 submodule 操作,因此不应把 checkout token 或 SSH key 持久化到本地 Git 配置。CodeQL 的初始化和结果上传使用 `github/codeql-action` 自身的 `token` 输入,默认值为 `${{ github.token }}`,不依赖 checkout 写入的 Git credential;这项加固不改变既有 `permissions`。 + 本轮正式质量审查确认:`actions/upload-artifact@v7.0.1` 的 release 与 tag ref 都指向 `043fb46d1a93c77aae656e7c1c64a875d1fc6a0a`;`actions/download-artifact@v8.0.1` 的 release 与 tag ref 都指向 `3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c`。`download-artifact` v8.0.1 tag 下 README 仍有一处 `@v7` 示例,属于示例文本滞后;release 元数据和精确 tag ref 一致,因此实现以 release/ref 指向的完整 SHA 为准,不因 README 的单处旧示例降级到 v7。 Action 固定默认采用官方稳定 release/tag 对应的完整 SHA。仅当官方 main 上的 verified commit 明确晚于最新 release,且退回该 release 会撤销安全加固或可复现性改进时,才允许在设计中记录 provenance 后固定该 verified commit;这个例外不允许使用 `@main` 等可变引用。本轮 `actions/setup-go@v7.0.0` 的 release、`v7.0.0` 与 `v7` tag 均指向 `b7ad1dad31e06c5925ef5d2fc7ad053ef454303e`,可直接替换 v6.5.0 SHA,现有输入和 Node 24 runner 要求不变。`apache/skywalking-eyes/header@315732dd4b8d3a015d8d9b91936b935a0b854817` 是 official main 上经 GitHub 验证、比 v0.8.0 release commit 多 27 个提交的固定提交;它已将内部 `setup-go` 固定到完整 SHA,并对 shell 输入进行环境变量和引用加固,因此保留该提交,避免降级到 v0.8.0。 @@ -112,7 +116,7 @@ Action 固定默认采用官方稳定 release/tag 对应的完整 SHA。仅当 License job: - `permissions: contents: read` -- checkout 固定到完整 SHA +- checkout 固定到完整 SHA,并设置 `persist-credentials: false` - SkyWalking Eyes 固定到完整 SHA - `timeout-minutes: 10` - 保持 `.licenserc.yaml` 和 `mode: check` @@ -141,6 +145,8 @@ with: 删除独立 `actions/cache` step,让 `setup-go` 成为 Go module/build cache 的唯一 owner。 +Checkout 显式禁用 credential persistence;后续 module 验证、格式检查、测试、lint 和 artifact 上传都不执行需要 Git remote 认证的命令。 + 模块验证执行 `go mod verify`。格式检查执行 `make check-fmt`。测试执行 `make test`,生成 `coverage.txt`。Lint 执行 `make lint`。随后使用固定 SHA 的 `actions/upload-artifact@v7.0.1` 上传 artifact:名称为 `coverage`,路径为 `coverage.txt`,文件缺失时报错,保留 1 天。 `Test and Lint` 继承 workflow 顶层的 `contents: read`,不声明也不获得 `id-token: write`。OIDC 权限只授予后续隔离的 `Upload Coverage` job。 @@ -250,7 +256,7 @@ jobs: security-events: write ``` -CodeQL 显式指定 `languages: go`,使用固定 commit SHA 的 `init` 和 `analyze`。当前官方形状是在 `init` 中设置 `build-mode: autobuild`,随后直接执行 `analyze`,不再增加显式 `github/codeql-action/autobuild` step。旧的 `init(build-mode: autobuild) -> autobuild -> analyze` 三步形状仍可兼容运行,但显式 `autobuild` 与 init 的 build mode 重复,属于冗余。不得复制 Dubbo-Go workflow 中手工 checkout PR merge commit 父节点的历史逻辑;使用 GitHub 当前标准 pull request checkout 语义。 +CodeQL 显式指定 `languages: go`,使用固定 commit SHA 的 `init` 和 `analyze`。其 checkout 同样设置 `persist-credentials: false`;`init` 和 `analyze` 使用 Action 自身默认的 `${{ github.token }}` 输入完成包访问和结果上传,不依赖本地 Git credential。当前官方形状是在 `init` 中设置 `build-mode: autobuild`,随后直接执行 `analyze`,不再增加显式 `github/codeql-action/autobuild` step。旧的 `init(build-mode: autobuild) -> autobuild -> analyze` 三步形状仍可兼容运行,但显式 `autobuild` 与 init 的 build mode 重复,属于冗余。不得复制 Dubbo-Go workflow 中手工 checkout PR merge commit 父节点的历史逻辑;使用 GitHub 当前标准 pull request checkout 语义。 ### 10. Dependabot @@ -294,6 +300,7 @@ PR 分支阶段只能通过严格 YAML 解析和字段/结构断言验证该文 - 使用 `actionlint v1.7.12` 检查全部 `.github/workflows/*.yml` 与 `*.yaml` - 解析 `.github/dependabot.yml`,确认 YAML 语法和必需字段 - 检查所有 `uses:` 都固定为完整 40 字符 SHA +- 确认全部 5 个 checkout step 都显式设置 `persist-credentials: false`,不存在 `true` 或缺失项,并扫描后续 step 不含需要 Git remote credential 的操作 - 确认当前两个 workflow 共 6 个逻辑 job、14 个 `uses:`;其中主 CI 为 5 个逻辑 job - 确认 `id-token: write` 只出现 1 次且位于 `Upload Coverage`,`Test and Lint` 无 OIDC - 确认 Codecov `version` 固定为 `v11.3.1` @@ -366,4 +373,4 @@ push 后: 9. 最终 Head 与验证基准一致。 10. PR #108 的 UDP P1 finding 仍单独对账,不因 CI 改造而被误报为已修复。 11. 收尾报告明确要求轮换或吊销旧 Travis 文件中暴露的 Codecov 和第三方 webhook 凭据,并确认 PR 没有再次复制其值。 -12. 静态政策断言与最终文件一致:主 CI 5 个逻辑 job、全部 workflow 合计 6 个逻辑 job 与 14 个 `uses:`,OIDC 只授予 `Upload Coverage`。 +12. 静态政策断言与最终文件一致:主 CI 5 个逻辑 job、全部 workflow 合计 6 个逻辑 job 与 14 个 `uses:`,OIDC 只授予 `Upload Coverage`;全部 5 个 checkout step 均显式禁用 credential persistence,且后续 step 不依赖 Git remote 认证。 From 7ff84481ffcd11f9293f9be50fc23a2c80d520d9 Mon Sep 17 00:00:00 2001 From: "Xin.Zh" Date: Mon, 17 Aug 2026 07:59:47 +0800 Subject: [PATCH 19/24] Document PR 108 review repair strategy Replace the WSS field-publication readiness assumption with a real TLS handshake and extend the UDP contract to cover non-positive limits, int32 overflow, bounded datagram allocation, and the production receive path. Record explicit red-green and mutation gates so the follow-up tests must fail when either the WSS shutdown classification or the original UDP allocation bug is restored. Constraint: Limit this commit to the two existing Issue #97 design documents; do not modify source, tests, workflows, public APIs, or merge state. Confidence: High; the plan maps all eight current review threads to three verified root causes and defines exact observable gates for each. Scope-risk: Documentation only; implementation and GitHub thread replies remain pending. Tested: WSL Go 1.25.1 baseline go test ./...; placeholder scan; design-plan consistency review; git diff --check; cached diff check. Not-tested: The new regression tests and production changes have not been written yet. Co-authored-by: OmX Signed-off-by: Xin.Zh --- ...-08-01-issue-97-remaining-runtime-fixes.md | 160 +++++++++++++++++- ...issue-97-remaining-runtime-fixes-design.md | 31 ++-- 2 files changed, 173 insertions(+), 18 deletions(-) diff --git a/doc/superpowers/plans/2026-08-01-issue-97-remaining-runtime-fixes.md b/doc/superpowers/plans/2026-08-01-issue-97-remaining-runtime-fixes.md index b25a1516..a3a849d1 100644 --- a/doc/superpowers/plans/2026-08-01-issue-97-remaining-runtime-fixes.md +++ b/doc/superpowers/plans/2026-08-01-issue-97-remaining-runtime-fixes.md @@ -1,6 +1,6 @@ # Issue #97 剩余确定性运行时问题实现计划 -> **面向 AI 代理的工作者:** 必需子技能:使用 superpowers:executing-plans 逐任务实现此计划。步骤使用复选框(`- [ ]`)语法跟踪进度。用户未授权 commit,因此每个任务以 diff/status 检查代替提交。 +> **面向 AI 代理的工作者:** 必需子技能:使用 superpowers:executing-plans 逐任务实现此计划。步骤使用复选框(`- [ ]`)语法跟踪进度。原始任务 1-3 保留历史完成状态;2026-08-17 review follow-up 已授权 commit 并 push 到现有 PR #108 分支,但未授权 merge。 **目标:** 修复 WSS 正常关闭 panic 和 UDP 接收缓冲区计算死分支,并用先失败、后通过的回归测试锁定行为。 @@ -14,10 +14,10 @@ - 修改 `transport/server_test.go`:增加 WSS 启动后正常关闭的集成回归测试。 - 修改 `transport/server.go`:将 WSS `Serve` 返回分类为预期关闭或需记录的运行错误。 -- 修改 `transport/session_test.go`:增加 UDP buffer 大小的表驱动边界测试。 -- 修改 `transport/session.go`:增加包内私有 `udpReadBufferSize` 并在 UDP 接收路径使用。 -- 保留 `doc/superpowers/specs/2026-08-01-issue-97-remaining-runtime-fixes-design.md`:批准后的设计依据。 -- 新增本计划文件:记录 TDD、验证和范围边界。 +- 修改 `transport/session_test.go`:增加 UDP buffer 大小的表驱动边界测试和真实 UDP 接收路径测试。 +- 修改 `transport/session.go`:使 `udpReadBufferSize` 对非正值、溢出和 UDP 物理上限保持安全,并规范化 `SetMaxMsgLen` 输入。 +- 修改 `doc/superpowers/specs/2026-08-01-issue-97-remaining-runtime-fixes-design.md`:记录 review follow-up 的已批准设计。 +- 修改本计划文件:保留原始执行记录,并追加 review follow-up 的 TDD、变异和发布步骤。 ### 任务 1:WSS 正常关闭回归测试与最小修复 @@ -307,4 +307,152 @@ git diff --stat git diff -- transport/server.go transport/server_test.go transport/session.go transport/session_test.go ``` -预期:无空白错误;生产代码和测试只覆盖方案 A;规格和计划文件未超出批准范围;不 commit、不 push。 +预期:无空白错误;生产代码和测试只覆盖原始批准范围。 + +### 任务 4:2026-08-17 review follow-up 测试红灯 + +**文件:** +- 修改:`transport/server_test.go` +- 修改:`transport/session_test.go` + +- [ ] **步骤 1:用真实 TLS 握手替换 WSS 字段发布屏障** + +读取 `server.crt`,加入测试 Root CA,并在 `server.RunEventLoop` 返回后执行: + +```go +certPEM, err := os.ReadFile(certPath) +if err != nil { + t.Fatal(err) +} +roots := x509.NewCertPool() +if !roots.AppendCertsFromPEM(certPEM) { + t.Fatal("failed to add WSS test certificate to root pool") +} + +conn, err := tls.Dial("tcp", server.Listener().Addr().String(), &tls.Config{ + MinVersion: tls.VersionTLS12, + RootCAs: roots, +}) +if err != nil { + server.Close() + t.Fatalf("WSS TLS handshake failed: %v", err) +} +if err := conn.Close(); err != nil { + t.Fatal(err) +} +``` + +删除轮询 `server.server != nil` 的 readiness loop。真实 TLS 握手成功才允许测试调用 `server.Close()`。 + +- [ ] **步骤 2:增加 UDP 非正值、极值和生产调用链测试** + +将 `TestUDPReadBufferSize` 的大消息预期改为 `maxUDPReadBufferSize`,并增加 `0`、`-1`、`math.MaxInt32`。增加一个 Reader,把收到的切片长度写入有缓冲 channel;真实 UDP 测试使用 `maxMsgLen=1`、发送 3 字节,并断言 Reader 收到 `udpReadBufferSize(1)` 即 2 字节: + +```go +type udpReadSizeReader struct { + readLen chan int +} + +func (r *udpReadSizeReader) Read(_ Session, data []byte) (any, int, error) { + r.readLen <- len(data) + return nil, 0, errTestReadFailure +} +``` + +测试必须启动真实 `handleUDPPackage`,在断言后关闭 UDP listener,并有界等待 handler 返回;不向生产代码添加测试 hook。 + +- [ ] **步骤 3:运行边界测试并确认红灯原因** + +运行: + +```bash +go test ./transport -run '^(TestUDPReadBufferSize|TestHandleUDPPackageUsesConfiguredReadBuffer)$' -count=1 +``` + +预期:`TestUDPReadBufferSize` 因 `udpReadBufferSize(0)` 返回 0、负值或极值溢出而 FAIL;真实生产路径子测试可以 PASS。失败必须来自缺失边界行为,不得来自测试夹具、端口或超时。 + +### 任务 5:UDP 最小修复与测试绿灯 + +**文件:** +- 修改:`transport/session.go` +- 测试:`transport/session_test.go` + +- [ ] **步骤 1:实现安全、有界的 UDP buffer 计算** + +在常量块增加 `maxUDPReadBufferSize = 64 * 1024`,并将 helper 改为: + +```go +func udpReadBufferSize(maxMsgLen int32) int { + if maxMsgLen <= 0 { + return maxUDPReadBufferSize + } + + bufferSize := int64(maxMsgLen) + int64(maxReadBufLen) + if doubledMaxMsgLen := int64(maxMsgLen) * 2; doubledMaxMsgLen < bufferSize { + bufferSize = doubledMaxMsgLen + } + if bufferSize > maxUDPReadBufferSize { + return maxUDPReadBufferSize + } + return int(bufferSize) +} +``` + +`SetMaxMsgLen` 将 `length <= 0` 保存为 0,将超过 `math.MaxInt32` 的正数保存为 `math.MaxInt32`,其余值按现有 `int32` 字段保存。公开方法签名不变。 + +- [ ] **步骤 2:运行目标测试确认绿灯** + +运行: + +```bash +go test ./transport -run '^(TestWSSServerCloseDoesNotPanic|TestUDPReadBufferSize|TestHandleUDPPackageUsesConfiguredReadBuffer)$' -count=1 +``` + +预期:三个测试 PASS,WSS 测试完成真实 TLS 握手,UDP handler 在关闭 listener 后有界返回。 + +- [ ] **步骤 3:验证两个回归测试能杀死对应变异** + +先将 `runWSSEventLoop` 的错误分类临时替换为 `if err != nil { panic(err) }`,运行 `TestWSSServerCloseDoesNotPanic`,预期出现 `panic: http: Server closed`;立即恢复文件。 + +再将 `handleUDPPackage` 的分配临时恢复为旧逻辑: + +```go +maxBufLen := int(s.maxMsgLen + maxReadBufLen) +if int(s.maxMsgLen<<1) < bufLen { + maxBufLen = int(s.maxMsgLen << 1) +} +bufp = gxbytes.AcquireBytes(maxBufLen) +``` + +运行 `TestHandleUDPPackageUsesConfiguredReadBuffer`,预期收到 3 字节而不是 2 字节并 FAIL;立即恢复文件。恢复后重跑三个目标测试并要求 PASS。 + +### 任务 6:review follow-up 完整验证与发布 + +**文件:** +- 验证:全部修改文件 +- GitHub:PR #108 当前 Head、检查和八个原 review 线程 + +- [ ] **步骤 1:运行格式、race、静态和全仓门禁** + +```bash +gofmt -w transport/server_test.go transport/session.go transport/session_test.go +git diff --check +go test -race ./transport -run '^(TestWSSServerCloseDoesNotPanic|TestUDPReadBufferSize|TestHandleUDPPackageUsesConfiguredReadBuffer)$' -count=20 +go test -race ./transport -count=1 +go vet ./... +go test ./... -count=1 +``` + +每条命令必须读取实际退出码和输出;Windows Go 1.26.2 的既有 `TestTCPClient` 基线失败单独记录,不修改该无关测试。 + +- [ ] **步骤 2:检查范围并 commit** + +检查 `git status --short`、`git diff --stat`、完整 diff 和 `git diff --check`。只暂存两份文档、`transport/server_test.go`、`transport/session.go`、`transport/session_test.go`,使用符合本地 Lore hook 的叙述式 commit message、Signed-off-by 和 `Co-authored-by: OmX `。 + +- [ ] **步骤 3:推送并复核最终 Head** + +推送 `codex/fix-issue-97-remaining`,重新获取 PR 的 `headRefOid`、完整检查、review decision、顶层评论和所有 review threads。Head 必须等于本地提交,检查失败或新反馈不得被旧证据覆盖。 + +- [ ] **步骤 4:在原线程回复并核对状态** + +使用 `repos/AlexStocks/getty/pulls/108/comments/{id}/replies` 回复对应行内线程,说明具体修复和验证;同根因线程分别回复但不新建重复顶层评论。回复后重新获取 `isResolved`、`isOutdated`;不代替 reviewer Resolve,也不 merge PR。 diff --git a/doc/superpowers/specs/2026-08-01-issue-97-remaining-runtime-fixes-design.md b/doc/superpowers/specs/2026-08-01-issue-97-remaining-runtime-fixes-design.md index b091bcbf..89d09428 100644 --- a/doc/superpowers/specs/2026-08-01-issue-97-remaining-runtime-fixes-design.md +++ b/doc/superpowers/specs/2026-08-01-issue-97-remaining-runtime-fixes-design.md @@ -16,14 +16,14 @@ - 修改 `transport/server.go` 的 WSS `Serve` 返回处理。 - 修改 `transport/session.go` 的 UDP 接收缓冲区大小计算。 - 在 `transport/server_test.go` 增加 WSS 正常启动和关闭的回归测试。 -- 在 `transport/session_test.go` 增加 UDP 缓冲区大小边界测试。 +- 在 `transport/session_test.go` 增加 UDP 缓冲区大小边界测试和真实 UDP 接收路径测试。 ### 本批不包含 - 不修改 `WithReconnectAttempts` 的语义。当前公开文档将其描述为最大重连尝试次数,现有测试也验证总尝试次数;把它改为连续失败次数需要单独设计和兼容性决策。 - 不修改 Issue #93 的 Bug 模板、自动回复或 Release Note workflow。 - 不重构 WS/WSS 服务生命周期之外的代码。 -- 不 commit、不 push、不创建 PR,也不修改或关闭 GitHub Issue。 +- 2026-08-17 review follow-up 已授权 commit 并 push 到现有 PR #108 分支;不 merge,也不修改或关闭 GitHub Issue。 ## 设计 @@ -56,8 +56,10 @@ func udpReadBufferSize(maxMsgLen int32) int 函数规则: -- 输入采用 Session 已经保存的正数 `maxMsgLen`。 -- 返回 `maxMsgLen + maxReadBufLen` 与 `2 * maxMsgLen` 中较小者。 +- `maxMsgLen <= 0` 继续表示现有的“不限制消息长度”语义,UDP 接收使用 `64 KiB` 的有界回退;该大小足以容纳普通 UDP 数据报,同时不会向 `AcquireBytes` 传递零或负值。 +- 正数输入先在 `int64` 中计算 `min(maxMsgLen + maxReadBufLen, 2 * maxMsgLen)`,避免 `int32` 加法和左移溢出。 +- 计算结果上限为 `64 KiB`。UDP 数据报的长度字段只有 16 位,更大的单次接收分配没有可观察收益,只会扩大内存风险。 +- `SetMaxMsgLen` 将非正值规范化为 `0`,并将超出 `int32` 的正数限制到 `math.MaxInt32`,避免公开 `int` 参数在保存时绕回负数。 - `handleUDPPackage` 只负责使用返回值申请和释放 buffer,不再保留尚未接收数据就读取 `bufLen` 的分支。 提取函数的目的是让边界规则可以直接测试,而不是暴露新的产品接口。 @@ -70,11 +72,11 @@ func udpReadBufferSize(maxMsgLen int32) int 1. 创建监听随机本地端口的 WSS Server。 2. 在 goroutine 中启动 `RunEventLoop`。 -3. 等待 listener 和 HTTP server 已发布,避免把异步启动竞态误当成关闭行为。 -4. 调用 `Close()`。 +3. 使用测试证书作为 Root CA 建立一次真实 TLS 连接;握手成功证明 `http.Server.Serve` 已经接受连接,而不只是 `server.server` 字段已经赋值。 +4. 关闭测试客户端连接,再调用 `Close()`。 5. 断言 `Close()` 和 event loop 在有界时间内返回。 -在修复前,测试应因服务 goroutine 执行 `panic(http.ErrServerClosed)` 而失败;修复后应正常通过。测试不得通过 sleep 猜测启动状态,应轮询可观察的 listener/server 状态并设置总超时。 +在修复前,测试应因服务 goroutine 执行 `panic(http.ErrServerClosed)` 而失败;修复后应正常通过。测试不得通过 sleep 或字段发布猜测启动状态。review follow-up 通过临时恢复无条件 panic 的变异再次确认测试会变红,然后恢复生产实现。 ### UDP 边界测试 @@ -86,17 +88,20 @@ func udpReadBufferSize(maxMsgLen int32) int | `4095` | `8190` | 低于交叉点一字节 | | `4096` | `8192` | 两个公式在交叉点相等 | | `4097` | `8193` | 高于交叉点后由 `maxMsgLen + 4096` 限制 | -| `128 * 1024` | `128 * 1024 + 4096` | 常见大消息配置 | +| `0` | `64 * 1024` | 未设置限制时使用完整 UDP 数据报回退 | +| `-1` | `64 * 1024` | 防御负值,不产生负分配 | +| `128 * 1024` | `64 * 1024` | 大配置受 UDP 数据报上限约束 | +| `math.MaxInt32` | `64 * 1024` | 极值不发生 `int32` 溢出或超大分配 | -测试先在未修改生产代码的状态下运行并确认失败,失败原因必须是 helper 尚不存在或旧逻辑不满足断言;随后只实现使测试通过所需的最小代码。 +另增加真实 `net.UDPConn`、`session` 和 `handleUDPPackage` 测试。测试发送一个长度大于 helper 结果的数据报,并由真实 Reader 记录收到的切片长度。把生产分配临时恢复为旧的 `bufLen` 死分支时,该测试必须失败;恢复 helper 调用后必须通过。这条变异验证保证测试覆盖生产调用链,而不只覆盖纯函数。 ## 验证 实现完成后在 WSL/Linux、Go 1.25.1 下依次运行: ```bash -go test ./transport -run '^(TestWSSServerCloseDoesNotPanic|TestUDPReadBufferSize)$' -count=1 -go test -race ./transport -run '^(TestWSSServerCloseDoesNotPanic|TestUDPReadBufferSize)$' -count=1 +go test ./transport -run '^(TestWSSServerCloseDoesNotPanic|TestUDPReadBufferSize|TestHandleUDPPackageUsesConfiguredReadBuffer)$' -count=1 +go test -race ./transport -run '^(TestWSSServerCloseDoesNotPanic|TestUDPReadBufferSize|TestHandleUDPPackageUsesConfiguredReadBuffer)$' -count=1 go test -race ./transport -count=1 go vet ./... go test ./... -count=1 @@ -107,9 +112,11 @@ go test ./... -count=1 ## 完成标准 - WSS 正常关闭路径不产生 panic,并能完成 WaitGroup 等待。 +- WSS 回归测试通过真实 TLS 握手证明 `Serve` 已经进入服务状态。 - 非预期 WSS `Serve` 错误仍被记录,不静默吞掉。 - UDP buffer 计算不再读取接收前的 `bufLen`。 -- UDP buffer 边界规则由表驱动测试锁定。 +- UDP buffer 对非正值和 `int32` 极值始终返回正数、有界结果。 +- UDP 生产调用链测试能杀死恢复旧分配逻辑的变异。 - 新测试经过明确的红灯和绿灯阶段。 - WSL/Linux race 测试、静态检查和适用的仓库测试获得新鲜验证结果。 - 用户原始 checkout 和其中的未跟踪内容保持不变。 From 36680e86cf56fc85edebf1824d441f2a21dc9fd1 Mon Sep 17 00:00:00 2001 From: "Xin.Zh" Date: Mon, 17 Aug 2026 08:08:51 +0800 Subject: [PATCH 20/24] Test UDP buffer limits through the real receive path Replace the WSS readiness poll with a verified TLS handshake, then exercise shutdown only after the secure listener has served a real client. Extend UDP limit coverage and drive handleUDPPackage through loopback sockets so the configured read size is observable at the Reader boundary. Constraint: Test-only follow-up; production code and public behavior are unchanged. Confidence: High; the boundary table fails for the expected current implementation while the real UDP path and WSS handshake tests pass independently. Scope-risk: Low; changes are limited to two transport test files and use bounded cleanup. Tested: WSL Go 1.25.1 targeted UDP red test, standalone UDP production-path test, standalone WSS test, gofmt, and git diff --check. Not-tested: Full suite, because the new UDP boundary assertions are intentionally red until the production follow-up lands. Co-authored-by: OmX Signed-off-by: Xin.Zh --- transport/server_test.go | 58 ++++++++++++++------- transport/session_test.go | 106 +++++++++++++++++++++++++++++++++++++- 2 files changed, 143 insertions(+), 21 deletions(-) diff --git a/transport/server_test.go b/transport/server_test.go index 1521bf1f..cc791d83 100644 --- a/transport/server_test.go +++ b/transport/server_test.go @@ -21,10 +21,13 @@ import ( "bufio" "bytes" "context" + "crypto/tls" + "crypto/x509" "errors" "io" "net" "net/http" + "os" "path/filepath" "strings" "testing" @@ -335,30 +338,45 @@ func TestWSSServerCloseDoesNotPanic(t *testing.T) { ) server.RunEventLoop(func(Session) error { return nil }) - deadline := time.Now().Add(time.Second) - for { - server.lock.RLock() - serving := server.server != nil - server.lock.RUnlock() - if serving { - break - } - if time.Now().After(deadline) { - t.Fatal("WSS event loop did not publish its HTTP server") + closeServer := func() { + t.Helper() + closed := make(chan struct{}) + go func() { + server.Close() + close(closed) + }() + select { + case <-closed: + case <-time.After(2 * time.Second): + t.Error("WSS server Close did not return") } - time.Sleep(time.Millisecond) } - - closed := make(chan struct{}) - go func() { - server.Close() - close(closed) + defer func() { + if !server.IsClosed() { + closeServer() + } }() - select { - case <-closed: - case <-time.After(2 * time.Second): - t.Fatal("WSS server Close did not return") + + certPEM, err := os.ReadFile(certPath) + if err != nil { + t.Fatal(err) + } + rootCAs := x509.NewCertPool() + if !rootCAs.AppendCertsFromPEM(certPEM) { + t.Fatal("failed to parse WSS server certificate") + } + clientConn, err := tls.DialWithDialer(&net.Dialer{Timeout: time.Second}, "tcp", server.Listener().Addr().String(), &tls.Config{ + MinVersion: tls.VersionTLS12, + RootCAs: rootCAs, + }) + if err != nil { + t.Fatalf("TLS handshake with WSS server failed: %v", err) } + if err := clientConn.Close(); err != nil { + t.Fatalf("close TLS client connection: %v", err) + } + + closeServer() } func TestWSServeWSRequestClosesSelfConnectConn(t *testing.T) { diff --git a/transport/session_test.go b/transport/session_test.go index ec9452f9..17b6cb94 100644 --- a/transport/session_test.go +++ b/transport/session_test.go @@ -20,7 +20,9 @@ package getty import ( "errors" "io" + "math" "net" + "strconv" "sync" "testing" "time" @@ -42,7 +44,10 @@ func TestUDPReadBufferSize(t *testing.T) { {name: "below crossover", maxMsgLen: maxReadBufLen - 1, want: 2 * (maxReadBufLen - 1)}, {name: "at crossover", maxMsgLen: maxReadBufLen, want: 2 * maxReadBufLen}, {name: "above crossover", maxMsgLen: maxReadBufLen + 1, want: 2*maxReadBufLen + 1}, - {name: "large message", maxMsgLen: 128 * 1024, want: 128*1024 + maxReadBufLen}, + {name: "zero message limit", maxMsgLen: 0, want: 64 * 1024}, + {name: "negative message limit", maxMsgLen: -1, want: 64 * 1024}, + {name: "large message", maxMsgLen: 128 * 1024, want: 64 * 1024}, + {name: "maximum message limit", maxMsgLen: math.MaxInt32, want: 64 * 1024}, } for _, tt := range tests { @@ -54,6 +59,105 @@ func TestUDPReadBufferSize(t *testing.T) { } } +func TestSetMaxMsgLenNormalizesLimits(t *testing.T) { + type testCase struct { + name string + length int + want int32 + } + tests := []testCase{ + {name: "negative becomes unlimited", length: -1, want: 0}, + {name: "zero remains unlimited", length: 0, want: 0}, + } + if strconv.IntSize == 64 { + oversized := int64(math.MaxInt32) + 1 + tests = append(tests, testCase{name: "oversized value is clamped", length: int(oversized), want: math.MaxInt32}) + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ss := &session{} + ss.SetMaxMsgLen(tt.length) + if ss.maxMsgLen != tt.want { + t.Fatalf("SetMaxMsgLen(%d) stored %d, want %d", tt.length, ss.maxMsgLen, tt.want) + } + }) + } +} + +type recordingErrorReader struct { + dataLengths chan<- int +} + +func (r recordingErrorReader) Read(_ Session, data []byte) (any, int, error) { + r.dataLengths <- len(data) + return nil, 0, errTestReadFailure +} + +func TestHandleUDPPackageUsesConfiguredReadBuffer(t *testing.T) { + listener, err := net.ListenUDP("udp", &net.UDPAddr{IP: net.ParseIP("127.0.0.1")}) + if err != nil { + t.Fatal(err) + } + + sender, err := net.DialUDP("udp", nil, listener.LocalAddr().(*net.UDPAddr)) + if err != nil { + _ = listener.Close() + t.Fatal(err) + } + defer sender.Close() + + dataLengths := make(chan int, 1) + ss := newUDPSession(listener, newServer(UDP_ENDPOINT)).(*session) + ss.SetMaxMsgLen(1) + ss.SetReader(recordingErrorReader{dataLengths: dataLengths}) + want := udpReadBufferSize(1) + if want != 2 { + t.Fatalf("udpReadBufferSize(1) = %d, want 2", want) + } + + handlerDone := make(chan error, 1) + go func() { + handlerDone <- ss.handleUDPPackage() + }() + + handlerStopped := false + stopHandler := func() bool { + if handlerStopped { + return true + } + _ = listener.Close() + select { + case <-handlerDone: + handlerStopped = true + return true + case <-time.After(time.Second): + return false + } + } + defer func() { + if !stopHandler() { + t.Error("handleUDPPackage did not return after closing the UDP listener") + } + }() + + if _, err := sender.Write([]byte{1, 2, 3}); err != nil { + t.Fatal(err) + } + select { + case got := <-dataLengths: + if got != want { + t.Fatalf("Reader data length = %d, want udpReadBufferSize(1) = %d", got, want) + } + case <-time.After(time.Second): + t.Fatal("Reader did not receive the UDP datagram") + } + + if !stopHandler() { + t.Fatal("handleUDPPackage did not return after closing the UDP listener") + } +} + type errorReader struct{} func (errorReader) Read(Session, []byte) (any, int, error) { From 1e2ea17a2f2ab484f1585a1520781c526a5327c1 Mon Sep 17 00:00:00 2001 From: "Xin.Zh" Date: Mon, 17 Aug 2026 08:18:54 +0800 Subject: [PATCH 21/24] Bound UDP receive buffers across limit edge cases Normalize non-positive and oversized SetMaxMsgLen inputs before storing the int32 session limit, then calculate UDP read capacity in int64 and cap it at the maximum useful datagram buffer size. This keeps the existing no-limit meaning for non-positive values while preventing zero-length reads, negative allocations, int32 arithmetic wraparound, and oversized per-session buffers. Constraint: Preserve the Session API and existing positive-limit sizing below 64 KiB; limit production changes to transport/session.go. Confidence: High; the boundary and setter tests fail on the previous implementation, pass after the change, and the real UDP receive-path test kills the restored pre-PR allocation branch. Scope-risk: Limited to UDP receive allocation and normalization of previously invalid SetMaxMsgLen values; TCP, WS, WSS, and handler control flow are unchanged. Tested: WSL Go 1.25.1 targeted green tests; targeted race test; GOARCH=386 targeted tests; WSS panic mutation; UDP old-allocation mutation; gofmt; git diff and cached diff checks. Not-tested: Full transport race, go vet, full repository tests, and GitHub CI are pending the final verification task. Co-authored-by: OmX Signed-off-by: Xin.Zh --- transport/session.go | 34 +++++++++++++++++++++++++--------- 1 file changed, 25 insertions(+), 9 deletions(-) diff --git a/transport/session.go b/transport/session.go index a2fa407e..eddddecf 100644 --- a/transport/session.go +++ b/transport/session.go @@ -23,6 +23,7 @@ import ( "crypto/tls" "fmt" "io" + "math" "net" "runtime" "sync" @@ -46,10 +47,11 @@ import ( ) const ( - maxReadBufLen = 4 * 1024 - netIOTimeout = 1e9 // 1s - period = 60 * 1e9 // 1 minute - pendingDuration = 3e9 + maxReadBufLen = 4 * 1024 + maxUDPReadBufferSize = 64 * 1024 + netIOTimeout = 1e9 // 1s + period = 60 * 1e9 // 1 minute + pendingDuration = 3e9 // MaxWheelTimeSpan 900s, 15 minute MaxWheelTimeSpan = 900e9 maxPacketLen = 16 * 1024 @@ -65,11 +67,18 @@ const ( ) func udpReadBufferSize(maxMsgLen int32) int { - maxBufLen := int(maxMsgLen + maxReadBufLen) - if doubledMaxMsgLen := int(maxMsgLen << 1); doubledMaxMsgLen < maxBufLen { - return doubledMaxMsgLen + if maxMsgLen <= 0 { + return maxUDPReadBufferSize } - return maxBufLen + + bufferSize := int64(maxMsgLen) + int64(maxReadBufLen) + if doubledMaxMsgLen := int64(maxMsgLen) * 2; doubledMaxMsgLen < bufferSize { + bufferSize = doubledMaxMsgLen + } + if bufferSize > maxUDPReadBufferSize { + return maxUDPReadBufferSize + } + return int(bufferSize) } var defaultTimerWheel *gxtime.TimerWheel @@ -354,7 +363,14 @@ func (s *session) SetMaxMsgLen(length int) { s.lock.Lock() defer s.lock.Unlock() - s.maxMsgLen = int32(length) + switch { + case length <= 0: + s.maxMsgLen = 0 + case int64(length) > int64(math.MaxInt32): + s.maxMsgLen = math.MaxInt32 + default: + s.maxMsgLen = int32(length) + } } // SetName set session name From 9cbc414b27a837e0910170c011063ae9ab253517 Mon Sep 17 00:00:00 2001 From: "Xin.Zh" Date: Mon, 17 Aug 2026 08:27:37 +0800 Subject: [PATCH 22/24] Check UDP test socket cleanup errors Report failures when the loopback UDP sender cannot close so the production-path regression test satisfies the repository errcheck policy without changing its observable behavior. Constraint: Limit this follow-up to the sender cleanup in transport/session_test.go; do not change production code or test coverage. Confidence: High; the targeted UDP tests pass and a fresh-cache make lint reports zero issues. Scope-risk: Test cleanup only. Tested: WSL Go 1.25.1 targeted UDP tests; gofmt; go vet through make lint; golangci-lint v2.4.0 with an isolated cache; git diff and cached diff checks. Not-tested: GitHub CI remains pending push. Co-authored-by: OmX Signed-off-by: Xin.Zh --- transport/session_test.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/transport/session_test.go b/transport/session_test.go index 17b6cb94..25b6b5a6 100644 --- a/transport/session_test.go +++ b/transport/session_test.go @@ -105,7 +105,11 @@ func TestHandleUDPPackageUsesConfiguredReadBuffer(t *testing.T) { _ = listener.Close() t.Fatal(err) } - defer sender.Close() + defer func() { + if err := sender.Close(); err != nil { + t.Errorf("close UDP sender: %v", err) + } + }() dataLengths := make(chan int, 1) ss := newUDPSession(listener, newServer(UDP_ENDPOINT)).(*session) From b1e17017984abb089db7dfc4096b66a81746cc47 Mon Sep 17 00:00:00 2001 From: "Xin.Zh" Date: Mon, 17 Aug 2026 08:36:56 +0800 Subject: [PATCH 23/24] Align PR 108 release plan with reviewed implementation Mirror the bounded TLS dial used by the WSS regression test, include transport/server.go in formatting and staging, and list the SetMaxMsgLen normalization test in each follow-up verification command. Constraint: Documentation-only response to the final CodeRabbit review; do not alter source, tests, workflows, or public behavior. Confidence: High; the plan snippets and file lists now match the tested implementation and actual five-file change set. Scope-risk: Documentation consistency only. Tested: Exact snippet comparison against transport/server_test.go; test-name and file-list scan; git diff and cached diff checks. Not-tested: GitHub checks for this documentation commit are pending push. Co-authored-by: OmX Signed-off-by: Xin.Zh --- .../2026-08-01-issue-97-remaining-runtime-fixes.md | 12 ++++++------ ...-08-01-issue-97-remaining-runtime-fixes-design.md | 4 ++-- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/doc/superpowers/plans/2026-08-01-issue-97-remaining-runtime-fixes.md b/doc/superpowers/plans/2026-08-01-issue-97-remaining-runtime-fixes.md index a3a849d1..26f1d0f1 100644 --- a/doc/superpowers/plans/2026-08-01-issue-97-remaining-runtime-fixes.md +++ b/doc/superpowers/plans/2026-08-01-issue-97-remaining-runtime-fixes.md @@ -329,7 +329,7 @@ if !roots.AppendCertsFromPEM(certPEM) { t.Fatal("failed to add WSS test certificate to root pool") } -conn, err := tls.Dial("tcp", server.Listener().Addr().String(), &tls.Config{ +conn, err := tls.DialWithDialer(&net.Dialer{Timeout: time.Second}, "tcp", server.Listener().Addr().String(), &tls.Config{ MinVersion: tls.VersionTLS12, RootCAs: roots, }) @@ -405,10 +405,10 @@ func udpReadBufferSize(maxMsgLen int32) int { 运行: ```bash -go test ./transport -run '^(TestWSSServerCloseDoesNotPanic|TestUDPReadBufferSize|TestHandleUDPPackageUsesConfiguredReadBuffer)$' -count=1 +go test ./transport -run '^(TestWSSServerCloseDoesNotPanic|TestUDPReadBufferSize|TestSetMaxMsgLenNormalizesLimits|TestHandleUDPPackageUsesConfiguredReadBuffer)$' -count=1 ``` -预期:三个测试 PASS,WSS 测试完成真实 TLS 握手,UDP handler 在关闭 listener 后有界返回。 +预期:四个测试 PASS,WSS 测试完成真实 TLS 握手,UDP handler 在关闭 listener 后有界返回。 - [ ] **步骤 3:验证两个回归测试能杀死对应变异** @@ -435,9 +435,9 @@ bufp = gxbytes.AcquireBytes(maxBufLen) - [ ] **步骤 1:运行格式、race、静态和全仓门禁** ```bash -gofmt -w transport/server_test.go transport/session.go transport/session_test.go +gofmt -w transport/server.go transport/server_test.go transport/session.go transport/session_test.go git diff --check -go test -race ./transport -run '^(TestWSSServerCloseDoesNotPanic|TestUDPReadBufferSize|TestHandleUDPPackageUsesConfiguredReadBuffer)$' -count=20 +go test -race ./transport -run '^(TestWSSServerCloseDoesNotPanic|TestUDPReadBufferSize|TestSetMaxMsgLenNormalizesLimits|TestHandleUDPPackageUsesConfiguredReadBuffer)$' -count=20 go test -race ./transport -count=1 go vet ./... go test ./... -count=1 @@ -447,7 +447,7 @@ go test ./... -count=1 - [ ] **步骤 2:检查范围并 commit** -检查 `git status --short`、`git diff --stat`、完整 diff 和 `git diff --check`。只暂存两份文档、`transport/server_test.go`、`transport/session.go`、`transport/session_test.go`,使用符合本地 Lore hook 的叙述式 commit message、Signed-off-by 和 `Co-authored-by: OmX `。 +检查 `git status --short`、`git diff --stat`、完整 diff 和 `git diff --check`。只暂存两份文档、`transport/server.go`、`transport/server_test.go`、`transport/session.go`、`transport/session_test.go`,使用符合本地 Lore hook 的叙述式 commit message、Signed-off-by 和 `Co-authored-by: OmX `。 - [ ] **步骤 3:推送并复核最终 Head** diff --git a/doc/superpowers/specs/2026-08-01-issue-97-remaining-runtime-fixes-design.md b/doc/superpowers/specs/2026-08-01-issue-97-remaining-runtime-fixes-design.md index 89d09428..a4731812 100644 --- a/doc/superpowers/specs/2026-08-01-issue-97-remaining-runtime-fixes-design.md +++ b/doc/superpowers/specs/2026-08-01-issue-97-remaining-runtime-fixes-design.md @@ -100,8 +100,8 @@ func udpReadBufferSize(maxMsgLen int32) int 实现完成后在 WSL/Linux、Go 1.25.1 下依次运行: ```bash -go test ./transport -run '^(TestWSSServerCloseDoesNotPanic|TestUDPReadBufferSize|TestHandleUDPPackageUsesConfiguredReadBuffer)$' -count=1 -go test -race ./transport -run '^(TestWSSServerCloseDoesNotPanic|TestUDPReadBufferSize|TestHandleUDPPackageUsesConfiguredReadBuffer)$' -count=1 +go test ./transport -run '^(TestWSSServerCloseDoesNotPanic|TestUDPReadBufferSize|TestSetMaxMsgLenNormalizesLimits|TestHandleUDPPackageUsesConfiguredReadBuffer)$' -count=1 +go test -race ./transport -run '^(TestWSSServerCloseDoesNotPanic|TestUDPReadBufferSize|TestSetMaxMsgLenNormalizesLimits|TestHandleUDPPackageUsesConfiguredReadBuffer)$' -count=1 go test -race ./transport -count=1 go vet ./... go test ./... -count=1 From 80c67d8ceb8e06e2d171ff60b00277e48cb47377 Mon Sep 17 00:00:00 2001 From: "Xin.Zh" Date: Mon, 17 Aug 2026 08:43:27 +0800 Subject: [PATCH 24/24] Align PR 108 mutation rerun checklist The review plan listed four target tests but described rerunning only three after restoring the production-path mutation. Point the restoration step at the same four-test command so the documented release gate covers the setter normalization regression as well. Co-authored-by: OmX Signed-off-by: Xin.Zh --- .../plans/2026-08-01-issue-97-remaining-runtime-fixes.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/superpowers/plans/2026-08-01-issue-97-remaining-runtime-fixes.md b/doc/superpowers/plans/2026-08-01-issue-97-remaining-runtime-fixes.md index 26f1d0f1..67942012 100644 --- a/doc/superpowers/plans/2026-08-01-issue-97-remaining-runtime-fixes.md +++ b/doc/superpowers/plans/2026-08-01-issue-97-remaining-runtime-fixes.md @@ -424,7 +424,7 @@ if int(s.maxMsgLen<<1) < bufLen { bufp = gxbytes.AcquireBytes(maxBufLen) ``` -运行 `TestHandleUDPPackageUsesConfiguredReadBuffer`,预期收到 3 字节而不是 2 字节并 FAIL;立即恢复文件。恢复后重跑三个目标测试并要求 PASS。 +运行 `TestHandleUDPPackageUsesConfiguredReadBuffer`,预期收到 3 字节而不是 2 字节并 FAIL;立即恢复文件。恢复后重跑步骤 2 中相同的四个目标测试并要求 PASS。 ### 任务 6:review follow-up 完整验证与发布