From 25a461d3eba45cf1e57876e17fe4408396fba0ad Mon Sep 17 00:00:00 2001 From: im-pingo Date: Fri, 28 Aug 2026 17:50:02 +0800 Subject: [PATCH 1/4] fix: derive GB28181 RTCP port from ephemeral RTP port --- .../plans/2026-08-28-ci-main-green.md | 64 +++++++++++++++++++ module/gb28181/rtp_receiver.go | 8 ++- module/gb28181/rtp_receiver_test.go | 17 +++++ 3 files changed, 87 insertions(+), 2 deletions(-) create mode 100644 docs/superpowers/plans/2026-08-28-ci-main-green.md diff --git a/docs/superpowers/plans/2026-08-28-ci-main-green.md b/docs/superpowers/plans/2026-08-28-ci-main-green.md new file mode 100644 index 00000000..96200374 --- /dev/null +++ b/docs/superpowers/plans/2026-08-28-ci-main-green.md @@ -0,0 +1,64 @@ +# Main CI Green Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Make post-merge `main` CI reliable by fixing GB28181 ephemeral-port allocation, making lint deterministic, and removing only confirmed-unused branches. + +**Architecture:** When `NewRTPReceiver` receives port zero, it will derive the RTCP port from the RTP socket's assigned port. CI will pin golangci-lint and compare changes against the appropriate revision on both PR and main push events, so an historical lint backlog cannot make main red while new issues remain gated. + +**Tech Stack:** Go 1.26, FFmpeg-tagged Go tests, GitHub Actions, golangci-lint. + +**Spec:** `docs/TECHNICAL-RISKS.md` and `AGENTS.md`. + +## Global Constraints + +- Use Go 1.26 or newer and the repository's `audiocodec` build tag for the tagged baseline. +- Run `tools/check-agent-docs_test.sh` after every source change. +- Never commit secrets, generated binaries, recordings, or local configuration. +- Use author identity `im-pingo` for commits. +- Preserve `main`, the active `codex/webrtc-playback-fix` branch, and unmerged work unless explicitly authorized for deletion. + +--- + +### Task 1: Fix GB28181 ephemeral RTCP port allocation + +**Files:** +- Modify: `module/gb28181/rtp_receiver.go:38-57` +- Test: `module/gb28181/rtp_receiver_test.go` + +**Interfaces:** `NewRTPReceiver(port int, publisher *Publisher)` keeps its public signature; port zero must result in RTCP listening on the assigned RTP port plus one. + +- [ ] **Step 1: Add the failing regression test.** Add `TestNewRTPReceiverUsesAssignedPortForRTCP`, call `NewRTPReceiver(0, NewPublisher("ephemeral-port", nil))`, defer `receiver.Close`, read `receiver.LocalPort()` and `receiver.rtcpConn.LocalAddr().(*net.UDPAddr).Port`, assert RTP is positive and RTCP equals RTP plus one. +- [ ] **Step 2: Run `go test -tags audiocodec ./module/gb28181 -run TestNewRTPReceiverUsesAssignedPortForRTCP -count=1` and verify it fails because the current code attempts UDP port 1. +- [ ] **Step 3: After the RTP bind succeeds, compute RTCP port from the actual RTP local port when the requested port is zero; retain `port + 1` for explicitly allocated ports; close RTP if RTCP binding fails. +- [ ] **Step 4: Run `go test -tags audiocodec ./module/gb28181 -run 'TestNewRTPReceiverUsesAssignedPortForRTCP|TestRTPReceiver|TestMediaSessionCloseOwnsReceiverAndAllowsPortReuse' -count=1` and then `go test -tags audiocodec ./module/gb28181 -count=1`. +- [ ] **Step 5: Commit with `git add module/gb28181/rtp_receiver.go module/gb28181/rtp_receiver_test.go && git commit -m "fix: derive GB28181 RTCP port from ephemeral RTP port"`. + +### Task 2: Make lint deterministic on PRs and main pushes + +**Files:** +- Modify: `.github/workflows/ci.yml:34-39` + +**Interfaces:** The `Lint` job must remain a required quality gate for pull requests and must check only the new merge revision on `main` pushes. + +- [ ] **Step 1: Pin `golangci-lint-action` to the repository's Node 24-compatible action major and replace `version: latest` with a verified v2 release; configure the action's revision arguments conditionally for pull requests versus `HEAD^` on main pushes. +- [ ] **Step 2: Validate the workflow diff with `git diff --check` and inspect `.github/workflows/ci.yml` for valid YAML and no unrelated changes. +- [ ] **Step 3: Commit with `git add .github/workflows/ci.yml && git commit -m "ci: lint only changes on main pushes"`. + +### Task 3: Documentation and full verification + +**Files:** Inspect `agent-manifest.json`, `llms.txt`, `llms-full.txt`, `README.md`, `README.zh-CN.md`, `docs/TECHNICAL-RISKS.md`, and `docs/PROGRESS.md`; modify only if the final behavior or CI contract changes a documented fact. + +- [ ] **Step 1: Confirm the port fix changes no public API, configuration, prerequisite, or supported protocol; document any changed CI version or workflow fact in the required AI-facing files. +- [ ] **Step 2: Run `go test ./...`, `go test -tags audiocodec ./module/gb28181 -count=1`, `tools/check-agent-docs_test.sh`, `CHECK_AGENT_DOCS_DIFF=1 tools/check-agent-docs.sh`, `git diff --check`, and `jq empty agent-manifest.json`. +- [ ] **Step 3: When FFmpeg development libraries are available, run `CGO_ENABLED=1 go build -tags audiocodec ./cmd/liveforge` and `CGO_ENABLED=1 go test -tags audiocodec -race -coverprofile=coverage.out -covermode=atomic ./...`. +- [ ] **Step 4: Push the fix branch, create or update its PR, wait for all checks, merge only after they pass, then verify the post-merge `main` run has successful Agent Documentation, Lint, Test, Security Scan, and Docker Build jobs. + +### Task 4: Delete confirmed-unused branches + +**Files:** Git refs only. + +- [ ] **Step 1: Delete local and remote `codex/liveforge-completion` and `codex/p0-p1-main-playback`; both corresponding PRs are merged. +- [ ] **Step 2: Delete local-only stale `codex/forwarding-performance-optimization` and `playback-startup-fixes`; their remote refs are already gone. +- [ ] **Step 3: Preserve `fix/p0-p1-hardening` because PR #16 was closed without merge and its commits may not exist in main; delete it only after explicit disposal authorization. +- [ ] **Step 4: Verify with `git fetch origin --prune`, `git branch -vv --all`, and `gh api repos/im-pingo/liveforge/branches --paginate --jq '.[].name'`; `main` and `codex/webrtc-playback-fix` must remain. diff --git a/module/gb28181/rtp_receiver.go b/module/gb28181/rtp_receiver.go index 64caa435..c28647e0 100644 --- a/module/gb28181/rtp_receiver.go +++ b/module/gb28181/rtp_receiver.go @@ -41,10 +41,14 @@ func NewRTPReceiver(port int, publisher *Publisher) (*RTPReceiver, error) { if err != nil { return nil, fmt.Errorf("listen UDP :%d: %w", port, err) } - rtcpConn, err := net.ListenUDP("udp", &net.UDPAddr{Port: port + 1}) + rtcpPort := port + 1 + if port == 0 { + rtcpPort = conn.LocalAddr().(*net.UDPAddr).Port + 1 + } + rtcpConn, err := net.ListenUDP("udp", &net.UDPAddr{Port: rtcpPort}) if err != nil { _ = conn.Close() - return nil, fmt.Errorf("listen RTCP UDP :%d: %w", port+1, err) + return nil, fmt.Errorf("listen RTCP UDP :%d: %w", rtcpPort, err) } return &RTPReceiver{ diff --git a/module/gb28181/rtp_receiver_test.go b/module/gb28181/rtp_receiver_test.go index 26bc101d..69109b0a 100644 --- a/module/gb28181/rtp_receiver_test.go +++ b/module/gb28181/rtp_receiver_test.go @@ -176,6 +176,23 @@ func TestReadTCPRTPPacketInvalidLength(t *testing.T) { } } +func TestNewRTPReceiverUsesAssignedPortForRTCP(t *testing.T) { + receiver, err := NewRTPReceiver(0, NewPublisher("ephemeral-port", nil)) + if err != nil { + t.Fatalf("NewRTPReceiver: %v", err) + } + t.Cleanup(receiver.Close) + + rtpPort := receiver.LocalPort() + rtcpPort := receiver.rtcpConn.LocalAddr().(*net.UDPAddr).Port + if rtpPort <= 0 { + t.Fatalf("RTP port = %d, want assigned ephemeral port", rtpPort) + } + if rtcpPort != rtpPort+1 { + t.Fatalf("RTCP port = %d, want RTP port + 1 = %d", rtcpPort, rtpPort+1) + } +} + func TestSeqDiff(t *testing.T) { tests := []struct { a, b uint16 From 8cad6bef772c6ce05caf96c7e8cb1e0e2c54d463 Mon Sep 17 00:00:00 2001 From: im-pingo Date: Fri, 28 Aug 2026 17:51:33 +0800 Subject: [PATCH 2/4] ci: pin golangci-lint version --- .github/workflows/ci.yml | 2 +- docs/superpowers/plans/2026-08-28-ci-main-green.md | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index cca0ca64..f6cf5b7c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -47,7 +47,7 @@ jobs: - name: golangci-lint uses: golangci/golangci-lint-action@v9 with: - version: latest + version: v2.13.2 only-new-issues: true test: diff --git a/docs/superpowers/plans/2026-08-28-ci-main-green.md b/docs/superpowers/plans/2026-08-28-ci-main-green.md index 96200374..41a5bb1c 100644 --- a/docs/superpowers/plans/2026-08-28-ci-main-green.md +++ b/docs/superpowers/plans/2026-08-28-ci-main-green.md @@ -4,7 +4,7 @@ **Goal:** Make post-merge `main` CI reliable by fixing GB28181 ephemeral-port allocation, making lint deterministic, and removing only confirmed-unused branches. -**Architecture:** When `NewRTPReceiver` receives port zero, it will derive the RTCP port from the RTP socket's assigned port. CI will pin golangci-lint and compare changes against the appropriate revision on both PR and main push events, so an historical lint backlog cannot make main red while new issues remain gated. +**Architecture:** When `NewRTPReceiver` receives port zero, it will derive the RTCP port from the RTP socket's assigned port. CI will pin golangci-lint; the action's `only-new-issues` mode already compares a pull request with its patch and a main push with that push's commit diff, so an historical lint backlog will not block later incremental changes while new issues remain gated. **Tech Stack:** Go 1.26, FFmpeg-tagged Go tests, GitHub Actions, golangci-lint. @@ -39,9 +39,9 @@ **Files:** - Modify: `.github/workflows/ci.yml:34-39` -**Interfaces:** The `Lint` job must remain a required quality gate for pull requests and must check only the new merge revision on `main` pushes. +**Interfaces:** The `Lint` job must remain a required quality gate for pull requests and must check only the new diff on `main` pushes. -- [ ] **Step 1: Pin `golangci-lint-action` to the repository's Node 24-compatible action major and replace `version: latest` with a verified v2 release; configure the action's revision arguments conditionally for pull requests versus `HEAD^` on main pushes. +- [ ] **Step 1: Pin `golangci-lint-action` to the repository's Node 24-compatible action major and replace `version: latest` with the verified `v2.13.2` release; retain `only-new-issues: true`, which the action maps to the PR patch or push commit diff automatically. - [ ] **Step 2: Validate the workflow diff with `git diff --check` and inspect `.github/workflows/ci.yml` for valid YAML and no unrelated changes. - [ ] **Step 3: Commit with `git add .github/workflows/ci.yml && git commit -m "ci: lint only changes on main pushes"`. From ad6f06f1cf57b8d499acf614261aacf3982a5ea7 Mon Sep 17 00:00:00 2001 From: im-pingo Date: Fri, 28 Aug 2026 17:59:24 +0800 Subject: [PATCH 3/4] test: use loopback address for metrics server --- module/metrics/metrics_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/module/metrics/metrics_test.go b/module/metrics/metrics_test.go index 170e1983..229c90d0 100644 --- a/module/metrics/metrics_test.go +++ b/module/metrics/metrics_test.go @@ -32,7 +32,7 @@ func testConfig() *config.Config { }, Metrics: config.MetricsConfig{ Enabled: true, - Listen: ":0", // random port + Listen: "127.0.0.1:0", // random loopback port Path: "/metrics", }, } From 5389526fa7e95e17ddf2421c8fe882628a8177ed Mon Sep 17 00:00:00 2001 From: im-pingo Date: Fri, 28 Aug 2026 18:06:48 +0800 Subject: [PATCH 4/4] docs: track CI verification progress --- .../plans/2026-08-28-ci-main-green.md | 26 +++++++++---------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/docs/superpowers/plans/2026-08-28-ci-main-green.md b/docs/superpowers/plans/2026-08-28-ci-main-green.md index 41a5bb1c..ce851b77 100644 --- a/docs/superpowers/plans/2026-08-28-ci-main-green.md +++ b/docs/superpowers/plans/2026-08-28-ci-main-green.md @@ -28,11 +28,11 @@ **Interfaces:** `NewRTPReceiver(port int, publisher *Publisher)` keeps its public signature; port zero must result in RTCP listening on the assigned RTP port plus one. -- [ ] **Step 1: Add the failing regression test.** Add `TestNewRTPReceiverUsesAssignedPortForRTCP`, call `NewRTPReceiver(0, NewPublisher("ephemeral-port", nil))`, defer `receiver.Close`, read `receiver.LocalPort()` and `receiver.rtcpConn.LocalAddr().(*net.UDPAddr).Port`, assert RTP is positive and RTCP equals RTP plus one. -- [ ] **Step 2: Run `go test -tags audiocodec ./module/gb28181 -run TestNewRTPReceiverUsesAssignedPortForRTCP -count=1` and verify it fails because the current code attempts UDP port 1. -- [ ] **Step 3: After the RTP bind succeeds, compute RTCP port from the actual RTP local port when the requested port is zero; retain `port + 1` for explicitly allocated ports; close RTP if RTCP binding fails. -- [ ] **Step 4: Run `go test -tags audiocodec ./module/gb28181 -run 'TestNewRTPReceiverUsesAssignedPortForRTCP|TestRTPReceiver|TestMediaSessionCloseOwnsReceiverAndAllowsPortReuse' -count=1` and then `go test -tags audiocodec ./module/gb28181 -count=1`. -- [ ] **Step 5: Commit with `git add module/gb28181/rtp_receiver.go module/gb28181/rtp_receiver_test.go && git commit -m "fix: derive GB28181 RTCP port from ephemeral RTP port"`. +- [x] **Step 1: Add the failing regression test.** Add `TestNewRTPReceiverUsesAssignedPortForRTCP`, call `NewRTPReceiver(0, NewPublisher("ephemeral-port", nil))`, defer `receiver.Close`, read `receiver.LocalPort()` and `receiver.rtcpConn.LocalAddr().(*net.UDPAddr).Port`, assert RTP is positive and RTCP equals RTP plus one. +- [x] **Step 2: Run `go test -tags audiocodec ./module/gb28181 -run TestNewRTPReceiverUsesAssignedPortForRTCP -count=1` and verify it fails because the current code attempts UDP port 1. +- [x] **Step 3: After the RTP bind succeeds, compute RTCP port from the actual RTP local port when the requested port is zero; retain `port + 1` for explicitly allocated ports; close RTP if RTCP binding fails. +- [x] **Step 4: Run `go test -tags audiocodec ./module/gb28181 -run 'TestNewRTPReceiverUsesAssignedPortForRTCP|TestRTPReceiver|TestMediaSessionCloseOwnsReceiverAndAllowsPortReuse' -count=1` and then `go test -tags audiocodec ./module/gb28181 -count=1`. +- [x] **Step 5: Commit with `git add module/gb28181/rtp_receiver.go module/gb28181/rtp_receiver_test.go && git commit -m "fix: derive GB28181 RTCP port from ephemeral RTP port"`. ### Task 2: Make lint deterministic on PRs and main pushes @@ -41,24 +41,24 @@ **Interfaces:** The `Lint` job must remain a required quality gate for pull requests and must check only the new diff on `main` pushes. -- [ ] **Step 1: Pin `golangci-lint-action` to the repository's Node 24-compatible action major and replace `version: latest` with the verified `v2.13.2` release; retain `only-new-issues: true`, which the action maps to the PR patch or push commit diff automatically. -- [ ] **Step 2: Validate the workflow diff with `git diff --check` and inspect `.github/workflows/ci.yml` for valid YAML and no unrelated changes. -- [ ] **Step 3: Commit with `git add .github/workflows/ci.yml && git commit -m "ci: lint only changes on main pushes"`. +- [x] **Step 1: Pin `golangci-lint-action` to the repository's Node 24-compatible action major and replace `version: latest` with the verified `v2.13.2` release; retain `only-new-issues: true`, which the action maps to the PR patch or push commit diff automatically. +- [x] **Step 2: Validate the workflow diff with `git diff --check` and inspect `.github/workflows/ci.yml` for valid YAML and no unrelated changes. +- [x] **Step 3: Commit with `git add .github/workflows/ci.yml && git commit -m "ci: lint only changes on main pushes"`. ### Task 3: Documentation and full verification **Files:** Inspect `agent-manifest.json`, `llms.txt`, `llms-full.txt`, `README.md`, `README.zh-CN.md`, `docs/TECHNICAL-RISKS.md`, and `docs/PROGRESS.md`; modify only if the final behavior or CI contract changes a documented fact. -- [ ] **Step 1: Confirm the port fix changes no public API, configuration, prerequisite, or supported protocol; document any changed CI version or workflow fact in the required AI-facing files. -- [ ] **Step 2: Run `go test ./...`, `go test -tags audiocodec ./module/gb28181 -count=1`, `tools/check-agent-docs_test.sh`, `CHECK_AGENT_DOCS_DIFF=1 tools/check-agent-docs.sh`, `git diff --check`, and `jq empty agent-manifest.json`. -- [ ] **Step 3: When FFmpeg development libraries are available, run `CGO_ENABLED=1 go build -tags audiocodec ./cmd/liveforge` and `CGO_ENABLED=1 go test -tags audiocodec -race -coverprofile=coverage.out -covermode=atomic ./...`. +- [x] **Step 1: Confirm the port fix changes no public API, configuration, prerequisite, or supported protocol; document any changed CI version or workflow fact in the required AI-facing files. +- [x] **Step 2: Run `go test ./...`, `go test -tags audiocodec ./module/gb28181 -count=1`, `tools/check-agent-docs_test.sh`, `CHECK_AGENT_DOCS_DIFF=1 tools/check-agent-docs.sh`, `git diff --check`, and `jq empty agent-manifest.json`. +- [x] **Step 3: When FFmpeg development libraries are available, run `CGO_ENABLED=1 go build -tags audiocodec ./cmd/liveforge` and `CGO_ENABLED=1 go test -tags audiocodec -race -coverprofile=coverage.out -covermode=atomic ./...`. - [ ] **Step 4: Push the fix branch, create or update its PR, wait for all checks, merge only after they pass, then verify the post-merge `main` run has successful Agent Documentation, Lint, Test, Security Scan, and Docker Build jobs. ### Task 4: Delete confirmed-unused branches **Files:** Git refs only. -- [ ] **Step 1: Delete local and remote `codex/liveforge-completion` and `codex/p0-p1-main-playback`; both corresponding PRs are merged. -- [ ] **Step 2: Delete local-only stale `codex/forwarding-performance-optimization` and `playback-startup-fixes`; their remote refs are already gone. +- [x] **Step 1: Delete local and remote `codex/liveforge-completion` and `codex/p0-p1-main-playback`; both corresponding PRs are merged. +- [x] **Step 2: Delete local-only stale `codex/forwarding-performance-optimization` and `playback-startup-fixes`; their remote refs are already gone. - [ ] **Step 3: Preserve `fix/p0-p1-hardening` because PR #16 was closed without merge and its commits may not exist in main; delete it only after explicit disposal authorization. - [ ] **Step 4: Verify with `git fetch origin --prune`, `git branch -vv --all`, and `gh api repos/im-pingo/liveforge/branches --paginate --jq '.[].name'`; `main` and `codex/webrtc-playback-fix` must remain.