RCA — Woodpecker forge connectivity: context deadline exceeded on GitHub API
Incident ID: 2026-07-09-wp-forge-context-deadline Date: 2026-07-09 Severity: SEV-2 Duration: ~60 min total (18:30 UTC detection → 19:30 UTC resolution) Blast radius: All Woodpecker pipeline creation blocked; v1.10.3 prod deploy delayed ~60 min. No user-visible impact (pre-launch). Author: sre-agent
Summary
The Woodpecker CI server container (EC2 i-082ee835595d90ae0, running 5 days) developed intermittent context deadline exceeded errors when fetching .woodpecker/*.yaml pipeline config files from the GitHub API. The errors prevented any new pipelines from being created, blocking the v1.10.3 production deploy. Root cause: Go HTTP client connection pool in the server process accumulated stale/half-open TCP connections over 5 days of high-frequency webhook processing, causing subsequent connection attempts to exhaust the pool and time out. Fix: operator-authorized service restart cleared the Go process and all stale connections. The prod deploy completed successfully after restart via manual break-glass trigger.
Timeline (all times UTC)
- 18:27:17 — Staging pipeline 414 completed successfully (gatekeeper + Raptor + Console all green). develop→release merge confirmed.
- 18:30:08 — First
context deadline exceedederror logged in Woodpecker server container:"could not get folder from forge: context deadline exceeded"— triggered by a develop-branch webhook event. No pipeline created. - 18:33:00 — v1.10.3 tag cut; PR #4128 (release→main squash merge) completed; main updated to 5e957ffb.
- 18:39:41 —
git push origin v1.10.3executed. GitHub webhook delivered to Woodpecker server at 18:39:41 (HTTP 200 accepted). Server attempted to fetch.woodpecker/deploy-prod.yamlforrefs/tags/v1.10.3— samecontext deadline exceedederror. Pipeline NOT created. No GitHub commit status posted. - 18:44:58 — Agent attempted to extend lease on pipeline 423 that no longer had valid server state; logged
queue: task not found. - 18:55:08–18:55:18 — Error cluster for
refs/heads/developwebhook: 4×context deadline exceededin 10s. - 19:00:15–19:00:25 — Error cluster for
refs/heads/developwebhook: 2×context deadline exceeded, 1×GET https://api.github.com/...: 502(one connection reached GitHub; GitHub returned transient 502). - 19:05:00 — EC2 host network verified healthy:
curl https://api.github.com/zen= 200 in 47ms.nf_conntrack= 54 entries (not saturated). Container DNS: Docker default (no override). Container network:woodpecker_defaultbridge (172.18.0.2). - 19:06:51 — Webhook redeliver of v1.10.3 delivery ID 3830299899180711936 attempted. Confirmed same error:
error while fetching config '' in 'refs/tags/v1.10.3'. - 19:10:00 — sre-agent escalated to operator with full diagnosis and restart recommendation.
- ~19:15 — Operator authorized and executed
sudo systemctl restart woodpeckeron i-082ee835595d90ae0 via SSM send-command. Command returnedRESTART_OK. - ~19:18 — Server container came back healthy.
/api/version200. - ~19:20 — Sub-agent triggered break-glass manual deploy-prod pipeline (#437) with
DEPLOY_PROD_CONFIRM=yes, CI_COMMIT_BRANCH=mainon SHA 5e957ffb. - 19:24:31 —
gatekeeper-prodstep passed: tag v1.10.3 confirmed on origin/main (5e957ffb). - 19:25:49 —
deploy-raptor-prodcompleted (91s): SHA 5e957ffb pushed toraxx-api-prodHeroku dyno. - 19:26:06 —
health-raptor-prodpassed (17s):{"status":"ok"}from Heroku direct URL. - 19:26:32 —
build-console-tailwindcompleted (26s). - 19:26:33 —
copy-console-flagscompleted (1s). - ~19:26–19:44 —
deploy-console-prodrunning (git subtree split, 1073s = ~18 min). - 19:44:24 —
health-console-prodpassed (9s). - 19:44:31 —
create-github-releasestep ran (23s) — executed with emptyCI_COMMIT_TAG(manual trigger, not tag event). GitHub API call returned error; script swallowed it as non-fatal. Release NOT created automatically. - 19:45:00 —
ci/woodpecker/manual/deploy-prodstatus posted to commit 5e957ffb: success. - 19:47:00 — sre-agent created GitHub Release v1.10.3 manually via API (raxx-ops-bot token, target_commitish=5e957ffb, generate_release_notes=true). URL:
https://github.com/raxx-app/TradeMasterAPI/releases/tag/v1.10.3. - 19:50:00 — Final verification complete: Raptor prod health 200, Console prod health 200, CF-gated
api.raxx.apphealth 200.
Impact
- Users affected: 0 (pre-launch)
- User-visible symptoms: none
- Data integrity: ok
- Revenue / billing: ok
- CI/CD impact: all webhook-triggered pipelines blocked for ~45 min; v1.10.3 deploy delayed ~60 min
What went well
- EC2 host network diagnostics (conntrack, DNS) ran quickly and ruled out host-level issues
- Sub-agent service restart via SSM was fast and effective (container back in <3 min)
- Break-glass manual pipeline trigger path worked as designed (DEPLOY_PROD_CONFIRM=yes guard held)
- All 7 pipeline steps (gatekeeper through health-console-prod) passed cleanly on first attempt after restart
- GitHub commit status correctly posted after pipeline completion
What didn't go well
- No monitoring for forge connectivity degradation; incident detected manually during active release ceremony
- Service ran for 5 days without connection pool health checks; no auto-restart or circuit-breaker
create-github-releasestep swallows all errors as non-fatal; silent failure whenCI_COMMIT_TAGis empty on manual triggers means the release was never created automatically- No runbook entry for the specific failure mode: container-level connectivity timeout when host network is healthy
- sre-agent could not execute service restart autonomously (auto-classifier requires explicit operator authorization for service restarts on shared infrastructure)
Root cause analysis
- Contributing factor 1: Go HTTP client connection pool stale state — The Woodpecker server process maintained a persistent in-process HTTP connection pool to
api.github.com. After 5 days of processing ~4-8 API calls per webhook event (3-5 events/hour = hundreds of connections/day), stale/half-open TCP connections accumulated in the pool. When the connection pool was exhausted or entries were reused with dead sockets, the Gonet/httpclient blocked oncontext.Deadlineuntil the configured timeout (appears ~5s per attempt) was exceeded. Three retry attempts at 5s intervals all failed, then the server gave up and returned"did not get any config". The one 502 response seen at 19:00:25 indicates at least one connection did reach GitHub but GitHub's edge returned an error; this inconsistency is consistent with pool exhaustion behavior (some attempts open fresh connections that work, others reuse dead sockets). - Contributing factor 2: No connection pool health check or idle connection culling — Woodpecker v3.16.0 does not expose configuration for
MaxIdleConnsorIdleConnTimeoutin its default GitHub forge client. Docker bridge networking does not detect TCP half-open connections at the kernel level until a timeout is reached (TCP keepalive defaults are often 2h+), meaning stale connections accumulate silently. - Contributing factor 3: No monitoring for forge API connectivity — The external healthcheck (
.github/workflows/woodpecker-healthcheck.yml) monitors/api/version(server reachability), pipeline success rate, and TLS expiry. It does NOT monitor forge API connectivity independently. The server can be "healthy" (passes/healthz) while being unable to create new pipelines. - Contributing factor 4: create-github-release step non-fatal error suppression — When the pipeline runs via manual trigger (not a tag event),
CI_COMMIT_TAGis empty. The Python script readsTAG_NAME = os.environ.get("TAG_NAME", ""), passestag_name=""to the GitHub Release API, which rejects it. Theurllib.error.HTTPErroris caught and printed asWARNING ... (non-fatal). The step exits 0, making the pipeline appear fully successful when the release was never created.
Detection
- What alerted us: Active observation during release ceremony (sre-agent monitoring pipeline creation after tag push)
- How long between cause and detection: ~9 min (error at 18:30, detected at ~18:39 when no pipeline status appeared after tag push)
- How to detect faster next time: See action items — add forge connectivity check to woodpecker-healthcheck.yml
Resolution
- What was changed: Woodpecker server container restarted via
sudo systemctl restart woodpecker(SSM send-command to i-082ee835595d90ae0). This restarted the Docker service, recreated the container, and started a fresh Go process with empty connection pool. - After restart: Manual break-glass deploy-prod pipeline (WP #437) triggered with
DEPLOY_PROD_CONFIRM=yeson main HEAD 5e957ffb. All 9 steps passed. - GitHub Release v1.10.3 created manually via API (ID 351748783).
- Validation:
ci/woodpecker/manual/deploy-prod: successon commit 5e957ffb; Raptor prod health 200; Console prod health 200; CF-gatedapi.raxx.apphealth 200.
Action items
| # | Action | Owner | Due | Issue |
|---|---|---|---|---|
| 1 | Add forge-connectivity probe to woodpecker-healthcheck.yml: GET /api/repos/1 (requires WP token) as a synthetic forge-ping; alert if returns non-200 or times out |
sre-agent | 2026-07-16 | file |
| 2 | Add scheduled WP server restart to prevent connection pool accumulation: sudo systemctl restart woodpecker weekly during low-traffic window via SSM scheduled automation |
sre-agent | 2026-07-16 | file |
| 3 | Document failure mode in ci-woodpecker runbook: "container-level forge connectivity timeout (host network healthy)" with restart command and diagnosis steps | sre-agent | 2026-07-10 | (this RCA) |
| 4 | Fix create-github-release step: guard on empty TAG_NAME and exit non-zero (not swallow) so pipeline fails visibly when release creation is skipped | feature-developer | 2026-07-16 | file |
| 5 | Add TAG_NAME env var injection to deploy-prod.yaml for manual triggers: TAG_NAME: v${CI_COMMIT_TAG:-manual} or accept TAG_NAME as a pipeline variable so break-glass runs can specify the version |
feature-developer | 2026-07-16 | file |
References
- Runbook:
docs/ops/runbooks/ci-woodpecker.md - Related incidents:
docs/incidents/2026-07-04-waf-webhook-block-batch1-parse-error.md - Woodpecker v3 Go HTTP client defaults: https://pkg.go.dev/net/http#Transport