update roadmap and fix gito webhook implementations
This commit is contained in:
parent
381c7eaddf
commit
4ac7f74d28
8 changed files with 53 additions and 43 deletions
|
|
@ -4,7 +4,7 @@
|
|||
|
||||
NomadCode는 Flutter 기반 앱, core 서비스, 공유 계약, agent-operation 규칙을 하나의 원레포로 묶어 AI-assisted development workflow를 조율하는 프로젝트다.
|
||||
|
||||
현재 로드맵은 `ROADMAP.md -> phase/<phase-slug>/PHASE.md -> phase/<phase-slug>/milestones/<milestone-slug>.md` scaffold를 기준으로 관리한다. React/Vite 웹 콘솔 제거, 서버/Plane/provider 기반 작업, Flutter-first 클라이언트 정리, Mattermost push plugin extraction, client integration 표준화, workspace 포트/환경 표준화, External Integration, Milestone Work Item Creation Sync, Plane Work Item Webhook Intake, Gito Branch Event Creation Sync Bridge, Gito proto-socket consumer wire readiness는 완료되었다. Agent-Ops MCP Control Plane의 최우선 활성 Milestone은 Gito HTTP Webhook Consumer Readiness로, Gito 제공 계약의 signed HTTP webhook delivery를 NomadCode 소비 경계로 반영한다.
|
||||
현재 로드맵은 `ROADMAP.md -> phase/<phase-slug>/PHASE.md -> phase/<phase-slug>/milestones/<milestone-slug>.md` scaffold를 기준으로 관리한다. React/Vite 웹 콘솔 제거, 서버/Plane/provider 기반 작업, Flutter-first 클라이언트 정리, Mattermost push plugin extraction, client integration 표준화, workspace 포트/환경 표준화, External Integration, Milestone Work Item Creation Sync, Plane Work Item Webhook Intake, Gito Branch Event Creation Sync Bridge, Gito proto-socket consumer wire readiness, Gito HTTP Webhook Consumer Readiness는 완료되었다. Agent-Ops MCP Control Plane의 활성 Milestone은 Roadmap Driven Agent-Ops Automation으로, roadmap/action core와 provider sync domain의 상위 방향을 정리한다.
|
||||
|
||||
IOP 외부 실행 호출은 OpenAI-compatible Responses API 방식을 기본 계약으로 채택하고, NomadCode/IOP 고유의 task, workspace, session, approval, artifact, notification 문맥은 별도 `iop` wrapper field가 아니라 `metadata` 확장으로 전달한다. A2A는 agent-to-agent delegation이 명확히 필요할 때 재검토하며, IOP native protocol은 NomadCode의 기본 외부 실행 호출 표면으로 쓰지 않는다.
|
||||
|
||||
|
|
@ -37,7 +37,7 @@ IOP 외부 실행 호출은 OpenAI-compatible Responses API 방식을 기본 계
|
|||
- 요약: Plane 확장, Jira-compatible provider 추상화, Mattermost, Agent Integrator, IOP OpenAI-compatible Responses 호출 경계를 실제 통합 adapter 흐름으로 확장했고, metadata 실행 문맥 전달, IOP Edge `/v1/responses`, NomadCode Core 원격 create/enqueue/poll smoke를 완료했다.
|
||||
- [진행중] Agent-Ops MCP Control Plane
|
||||
- 경로: `agent-roadmap/phase/agent-ops-mcp-control-plane/PHASE.md`
|
||||
- 요약: 로드맵 기반 agent-ops 운영 자동화, Plane/Jira 같은 work item provider와 Milestone item의 양방향 동기화 도메인, 외부 agent용 MCP 제어 표면을 다루며, 최우선으로 Gito HTTP Webhook Consumer Readiness에서 Gito branch event 소비 표면을 signed HTTP webhook/callback으로 재정렬한다.
|
||||
- 요약: 로드맵 기반 agent-ops 운영 자동화, Plane/Jira 같은 work item provider와 Milestone item의 양방향 동기화 도메인, 외부 agent용 MCP 제어 표면을 다루며, 현재 Roadmap Driven Agent-Ops Automation에서 roadmap/action core와 provider sync domain의 상위 방향을 정리한다.
|
||||
- [계획] Project Workspace Management UX
|
||||
- 경로: `agent-roadmap/phase/project-workspace-management-ux/PHASE.md`
|
||||
- 요약: client integration 표준화, core workflow, 외부 통합 기준 이후 프로젝트 단위 앱 UX를 다루며, provider slot 기반 외부 console composition은 계획 후보로 둔다.
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ NomadCode Core의 Gito branch event consumer를 Gito 제공 계약 `gito.forgejo
|
|||
|
||||
## 상태
|
||||
|
||||
[진행중]
|
||||
[완료]
|
||||
|
||||
## 승격 조건
|
||||
|
||||
|
|
@ -53,19 +53,23 @@ NomadCode Core가 Gito branch event delivery를 외부 HTTP callback으로 받
|
|||
|
||||
HTTP webhook wakeup을 기존 branch revision 재검증과 creation sync enqueue 의미 로직으로 연결한다.
|
||||
|
||||
- [ ] [scan-enqueue] HTTP receiver에서 받은 on-target event가 target branch fetch/scan 이후 `EnqueueRoadmapCreationSync`로 이어진다. 검증: 한 건의 Gito webhook delivery가 target branch 재검증 후 정확히 하나의 creation sync job으로 수렴한다.
|
||||
- [x] [scan-enqueue] HTTP receiver에서 받은 on-target event가 target branch fetch/scan 이후 `EnqueueRoadmapCreationSync`로 이어진다. 검증: 한 건의 Gito webhook delivery가 target branch 재검증 후 정확히 하나의 creation sync job으로 수렴한다.
|
||||
- [x] [config-docs] `GITO_PROTO_SOCKET_URL` 중심의 소비자 설정을 webhook callback endpoint, signing secret, Gito subscription bootstrap 기준으로 교체한다. 검증: secret 원문 없이 local/dev webhook consumer 설정 경로가 문서화된다.
|
||||
- [x] [failure-ops] invalid signature, malformed payload, not-ready revision, duplicate delivery, Gito retry가 core server shutdown이나 중복 Plane mutation으로 이어지지 않는다. 검증: 장애 입력이 안전하게 reject/drop/retry 후보로 기록된다.
|
||||
|
||||
## 완료 리뷰
|
||||
|
||||
- 상태: 없음
|
||||
- 요청일: 없음
|
||||
- 완료 근거: 없음
|
||||
- 상태: 통과
|
||||
- 요청일: 2026-06-19
|
||||
- 완료 근거:
|
||||
- `scan-enqueue`가 `agent-task/archive/2026/06/m-gito-http-webhook-consumer-readiness/03+01,02_http_scan_enqueue/complete.log`의 Roadmap Completion과 Spec Completion(S04) PASS 근거로 완료되었다.
|
||||
- 모든 기능 Task가 체크되었고 SDD는 `[승인됨]`/잠금 해제, workspace lock은 `agent-ops/bin/roadmap-dependency-checker.sh` 결과 `true`로 확인했다.
|
||||
- 코드 레벨 종료 감사에서 Gito 제공 계약에 맞춰 `X-Gito-Delivery` 누락 delivery를 side effect 없이 `400`으로 거부하도록 보강했고, 관련 테스트를 갱신했다.
|
||||
- 최종 검증은 `cd services/core && go test -count=1 ./cmd/server ./internal/http ./internal/gitosync ./internal/config`, `cd services/core && go test -count=1 ./...`, `git diff --check` PASS로 확인했다.
|
||||
- 리뷰 필요:
|
||||
- [ ] 사용자가 완료 결과를 확인했다
|
||||
- [ ] archive 이동을 승인했다
|
||||
- 리뷰 코멘트: 없음
|
||||
- [x] 사용자가 완료 결과를 확인했다
|
||||
- [x] archive 이동을 승인했다
|
||||
- 리뷰 코멘트: 코드 레벨 종료 감사와 소규모 계약 보강 후 완료/archive 대상으로 확정했다.
|
||||
|
||||
## 범위 제외
|
||||
|
||||
|
|
@ -6,7 +6,7 @@
|
|||
|
||||
## 목표
|
||||
|
||||
로드맵 기반 agent-ops 운영 루프, Plane/Jira 같은 work item provider와 Milestone item의 양방향 동기화 도메인, 외부 agent 제어 표면을 Core action과 MCP tool 계층으로 분리한다. 현재는 Milestone Work Item Creation Sync, Plane Work Item Webhook Intake, Gito Branch Event Creation Sync Bridge, Gito Proto-Socket Consumer Wire Readiness를 완료/archive했고, Gito HTTP Webhook Consumer Readiness를 최우선 활성 Milestone으로 두어 Gito branch event 소비 표면을 signed HTTP webhook/callback으로 재정렬한다.
|
||||
로드맵 기반 agent-ops 운영 루프, Plane/Jira 같은 work item provider와 Milestone item의 양방향 동기화 도메인, 외부 agent 제어 표면을 Core action과 MCP tool 계층으로 분리한다. 현재는 Gito HTTP Webhook Consumer Readiness까지 완료/archive했고, Roadmap Driven Agent-Ops Automation을 활성 Milestone으로 두어 roadmap/action core와 provider sync domain의 상위 방향을 정리한다.
|
||||
|
||||
## Milestone 흐름
|
||||
|
||||
|
|
@ -34,13 +34,13 @@
|
|||
- 경로: `agent-roadmap/archive/phase/agent-ops-mcp-control-plane/milestones/gito-protosocket-consumer-wire-readiness.md`
|
||||
- 요약: NomadCode/Gito 연동 MVP의 Gito `/proto-socket` binary transport/decode, bridge e2e, failure/reconnect 운영 기준 검증을 완료했고, 사용자 승인 후 archive했다.
|
||||
|
||||
- [진행중] Gito HTTP Webhook Consumer Readiness
|
||||
- 경로: `agent-roadmap/phase/agent-ops-mcp-control-plane/milestones/gito-http-webhook-consumer-readiness.md`
|
||||
- 요약: 최우선 작업으로, Gito `gito.forgejo-branch-events.v1` 제공 계약의 signed HTTP webhook delivery를 NomadCode Core가 소비하고, 기존 branch revision 재검증/creation sync enqueue 의미 로직으로 수렴하도록 재정렬한다.
|
||||
- [완료] Gito HTTP Webhook Consumer Readiness
|
||||
- 경로: `agent-roadmap/archive/phase/agent-ops-mcp-control-plane/milestones/gito-http-webhook-consumer-readiness.md`
|
||||
- 요약: Gito `gito.forgejo-branch-events.v1` signed HTTP webhook delivery를 NomadCode Core가 소비하고 branch revision 재검증/creation sync enqueue로 수렴하는 구현을 코드 레벨 종료 감사와 소규모 계약 보강 후 archive했다.
|
||||
|
||||
- [계획] Roadmap Driven Agent-Ops Automation
|
||||
- [진행중] Roadmap Driven Agent-Ops Automation
|
||||
- 경로: `agent-roadmap/phase/agent-ops-mcp-control-plane/milestones/roadmap-driven-agent-ops-automation.md`
|
||||
- 요약: roadmap/action core와 Plane/Jira 기반 Milestone item 동기화의 상위 방향과 계약을 정리하는 설계 마일스톤이며, 실제 구현은 slice 마일스톤으로 진행한다.
|
||||
- 요약: roadmap/action core와 Plane/Jira 기반 Milestone item 동기화의 상위 방향과 계약을 정리하는 활성 설계 마일스톤이며, 실제 구현은 slice 마일스톤으로 진행한다.
|
||||
|
||||
- [계획] Milestone Execution Lifecycle Sync
|
||||
- 경로: `agent-roadmap/phase/agent-ops-mcp-control-plane/milestones/milestone-execution-lifecycle-sync.md`
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ NomadCode가 로드맵을 중심으로 사용자 입력, 실행 상태, 출력
|
|||
|
||||
## 상태
|
||||
|
||||
[계획]
|
||||
[진행중]
|
||||
|
||||
## 승격 조건
|
||||
|
||||
|
|
@ -20,6 +20,7 @@ NomadCode가 로드맵을 중심으로 사용자 입력, 실행 상태, 출력
|
|||
## 구현 잠금
|
||||
|
||||
- 상태: 해제
|
||||
- SDD: 불필요 - 상위 방향과 계약 정리용 문서 마일스톤이며, 기능 Task의 문서/계약 산출물 검증으로 닫는다.
|
||||
- 결정 필요: 없음
|
||||
|
||||
## 범위
|
||||
|
|
|
|||
|
|
@ -200,6 +200,7 @@ func TestGitoHTTPWebhookDeliveryEnqueuesCreationSyncOnce(t *testing.T) {
|
|||
sig := signBody(secret, body)
|
||||
req.Header.Set("X-Gito-Signature", sig)
|
||||
req.Header.Set("X-Gito-Event", "branch.updated")
|
||||
req.Header.Set("X-Gito-Delivery", "delivery-1")
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
|
|
@ -269,6 +270,7 @@ func TestGitoHTTPWebhookDeliveryRejectedOnInvalidSignature(t *testing.T) {
|
|||
// Wrong secret.
|
||||
req.Header.Set("X-Gito-Signature", signBody("wrong-secret", body))
|
||||
req.Header.Set("X-Gito-Event", "branch.updated")
|
||||
req.Header.Set("X-Gito-Delivery", "delivery-invalid-signature")
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
handler.ReceiveGitoWebhook(w, req)
|
||||
|
|
@ -297,6 +299,7 @@ func TestGitoHTTPWebhookDeliveryOffTargetIgnored(t *testing.T) {
|
|||
req := httptest.NewRequest(http.MethodPost, "/api/integrations/gito/webhook", bytes.NewReader(body))
|
||||
req.Header.Set("X-Gito-Signature", sig)
|
||||
req.Header.Set("X-Gito-Event", "branch.updated")
|
||||
req.Header.Set("X-Gito-Delivery", "delivery-off-target")
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
handler.ReceiveGitoWebhook(w, req)
|
||||
|
|
|
|||
|
|
@ -134,6 +134,11 @@ func (h *Handler) ReceiveGitoWebhook(w stdhttp.ResponseWriter, r *stdhttp.Reques
|
|||
writeError(w, stdhttp.StatusBadRequest, "unexpected gito event type")
|
||||
return
|
||||
}
|
||||
deliveryID := strings.TrimSpace(r.Header.Get("X-Gito-Delivery"))
|
||||
if deliveryID == "" {
|
||||
writeError(w, stdhttp.StatusBadRequest, "missing gito delivery id")
|
||||
return
|
||||
}
|
||||
|
||||
var p gitoWebhookPayload
|
||||
if err := json.Unmarshal(body, &p); err != nil {
|
||||
|
|
@ -163,16 +168,13 @@ func (h *Handler) ReceiveGitoWebhook(w stdhttp.ResponseWriter, r *stdhttp.Reques
|
|||
return
|
||||
}
|
||||
|
||||
// HTTP-layer idempotency: check delivery key first (when header present),
|
||||
// then revision key (when after is set). Mark both after successful handling.
|
||||
deliveryID := strings.TrimSpace(r.Header.Get("X-Gito-Delivery"))
|
||||
// HTTP-layer idempotency: check delivery key first, then revision key
|
||||
// when after is set. Mark both after successful handling.
|
||||
var deliveryKey, revisionKey string
|
||||
if deliveryID != "" {
|
||||
deliveryKey = "delivery:" + deliveryID
|
||||
if h.gitoDelivery.hasSeen(deliveryKey) {
|
||||
writeJSON(w, stdhttp.StatusAccepted, map[string]string{"status": "duplicate"})
|
||||
return
|
||||
}
|
||||
deliveryKey = "delivery:" + deliveryID
|
||||
if h.gitoDelivery.hasSeen(deliveryKey) {
|
||||
writeJSON(w, stdhttp.StatusAccepted, map[string]string{"status": "duplicate"})
|
||||
return
|
||||
}
|
||||
if ev.After != "" {
|
||||
revisionKey = "revision:" + ev.RepoID + ":" + ev.Branch + ":" + ev.After
|
||||
|
|
@ -193,9 +195,7 @@ func (h *Handler) ReceiveGitoWebhook(w stdhttp.ResponseWriter, r *stdhttp.Reques
|
|||
}
|
||||
}
|
||||
|
||||
if deliveryKey != "" {
|
||||
h.gitoDelivery.markSeen(deliveryKey)
|
||||
}
|
||||
h.gitoDelivery.markSeen(deliveryKey)
|
||||
if revisionKey != "" {
|
||||
h.gitoDelivery.markSeen(revisionKey)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -112,6 +112,7 @@ func TestGitoWebhookRouteBypassesBasicAuth(t *testing.T) {
|
|||
req := httptest.NewRequest(http.MethodPost, "/api/integrations/gito/webhook", strings.NewReader(body))
|
||||
req.Header.Set("X-Gito-Signature", testGitoSignature("secret", body))
|
||||
req.Header.Set("X-Gito-Event", "branch.updated")
|
||||
req.Header.Set("X-Gito-Delivery", "delivery-off-target")
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
router := NewRouter(h, nil, AuthConfig{Username: "nomadcode", Password: "required"}, nil, "")
|
||||
|
|
@ -144,25 +145,23 @@ func TestReceiveGitoWebhookDeduplicatesSameDeliveryID(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestReceiveGitoWebhookDeduplicatesSameRevisionWithoutDeliveryID(t *testing.T) {
|
||||
func TestReceiveGitoWebhookRejectsMissingDeliveryIDWithoutHandlerCall(t *testing.T) {
|
||||
fake := &fakeGitoBranchEventHandler{}
|
||||
h := newGitoHandlerForTest()
|
||||
h.SetGitoBranchEventHandler(fake)
|
||||
|
||||
for i := range 2 {
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/integrations/gito/webhook", strings.NewReader(gitoTargetBody))
|
||||
req.Header.Set("X-Gito-Signature", testGitoSignature("secret", gitoTargetBody))
|
||||
req.Header.Set("X-Gito-Event", "branch.updated")
|
||||
// No X-Gito-Delivery header
|
||||
rec := httptest.NewRecorder()
|
||||
h.ReceiveGitoWebhook(rec, req)
|
||||
if rec.Code != http.StatusAccepted {
|
||||
t.Fatalf("call %d: expected 202, got %d: %s", i+1, rec.Code, rec.Body.String())
|
||||
}
|
||||
}
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/integrations/gito/webhook", strings.NewReader(gitoTargetBody))
|
||||
req.Header.Set("X-Gito-Signature", testGitoSignature("secret", gitoTargetBody))
|
||||
req.Header.Set("X-Gito-Event", "branch.updated")
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
if fake.callCount != 1 {
|
||||
t.Fatalf("expected handler called once, got %d", fake.callCount)
|
||||
h.ReceiveGitoWebhook(rec, req)
|
||||
|
||||
if rec.Code != http.StatusBadRequest {
|
||||
t.Fatalf("expected 400, got %d: %s", rec.Code, rec.Body.String())
|
||||
}
|
||||
if fake.callCount != 0 {
|
||||
t.Fatalf("expected handler not called, got %d", fake.callCount)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -176,6 +175,7 @@ func TestReceiveGitoWebhookRejectsWrongBodyTypeWithoutHandlerCall(t *testing.T)
|
|||
req := httptest.NewRequest(http.MethodPost, "/api/integrations/gito/webhook", strings.NewReader(wrongTypeBody))
|
||||
req.Header.Set("X-Gito-Signature", testGitoSignature("secret", wrongTypeBody))
|
||||
req.Header.Set("X-Gito-Event", "branch.updated")
|
||||
req.Header.Set("X-Gito-Delivery", "delivery-wrong-type")
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
h.ReceiveGitoWebhook(rec, req)
|
||||
|
|
@ -198,6 +198,7 @@ func TestReceiveGitoWebhookRejectsMalformedChangedFilesWithoutHandlerCall(t *tes
|
|||
req := httptest.NewRequest(http.MethodPost, "/api/integrations/gito/webhook", strings.NewReader(malformedBody))
|
||||
req.Header.Set("X-Gito-Signature", testGitoSignature("secret", malformedBody))
|
||||
req.Header.Set("X-Gito-Event", "branch.updated")
|
||||
req.Header.Set("X-Gito-Delivery", "delivery-malformed-files")
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
h.ReceiveGitoWebhook(rec, req)
|
||||
|
|
@ -220,6 +221,7 @@ func TestReceiveGitoWebhookRejectsMalformedChangedFileItemWithoutHandlerCall(t *
|
|||
req := httptest.NewRequest(http.MethodPost, "/api/integrations/gito/webhook", strings.NewReader(malformedItemBody))
|
||||
req.Header.Set("X-Gito-Signature", testGitoSignature("secret", malformedItemBody))
|
||||
req.Header.Set("X-Gito-Event", "branch.updated")
|
||||
req.Header.Set("X-Gito-Delivery", "delivery-malformed-file-item")
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
h.ReceiveGitoWebhook(rec, req)
|
||||
|
|
|
|||
Loading…
Reference in a new issue