Skip to content

Commit 0aa5844

Browse files
committed
docs: add webhook operations guardrails
1 parent f66427a commit 0aa5844

11 files changed

Lines changed: 288 additions & 8 deletions

File tree

guide/docker.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,33 @@ balancer, keep health and status endpoints internal. `/health`, `/ready`, and
3030
to the public internet. See the [Operations Guide](operations.md#http-exposure)
3131
for endpoint exposure guidance.
3232

33+
Example reverse-proxy policy:
34+
35+
| Path | Public Internet | Internal Network |
36+
|------|-----------------|------------------|
37+
| `/mcp` | Allowed only with bearer auth and network policy | Allowed |
38+
| `/webhook` | Allowed only with HMAC secret and repo allowlist | Allowed |
39+
| `/health` | Blocked | Allowed |
40+
| `/ready` | Blocked | Allowed |
41+
| `/status` | Blocked | Allowed |
42+
43+
For webhook service mode, use a canonical clone base URL and keep one
44+
organization/owner per CCG instance unless the repo names are guaranteed unique:
45+
46+
```bash
47+
docker run -d -p 8080:8080 \
48+
-e CCG_HTTP_BEARER_TOKEN="$CCG_HTTP_BEARER_TOKEN" \
49+
-e CCG_DB_DRIVER=postgres \
50+
-e CCG_DB_DSN="$CCG_DB_DSN" \
51+
-e CCG_REPO_ROOT=/data/repos \
52+
-v ccg-repos:/data/repos \
53+
--entrypoint ccg ccg \
54+
serve --transport streamable-http --http-addr :8080 \
55+
--allow-repo "acme/*" \
56+
--webhook-secret "$WEBHOOK_SECRET" \
57+
--repo-clone-base-url https://github.com
58+
```
59+
3360
For the mounted default local SQLite database, use an explicit migration command
3461
when upgrading CCG against an existing schema:
3562

guide/ko/docker.md

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,32 @@ docker run -d -p 8080:8080 \
2727

2828
이 컨테이너가 Ingress, 리버스 프록시 또는 로드 밸런서를 통해 노출되는 경우, 헬스 체크 및 상태 엔드포인트는 내부용으로 유지하십시오. `/health`, `/ready`, `/status`는 신뢰할 수 있는 운영 체크를 위한 것이며 공용 인터넷에 노출되어서는 안 됩니다. 엔드포인트 노출 가이드는 [운영 가이드](operations.md#http-exposure)를 참조하십시오.
2929

30+
리버스 프록시 정책 예시:
31+
32+
| 경로 | 공용 인터넷 | 내부 네트워크 |
33+
|------|-------------|---------------|
34+
| `/mcp` | Bearer 인증 및 네트워크 정책이 있을 때만 허용 | 허용 |
35+
| `/webhook` | HMAC secret 및 repo allowlist가 있을 때만 허용 | 허용 |
36+
| `/health` | 차단 | 허용 |
37+
| `/ready` | 차단 | 허용 |
38+
| `/status` | 차단 | 허용 |
39+
40+
웹훅 서비스 모드에서는 canonical clone base URL을 사용하고, repo 이름 중복이 없다는 보장이 없다면 CCG 인스턴스 하나에는 하나의 조직/owner만 연결하십시오.
41+
42+
```bash
43+
docker run -d -p 8080:8080 \
44+
-e CCG_HTTP_BEARER_TOKEN="$CCG_HTTP_BEARER_TOKEN" \
45+
-e CCG_DB_DRIVER=postgres \
46+
-e CCG_DB_DSN="$CCG_DB_DSN" \
47+
-e CCG_REPO_ROOT=/data/repos \
48+
-v ccg-repos:/data/repos \
49+
--entrypoint ccg ccg \
50+
serve --transport streamable-http --http-addr :8080 \
51+
--allow-repo "acme/*" \
52+
--webhook-secret "$WEBHOOK_SECRET" \
53+
--repo-clone-base-url https://github.com
54+
```
55+
3056
마운트된 기본 로컬 SQLite 데이터베이스의 경우, 기존 스키마에 대해 CCG를 업그레이드할 때는 명시적인 마이그레이션 명령어를 사용하십시오:
3157

3258
```bash

guide/ko/operations.md

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,15 @@ CCG가 Ingress, 리버스 프록시 또는 로드 밸런서 뒤에 있는 경우
168168
- 지속성 있는 로컬 스토리지를 위한 `--repo-root`
169169
- 팀용 또는 상시 가동 배포를 위한 `--db-driver postgres`
170170

171+
웹훅 네임스페이스 추출은 마지막 저장소 이름을 사용합니다. 예를 들어 `acme/api``api` 네임스페이스에 저장되고 `$REPO_ROOT/api`에 checkout됩니다. 이 전략은 단일 owner 웹훅 배포를 위한 것입니다. 허용 목록이 여러 owner를 포함하면 서버는 경고를 남깁니다. `acme/api``external/api`가 같은 네임스페이스 및 checkout 경로에서 충돌할 수 있기 때문입니다.
172+
173+
권장 정책:
174+
175+
- 웹훅 CCG 인스턴스 하나에는 하나의 조직/owner를 사용하십시오.
176+
- 인스턴스 안에서 마지막 저장소 이름이 중복되지 않게 유지하십시오.
177+
- 서로 다른 조직에 같은 repo 이름이 있을 수 있으면 CCG 인스턴스를 분리하십시오.
178+
- `*/*`는 일반 운영 allowlist가 아니라 개발 또는 격리된 환경에서만 신중히 사용하십시오.
179+
171180
SQLite 웹훅 배포는 `--webhook-workers` 또는 `CCG_WEBHOOK_WORKERS`가 명시적으로 설정되지 않으면 기본적으로 하나의 워커를 사용합니다. 이는 동일한 SQLite 데이터베이스에 대해 여러 동시 쓰기 작업이 발생하는 것을 방지하기 위함입니다. PostgreSQL의 경우, 저장소 업데이트 시간, 데이터베이스 용량 및 허용 가능한 큐 대기 시간에 따라 워커 수를 조정해야 합니다.
172181

173182
큐 메모리를 제한하려면 `--webhook-max-tracked-repos`를 사용하십시오. 큐가 가득 차면 새로운 저장소 요청은 `429 Too Many Requests`로 거부됩니다. 이러한 현상이 반복되면 규모 확장이나 범위 조정 문제로 취급해야 합니다.
@@ -210,9 +219,20 @@ SQLite 웹훅 배포는 `--webhook-workers` 또는 `CCG_WEBHOOK_WORKERS`가 명
210219

211220
SIGINT/SIGTERM 수신 시:
212221

213-
1. HTTP는 5초의 종료 윈도우와 함께 새로운 요청 수락을 중단합니다.
222+
1. HTTP는 `--webhook-shutdown-timeout`(기본 30초)의 종료 윈도우와 함께 새로운 요청 수락을 중단합니다.
214223
2. 동기화 컨텍스트가 취소되고 진행 중인 복제/빌드 작업이 `context.Done()`을 감지합니다.
215-
3. SyncQueue 워커는 드레인(drain)을 위해 최대 30초의 시간을 갖습니다.
224+
3. SyncQueue 워커는 드레인(drain)을 위해 최대 `--webhook-shutdown-timeout`의 시간을 갖습니다.
225+
226+
큐 종료가 시작된 뒤 들어온 웹훅 delivery는 `503 Service Unavailable`을 반환하므로 GitHub/Gitea가 이벤트를 성공 처리하지 않고 재시도할 수 있습니다. 종료 직전에 수락된 요청은 프로세스 종료로 취소될 수 있으므로, 새 push 또는 수동 네임스페이스 업데이트로 복구하십시오.
227+
228+
수동 복구:
229+
230+
```bash
231+
ccg update /data/repos/api --namespace api
232+
ccg status --namespace api --errors
233+
```
234+
235+
검색, 커뮤니티, 저장된 flow가 여전히 오래된 것처럼 보이면 namespace `api`에 대해 필요한 postprocess 플래그와 함께 MCP `run_postprocess` 도구를 호출하십시오.
216236

217237
Streamable HTTP 서버는 MCP 스트림이 장시간 유지될 수 있으므로 고정된 `WriteTimeout`을 사용하지 않습니다. 서비스가 인터넷에 노출된 경우 리버스 프록시에서 유휴 연결 제한 및 요청 버퍼링 정책을 설정하십시오.
218238

guide/ko/webhook.md

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,23 @@ ccg serve --transport streamable-http \
7676
2. 일치하는 규칙이 없으면 거부됩니다.
7777
3. 웹훅 페이로드의 `ref` 필드에서 `refs/heads/` 접두사는 자동으로 제거됩니다.
7878

79+
### 네임스페이스 전략과 단일 조직 운영
80+
81+
웹훅 동기화는 `org/repo` 중 마지막 저장소 이름을 그래프 네임스페이스와 checkout 디렉터리로 사용합니다. 예를 들어 `acme/api``api` 네임스페이스와 `/data/repos/api` checkout으로 매핑됩니다.
82+
83+
이 방식은 단일 조직 배포에서 짧고 예측 가능한 이름을 유지하기 위한 전략이며, 권장 운영 모델입니다. 허용 목록이 `acme/*``external/shared`처럼 여러 owner를 포함하거나 `*/*`를 사용하면, 같은 마지막 저장소 이름이 충돌할 수 있으므로 CCG는 시작 시 경고 로그를 남깁니다.
84+
85+
| 저장소 | 파생 네임스페이스 |
86+
|------|-------------------|
87+
| `acme/api` | `api` |
88+
| `external/api` | `api` |
89+
90+
운영 정책:
91+
92+
- 웹훅 CCG 인스턴스 하나에는 하나의 owner/조직을 사용하는 것을 권장합니다.
93+
- 같은 인스턴스 안에 마지막 저장소 이름이 같은 저장소를 허용하지 마십시오.
94+
- 여러 owner의 동기화가 필요하면 별도 CCG 인스턴스를 사용하거나, 네임스페이스 전략을 변경한 뒤 활성화하십시오.
95+
7996
## 서명 검증 (Signature Verification)
8097

8198
HMAC-SHA256을 사용하여 웹훅 페이로드를 검증합니다.
@@ -95,11 +112,12 @@ HMAC-SHA256을 사용하여 웹훅 페이로드를 검증합니다.
95112

96113
SIGINT/SIGTERM 수신 시:
97114

98-
1. **HTTP 서버 종료** — 새로운 요청 수락 중단 (5초 타임아웃)
115+
1. **HTTP 서버 종료** — 새로운 요청 수락 중단 (`--webhook-shutdown-timeout`, 기본 30초)
99116
2. **동기화 컨텍스트 취소** — 진행 중인 복제/빌드 작업에 `context.Done()` 전달
100-
3. **워커 드레인(drain)** — SyncQueue 워커가 종료될 때까지 대기 (30초 타임아웃)
117+
3. **워커 드레인(drain)** — SyncQueue 워커가 종료될 때까지 대기 (`--webhook-shutdown-timeout`, 기본 30초)
101118

102119
진행 중인 복제/빌드 작업은 컨텍스트 취소를 수신하고 즉시 중단되어 종료 대기 시간을 최소화합니다.
120+
종료가 시작된 뒤 새 웹훅 delivery가 들어오면 `503 Service Unavailable`을 반환하므로, provider가 성공 처리 대신 재시도할 수 있습니다.
103121

104122
## 파이프라인 (Pipeline)
105123

@@ -201,6 +219,23 @@ include_paths:
201219

202220
CCG는 현재 웹훅 운영을 위한 `/metrics` 엔드포인트를 제공하지 않습니다. `/status`를 주요 구조화된 런타임 뷰로 활용하십시오.
203221

222+
### 복구 Runbook
223+
224+
`/status`가 degraded 웹훅 동기화를 보고하거나, 큐 대기 시간이 계속 증가하거나, 배포 재시작 중 수락된 이벤트가 중단되었을 수 있을 때는 아래 절차를 사용하십시오.
225+
226+
1. `/status.webhook.recent_repos`와 로그에서 `repo`, `branch`, `last_error`를 확인합니다.
227+
2. 잘못된 `.ccg.yaml` `include_paths`처럼 재시도 불가능한 저장소 설정 오류를 수정합니다.
228+
3. upstream provider가 복구를 주도해야 한다면 같은 브랜치에 새 push를 발생시킵니다.
229+
4. 수동 복구가 필요하면 checkout 디렉터리 기준으로 네임스페이스를 업데이트합니다.
230+
231+
```bash
232+
ccg update /data/repos/api --namespace api
233+
ccg status --namespace api --errors
234+
```
235+
236+
5. 검색, 커뮤니티, 저장된 flow가 여전히 오래된 것처럼 보이면 namespace `api`에 대해 필요한 postprocess 플래그와 함께 MCP `run_postprocess` 도구를 호출합니다.
237+
6. `/status`를 다시 확인합니다. 같은 저장소의 동기화가 성공하면 저장소별 unresolved error가 해제됩니다.
238+
204239
배포 프로필, 데이터베이스 선택, 네임스페이스 크기 가이드 및 일반적인 실패 모드에 대해서는 [운영(Operations)](operations.md)을 참조하십시오.
205240

206241
## 패닉 복구 (Panic Recovery)

guide/operations.md

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -205,6 +205,21 @@ Webhook deployments should be configured with:
205205
- `--repo-root` on durable local storage
206206
- `--db-driver postgres` for team or always-on deployments
207207
208+
Webhook namespace extraction uses the final repository name. For example,
209+
`acme/api` is stored in namespace `api` and checked out under
210+
`$REPO_ROOT/api`. This is intended for single-owner webhook deployments. If an
211+
allowlist spans multiple owners, the server logs a warning because `acme/api`
212+
and `external/api` would collide in the same namespace and checkout path.
213+
214+
Recommended policy:
215+
216+
- Use one organization/owner per webhook CCG instance.
217+
- Keep repo final names unique inside an instance.
218+
- Use separate CCG instances when different organizations may contain matching
219+
repo names.
220+
- Treat `*/*` as a development or carefully isolated configuration, not a
221+
normal production allowlist.
222+
208223
SQLite webhook deployments default to one worker unless
209224
`--webhook-workers` or `CCG_WEBHOOK_WORKERS` is explicitly set. This avoids
210225
creating multiple concurrent writers against the same SQLite database. With
@@ -268,10 +283,26 @@ single event is roughly 46 minutes before it is abandoned.
268283
269284
On SIGINT/SIGTERM:
270285
271-
1. HTTP stops accepting new requests with a 5-second shutdown window.
286+
1. HTTP stops accepting new requests with `--webhook-shutdown-timeout` (default
287+
30 seconds).
272288
2. The sync context is cancelled and in-progress clone/build work observes
273289
`context.Done()`.
274-
3. SyncQueue workers get up to 30 seconds to drain.
290+
3. SyncQueue workers get up to `--webhook-shutdown-timeout` to drain.
291+
292+
Webhook deliveries received after queue shutdown starts return `503 Service
293+
Unavailable`, which lets GitHub/Gitea retry instead of treating the event as
294+
accepted. A request accepted immediately before shutdown can still be cancelled
295+
by process shutdown; recover it with a new push or a manual namespace update.
296+
297+
Manual recovery:
298+
299+
```bash
300+
ccg update /data/repos/api --namespace api
301+
ccg status --namespace api --errors
302+
```
303+
304+
If search, communities, or saved flows still look stale, call the MCP
305+
`run_postprocess` tool for namespace `api` with the needed postprocess flags.
275306
276307
The Streamable HTTP server does not use a fixed `WriteTimeout` because MCP
277308
streams can be long-lived. Put idle connection limits and request buffering

guide/webhook.md

Lines changed: 50 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,30 @@ Use the `--allow-repo` flag to configure allowed branches per repository.
8282
2. Rejected if no matching rule found
8383
3. `refs/heads/` prefix is automatically stripped from the `ref` field in webhook payload
8484

85+
### Namespace Strategy and Single-Org Operation
86+
87+
Webhook sync derives the graph namespace and checkout directory from the
88+
repository name portion of `org/repo`. For example, `acme/api` maps to the
89+
`api` namespace and `/data/repos/api` checkout.
90+
91+
This keeps single-organization deployments short and predictable, which is the
92+
recommended operating model. If the allowlist spans multiple owners, such as
93+
`acme/*` plus `external/shared` or `*/*`, CCG logs a startup warning because
94+
repositories with the same final name can collide:
95+
96+
| Repo | Derived namespace |
97+
|------|-------------------|
98+
| `acme/api` | `api` |
99+
| `external/api` | `api` |
100+
101+
Operational policy:
102+
103+
- Prefer one owner/organization per webhook CCG instance.
104+
- Do not allow two repositories with the same final repo name in the same
105+
instance.
106+
- If multi-owner sync becomes necessary, run separate CCG instances or change
107+
the namespace strategy before enabling those rules.
108+
85109
## Signature Verification
86110

87111
Verifies webhook payload with HMAC-SHA256.
@@ -101,11 +125,12 @@ By default, webhook requests fail closed unless `--webhook-secret` is configured
101125

102126
On SIGINT/SIGTERM:
103127

104-
1. **HTTP server shutdown** — stops accepting new requests (5-second timeout)
128+
1. **HTTP server shutdown** — stops accepting new requests (`--webhook-shutdown-timeout`, default 30 seconds)
105129
2. **sync context cancel** — propagates `context.Done()` to in-progress clone/build operations
106-
3. **worker drain** — waits for SyncQueue workers to finish (30-second timeout)
130+
3. **worker drain** — waits for SyncQueue workers to finish (`--webhook-shutdown-timeout`, default 30 seconds)
107131

108132
In-progress clone/build operations receive the context cancel and stop immediately, minimizing shutdown wait time.
133+
After shutdown begins, new webhook deliveries are rejected with `503 Service Unavailable` so providers can retry instead of treating the event as successfully accepted.
109134

110135
## Pipeline
111136

@@ -234,6 +259,29 @@ same repo clears that repo's error state.
234259
CCG does not currently expose a `/metrics` endpoint for webhook operations.
235260
Treat `/status` as the primary structured runtime view.
236261

262+
### Recovery Runbook
263+
264+
Use this runbook when `/status` reports a degraded webhook sync, queue age keeps
265+
growing, or a deployment restart may have interrupted an accepted event.
266+
267+
1. Check `/status.webhook.recent_repos` and logs for `repo`, `branch`, and
268+
`last_error`.
269+
2. Fix non-retryable repository configuration failures, such as malformed
270+
`.ccg.yaml` `include_paths`.
271+
3. Trigger a new push on the same branch when the upstream provider should
272+
drive recovery.
273+
4. For manual recovery, update the namespace from the checkout directory:
274+
275+
```bash
276+
ccg update /data/repos/api --namespace api
277+
ccg status --namespace api --errors
278+
```
279+
280+
5. If search, communities, or saved flows still look stale, call the MCP
281+
`run_postprocess` tool for namespace `api` with the needed postprocess flags.
282+
6. Recheck `/status`; a successful sync for the same repo clears the repo-level
283+
unresolved error.
284+
237285
For deployment profiles, database choice, namespace size guidance, and common
238286
failure modes, see [Operations](operations.md).
239287

internal/analysis/deadcode/service.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,9 @@ func (s *Service) FindPage(ctx context.Context, opts Options) (Result, error) {
9292
return Result{Items: nodes, Pagination: paging.BuildPage(req, len(nodes), hasMore)}, nil
9393
}
9494

95+
// normalizePathPrefix cleans a path prefix and returns an empty string if the cleaned result is ".".
96+
// @intent ensure path prefix filtering is consistent regardless of trailing slashes or "." input
97+
// @domainRule a path prefix of "." should be treated the same as an empty prefix (no filtering)
9598
func normalizePathPrefix(prefix string) string {
9699
if prefix == "" {
97100
return ""

internal/analysis/largefunc/service.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,9 @@ func (s *Service) FindPage(ctx context.Context, opts Options) (Result, error) {
9494
return Result{Items: nodes, Pagination: paging.BuildPage(req, len(nodes), hasMore)}, nil
9595
}
9696

97+
// normalizePathPrefix cleans a path prefix and returns an empty string if the cleaned result is ".".
98+
// @intent ensure path prefix filtering is consistent regardless of trailing slashes or "." input
99+
// @domainRule a path prefix of "." should be treated the same as an empty prefix (no filtering)
97100
func normalizePathPrefix(prefix string) string {
98101
if prefix == "" {
99102
return ""

internal/server/serve.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -253,6 +253,12 @@ func RunStreamableHTTP(deps *cli.Deps, srv *mcpgo.MCPServer, cfg cli.ServeConfig
253253
for _, s := range cfg.AllowRepo {
254254
rules = append(rules, webhook.ParseRepoRule(s))
255255
}
256+
if spansMultipleOwners, owners := webhook.AllowRulesSpanMultipleOwners(rules); spansMultipleOwners {
257+
deps.Logger.Warn("webhook allow-repo spans multiple owners; repo-name namespace strategy can collide for equal repo names",
258+
"owners", owners,
259+
"namespace_strategy", "repo_name",
260+
)
261+
}
256262
filter := webhook.NewRepoFilterFromRules(rules)
257263
secret := []byte(cfg.WebhookSecret)
258264
repoLocker := webhook.NewRepoLocker(30 * time.Second)

internal/webhook/allowlist.go

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package webhook
33

44
import (
55
"path"
6+
"sort"
67
"strings"
78
)
89

@@ -153,6 +154,37 @@ func ParseRepoRule(s string) RepoRule {
153154
return RepoRule{Pattern: pattern, Branches: branches}
154155
}
155156

157+
// AllowRuleOwners returns the positive repository owner prefixes present in allow rules.
158+
// @intent let server startup warn when repo-name namespace extraction is used with multi-owner webhook allowlists.
159+
// @domainRule deny rules are ignored because they only narrow the positive admission surface.
160+
func AllowRuleOwners(rules []RepoRule) []string {
161+
owners := make(map[string]struct{})
162+
for _, rule := range rules {
163+
pattern := strings.TrimSpace(rule.Pattern)
164+
if pattern == "" || strings.HasPrefix(pattern, "!") {
165+
continue
166+
}
167+
owner, _, ok := strings.Cut(pattern, "/")
168+
if !ok || owner == "" {
169+
continue
170+
}
171+
owners[owner] = struct{}{}
172+
}
173+
out := make([]string, 0, len(owners))
174+
for owner := range owners {
175+
out = append(out, owner)
176+
}
177+
sort.Strings(out)
178+
return out
179+
}
180+
181+
// AllowRulesSpanMultipleOwners reports whether webhook rules admit repositories under more than one owner prefix.
182+
// @intent identify configurations that can collide under the current repo-name namespace strategy.
183+
func AllowRulesSpanMultipleOwners(rules []RepoRule) (bool, []string) {
184+
owners := AllowRuleOwners(rules)
185+
return len(owners) > 1 || (len(owners) == 1 && owners[0] == "*"), owners
186+
}
187+
156188
// @intent apply one compiled allow or deny pattern to a repository full name during filter evaluation.
157189
func (r *allowRule) match(repoFullName string) bool {
158190
if r.wildcard {

0 commit comments

Comments
 (0)