diff --git a/agent-roadmap/phase/agent-ops-mcp-control-plane/milestones/gito-http-webhook-consumer-readiness.md b/agent-roadmap/phase/agent-ops-mcp-control-plane/milestones/gito-http-webhook-consumer-readiness.md index a34f91e..f93ff9e 100644 --- a/agent-roadmap/phase/agent-ops-mcp-control-plane/milestones/gito-http-webhook-consumer-readiness.md +++ b/agent-roadmap/phase/agent-ops-mcp-control-plane/milestones/gito-http-webhook-consumer-readiness.md @@ -19,7 +19,7 @@ NomadCode Core의 Gito branch event consumer를 Gito 제공 계약 `gito.forgejo ## 구현 잠금 -- 상태: 잠금 +- 상태: 해제 - SDD: 필요 - SDD 문서: `agent-roadmap/sdd/agent-ops-mcp-control-plane/gito-http-webhook-consumer-readiness/SDD.md` - 잠금 해제 조건: @@ -27,7 +27,7 @@ NomadCode Core의 Gito branch event consumer를 Gito 제공 계약 `gito.forgejo - [x] SDD 사용자 리뷰가 없거나 승인/해결되었다 - [x] Acceptance Scenario가 Milestone 기능 Task와 연결되어 있다 - [x] Evidence Map이 plan의 `Spec Targets`와 완료 시 `Spec Completion`으로 검증 가능하게 연결되어 있다 - - [ ] Workspace lock `nomadcode:gito-http-webhook-consumer-readiness`의 모든 `rely-on.status`가 `enable`이다 + - [x] Workspace lock `nomadcode:gito-http-webhook-consumer-readiness`의 모든 `rely-on.status`가 `enable`이다 - 결정 필요: 없음 ## 범위 @@ -88,4 +88,4 @@ HTTP webhook wakeup을 기존 branch revision 재검증과 creation sync enqueue - 외부 의존 잠금: Gito `agent-roadmap/phase/provider-change-request-integration/milestones/webhook-revision-reconcile.md`의 consumer webhook subscription과 signed HTTP delivery 구현이 완료되어야 실제 연동 구현/검증을 닫을 수 있다. - 후속 작업: Milestone Execution Lifecycle Sync - 확인 필요: - - [ ] SDD 작성과 gate 확인 후 구현 잠금을 해제한다. + - [x] SDD 작성과 gate 확인 후 구현 잠금을 해제한다. diff --git a/agent-roadmap/sdd/agent-ops-mcp-control-plane/gito-http-webhook-consumer-readiness/SDD.md b/agent-roadmap/sdd/agent-ops-mcp-control-plane/gito-http-webhook-consumer-readiness/SDD.md index d3cdb97..5bbb6e9 100644 --- a/agent-roadmap/sdd/agent-ops-mcp-control-plane/gito-http-webhook-consumer-readiness/SDD.md +++ b/agent-roadmap/sdd/agent-ops-mcp-control-plane/gito-http-webhook-consumer-readiness/SDD.md @@ -93,7 +93,7 @@ - `gito:agent-roadmap/phase/provider-change-request-integration/milestones/webhook-revision-reconcile.md` - `.agent-roadmap-sync/locks.yaml` entry: `nomadcode:gito-http-webhook-consumer-readiness` -- 현재 lock 상태: `disable`. Gito의 consumer webhook subscription과 signed HTTP delivery 구현이 완료되어야 NomadCode Milestone의 실제 연동 구현/검증을 닫을 수 있다. +- 현재 lock 상태: `enable`. Gito의 consumer webhook subscription과 signed HTTP delivery 구현이 완료되어 NomadCode Milestone의 구현 잠금 해제 조건을 충족했다. ## Drift Check diff --git a/agent-task/archive/2026/06/m-gito-http-webhook-consumer-readiness/01_payload_boundary/code_review_local_G04_0.log b/agent-task/archive/2026/06/m-gito-http-webhook-consumer-readiness/01_payload_boundary/code_review_local_G04_0.log new file mode 100644 index 0000000..ef0a666 --- /dev/null +++ b/agent-task/archive/2026/06/m-gito-http-webhook-consumer-readiness/01_payload_boundary/code_review_local_G04_0.log @@ -0,0 +1,297 @@ + + +# Code Review Reference - API + +> **[IMPLEMENTING AGENT — READ FIRST] Filling in this file is the mandatory final step of implementation.** +> The task is NOT complete until every implementation-owned section below is filled in. +> Complete the `구현 체크리스트`; the final checklist item is mandatory before saving. +> Fill implementation-owned sections, then stop with active files in place and report ready for review. +> If implementation is blocked by a selected SDD decision or selected Milestone `구현 잠금 > 결정 필요` item, fill `사용자 리뷰 요청` with linked evidence and stop with active files in place; code-review decides whether to write `USER_REVIEW.md`. Environment/secret/service blockers, generic scope changes, repeated failures, and evidence gaps that a follow-up agent can close are normal follow-up issues, not user-review blockers by themselves. +> Do not ask the user directly, present choices in chat, or call `request_user_input` during implementation; record only SDD/Milestone lock decisions in `사용자 리뷰 요청` and stop for code-review. +> Finalization (`코드리뷰 결과`, log rename, `complete.log`, archive moves, `코드리뷰 전용 체크리스트`) is review-agent-only, even after compaction/resume. +> Follow the ownership table at the bottom of this file for which sections you own. + +## 개요 + +date=2026-06-19 +task=m-gito-http-webhook-consumer-readiness/01_payload_boundary, plan=0, tag=API + +## Roadmap Targets + +- Milestone: `agent-roadmap/phase/agent-ops-mcp-control-plane/milestones/gito-http-webhook-consumer-readiness.md` +- Task ids: + - `payload-boundary`: Gito base payload를 repo/branch/revision/change hint 중심으로 해석하고 NomadCode 전용 필드를 요구하지 않는다. +- Completion mode: check-on-pass + +## Spec Targets + +- SDD: `agent-roadmap/sdd/agent-ops-mcp-control-plane/gito-http-webhook-consumer-readiness/SDD.md` +- Acceptance scenarios: + - `S02`: task=`payload-boundary`; evidence=`payload decode/filter tests without NomadCode-specific fields` +- Completion mode: spec-check-on-pass + +## 이 파일을 읽는 리뷰 에이전트에게 + +> **[REVIEW AGENT ONLY]** 아래 종결 절차는 코드리뷰 에이전트 전용이다. 구현 에이전트는 이 섹션을 실행하지 않는다. + +각 항목의 구현을 실제 소스 파일과 대조하고, `검증 결과` 섹션의 출력이 코드와 일치하는지 확인하세요. +리뷰 완료는 아래 순서까지 끝난 상태를 의미합니다. + +1. 판정을 append한다. +2. `CODE_REVIEW-local-G04.md` → `code_review_local_G04_N.log`, `PLAN-local-G04.md` → `plan_local_G04_M.log`로 아카이브한다. +3. PASS이면 `complete.log` 작성 후 active task 디렉터리를 `agent-task/archive/YYYY/MM/m-gito-http-webhook-consumer-readiness/01_payload_boundary/`로 이동한다. WARN/FAIL이면 user-review gate를 확인한 뒤 다음 active plan/review 파일 또는 `USER_REVIEW.md`를 작성한다. `USER_REVIEW.md`가 연결된 SDD/Milestone 결정으로 완료/PASS 해소되면 code-review가 `USER_REVIEW.md`를 해소 상태로 갱신하고 `complete.log` 작성 후 archive 이동한다. +4. PASS이고 task group이 `m-`이면 완료 이벤트 메타데이터를 보고한다. roadmap 상태 체크와 `update-roadmap` 호출은 런타임 책임이다. +5. 적용 가능한 `코드리뷰 전용 체크리스트` 항목을 최종 `.log` 위치에서 체크한 뒤 보고한다. + +--- + +## 구현 항목별 완료 여부 + +| 항목 | 완료 여부 | +|------|---------| +| [API-1] 공통 branch.updated payload decoder | [x] | + +## 구현 체크리스트 + +- [x] `gitoevents`에 HTTP JSON body와 proto-socket envelope가 공유하는 `branch.updated` base payload decoder를 추가하고, `payload-boundary` 검증을 포함한다. +- [x] `cd services/core && go test -count=1 ./internal/gitoevents`를 실행한다. +- [x] `cd services/core && go test -count=1 ./...`를 실행한다. +- [x] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다. + +## 코드리뷰 전용 체크리스트 + +> **[REVIEW AGENT ONLY]** 이 체크리스트는 코드리뷰 에이전트만 사용한다. +> 구현 에이전트는 이 섹션을 수정하거나 체크하지 않는다. + +- [x] `코드리뷰 결과`에 `PASS`, `WARN`, `FAIL` 중 하나의 판정을 append한다. +- [x] 판정과 `차원별 평가`, Required/Suggested/Nit 분류가 서로 일치한다. +- [x] active `CODE_REVIEW-*-G??.md`를 `code_review_local_G04_N.log`로 아카이브한다. +- [x] active `PLAN-*-G??.md`를 `plan_local_G04_M.log`로 아카이브한다. +- [x] `.gitignore`의 Agent-Ops 관리 block이 `agent-task/**/*.md`와 `agent-task/**/*.log`를 unignore하고 `agent-roadmap/current.md`를 ignore하는지 확인한다. +- [x] PASS이면 `agent-ops/skills/common/code-review/templates/complete-log-template.md` 기준으로 `complete.log`를 작성하고 active `.md` 파일을 남기지 않는다. +- [x] PASS이면 active task 디렉터리 `agent-task/m-gito-http-webhook-consumer-readiness/01_payload_boundary/`를 `agent-task/archive/YYYY/MM/m-gito-http-webhook-consumer-readiness/01_payload_boundary/`로 이동하고 최종 archive 경로에서 이 체크리스트를 갱신한다. +- [x] PASS이고 task group이 `m-`이면 런타임이 읽을 완료 이벤트 메타데이터를 보고하고, roadmap 수정이나 `update-roadmap` 직접 호출을 하지 않는다. +- [x] PASS split 작업이면 이동 후 빈 active parent `agent-task/m-gito-http-webhook-consumer-readiness/`를 제거하거나, 남은 sibling/file이 있어 유지했다고 확인한다. +- [ ] WARN/FAIL이고 user-review gate가 트리거되지 않았으면 다음 active `PLAN-local-G04.md`와 `CODE_REVIEW-local-G04.md`를 작성하고 `complete.log`를 작성하지 않는다. +- [ ] USER_REVIEW이면 `agent-ops/skills/common/code-review/templates/user-review-template.md` 기준으로 `USER_REVIEW.md`를 작성하고 active `PLAN-*.md`, `CODE_REVIEW-*.md`, `complete.log`를 남기지 않는다. +- [ ] USER_REVIEW가 연결된 SDD/Milestone 결정으로 완료/PASS 해소되면 `USER_REVIEW.md`를 해소 상태로 갱신하고 `complete.log`를 작성한 뒤 task directory를 archive로 이동한다. + +## 계획 대비 변경 사항 + +계획에 명시된 모든 항목을 그대로 구현했다. 추가 변경사항 없음. + +## 주요 설계 결정 + +1. **`DecodeBranchUpdatedPayload`의 public export**: HTTP receiver가 envelope 없이 직접 호출해야 하므로 package-level exported function으로 정의했다. +2. **type 검증 유연성**: `type` 필드가 없으면 Accept, `branch.updated`이면 Accept, 다른 값이면 Reject하는 3자 경로로 설계했다. HTTP webhook이 type 필드를 생략할 수 있기 때문이다. +3. **`DecodeBranchUpdatedEnvelope`의 위임 구조**: 기존 envelope 검증(channel/action/null)을 유지하고 payload decode만 새 함수로 위임했다. 기존 코드 호환성을 최대화하면서 공통 재사용을 달성했다. + +## 사용자 리뷰 요청 + +_기본값은 `없음`이다. 구현 중 새 결정이 필요해 보여도 직접 질문하거나 선택지를 제시하거나 `request_user_input`을 호출하지 않는다. 이 섹션은 선택된 SDD 결정 또는 선택된 Milestone `구현 잠금 > 결정 필요` 항목이 실구현을 차단할 때만 채운다. 외부 환경/secret/서비스 준비, 검증 증거 공백, 반복 실패, 일반 범위 조정은 사용자 리뷰 요청이 아니며 `검증 결과`, `계획 대비 변경 사항`, 또는 code-review의 일반 follow-up plan으로 처리한다._ + +- 상태: 없음 +- 사유 유형: 없음 +- 연결 대상: 없음 +- 결정 필요: 없음 +- 차단 근거: 없음 +- 실행한 검증/명령: 없음 +- 자동 후속 불가 이유: 없음 +- 재개 조건: 없음 + +## 리뷰어를 위한 체크포인트 + +- `DecodeBranchUpdatedEnvelope`가 channel/action 검증을 유지하면서 common payload decoder에 위임하는지 확인한다. +- 새 payload decoder가 NomadCode 전용 필드를 요구하지 않는지 확인한다. +- malformed `changed_files`와 wrong `type`이 partial event 없이 reject되는지 확인한다. + +## 검증 결과 + +_구현 에이전트가 각 중간 검증 및 최종 검증 명령 실행 후 출력을 여기에 붙여 넣는다._ + +필수 규칙: +- 검증 명령은 고정된 계약이다. 임의로 대체하지 않는다. +- 대체가 필요하면 `계획 대비 변경 사항`에 이유와 대체 명령을 기록한다. +- `검증 결과`에는 실제 stdout/stderr를 붙여 넣는다. +- 사용자 리뷰 요청으로 명령을 끝까지 실행하지 못했다면 `사용자 리뷰 요청`에 실행한 명령, 실제 출력, 미실행 명령의 사유를 기록한다. +- mobile/UI hang, timeout, 또는 2분 무진행은 blind retry를 중단하고 focused rerun 명령과 screenshot/window/UI-tree evidence path를 남기며, 불가능하면 정확한 사유를 남긴다. + +### API-1 중간 검증 +``` +$ cd services/core && go test -count=1 ./internal/gitoevents +=== RUN TestClientSendsSubscribeEnvelope +--- PASS: TestClientSendsSubscribeEnvelope (0.00s) +=== RUN TestClientForwardsTargetEventToHandler +--- PASS: TestClientForwardsTargetEventToHandler (0.00s) +=== RUN TestClientDropsOffTargetAndMalformedEvents +--- PASS: TestClientDropsOffTargetAndMalformedEvents (0.00s) +=== RUN TestClientStopsOnContextCancel +--- PASS: TestClientStopsOnContextCancel (0.00s) +=== RUN TestNewClientRequiresRepoID +--- PASS: TestNewClientRequiresRepoID (0.00s) +=== RUN TestNewClientRequiresTransport +--- PASS: TestNewClientRequiresTransport (0.00s) +=== RUN TestBuildSubscribeEnvelopeMatchesContract +--- PASS: TestBuildSubscribeEnvelopeMatchesContract (0.00s) +=== RUN TestBuildSubscribeEnvelopeDefaultsBranch +--- PASS: TestBuildSubscribeEnvelopeDefaultsBranch (0.00s) +=== RUN TestDecodeBranchUpdatedEnvelope +--- PASS: TestDecodeBranchUpdatedEnvelope (0.00s) +=== RUN TestDecodeBranchUpdatedEnvelopeAcceptsConsumerNeutralBasePayload +--- PASS: TestDecodeBranchUpdatedEnvelopeAcceptsConsumerNeutralBasePayload (0.00s) +=== RUN TestDecodeBranchUpdatedEnvelopeIgnoresOptionalConsumerExtensions +--- PASS: TestDecodeBranchUpdatedEnvelopeIgnoresOptionalConsumerExtensions (0.00s) +=== RUN TestDecodeBranchUpdatedEnvelopeRejectsWrongAction +--- PASS: TestDecodeBranchUpdatedEnvelopeRejectsWrongAction (0.00s) +=== RUN TestDecodeBranchUpdatedEnvelopeRejectsWrongChannel +--- PASS: TestDecodeBranchUpdatedEnvelopeRejectsWrongChannel (0.00s) +=== RUN TestDecodeBranchUpdatedEnvelopeRejectsMalformedChangedFiles +--- PASS: TestDecodeBranchUpdatedEnvelopeRejectsMalformedChangedFiles (0.00s) +=== RUN TestDecodeBranchUpdatedEnvelopeRejectsMissingRepoID +--- PASS: TestDecodeBranchUpdatedEnvelopeRejectsMissingRepoID (0.00s) +=== RUN TestDecodeBranchUpdatedPayloadAcceptsBasePayloadWithoutNomadCodeFields +--- PASS: TestDecodeBranchUpdatedPayloadAcceptsBasePayloadWithoutNomadCodeFields (0.00s) +=== RUN TestDecodeBranchUpdatedPayloadRejectsWrongType +--- PASS: TestDecodeBranchUpdatedPayloadRejectsWrongType (0.00s) +=== RUN TestDecodeBranchUpdatedPayloadRejectsMalformedChangedFiles +--- PASS: TestDecodeBranchUpdatedPayloadRejectsMalformedChangedFiles (0.00s) +=== RUN TestDecodeBranchUpdatedPayloadRejectsMissingRepoID +--- PASS: TestDecodeBranchUpdatedPayloadRejectsMissingRepoID (0.00s) +=== RUN TestDecodeBranchUpdatedPayloadRejectsMissingBranch +--- PASS: TestDecodeBranchUpdatedPayloadRejectsMissingBranch (0.00s) +=== RUN TestDecodeBranchUpdatedPayloadEmptyChangedFiles +--- PASS: TestDecodeBranchUpdatedPayloadEmptyChangedFiles (0.00s) +=== RUN TestDecodeBranchUpdatedPayloadNilPayload +--- PASS: TestDecodeBranchUpdatedPayloadNilPayload (0.00s) +=== RUN TestDecodeBranchUpdatedPayloadAllowsNilChangedFiles +--- PASS: TestDecodeBranchUpdatedPayloadAllowsNilChangedFiles (0.00s) +=== RUN TestDecodeBranchUpdatedPayloadHandlesMalformedChangedFileItem +--- PASS: TestDecodeBranchUpdatedPayloadHandlesMalformedChangedFileItem (0.00s) +=== RUN TestDecodeBranchUpdatedPayloadWithoutTypeField +--- PASS: TestDecodeBranchUpdatedPayloadWithoutTypeField (0.00s) +=== RUN TestIsTarget +--- PASS: TestIsTarget (0.00s) +=== RUN TestMilestoneChangedFiles +--- PASS: TestMilestoneChangedFiles (0.00s) +PASS +ok github.com/nomadcode/nomadcode-core/internal/gitoevents 0.005s +``` + +### 최종 검증 +``` +$ cd services/core && go test -count=1 ./internal/gitoevents +=== RUN TestClientSendsSubscribeEnvelope +--- PASS: TestClientSendsSubscribeEnvelope (0.00s) +=== RUN TestClientForwardsTargetEventToHandler +--- PASS: TestClientForwardsTargetEventToHandler (0.00s) +=== RUN TestClientDropsOffTargetAndMalformedEvents +--- PASS: TestClientDropsOffTargetAndMalformedEvents (0.00s) +=== RUN TestClientStopsOnContextCancel +--- PASS: TestClientStopsOnContextCancel (0.00s) +=== RUN TestNewClientRequiresRepoID +--- PASS: TestNewClientRequiresRepoID (0.00s) +=== RUN TestNewClientRequiresTransport +--- PASS: TestNewClientRequiresTransport (0.00s) +=== RUN TestBuildSubscribeEnvelopeMatchesContract +--- PASS: TestBuildSubscribeEnvelopeMatchesContract (0.00s) +=== RUN TestBuildSubscribeEnvelopeDefaultsBranch +--- PASS: TestBuildSubscribeEnvelopeDefaultsBranch (0.00s) +=== RUN TestDecodeBranchUpdatedEnvelope +--- PASS: TestDecodeBranchUpdatedEnvelope (0.00s) +=== RUN TestDecodeBranchUpdatedEnvelopeAcceptsConsumerNeutralBasePayload +--- PASS: TestDecodeBranchUpdatedEnvelopeAcceptsConsumerNeutralBasePayload (0.00s) +=== RUN TestDecodeBranchUpdatedEnvelopeIgnoresOptionalConsumerExtensions +--- PASS: TestDecodeBranchUpdatedEnvelopeIgnoresOptionalConsumerExtensions (0.00s) +=== RUN TestDecodeBranchUpdatedEnvelopeRejectsWrongAction +--- PASS: TestDecodeBranchUpdatedEnvelopeRejectsWrongAction (0.00s) +=== RUN TestDecodeBranchUpdatedEnvelopeRejectsWrongChannel +--- PASS: TestDecodeBranchUpdatedEnvelopeRejectsWrongChannel (0.00s) +=== RUN TestDecodeBranchUpdatedEnvelopeRejectsMalformedChangedFiles +--- PASS: TestDecodeBranchUpdatedEnvelopeRejectsMalformedChangedFiles (0.00s) +=== RUN TestDecodeBranchUpdatedEnvelopeRejectsMissingRepoID +--- PASS: TestDecodeBranchUpdatedEnvelopeRejectsMissingRepoID (0.00s) +=== RUN TestDecodeBranchUpdatedPayloadAcceptsBasePayloadWithoutNomadCodeFields +--- PASS: TestDecodeBranchUpdatedPayloadAcceptsBasePayloadWithoutNomadCodeFields (0.00s) +=== RUN TestDecodeBranchUpdatedPayloadRejectsWrongType +--- PASS: TestDecodeBranchUpdatedPayloadRejectsWrongType (0.00s) +=== RUN TestDecodeBranchUpdatedPayloadRejectsMalformedChangedFiles +--- PASS: TestDecodeBranchUpdatedPayloadRejectsMalformedChangedFiles (0.00s) +=== RUN TestDecodeBranchUpdatedPayloadRejectsMissingRepoID +--- PASS: TestDecodeBranchUpdatedPayloadRejectsMissingRepoID (0.00s) +=== RUN TestDecodeBranchUpdatedPayloadRejectsMissingBranch +--- PASS: TestDecodeBranchUpdatedPayloadRejectsMissingBranch (0.00s) +=== RUN TestDecodeBranchUpdatedPayloadEmptyChangedFiles +--- PASS: TestDecodeBranchUpdatedPayloadEmptyChangedFiles (0.00s) +=== RUN TestDecodeBranchUpdatedPayloadNilPayload +--- PASS: TestDecodeBranchUpdatedPayloadNilPayload (0.00s) +=== RUN TestDecodeBranchUpdatedPayloadAllowsNilChangedFiles +--- PASS: TestDecodeBranchUpdatedPayloadAllowsNilChangedFiles (0.00s) +=== RUN TestDecodeBranchUpdatedPayloadHandlesMalformedChangedFileItem +--- PASS: TestDecodeBranchUpdatedPayloadHandlesMalformedChangedFileItem (0.00s) +=== RUN TestDecodeBranchUpdatedPayloadWithoutTypeField +--- PASS: TestDecodeBranchUpdatedPayloadWithoutTypeField (0.00s) +=== RUN TestIsTarget +--- PASS: TestIsTarget (0.00s) +=== RUN TestMilestoneChangedFiles +--- PASS: TestMilestoneChangedFiles (0.00s) +PASS +ok github.com/nomadcode/nomadcode-core/internal/gitoevents 0.005s + +$ cd services/core && go test -count=1 ./... +ok github.com/nomadcode/nomadcode-core/cmd/plane-smoke 0.007s +? github.com/nomadcode/nomadcode-core/cmd/server [no test files] +ok github.com/nomadcode/nomadcode-core/internal/adapters/a2a 0.011s +ok github.com/nomadcode/nomadcode-core/internal/adapters/jira 0.026s +ok github.com/nomadcode/nomadcode-core/internal/adapters/mattermost 0.012s +ok github.com/nomadcode/nomadcode-core/internal/adapters/openai 0.016s +ok github.com/nomadcode/nomadcode-core/internal/adapters/plane 0.016s +? github.com/nomadcode/nomadcode-core/internal/agent [no test files] +ok github.com/nomadcode/nomadcode-core/internal/authoring 0.012s +ok github.com/nomadcode/nomadcode-core/internal/config 0.006s +? github.com/nomadcode/nomadcode-core/internal/db [no test files] +ok github.com/nomadcode/nomadcode-core/internal/gitoevents 0.007s +ok github.com/nomadcode/nomadcode-core/internal/gitosync 1.381s +ok github.com/nomadcode/nomadcode-core/internal/http 0.010s +? github.com/nomadcode/nomadcode-core/internal/model [no test files] +ok github.com/nomadcode/nomadcode-core/internal/notification 0.006s +ok github.com/nomadcode/nomadcode-core/internal/projectsync 0.009s +ok github.com/nomadcode/nomadcode-core/internal/protosocket 0.014s +ok github.com/nomadcode/nomadcode-core/internal/roadmapsync 0.007s +ok github.com/nomadcode/nomadcode-core/internal/roadmapsyncpipeline 0.009s +ok github.com/nomadcode/nomadcode-core/internal/scheduler 2.014s +ok github.com/nomadcode/nomadcode-core/internal/storage 0.007s +ok github.com/nomadcode/nomadcode-core/internal/workflow 0.007s +ok github.com/nomadcode/nomadcode-core/internal/workitem 0.005s +ok github.com/nomadcode/nomadcode-core/internal/workitempipeline 0.004s +``` + +--- + +> **[IMPLEMENTING AGENT — BEFORE SAVING] Have you filled in every implementation-owned section: completion table, implementation checklist, changes from plan, design decisions, and verification output?** +> If anything is blank, go back and fill it in before saving this file. +> Leave review-agent-only sections unchanged. + +## 코드리뷰 결과 + +- 종합 판정: PASS +- 차원별 평가: + - correctness: Pass + - completeness: Pass + - test coverage: Pass + - API contract: Pass + - code quality: Pass + - plan deviation: Pass + - verification trust: Pass + - spec conformance: Pass +- 발견된 문제: 없음 +- 다음 단계: PASS이므로 `complete.log`를 작성하고 active task directory를 archive로 이동한다. + +### 리뷰 근거 + +- `DecodeBranchUpdatedEnvelope`는 기존 channel/action/null payload 검증을 유지하고 payload decode만 `DecodeBranchUpdatedPayload`에 위임한다. +- `DecodeBranchUpdatedPayload`는 Gito base payload의 `repo_id`, `branch`, revision/change hint를 decode하고 NomadCode 전용 필드를 요구하지 않는다. +- wrong `type`, malformed `changed_files`, missing `repo_id`/`branch`, nil payload 회귀 테스트가 포함되어 있다. +- 리뷰 중 `gofmt`만 직접 적용했다. +- 검증: + - `cd services/core && go test -count=1 ./internal/gitoevents`: PASS + - `cd services/core && go test -count=1 ./...`: PASS diff --git a/agent-task/archive/2026/06/m-gito-http-webhook-consumer-readiness/01_payload_boundary/complete.log b/agent-task/archive/2026/06/m-gito-http-webhook-consumer-readiness/01_payload_boundary/complete.log new file mode 100644 index 0000000..b56f39e --- /dev/null +++ b/agent-task/archive/2026/06/m-gito-http-webhook-consumer-readiness/01_payload_boundary/complete.log @@ -0,0 +1,49 @@ +# Complete - m-gito-http-webhook-consumer-readiness/01_payload_boundary + +## 완료 일시 + +2026-06-19 + +## 요약 + +Gito `branch.updated` base payload decoder를 공통 경계로 분리하고 payload-boundary 검증을 완료했다. loop count=1, final verdict=PASS. + +## 루프 이력 + +| Plan | Review | Verdict | 메모 | +|------|--------|---------|------| +| `plan_local_G04_0.log` | `code_review_local_G04_0.log` | PASS | 계획된 decoder API, tests, 검증이 충족됨 | + +## 구현/정리 내용 + +- `DecodeBranchUpdatedPayload`를 추가해 HTTP JSON body와 proto-socket envelope가 같은 base payload decode 경계를 공유하게 했다. +- `DecodeBranchUpdatedEnvelope`는 기존 channel/action/null payload 검증을 유지하고 payload decode를 새 함수에 위임한다. +- NomadCode 전용 필드 없이 Gito base payload를 decode하는 테스트와 wrong type, malformed `changed_files`, missing required field 회귀 테스트를 추가했다. +- 리뷰 중 `services/core/internal/gitoevents/events.go`와 `services/core/internal/gitoevents/events_test.go`에 `gofmt`를 적용했다. + +## 최종 검증 + +- `cd services/core && go test -count=1 ./internal/gitoevents` - PASS; `ok github.com/nomadcode/nomadcode-core/internal/gitoevents 0.004s` +- `cd services/core && go test -count=1 ./...` - PASS; core 전체 Go packages 통과 + +## Roadmap Completion + +- Milestone: `agent-roadmap/phase/agent-ops-mcp-control-plane/milestones/gito-http-webhook-consumer-readiness.md` +- Completed task ids: + - `payload-boundary`: PASS; evidence=`plan_local_G04_0.log`, `code_review_local_G04_0.log`; verification=`cd services/core && go test -count=1 ./internal/gitoevents`, `cd services/core && go test -count=1 ./...` +- Not completed task ids: 없음 + +## Spec Completion + +- SDD: `agent-roadmap/sdd/agent-ops-mcp-control-plane/gito-http-webhook-consumer-readiness/SDD.md` +- Completed scenario ids: + - `S02`: PASS; task=`payload-boundary`; evidence=`plan_local_G04_0.log`, `code_review_local_G04_0.log`; verification=`cd services/core && go test -count=1 ./internal/gitoevents`, `cd services/core && go test -count=1 ./...` +- Not completed scenario ids: 없음 + +## 잔여 Nit + +- 없음 + +## 후속 작업 + +- 없음 diff --git a/agent-task/archive/2026/06/m-gito-http-webhook-consumer-readiness/01_payload_boundary/plan_local_G04_0.log b/agent-task/archive/2026/06/m-gito-http-webhook-consumer-readiness/01_payload_boundary/plan_local_G04_0.log new file mode 100644 index 0000000..cbd8b6a --- /dev/null +++ b/agent-task/archive/2026/06/m-gito-http-webhook-consumer-readiness/01_payload_boundary/plan_local_G04_0.log @@ -0,0 +1,224 @@ + + +# PLAN local-G04 - API + +## 이 파일을 읽는 구현 에이전트에게 + +이 작업은 구현 후 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 실제 변경 내용과 검증 출력으로 채워야 완료된다. 구현 에이전트는 검증을 실행하고 active 파일을 유지한 채 리뷰 준비를 보고한다. 종결, 로그 rename, `complete.log`, task directory archive는 code-review 스킬 전용이다. 선택된 SDD 결정이나 Milestone `구현 잠금 > 결정 필요` 항목이 실구현을 막을 때만 review stub의 `사용자 리뷰 요청` 섹션을 채우고 멈춘다. 구현 중 사용자에게 직접 묻거나 선택지를 제시하거나 `request_user_input`을 호출하지 않는다. 외부 환경, secret, 서비스 준비, 검증 증거 공백은 사용자 리뷰 요청이 아니라 검증 결과나 후속 plan으로 처리한다. + +## 배경 + +현 Milestone은 Gito `branch.updated`를 HTTP webhook JSON body로 받아야 한다. 현재 `gitoevents` decoder는 proto-socket envelope에 묶여 있어 HTTP receiver가 같은 base payload 계약을 재사용하기 어렵다. 이 선행 작업은 NomadCode 전용 필드 없이 Gito base payload를 decode하는 공통 경계를 만든다. + +## 사용자 리뷰 요청 흐름 + +사용자 리뷰 요청은 선택된 SDD 결정 또는 선택된 Milestone lock 결정만 active review stub의 `사용자 리뷰 요청` 섹션에 기록한다. 이 섹션은 `agent-ops/skills/common/_templates/implementation-user-review-request-section.md` 형식을 따른다. 구현 중 직접 사용자에게 질문하지 않으며, code-review가 정당성을 검증하고 실제 `USER_REVIEW.md` 작성 여부를 결정한다. + +## Roadmap Targets + +- Milestone: `agent-roadmap/phase/agent-ops-mcp-control-plane/milestones/gito-http-webhook-consumer-readiness.md` +- Task ids: + - `payload-boundary`: Gito base payload를 repo/branch/revision/change hint 중심으로 해석하고 NomadCode 전용 필드를 요구하지 않는다. +- Completion mode: check-on-pass + +## Spec Targets + +- SDD: `agent-roadmap/sdd/agent-ops-mcp-control-plane/gito-http-webhook-consumer-readiness/SDD.md` +- Acceptance scenarios: + - `S02`: task=`payload-boundary`; evidence=`payload decode/filter tests without NomadCode-specific fields` +- Completion mode: spec-check-on-pass + +## 분석 결과 + +### 읽은 파일 + +- `AGENTS.md` +- `agent-ops/rules/project/rules.md` +- `agent-ops/rules/private/rules.md` +- `agent-ops/rules/common/rules-roadmap.md` +- `agent-ops/rules/project/domain/core/rules.md` +- `agent-ops/rules/project/domain/workspace-ops/rules.md` +- `agent-ops/skills/common/router.md` +- `agent-ops/skills/common/plan/SKILL.md` +- `agent-ops/skills/common/roadmap-sdd/SKILL.md` +- `agent-ops/skills/common/update-roadmap/SKILL.md` +- `agent-ops/skills/common/check-roadmap-dependency/SKILL.md` +- `agent-ops/skills/common/_templates/implementation-user-review-request-section.md` +- `agent-test/local/rules.md` +- `agent-test/local/core-smoke.md` +- `agent-test/local/workspace-ops-smoke.md` +- `agent-roadmap/current.md` +- `agent-roadmap/phase/agent-ops-mcp-control-plane/PHASE.md` +- `agent-roadmap/phase/agent-ops-mcp-control-plane/milestones/gito-http-webhook-consumer-readiness.md` +- `agent-roadmap/sdd/agent-ops-mcp-control-plane/gito-http-webhook-consumer-readiness/SDD.md` +- `../gito/agent-contract/provided/gito-forgejo-branch-events-v1.md` +- `services/core/go.mod` +- `services/core/internal/gitoevents/events.go` +- `services/core/internal/gitoevents/events_test.go` +- `services/core/internal/gitoevents/client.go` +- `services/core/internal/gitoevents/client_test.go` +- `services/core/internal/gitosync/bridge.go` +- `services/core/internal/gitosync/bridge_test.go` +- `services/core/internal/gitosync/scanner.go` +- `services/core/internal/http/router.go` +- `services/core/internal/http/handlers.go` +- `services/core/internal/http/handlers_test.go` +- `services/core/internal/http/plane_webhook.go` +- `services/core/internal/http/plane_webhook_test.go` +- `services/core/internal/config/config.go` +- `services/core/internal/config/config_test.go` +- `services/core/cmd/server/main.go` + +### 테스트 환경 규칙 + +- 선택한 test_env: `local`. +- `agent-test/local/rules.md`를 읽었다. +- matching profile: `agent-test/local/core-smoke.md`를 읽었다. +- 적용 명령: core 변경은 최소 `cd services/core && go test ./...`; focused unit은 `cd services/core && go test -count=1 ./internal/gitoevents`. +- `core-smoke.md`의 기준 출력 예시는 `<확인 필요>`지만 필수 검증 명령은 구체적이다. +- dependency manifest는 `services/core/go.mod`를 확인했다. 새 package 추가는 필요 없다. + +### 테스트 커버리지 공백 + +- 기존 `TestDecodeBranchUpdatedEnvelopeAcceptsConsumerNeutralBasePayload`는 proto-socket envelope 내부 payload만 검증한다. +- HTTP JSON body가 같은 base payload decoder를 직접 재사용하는지는 아직 테스트가 없다. 이 작업에서 `DecodeBranchUpdatedPayload` 테스트를 추가한다. +- changed_files malformed, missing repo_id/branch 테스트는 기존 envelope path에 있다. 새 payload decoder에도 최소 base payload, wrong type/action, malformed changed_files 회귀를 추가한다. + +### 심볼 참조 + +- renamed/removed symbol: 없음. +- 새 symbol 후보: `DecodeBranchUpdatedPayload`. + +### 분할 판단 + +- split decision policy를 먼저 평가했다. +- shared task group: `m-gito-http-webhook-consumer-readiness`. +- sibling subtasks: + - `01_payload_boundary`: 독립 선행 작업. payload decoder foundation과 tests. + - `02+01_http_receiver_idempotency`: `01_payload_boundary`의 `complete.log` 필요. HTTP receiver가 공통 decoder를 소비한다. +- API/foundation과 HTTP route/wiring/idempotency의 위험과 검증 전략이 달라 split한다. +- 이 subtask는 predecessor가 없는 독립 작업이다. + +### 범위 결정 근거 + +- 포함: `services/core/internal/gitoevents/events.go`, `services/core/internal/gitoevents/events_test.go`. +- 제외: `services/core/internal/http/**`, `services/core/cmd/server/main.go`, `services/core/internal/config/**`, docs. HTTP route, signing, idempotency, config wiring은 `02+01_http_receiver_idempotency`가 담당한다. +- 제외: Gito 계약 원문 변경. SDD가 외부 계약 source of truth로 링크한다. + +### 빌드 등급 + +- `local-G04`: 내부 decoder API와 focused tests에 한정된 bounded Go 변경이며, 검증은 deterministic unit + core test로 가능하다. + +## 구현 체크리스트 + +- [x] `gitoevents`에 HTTP JSON body와 proto-socket envelope가 공유하는 `branch.updated` base payload decoder를 추가하고, `payload-boundary` 검증을 포함한다. +- [x] `cd services/core && go test -count=1 ./internal/gitoevents`를 실행한다. +- [x] `cd services/core && go test -count=1 ./...`를 실행한다. +- [x] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다. + +### [API-1] 공통 branch.updated payload decoder + +#### 문제 + +`services/core/internal/gitoevents/events.go:76`의 `DecodeBranchUpdatedEnvelope`가 envelope channel/action 검증과 payload decode를 한 함수에 묶고 있다. HTTP receiver는 Gito contract의 JSON body를 envelope 없이 받으므로 같은 base payload를 재사용할 공통 decoder가 없다. + +Before: + +```go +// services/core/internal/gitoevents/events.go:76 +// DecodeBranchUpdatedEnvelope validates the envelope channel/action and decodes +// its payload into a BranchUpdatedEvent. It rejects wrong channel/action and +// malformed payloads with a descriptive error instead of returning a partial +// event. +func DecodeBranchUpdatedEnvelope(env protosocket.Envelope) (BranchUpdatedEvent, error) { + var ev BranchUpdatedEvent + + if env.Channel != EventChannel { + return ev, fmt.Errorf("gitoevents: unexpected channel %q, want %q", env.Channel, EventChannel) + } + if env.Action != BranchUpdatedAction { + return ev, fmt.Errorf("gitoevents: unexpected action %q, want %q", env.Action, BranchUpdatedAction) + } + if env.Payload == nil { + return ev, fmt.Errorf("gitoevents: missing payload") + } + + ev.ID = stringField(env.Payload, "id") + ev.Type = stringField(env.Payload, "type") + ev.Provider = stringField(env.Payload, "provider") + ev.DeliveryID = stringField(env.Payload, "delivery_id") + ev.RepoID = stringField(env.Payload, "repo_id") + ev.Branch = stringField(env.Payload, "branch") + ev.Before = stringField(env.Payload, "before") + ev.After = stringField(env.Payload, "after") + ev.ObservedAt = stringField(env.Payload, "observed_at") + ev.CreatedAt = stringField(env.Payload, "created_at") +``` + +#### 해결 방법 + +- `DecodeBranchUpdatedPayload(payload map[string]any) (BranchUpdatedEvent, error)`를 추가한다. +- `DecodeBranchUpdatedEnvelope`는 channel/action/null 검증 후 `DecodeBranchUpdatedPayload(env.Payload)`만 호출한다. +- payload decoder는 `repo_id`와 `branch`를 필수로 유지하고, `type`이 비어 있지 않으면 `branch.updated`여야 한다. +- `provider`, `delivery_id`, timestamp, optional extension fields는 필수로 만들지 않는다. + +After outline: + +```go +func DecodeBranchUpdatedEnvelope(env protosocket.Envelope) (BranchUpdatedEvent, error) { + if env.Channel != EventChannel { + return BranchUpdatedEvent{}, fmt.Errorf("gitoevents: unexpected channel %q, want %q", env.Channel, EventChannel) + } + if env.Action != BranchUpdatedAction { + return BranchUpdatedEvent{}, fmt.Errorf("gitoevents: unexpected action %q, want %q", env.Action, BranchUpdatedAction) + } + if env.Payload == nil { + return BranchUpdatedEvent{}, fmt.Errorf("gitoevents: missing payload") + } + return DecodeBranchUpdatedPayload(env.Payload) +} + +func DecodeBranchUpdatedPayload(payload map[string]any) (BranchUpdatedEvent, error) { + // Fill BranchUpdatedEvent from base payload fields. + // Reject missing repo_id/branch and non-branch.updated type. +} +``` + +#### 수정 파일 및 체크리스트 + +- [x] `services/core/internal/gitoevents/events.go`: common payload decoder 추가, envelope decoder 위임. +- [x] `services/core/internal/gitoevents/events_test.go`: base payload without NomadCode fields, wrong type, malformed changed_files 테스트 추가. + +#### 테스트 작성 + +- 작성한다. +- `services/core/internal/gitoevents/events_test.go` + - `TestDecodeBranchUpdatedPayloadAcceptsBasePayloadWithoutNomadCodeFields`: Gito base JSON shape map에서 repo/branch/after/changed_files decode. + - `TestDecodeBranchUpdatedPayloadRejectsWrongType`: `type != branch.updated` reject. + - `TestDecodeBranchUpdatedPayloadRejectsMalformedChangedFiles`: changed_files malformed reject. + +#### 중간 검증 + +```bash +cd services/core && go test -count=1 ./internal/gitoevents +``` + +예상: PASS. Go test cache output은 허용하지 않는다. + +## 수정 파일 요약 + +| 파일 | 항목 | +|------|------| +| `services/core/internal/gitoevents/events.go` | API-1 | +| `services/core/internal/gitoevents/events_test.go` | API-1 | + +## 최종 검증 + +```bash +cd services/core && go test -count=1 ./internal/gitoevents +cd services/core && go test -count=1 ./... +``` + +예상: 두 명령 모두 PASS. Go test cache output은 허용하지 않는다. + +모든 코드 변경 완료 후 반드시 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 채운다. 이 파일 작성이 구현의 마지막 단계다. diff --git a/agent-task/archive/2026/06/m-gito-http-webhook-consumer-readiness/02+01_http_receiver_idempotency/code_review_local_G06_0.log b/agent-task/archive/2026/06/m-gito-http-webhook-consumer-readiness/02+01_http_receiver_idempotency/code_review_local_G06_0.log new file mode 100644 index 0000000..455c68d --- /dev/null +++ b/agent-task/archive/2026/06/m-gito-http-webhook-consumer-readiness/02+01_http_receiver_idempotency/code_review_local_G06_0.log @@ -0,0 +1,266 @@ + + +# Code Review Reference - API + +> **[IMPLEMENTING AGENT — READ FIRST] Filling in this file is the mandatory final step of implementation.** +> The task is NOT complete until every implementation-owned section below is filled in. +> Complete the `구현 체크리스트`; the final checklist item is mandatory before saving. +> Fill implementation-owned sections, then stop with active files in place and report ready for review. +> If implementation is blocked by a selected SDD decision or selected Milestone `구현 잠금 > 결정 필요` item, fill `사용자 리뷰 요청` with linked evidence and stop with active files in place; code-review decides whether to write `USER_REVIEW.md`. Environment/secret/service blockers, generic scope changes, repeated failures, and evidence gaps that a follow-up agent can close are normal follow-up issues, not user-review blockers by themselves. +> Do not ask the user directly, present choices in chat, or call `request_user_input` during implementation; record only SDD/Milestone lock decisions in `사용자 리뷰 요청` and stop for code-review. +> Finalization (`코드리뷰 결과`, log rename, `complete.log`, archive moves, `코드리뷰 전용 체크리스트`) is review-agent-only, even after compaction/resume. +> Follow the ownership table at the bottom of this file for which sections you own. + +## 개요 + +date=2026-06-19 +task=m-gito-http-webhook-consumer-readiness/02+01_http_receiver_idempotency, plan=0, tag=API + +## Roadmap Targets + +- Milestone: `agent-roadmap/phase/agent-ops-mcp-control-plane/milestones/gito-http-webhook-consumer-readiness.md` +- Task ids: + - `receiver`: Gito `branch.updated` HTTP callback endpoint를 추가한다. + - `idempotency`: `X-Gito-Delivery`와 revision key 기준으로 duplicate delivery를 중복 enqueue 없이 처리한다. +- Completion mode: check-on-pass + +## Spec Targets + +- SDD: `agent-roadmap/sdd/agent-ops-mcp-control-plane/gito-http-webhook-consumer-readiness/SDD.md` +- Acceptance scenarios: + - `S01`: task=`receiver`; evidence=`HTTP handler unit/integration test, invalid signature test` + - `S03`: task=`idempotency`; evidence=`duplicate delivery/revision idempotency test` +- Completion mode: spec-check-on-pass + +## 이 파일을 읽는 리뷰 에이전트에게 + +> **[REVIEW AGENT ONLY]** 아래 종결 절차는 코드리뷰 에이전트 전용이다. 구현 에이전트는 이 섹션을 실행하지 않는다. + +각 항목의 구현을 실제 소스 파일과 대조하고, `검증 결과` 섹션의 출력이 코드와 일치하는지 확인하세요. +리뷰 완료는 아래 순서까지 끝난 상태를 의미합니다. + +1. 판정을 append한다. +2. `CODE_REVIEW-local-G06.md` → `code_review_local_G06_N.log`, `PLAN-local-G06.md` → `plan_local_G06_M.log`로 아카이브한다. +3. PASS이면 `complete.log` 작성 후 active task 디렉터리를 `agent-task/archive/YYYY/MM/m-gito-http-webhook-consumer-readiness/02+01_http_receiver_idempotency/`로 이동한다. WARN/FAIL이면 user-review gate를 확인한 뒤 다음 active plan/review 파일 또는 `USER_REVIEW.md`를 작성한다. `USER_REVIEW.md`가 연결된 SDD/Milestone 결정으로 완료/PASS 해소되면 code-review가 `USER_REVIEW.md`를 해소 상태로 갱신하고 `complete.log` 작성 후 archive 이동한다. +4. PASS이고 task group이 `m-`이면 완료 이벤트 메타데이터를 보고한다. roadmap 상태 체크와 `update-roadmap` 호출은 런타임 책임이다. +5. 적용 가능한 `코드리뷰 전용 체크리스트` 항목을 최종 `.log` 위치에서 체크한 뒤 보고한다. + +--- + +## 구현 항목별 완료 여부 + +| 항목 | 완료 여부 | +|------|---------| +| [API-1] Gito HTTP webhook receiver | [x] | +| [API-2] Delivery/revision duplicate guard | [x] | +| [API-3] Config/server wiring | [x] | + +## 구현 체크리스트 + +- [x] `01_payload_boundary` predecessor `complete.log`가 있는지 확인하고, 없으면 구현을 시작하지 않는다. +- [x] Gito HTTP webhook receiver와 route를 추가하고, signed `branch.updated` delivery accepted 및 wrong event/invalid signature side-effect-free reject를 검증한다. +- [x] `X-Gito-Delivery`와 revision key 기준 process-lifetime duplicate guard를 추가하고, duplicate delivery/revision이 handler를 한 번만 호출함을 검증한다. +- [x] config/server wiring을 추가해 Gito webhook secret/repo/branch/develop checkout/todo state가 준비된 경우 HTTP receiver가 `gitosync.Bridge`를 호출하게 한다. +- [x] `cd services/core && go test -count=1 ./internal/http ./internal/config ./cmd/server`를 실행한다. +- [x] `cd services/core && go test -count=1 ./...`를 실행한다. +- [x] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다. + +## 코드리뷰 전용 체크리스트 + +> **[REVIEW AGENT ONLY]** 이 체크리스트는 코드리뷰 에이전트만 사용한다. +> 구현 에이전트는 이 섹션을 수정하거나 체크하지 않는다. + +- [x] `코드리뷰 결과`에 `PASS`, `WARN`, `FAIL` 중 하나의 판정을 append한다. +- [x] 판정과 `차원별 평가`, Required/Suggested/Nit 분류가 서로 일치한다. +- [x] active `CODE_REVIEW-*-G??.md`를 `code_review_local_G06_N.log`로 아카이브한다. +- [x] active `PLAN-*-G??.md`를 `plan_local_G06_M.log`로 아카이브한다. +- [x] `.gitignore`의 Agent-Ops 관리 block이 `agent-task/**/*.md`와 `agent-task/**/*.log`를 unignore하고 `agent-roadmap/current.md`를 ignore하는지 확인한다. +- [ ] PASS이면 `agent-ops/skills/common/code-review/templates/complete-log-template.md` 기준으로 `complete.log`를 작성하고 active `.md` 파일을 남기지 않는다. +- [ ] PASS이면 active task 디렉터리 `agent-task/m-gito-http-webhook-consumer-readiness/02+01_http_receiver_idempotency/`를 `agent-task/archive/YYYY/MM/m-gito-http-webhook-consumer-readiness/02+01_http_receiver_idempotency/`로 이동하고 최종 archive 경로에서 이 체크리스트를 갱신한다. +- [ ] PASS이고 task group이 `m-`이면 런타임이 읽을 완료 이벤트 메타데이터를 보고하고, roadmap 수정이나 `update-roadmap` 직접 호출을 하지 않는다. +- [ ] PASS split 작업이면 이동 후 빈 active parent `agent-task/m-gito-http-webhook-consumer-readiness/`를 제거하거나, 남은 sibling/file이 있어 유지했다고 확인한다. +- [x] WARN/FAIL이고 user-review gate가 트리거되지 않았으면 다음 active `PLAN-local-G06.md`와 `CODE_REVIEW-local-G06.md`를 작성하고 `complete.log`를 작성하지 않는다. +- [ ] USER_REVIEW이면 `agent-ops/skills/common/code-review/templates/user-review-template.md` 기준으로 `USER_REVIEW.md`를 작성하고 active `PLAN-*.md`, `CODE_REVIEW-*.md`, `complete.log`를 남기지 않는다. +- [ ] USER_REVIEW가 연결된 SDD/Milestone 결정으로 완료/PASS 해소되면 `USER_REVIEW.md`를 해소 상태로 갱신하고 `complete.log`를 작성한 뒤 task directory를 archive로 이동한다. + +## 계획 대비 변경 사항 + +**[API-1] `gitoevents.DecodeBranchUpdatedPayload` 대신 로컬 JSON 디코딩 사용** + +- 계획: `body는 gitoevents.DecodeBranchUpdatedPayload로 decode한다` +- 변경: `gito_webhook.go`에서 `gitoevents` 패키지를 import하지 않고 로컬 `gitoWebhookPayload` struct로 JSON 직접 decode. +- 사유: `internal/http` → `internal/gitoevents` → `internal/protosocket` ← `internal/protosocket/tasks_test.go` → `internal/http` import cycle 발생. `tasks_test.go`가 `package protosocket`(in-package test)로 선언되어 있어 `http` 패키지 import 시 cycle이 생긴다. `gitoevents.DecodeBranchUpdatedPayload`가 검증하는 필드(repo_id, branch 필수)는 로컬 디코더에서 동일하게 검증한다. + +**[API-1] `GitoBranchEventHandler` 인터페이스가 `gitoevents.BranchUpdatedEvent` 대신 로컬 `GitoBranchUpdatedEvent` 사용** + +- 계획: 인터페이스 파라미터 타입을 명시하지 않았으나 `gitoevents.BranchUpdatedEvent` 기반으로 설계됨. +- 변경: `http` 패키지 내 `GitoBranchUpdatedEvent` struct 정의(RepoID, Branch, Before, After). `gitosync.Bridge`는 `main.go`의 `gitoBridgeHTTPAdapter`를 통해 `http.GitoBranchUpdatedEvent` → `gitoevents.BranchUpdatedEvent` 변환 후 위임. +- 사유: 위 import cycle 방지. + +## 주요 설계 결정 + +**1. import cycle 해결을 위한 adapter 패턴** + +`internal/http` 패키지가 `internal/gitoevents`를 import하면 `internal/protosocket/tasks_test.go`와의 순환이 발생한다. `http` 패키지에 로컬 이벤트 타입(`GitoBranchUpdatedEvent`)과 인터페이스(`GitoBranchEventHandler`)를 정의하고, `cmd/server/main.go`에 `gitoBridgeHTTPAdapter`(wrapper)를 두어 `gitosync.Bridge`를 `GitoBranchEventHandler`로 연결했다. + +**2. 공유 Bridge 패턴** + +proto-socket runner와 HTTP webhook consumer가 하나의 `gitosync.Bridge` 인스턴스를 공유한다. Bridge 내부의 in-memory revision guard는 공유되므로 두 consumer가 동시 운영될 때 bridge 레벨 중복 처리를 자동으로 방지한다. + +**3. HTTP 레이어 idempotency store의 위치** + +`gitoDeliveryStore`를 `Handler` 필드로 두어 handler 라이프사이클과 동일하게 관리한다. `NewHandler`에서 초기화하고, 테스트용 `newHandlerForTest`에도 초기화를 추가했다. + +**4. duplicate 응답 shape** + +`{"status":"duplicate"}` 202 Accepted. 계획이 `duplicate` 또는 `accepted+duplicate:true` 두 형태를 허용했으며, `duplicate` 단일 status 형태를 선택했다. 내부 contract로만 고정한다는 계획에 따라 별도 문서화 없음. + +**5. `GitoWebhookConsumerEnabled()` required fields** + +secret, repo id, develop repo path, roadmap todo state id. branch는 default "develop"이므로 required에서 제외. 기존 `GitoBranchEventsEnabled()`와 동일한 필드 집합(secret만 proto socket URL 대신 webhook secret으로 교체). + +## 사용자 리뷰 요청 + +_기본값은 `없음`이다. 구현 중 새 결정이 필요해 보여도 직접 질문하거나 선택지를 제시하거나 `request_user_input`을 호출하지 않는다. 이 섹션은 선택된 SDD 결정 또는 선택된 Milestone `구현 잠금 > 결정 필요` 항목이 실구현을 차단할 때만 채운다. 외부 환경/secret/서비스 준비, 검증 증거 공백, 반복 실패, 일반 범위 조정은 사용자 리뷰 요청이 아니며 `검증 결과`, `계획 대비 변경 사항`, 또는 code-review의 일반 follow-up plan으로 처리한다._ + +- 상태: 없음 +- 사유 유형: 없음 +- 연결 대상: 없음 +- 결정 필요: 없음 +- 차단 근거: 없음 +- 실행한 검증/명령: 없음 +- 자동 후속 불가 이유: 없음 +- 재개 조건: 없음 + +## 리뷰어를 위한 체크포인트 + +- Gito webhook route가 protected basic auth router 밖에 있고 Plane route와 충돌하지 않는지 확인한다. +- invalid signature, wrong event, malformed payload가 handler/bridge를 호출하지 않는지 확인한다. +- duplicate delivery id와 duplicate revision key가 sequential duplicate에서 handler/bridge를 한 번만 호출하는지 확인한다. +- config/server wiring이 기존 proto-socket runner를 무리하게 제거하지 않는지 확인한다. + +## 검증 결과 + +_구현 에이전트가 각 중간 검증 및 최종 검증 명령 실행 후 출력을 여기에 붙여 넣는다._ + +필수 규칙: +- 검증 명령은 고정된 계약이다. 임의로 대체하지 않는다. +- 대체가 필요하면 `계획 대비 변경 사항`에 이유와 대체 명령을 기록한다. +- `검증 결과`에는 실제 stdout/stderr를 붙여 넣는다. +- 사용자 리뷰 요청으로 명령을 끝까지 실행하지 못했다면 `사용자 리뷰 요청`에 실행한 명령, 실제 출력, 미실행 명령의 사유를 기록한다. +- mobile/UI hang, timeout, 또는 2분 무진행은 blind retry를 중단하고 focused rerun 명령과 screenshot/window/UI-tree evidence path를 남기며, 불가능하면 정확한 사유를 남긴다. + +### API-1 중간 검증 +``` +$ cd services/core && go test -count=1 ./internal/http +ok github.com/nomadcode/nomadcode-core/internal/http 0.010s +``` + +### API-2 중간 검증 +``` +$ cd services/core && go test -count=1 ./internal/http +ok github.com/nomadcode/nomadcode-core/internal/http 0.010s +``` + +### API-3 중간 검증 +``` +$ cd services/core && go test -count=1 ./internal/config ./cmd/server +ok github.com/nomadcode/nomadcode-core/internal/config 0.002s +? github.com/nomadcode/nomadcode-core/cmd/server [no test files] +``` + +### 최종 검증 +``` +$ cd services/core && go test -count=1 ./internal/http ./internal/config ./cmd/server +ok github.com/nomadcode/nomadcode-core/internal/http 0.010s +ok github.com/nomadcode/nomadcode-core/internal/config 0.002s +? github.com/nomadcode/nomadcode-core/cmd/server [no test files] + +$ cd services/core && go test -count=1 ./... +ok github.com/nomadcode/nomadcode-core/cmd/plane-smoke 0.004s +? github.com/nomadcode/nomadcode-core/cmd/server [no test files] +ok github.com/nomadcode/nomadcode-core/internal/adapters/a2a 0.011s +ok github.com/nomadcode/nomadcode-core/internal/adapters/jira 0.010s +ok github.com/nomadcode/nomadcode-core/internal/adapters/mattermost 0.007s +ok github.com/nomadcode/nomadcode-core/internal/adapters/openai 0.008s +ok github.com/nomadcode/nomadcode-core/internal/adapters/plane 0.007s +? github.com/nomadcode/nomadcode-core/internal/agent [no test files] +ok github.com/nomadcode/nomadcode-core/internal/authoring 0.003s +ok github.com/nomadcode/nomadcode-core/internal/config 0.002s +? github.com/nomadcode/nomadcode-core/internal/db [no test files] +ok github.com/nomadcode/nomadcode-core/internal/gitoevents 0.004s +ok github.com/nomadcode/nomadcode-core/internal/gitosync 1.380s +ok github.com/nomadcode/nomadcode-core/internal/http 0.010s +? github.com/nomadcode/nomadcode-core/internal/model [no test files] +ok github.com/nomadcode/nomadcode-core/internal/notification 0.005s +ok github.com/nomadcode/nomadcode-core/internal/projectsync 0.005s +ok github.com/nomadcode/nomadcode-core/internal/protosocket 0.010s +ok github.com/nomadcode/nomadcode-core/internal/roadmapsync 0.004s +ok github.com/nomadcode/nomadcode-core/internal/roadmapsyncpipeline 0.004s +ok github.com/nomadcode/nomadcode-core/internal/scheduler 2.011s +ok github.com/nomadcode/nomadcode-core/internal/storage 0.004s +ok github.com/nomadcode/nomadcode-core/internal/workflow 0.005s +ok github.com/nomadcode/nomadcode-core/internal/workitem 0.003s +ok github.com/nomadcode/nomadcode-core/internal/workitempipeline 0.004s +``` + +--- + +> **[IMPLEMENTING AGENT — BEFORE SAVING] Have you filled in every implementation-owned section: completion table, implementation checklist, changes from plan, design decisions, and verification output?** +> If anything is blank, go back and fill it in before saving this file. +> Leave review-agent-only sections unchanged. + +## 코드리뷰 결과 + +- 종합 판정: FAIL +- 차원별 평가: + - Correctness: Fail + - Completeness: Fail + - Test coverage: Fail + - API contract: Fail + - Code quality: Pass + - Plan deviation: Fail + - Verification trust: Pass + - Spec conformance: Fail +- 발견된 문제: + - Required: `services/core/internal/http/gito_webhook.go:97`에서 HTTP body를 로컬 `gitoWebhookPayload`로만 decode하면서 `gitoevents.DecodeBranchUpdatedPayload`가 보장하던 `type == "branch.updated"` 검증과 `changed_files` shape 검증이 사라졌습니다. 이 상태에서는 `X-Gito-Event: branch.updated` 헤더만 맞으면 body `type`이 다른 이벤트이거나 `changed_files`가 문자열처럼 잘못된 payload도 accepted/handler 호출 경로로 들어갈 수 있어, 계획의 payload decode 계약과 SDD S01의 "payload shape가 유효하다" 조건을 충족하지 못합니다. import cycle을 피하더라도 공통 decoder를 dependency-light package로 분리하거나 로컬 decoder에 동일 검증을 추가하고, wrong body type 및 malformed `changed_files` 테스트를 추가해야 합니다. +- 다음 단계: + - FAIL: active 파일을 archive한 뒤 payload 계약 검증 복구를 위한 follow-up `PLAN-local-G06.md`와 `CODE_REVIEW-local-G06.md`를 작성한다. + +### 리뷰 검증 + +```bash +$ git diff --check +# PASS (no output) + +$ cd services/core && go test -count=1 ./internal/http ./internal/config ./cmd/server +ok github.com/nomadcode/nomadcode-core/internal/http 0.007s +ok github.com/nomadcode/nomadcode-core/internal/config 0.004s +? github.com/nomadcode/nomadcode-core/cmd/server [no test files] + +$ cd services/core && go test -count=1 ./... +ok github.com/nomadcode/nomadcode-core/cmd/plane-smoke 0.003s +? github.com/nomadcode/nomadcode-core/cmd/server [no test files] +ok github.com/nomadcode/nomadcode-core/internal/adapters/a2a 0.008s +ok github.com/nomadcode/nomadcode-core/internal/adapters/jira 0.011s +ok github.com/nomadcode/nomadcode-core/internal/adapters/mattermost 0.007s +ok github.com/nomadcode/nomadcode-core/internal/adapters/openai 0.009s +? github.com/nomadcode/nomadcode-core/internal/agent [no test files] +ok github.com/nomadcode/nomadcode-core/internal/authoring 0.004s +ok github.com/nomadcode/nomadcode-core/internal/config 0.002s +? github.com/nomadcode/nomadcode-core/internal/db [no test files] +ok github.com/nomadcode/nomadcode-core/internal/gitoevents 0.007s +ok github.com/nomadcode/nomadcode-core/internal/gitosync 1.382s +ok github.com/nomadcode/nomadcode-core/internal/http 0.009s +? github.com/nomadcode/nomadcode-core/internal/model [no test files] +ok github.com/nomadcode/nomadcode-core/internal/notification 0.002s +ok github.com/nomadcode/nomadcode-core/internal/projectsync 0.005s +ok github.com/nomadcode/nomadcode-core/internal/protosocket 0.011s +ok github.com/nomadcode/nomadcode-core/internal/roadmapsync 0.004s +ok github.com/nomadcode/nomadcode-core/internal/roadmapsyncpipeline 0.005s +ok github.com/nomadcode/nomadcode-core/internal/scheduler 2.014s +ok github.com/nomadcode/nomadcode-core/internal/storage 0.005s +ok github.com/nomadcode/nomadcode-core/internal/workflow 0.004s +ok github.com/nomadcode/nomadcode-core/internal/workitem 0.003s +ok github.com/nomadcode/nomadcode-core/internal/workitempipeline 0.003s +``` diff --git a/agent-task/m-gito-http-webhook-consumer-readiness/02+01_http_receiver_idempotency/CODE_REVIEW-local-G06.md b/agent-task/archive/2026/06/m-gito-http-webhook-consumer-readiness/02+01_http_receiver_idempotency/code_review_local_G06_1.log similarity index 89% rename from agent-task/m-gito-http-webhook-consumer-readiness/02+01_http_receiver_idempotency/CODE_REVIEW-local-G06.md rename to agent-task/archive/2026/06/m-gito-http-webhook-consumer-readiness/02+01_http_receiver_idempotency/code_review_local_G06_1.log index de0a5cc..31bb6c5 100644 --- a/agent-task/m-gito-http-webhook-consumer-readiness/02+01_http_receiver_idempotency/CODE_REVIEW-local-G06.md +++ b/agent-task/archive/2026/06/m-gito-http-webhook-consumer-readiness/02+01_http_receiver_idempotency/code_review_local_G06_1.log @@ -90,16 +90,16 @@ task=m-gito-http-webhook-consumer-readiness/02+01_http_receiver_idempotency, pla > **[REVIEW AGENT ONLY]** 이 체크리스트는 코드리뷰 에이전트만 사용한다. > 구현 에이전트는 이 섹션을 수정하거나 체크하지 않는다. -- [ ] `코드리뷰 결과`에 `PASS`, `WARN`, `FAIL` 중 하나의 판정을 append한다. -- [ ] 판정과 `차원별 평가`, Required/Suggested/Nit 분류가 서로 일치한다. -- [ ] active `CODE_REVIEW-*-G??.md`를 `code_review_local_G06_N.log`로 아카이브한다. -- [ ] active `PLAN-*-G??.md`를 `plan_local_G06_M.log`로 아카이브한다. -- [ ] `.gitignore`의 Agent-Ops 관리 block이 `agent-task/**/*.md`와 `agent-task/**/*.log`를 unignore하고 `agent-roadmap/current.md`를 ignore하는지 확인한다. +- [x] `코드리뷰 결과`에 `PASS`, `WARN`, `FAIL` 중 하나의 판정을 append한다. +- [x] 판정과 `차원별 평가`, Required/Suggested/Nit 분류가 서로 일치한다. +- [x] active `CODE_REVIEW-*-G??.md`를 `code_review_local_G06_N.log`로 아카이브한다. +- [x] active `PLAN-*-G??.md`를 `plan_local_G06_M.log`로 아카이브한다. +- [x] `.gitignore`의 Agent-Ops 관리 block이 `agent-task/**/*.md`와 `agent-task/**/*.log`를 unignore하고 `agent-roadmap/current.md`를 ignore하는지 확인한다. - [ ] PASS이면 `agent-ops/skills/common/code-review/templates/complete-log-template.md` 기준으로 `complete.log`를 작성하고 active `.md` 파일을 남기지 않는다. - [ ] PASS이면 active task 디렉터리 `agent-task/m-gito-http-webhook-consumer-readiness/02+01_http_receiver_idempotency/`를 `agent-task/archive/YYYY/MM/m-gito-http-webhook-consumer-readiness/02+01_http_receiver_idempotency/`로 이동하고 최종 archive 경로에서 이 체크리스트를 갱신한다. - [ ] PASS이고 task group이 `m-`이면 런타임이 읽을 완료 이벤트 메타데이터를 보고하고, roadmap 수정이나 `update-roadmap` 직접 호출을 하지 않는다. - [ ] PASS split 작업이면 이동 후 빈 active parent `agent-task/m-gito-http-webhook-consumer-readiness/`를 제거하거나, 남은 sibling/file이 있어 유지했다고 확인한다. -- [ ] WARN/FAIL이고 user-review gate가 트리거되지 않았으면 다음 active `PLAN-local-G06.md`와 `CODE_REVIEW-local-G06.md`를 작성하고 `complete.log`를 작성하지 않는다. +- [x] WARN/FAIL이고 user-review gate가 트리거되지 않았으면 다음 active `PLAN-local-G06.md`와 `CODE_REVIEW-local-G06.md`를 작성하고 `complete.log`를 작성하지 않는다. - [ ] USER_REVIEW이면 `agent-ops/skills/common/code-review/templates/user-review-template.md` 기준으로 `USER_REVIEW.md`를 작성하고 active `PLAN-*.md`, `CODE_REVIEW-*.md`, `complete.log`를 남기지 않는다. - [ ] USER_REVIEW가 연결된 SDD/Milestone 결정으로 완료/PASS 해소되면 `USER_REVIEW.md`를 해소 상태로 갱신하고 `complete.log`를 작성한 뒤 task directory를 archive로 이동한다. @@ -184,3 +184,19 @@ ok github.com/nomadcode/nomadcode-core/internal/workitempipeline 0.010s > **[IMPLEMENTING AGENT — BEFORE SAVING] Have you filled in every implementation-owned section: completion table, implementation checklist, changes from plan, design decisions, and verification output?** > If anything is blank, go back and fill it in before saving this file. > Leave review-agent-only sections unchanged. + +## 코드리뷰 결과 + +- 종합 판정: FAIL +- 차원별 평가: + - correctness: Fail + - completeness: Fail + - test coverage: Fail + - API contract: Fail + - code quality: Pass + - plan deviation: Fail + - verification trust: Pass + - spec conformance: Fail +- 발견된 문제: + - Required: `services/core/internal/http/gito_webhook.go:96`에서 `changed_files`가 `[]interface{}`인지까지만 확인하고 각 원소가 object인지 검증하지 않습니다. 계획과 SDD는 `changed_files`가 present이면 list of objects여야 한다고 요구하고, `gitoevents.decodeChangedFiles`는 `changed_files[0]` 같은 non-object 항목을 reject합니다. 현재 HTTP path는 `{"changed_files":["not-an-object"]}`를 유효 payload로 받아 handler path까지 보낼 수 있습니다. `validateBranchUpdatedPayload`에서 배열 원소마다 `map[string]interface{}`인지 확인하고, `services/core/internal/http/gito_webhook_test.go`에 해당 payload가 400이며 handler를 호출하지 않는 테스트를 추가하세요. +- 다음 단계: FAIL follow-up plan/review를 작성해 위 Required issue만 좁게 수정한다. diff --git a/agent-task/archive/2026/06/m-gito-http-webhook-consumer-readiness/02+01_http_receiver_idempotency/code_review_local_G06_2.log b/agent-task/archive/2026/06/m-gito-http-webhook-consumer-readiness/02+01_http_receiver_idempotency/code_review_local_G06_2.log new file mode 100644 index 0000000..dad4aa3 --- /dev/null +++ b/agent-task/archive/2026/06/m-gito-http-webhook-consumer-readiness/02+01_http_receiver_idempotency/code_review_local_G06_2.log @@ -0,0 +1,205 @@ + + +# Code Review Reference - REVIEW_REVIEW_API + +> **[IMPLEMENTING AGENT — READ FIRST] Filling in this file is the mandatory final step of implementation.** +> The task is NOT complete until every implementation-owned section below is filled in. +> Complete the `구현 체크리스트`; the final checklist item is mandatory before saving. +> Fill implementation-owned sections, then stop with active files in place and report ready for review. +> If implementation is blocked by a selected SDD decision or selected Milestone `구현 잠금 > 결정 필요` item, fill `사용자 리뷰 요청` with evidence and stop with active files in place; code-review decides whether to write `USER_REVIEW.md`. Environment/secret/service setup, generic scope conflicts, loop exhaustion, and evidence gaps that a follow-up agent can close are normal follow-up issues, not user-review blockers by themselves. +> Do not ask the user directly, present choices in chat, or call `request_user_input` during implementation; record only the linked SDD/Milestone lock decision in `사용자 리뷰 요청` and stop for code-review. +> Finalization (`코드리뷰 결과`, log rename, `complete.log`, archive moves, `코드리뷰 전용 체크리스트`) is review-agent-only, even after compaction/resume. +> Follow the ownership table at the bottom of this file for which sections you own. + +## 개요 + +date=2026-06-19 +task=m-gito-http-webhook-consumer-readiness/02+01_http_receiver_idempotency, plan=2, tag=REVIEW_REVIEW_API + +## Roadmap Targets + +- Milestone: `agent-roadmap/phase/agent-ops-mcp-control-plane/milestones/gito-http-webhook-consumer-readiness.md` +- Task ids: + - `receiver`: Gito `branch.updated` HTTP callback endpoint를 추가한다. + - `idempotency`: `X-Gito-Delivery`와 revision key 기준으로 duplicate delivery를 중복 enqueue 없이 처리한다. +- Completion mode: check-on-pass + +## Spec Targets + +- SDD: `agent-roadmap/sdd/agent-ops-mcp-control-plane/gito-http-webhook-consumer-readiness/SDD.md` +- Acceptance scenarios: + - `S01`: task=`receiver`; evidence=`HTTP handler unit/integration test, invalid signature test` + - `S03`: task=`idempotency`; evidence=`duplicate delivery/revision idempotency test` +- Completion mode: spec-check-on-pass + +## Archive Evidence Snapshot + +- Archived plan: `agent-task/m-gito-http-webhook-consumer-readiness/02+01_http_receiver_idempotency/plan_local_G06_1.log` +- Archived review: `agent-task/m-gito-http-webhook-consumer-readiness/02+01_http_receiver_idempotency/code_review_local_G06_1.log` +- Verdict: FAIL +- Issue summary: + - Required: `services/core/internal/http/gito_webhook.go:96` validates `changed_files` only as an array, not as a list of objects, so `{"changed_files":["not-an-object"]}` can still enter the accepted/handler path even though the SDD and `gitoevents.DecodeBranchUpdatedPayload` contract reject non-object items. + - Suggested: 없음 + - Nit: 없음 +- Affected files: + - `services/core/internal/http/gito_webhook.go` + - `services/core/internal/http/gito_webhook_test.go` +- Verification evidence: + - `cd services/core && go test -count=1 ./internal/http ./internal/config ./cmd/server` - PASS. + - `cd services/core && go test -count=1 ./...` - PASS. + - Existing HTTP receiver tests are insufficient because they cover `changed_files` as a non-list string but not a list containing non-object elements. +- Roadmap/spec carryover: + - Roadmap Targets remain `receiver` and `idempotency`. + - Spec Targets remain S01 and S03. This follow-up directly repairs S01; S03 should remain green. +- Narrow reread allowed if needed: + - `agent-task/m-gito-http-webhook-consumer-readiness/02+01_http_receiver_idempotency/plan_local_G06_1.log` + - `agent-task/m-gito-http-webhook-consumer-readiness/02+01_http_receiver_idempotency/code_review_local_G06_1.log` + +## 이 파일을 읽는 리뷰 에이전트에게 + +> **[REVIEW AGENT ONLY]** 아래 종결 절차는 코드리뷰 에이전트 전용이다. 구현 에이전트는 이 섹션을 실행하지 않는다. + +각 항목의 구현을 실제 소스 파일과 대조하고, `검증 결과` 섹션의 출력이 코드와 일치하는지 확인하세요. +리뷰 완료는 아래 순서까지 끝난 상태를 의미합니다. + +1. 판정을 append한다. +2. `CODE_REVIEW-local-G06.md` -> `code_review_local_G06_N.log`, `PLAN-local-G06.md` -> `plan_local_G06_M.log`로 아카이브한다. +3. PASS이면 `complete.log` 작성 후 active task 디렉터리를 `agent-task/archive/YYYY/MM/m-gito-http-webhook-consumer-readiness/02+01_http_receiver_idempotency/`로 이동한다. WARN/FAIL이면 user-review gate를 확인한 뒤 다음 active plan/review 파일 또는 `USER_REVIEW.md`를 작성한다. +4. PASS이고 task group이 `m-`이면 완료 이벤트 메타데이터를 보고한다. roadmap 상태 체크와 `update-roadmap` 호출은 런타임 책임이다. +5. 적용 가능한 `코드리뷰 전용 체크리스트` 항목을 최종 `.log` 위치에서 체크한 뒤 보고한다. + +--- + +## 구현 항목별 완료 여부 + +| 항목 | 완료 여부 | +|------|---------| +| [REVIEW_REVIEW_API-1] `changed_files` element contract validation | [x] | + +## 구현 체크리스트 + +- [x] `validateBranchUpdatedPayload`가 `changed_files` present 시 배열 여부뿐 아니라 각 element가 JSON object인지 검증하고, non-object element를 400 reject하도록 보강한다. +- [x] `changed_files` 배열 안의 non-object item이 400이고 handler를 호출하지 않음을 검증하는 HTTP receiver test를 추가하며 기존 wrong body type, malformed string, accepted/duplicate tests가 계속 통과하게 한다. +- [x] `cd services/core && go test -count=1 ./internal/http`를 실행한다. +- [x] `cd services/core && go test -count=1 ./internal/http ./internal/config ./cmd/server`를 실행한다. +- [x] `cd services/core && go test -count=1 ./...`를 실행한다. +- [x] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다. + +## 코드리뷰 전용 체크리스트 + +> **[REVIEW AGENT ONLY]** 이 체크리스트는 코드리뷰 에이전트만 사용한다. +> 구현 에이전트는 이 섹션을 수정하거나 체크하지 않는다. + +- [x] `코드리뷰 결과`에 `PASS`, `WARN`, `FAIL` 중 하나의 판정을 append한다. +- [x] 판정과 `차원별 평가`, Required/Suggested/Nit 분류가 서로 일치한다. +- [x] active `CODE_REVIEW-*-G??.md`를 `code_review_local_G06_N.log`로 아카이브한다. +- [x] active `PLAN-*-G??.md`를 `plan_local_G06_M.log`로 아카이브한다. +- [x] `.gitignore`의 Agent-Ops 관리 block이 `agent-task/**/*.md`와 `agent-task/**/*.log`를 unignore하고 `agent-roadmap/current.md`를 ignore하는지 확인한다. +- [x] PASS이면 `agent-ops/skills/common/code-review/templates/complete-log-template.md` 기준으로 `complete.log`를 작성하고 active `.md` 파일을 남기지 않는다. +- [x] PASS이면 active task 디렉터리 `agent-task/m-gito-http-webhook-consumer-readiness/02+01_http_receiver_idempotency/`를 `agent-task/archive/YYYY/MM/m-gito-http-webhook-consumer-readiness/02+01_http_receiver_idempotency/`로 이동하고 최종 archive 경로에서 이 체크리스트를 갱신한다. +- [x] PASS이고 task group이 `m-`이면 런타임이 읽을 완료 이벤트 메타데이터를 보고하고, roadmap 수정이나 `update-roadmap` 직접 호출을 하지 않는다. +- [x] PASS split 작업이면 이동 후 빈 active parent `agent-task/m-gito-http-webhook-consumer-readiness/`를 제거하거나, 남은 sibling/file이 있어 유지했다고 확인한다. +- [ ] WARN/FAIL이고 user-review gate가 트리거되지 않았으면 다음 active `PLAN-local-G06.md`와 `CODE_REVIEW-local-G06.md`를 작성하고 `complete.log`를 작성하지 않는다. +- [ ] USER_REVIEW이면 `agent-ops/skills/common/code-review/templates/user-review-template.md` 기준으로 `USER_REVIEW.md`를 작성하고 active `PLAN-*.md`, `CODE_REVIEW-*.md`, `complete.log`를 남기지 않는다. +- [ ] USER_REVIEW가 연결된 SDD/Milestone 결정으로 완료/PASS 해소되면 `USER_REVIEW.md`를 해소 상태로 갱신하고 `complete.log`를 작성한 뒤 task directory를 archive로 이동한다. + +## 계획 대비 변경 사항 + +계획에 명시된 범위 내에서만 구현했다. 계획에 없던 package 생성, config 변경, 다른 기능 수정은 하지 않았다. + +## 주요 설계 결정 + +- `gitoevents` package를 import하지 않고, 기존 `interface{}` 타입에 대한 type assertion으로 `changed_files` element의 object 검증을 구현했다. import cycle을 피하기 위한 결정이다. +- 에러 메시지는 `payload changed_files[] is not an object` 형식으로 인덱스를 포함하여 어떤 element가 문제인지 식별할 수 있게 설계했다. + +## 사용자 리뷰 요청 + +_기본값은 `없음`이다. 구현 중 새 결정이 필요해 보여도 직접 질문하거나 선택지를 제시하거나 `request_user_input`을 호출하지 않는다. 이 섹션은 선택된 SDD 결정 또는 선택된 Milestone `구현 잠금 > 결정 필요` 항목이 실구현을 차단할 때만 채운다. 외부 환경/secret/서비스 준비, 검증 증거 공백, 반복 실패, 일반 범위 조정은 사용자 리뷰 요청이 아니며 `검증 결과`, `계획 대비 변경 사항`, 또는 code-review의 일반 follow-up plan으로 처리한다._ + +- 상태: 없음 +- 사유 유형: 없음 +- 연결 대상: 없음 +- 결정 필요: 없음 +- 차단 근거: 없음 +- 실행한 검증/명령: 없음 +- 자동 후속 불가 이유: 없음 +- 재개 조건: 없음 + +## 리뷰어를 위한 체크포인트 + +- HTTP body `changed_files`가 array여도 element가 object가 아니면 400이고 handler call이 없는지 확인한다. +- 기존 wrong body type, `changed_files` non-list string, delivery/revision duplicate tests가 계속 통과하는지 확인한다. +- import cycle이 재발하지 않았고 `go test -count=1 ./...`가 통과했는지 확인한다. + +## 검증 결과 + +_구현 에이전트가 각 중간 검증 및 최종 검증 명령 실행 후 출력을 여기에 붙여 넣는다._ + +필수 규칙: +- 검증 명령은 고정된 계약이다. 임의로 대체하지 않는다. +- 대체가 필요하면 `계획 대비 변경 사항`에 이유와 대체 명령을 기록한다. +- `검증 결과`에는 실제 stdout/stderr를 붙여 넣는다. +- 사용자 리뷰 요청으로 명령을 끝까지 실행하지 못했다면 `사용자 리뷰 요청`에 실행한 명령, 실제 출력, 미실행 명령의 사유를 기록한다. +- mobile/UI hang, timeout, 또는 2분 무진행은 blind retry를 중단하고 focused rerun 명령과 screenshot/window/UI-tree evidence path를 남기며, 불가능하면 정확한 사유를 남긴다. + +### REVIEW_REVIEW_API-1 중간 검증 +```bash +$ cd services/core && go test -count=1 ./internal/http +ok github.com/nomadcode/nomadcode-core/internal/http 0.008s +``` + +### 최종 검증 +```bash +$ cd services/core && go test -count=1 ./internal/http ./internal/config ./cmd/server +ok github.com/nomadcode/nomadcode-core/internal/http 0.009s +ok github.com/nomadcode/nomadcode-core/internal/config 0.006s +? github.com/nomadcode/nomadcode-core/cmd/server [no test files] + +$ cd services/core && go test -count=1 ./... +ok github.com/nomadcode/nomadcode-core/cmd/plane-smoke 0.016s +? github.com/nomadcode/nomadcode-core/cmd/server [no test files] +ok github.com/nomadcode/nomadcode-core/internal/adapters/a2a 0.016s +ok github.com/nomadcode/nomadcode-core/internal/adapters/jira 0.016s +ok github.com/nomadcode/nomadcode-core/internal/adapters/mattermost 0.020s +ok github.com/nomadcode/nomadcode-core/internal/adapters/openai 0.016s +ok github.com/nomadcode/nomadcode-core/internal/adapters/plane 0.037s +? github.com/nomadcode/nomadcode-core/internal/agent [no test files] +ok github.com/nomadcode/nomadcode-core/internal/authoring 0.034s +ok github.com/nomadcode/nomadcode-core/internal/config 0.030s +? github.com/nomadcode/nomadcode-core/internal/db [no test files] +ok github.com/nomadcode/nomadcode-core/internal/gitoevents 0.014s +ok github.com/nomadcode/nomadcode-core/internal/gitosync 1.389s +ok github.com/nomadcode/nomadcode-core/internal/http 0.025s +? github.com/nomadcode/nomadcode-core/internal/model [no test files] +ok github.com/nomadcode/nomadcode-core/internal/notification 0.020s +ok github.com/nomadcode/nomadcode-core/internal/projectsync 0.020s +ok github.com/nomadcode/nomadcode-core/internal/protosocket 0.026s +ok github.com/nomadcode/nomadcode-core/internal/roadmapsync 0.017s +ok github.com/nomadcode/nomadcode-core/internal/roadmapsyncpipeline 0.018s +ok github.com/nomadcode/nomadcode-core/internal/scheduler 2.019s +ok github.com/nomadcode/nomadcode-core/internal/storage 0.016s +ok github.com/nomadcode/nomadcode-core/internal/workflow 0.017s +ok github.com/nomadcode/nomadcode-core/internal/workitem 0.016s +ok github.com/nomadcode/nomadcode-core/internal/workitempipeline 0.025s +``` + +--- + +> **[IMPLEMENTING AGENT — BEFORE SAVING] Have you filled in every implementation-owned section: completion table, implementation checklist, changes from plan, design decisions, and verification output?** +> If anything is blank, go back and fill it in before saving this file. +> Leave review-agent-only sections unchanged. + +## 코드리뷰 결과 + +- 종합 판정: PASS +- 차원별 평가: + - correctness: Pass + - completeness: Pass + - test coverage: Pass + - API contract: Pass + - code quality: Pass + - plan deviation: Pass + - verification trust: Pass + - spec conformance: Pass +- 발견된 문제: 없음 +- 다음 단계: PASS 종결 처리한다. diff --git a/agent-task/archive/2026/06/m-gito-http-webhook-consumer-readiness/02+01_http_receiver_idempotency/complete.log b/agent-task/archive/2026/06/m-gito-http-webhook-consumer-readiness/02+01_http_receiver_idempotency/complete.log new file mode 100644 index 0000000..a0e9e32 --- /dev/null +++ b/agent-task/archive/2026/06/m-gito-http-webhook-consumer-readiness/02+01_http_receiver_idempotency/complete.log @@ -0,0 +1,54 @@ +# Complete - m-gito-http-webhook-consumer-readiness/02+01_http_receiver_idempotency + +## 완료 일시 + +2026-06-19 + +## 요약 + +Gito HTTP webhook receiver payload validation/idempotency subtask completed after 3 review loops; final verdict PASS. + +## 루프 이력 + +| Plan | Review | Verdict | 메모 | +|------|--------|---------|------| +| `plan_local_G06_0.log` | `code_review_local_G06_0.log` | FAIL | HTTP receiver local decode missed body `type` and malformed `changed_files` validation. | +| `plan_local_G06_1.log` | `code_review_local_G06_1.log` | FAIL | `changed_files` array elements were not validated as objects. | +| `plan_local_G06_2.log` | `code_review_local_G06_2.log` | PASS | `changed_files` item object validation and HTTP receiver regression test passed. | + +## 구현/정리 내용 + +- Gito HTTP webhook body validation now rejects non-`branch.updated` body `type`, missing `repo_id`/`branch`, non-list `changed_files`, and non-object `changed_files` items before handler invocation. +- Added HTTP receiver tests covering wrong body `type`, malformed `changed_files`, malformed `changed_files` item, signature rejection, and delivery/revision duplicate behavior. +- Removed an unused `missingGitoFields` helper during review cleanup. + +## 최종 검증 + +- `cd services/core && go test -count=1 ./internal/http` - PASS; `ok github.com/nomadcode/nomadcode-core/internal/http`. +- `cd services/core && go test -count=1 ./internal/http ./internal/config ./cmd/server` - PASS; `internal/http`, `internal/config`, and `cmd/server` passed/no-test-files. +- `cd services/core && go test -count=1 ./...` - PASS; all core packages passed/no-test-files. +- `git diff --check` - PASS; no output. + +## Roadmap Completion + +- Milestone: `agent-roadmap/phase/agent-ops-mcp-control-plane/milestones/gito-http-webhook-consumer-readiness.md` +- Completed task ids: + - `receiver`: PASS; evidence=`plan_local_G06_2.log`, `code_review_local_G06_2.log`; verification=`cd services/core && go test -count=1 ./internal/http ./internal/config ./cmd/server`, `cd services/core && go test -count=1 ./...` + - `idempotency`: PASS; evidence=`plan_local_G06_2.log`, `code_review_local_G06_2.log`; verification=`cd services/core && go test -count=1 ./internal/http ./internal/config ./cmd/server`, `cd services/core && go test -count=1 ./...` +- Not completed task ids: 없음 + +## Spec Completion + +- SDD: `agent-roadmap/sdd/agent-ops-mcp-control-plane/gito-http-webhook-consumer-readiness/SDD.md` +- Completed scenario ids: + - `S01`: PASS; task=`receiver`; evidence=`plan_local_G06_2.log`, `code_review_local_G06_2.log`; verification=`cd services/core && go test -count=1 ./internal/http ./internal/config ./cmd/server`, `cd services/core && go test -count=1 ./...` + - `S03`: PASS; task=`idempotency`; evidence=`plan_local_G06_2.log`, `code_review_local_G06_2.log`; verification=`cd services/core && go test -count=1 ./internal/http ./internal/config ./cmd/server`, `cd services/core && go test -count=1 ./...` +- Not completed scenario ids: 없음 + +## 잔여 Nit + +- 없음 + +## 후속 작업 + +- 없음 diff --git a/agent-task/archive/2026/06/m-gito-http-webhook-consumer-readiness/02+01_http_receiver_idempotency/plan_local_G06_0.log b/agent-task/archive/2026/06/m-gito-http-webhook-consumer-readiness/02+01_http_receiver_idempotency/plan_local_G06_0.log new file mode 100644 index 0000000..3ed8c01 --- /dev/null +++ b/agent-task/archive/2026/06/m-gito-http-webhook-consumer-readiness/02+01_http_receiver_idempotency/plan_local_G06_0.log @@ -0,0 +1,325 @@ + + +# PLAN local-G06 - API + +## 이 파일을 읽는 구현 에이전트에게 + +이 작업은 `agent-task/m-gito-http-webhook-consumer-readiness/01_payload_boundary/complete.log` 또는 matching archive `complete.log`가 있어야 시작할 수 있다. 구현 후 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 실제 변경 내용과 검증 출력으로 채워야 완료된다. 구현 에이전트는 검증을 실행하고 active 파일을 유지한 채 리뷰 준비를 보고한다. 종결, 로그 rename, `complete.log`, task directory archive는 code-review 스킬 전용이다. 선택된 SDD 결정이나 Milestone `구현 잠금 > 결정 필요` 항목이 실구현을 막을 때만 review stub의 `사용자 리뷰 요청` 섹션을 채우고 멈춘다. 구현 중 사용자에게 직접 묻거나 선택지를 제시하거나 `request_user_input`을 호출하지 않는다. + +## 배경 + +NomadCode Core는 아직 Gito signed HTTP webhook delivery를 받는 endpoint가 없다. 기존 Gito 소비자는 proto-socket runner로 시작되고, HTTP router에는 Plane webhook만 공개 callback으로 등록되어 있다. 이 작업은 Gito HTTP callback, signature 검증, duplicate delivery/revision guard를 `gitosync` bridge로 연결한다. + +## 사용자 리뷰 요청 흐름 + +사용자 리뷰 요청은 선택된 SDD 결정 또는 선택된 Milestone lock 결정만 active review stub의 `사용자 리뷰 요청` 섹션에 기록한다. 이 섹션은 `agent-ops/skills/common/_templates/implementation-user-review-request-section.md` 형식을 따른다. 구현 중 직접 사용자에게 질문하지 않으며, code-review가 정당성을 검증하고 실제 `USER_REVIEW.md` 작성 여부를 결정한다. + +## Roadmap Targets + +- Milestone: `agent-roadmap/phase/agent-ops-mcp-control-plane/milestones/gito-http-webhook-consumer-readiness.md` +- Task ids: + - `receiver`: Gito `branch.updated` HTTP callback endpoint를 추가한다. + - `idempotency`: `X-Gito-Delivery`와 revision key 기준으로 duplicate delivery를 중복 enqueue 없이 처리한다. +- Completion mode: check-on-pass + +## Spec Targets + +- SDD: `agent-roadmap/sdd/agent-ops-mcp-control-plane/gito-http-webhook-consumer-readiness/SDD.md` +- Acceptance scenarios: + - `S01`: task=`receiver`; evidence=`HTTP handler unit/integration test, invalid signature test` + - `S03`: task=`idempotency`; evidence=`duplicate delivery/revision idempotency test` +- Completion mode: spec-check-on-pass + +## 분석 결과 + +### 읽은 파일 + +- `AGENTS.md` +- `agent-ops/rules/project/rules.md` +- `agent-ops/rules/private/rules.md` +- `agent-ops/rules/common/rules-roadmap.md` +- `agent-ops/rules/project/domain/core/rules.md` +- `agent-ops/rules/project/domain/workspace-ops/rules.md` +- `agent-ops/skills/common/router.md` +- `agent-ops/skills/common/plan/SKILL.md` +- `agent-ops/skills/common/roadmap-sdd/SKILL.md` +- `agent-ops/skills/common/update-roadmap/SKILL.md` +- `agent-ops/skills/common/check-roadmap-dependency/SKILL.md` +- `agent-ops/skills/common/_templates/implementation-user-review-request-section.md` +- `agent-test/local/rules.md` +- `agent-test/local/core-smoke.md` +- `agent-test/local/workspace-ops-smoke.md` +- `agent-roadmap/current.md` +- `agent-roadmap/phase/agent-ops-mcp-control-plane/PHASE.md` +- `agent-roadmap/phase/agent-ops-mcp-control-plane/milestones/gito-http-webhook-consumer-readiness.md` +- `agent-roadmap/sdd/agent-ops-mcp-control-plane/gito-http-webhook-consumer-readiness/SDD.md` +- `../gito/agent-contract/provided/gito-forgejo-branch-events-v1.md` +- `services/core/go.mod` +- `services/core/internal/gitoevents/events.go` +- `services/core/internal/gitoevents/events_test.go` +- `services/core/internal/gitoevents/client.go` +- `services/core/internal/gitoevents/client_test.go` +- `services/core/internal/gitosync/bridge.go` +- `services/core/internal/gitosync/bridge_test.go` +- `services/core/internal/gitosync/scanner.go` +- `services/core/internal/http/router.go` +- `services/core/internal/http/handlers.go` +- `services/core/internal/http/handlers_test.go` +- `services/core/internal/http/plane_webhook.go` +- `services/core/internal/http/plane_webhook_test.go` +- `services/core/internal/config/config.go` +- `services/core/internal/config/config_test.go` +- `services/core/cmd/server/main.go` + +### 테스트 환경 규칙 + +- 선택한 test_env: `local`. +- `agent-test/local/rules.md`와 `agent-test/local/core-smoke.md`를 읽었다. +- 적용 명령: focused HTTP/config tests plus `cd services/core && go test -count=1 ./...`. +- `core-smoke.md`는 core 코드 변경에 최소 `cd services/core && go test ./...`를 요구한다. +- 새 dependency는 필요 없으며 `services/core/go.mod`에 기존 `chi`, Go stdlib crypto/http/json/sync로 충분하다. + +### 테스트 커버리지 공백 + +- 현재 `router.go`에는 Plane webhook bypass-auth 테스트만 있고 Gito webhook route는 없다. +- 현재 `plane_webhook.go`에는 Plane hex signature verifier만 있고 `sha256=` prefix를 허용하는 Gito verifier가 없다. +- 현재 `gitosync.Bridge`는 revision key process-lifetime dedupe만 제공한다. HTTP `X-Gito-Delivery` duplicate guard는 없다. +- 새 테스트가 필요한 동작: + - signed `branch.updated` HTTP delivery accepted and handler called once. + - wrong `X-Gito-Event` rejected without handler call. + - invalid signature rejected without handler call. + - same `X-Gito-Delivery` duplicate does not call handler twice. + - same revision key duplicate without delivery id does not call handler twice. + - route bypasses basic auth. + +### 심볼 참조 + +- renamed/removed symbol: 없음. +- 새 symbol 후보: `GitoWebhookConfig`, `SetGitoWebhookConfig`, `SetGitoBranchEventHandler`, `ReceiveGitoWebhook`, `GitoWebhookConsumerEnabled`. + +### 분할 판단 + +- split decision policy를 먼저 평가했다. +- shared task group: `m-gito-http-webhook-consumer-readiness`. +- sibling subtasks: + - `01_payload_boundary`: common base payload decoder. + - `02+01_http_receiver_idempotency`: HTTP receiver, signature, idempotency, route/config/server wiring. Depends on `01`. +- predecessor `01`: 현재 active/archive `complete.log` 없음. 구현 시작 전 `agent-task/m-gito-http-webhook-consumer-readiness/01_payload_boundary/complete.log` 또는 matching archive complete.log가 필요하다. +- split 이유: decoder foundation과 HTTP route/wiring/idempotency는 dependency boundary와 risk profile이 다르다. + +### 범위 결정 근거 + +- 포함: HTTP callback endpoint, Gito webhook config fields, process-lifetime duplicate guard, server wiring into `gitosync.Bridge`. +- 제외: `scan-enqueue` S04의 full branch scanner -> `EnqueueRoadmapCreationSync` integration proof. 이 plan은 handler seam 호출 1회까지만 검증하고, full enqueue bridge proof는 `sync-bridge` Epic의 `scan-enqueue` Task에서 한다. +- 제외: README/compose/env sample update. `config-docs` Task가 담당한다. +- 제외: Gito provider webhook producer/outbound worker. Milestone 범위 제외와 SDD 비목표에 있다. + +### 빌드 등급 + +- `local-G06`: 새 HTTP callback surface, config/server wiring, idempotency guard가 있지만 범위는 core 내부 Go와 deterministic unit tests로 제한된다. + +## 구현 체크리스트 + +- [ ] `01_payload_boundary` predecessor `complete.log`가 있는지 확인하고, 없으면 구현을 시작하지 않는다. +- [ ] Gito HTTP webhook receiver와 route를 추가하고, signed `branch.updated` delivery accepted 및 wrong event/invalid signature side-effect-free reject를 검증한다. +- [ ] `X-Gito-Delivery`와 revision key 기준 process-lifetime duplicate guard를 추가하고, duplicate delivery/revision이 handler를 한 번만 호출함을 검증한다. +- [ ] config/server wiring을 추가해 Gito webhook secret/repo/branch/develop checkout/todo state가 준비된 경우 HTTP receiver가 `gitosync.Bridge`를 호출하게 한다. +- [ ] `cd services/core && go test -count=1 ./internal/http ./internal/config ./cmd/server`를 실행한다. +- [ ] `cd services/core && go test -count=1 ./...`를 실행한다. +- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다. + +## 의존 관계 및 구현 순서 + +- `02+01_http_receiver_idempotency`는 sibling predecessor `01_payload_boundary`에 의존한다. +- runtime source of truth: directory name `02+01_http_receiver_idempotency`. +- 구현 시작 전 확인 후보: + - active: `agent-task/m-gito-http-webhook-consumer-readiness/01_payload_boundary/complete.log` + - archive: `agent-task/archive/*/*/m-gito-http-webhook-consumer-readiness/01_payload_boundary/complete.log` +- predecessor complete.log가 없으면 이 plan의 구현은 blocked이며 사용자 리뷰 요청이 아니라 dependency 대기 상태다. + +### [API-1] Gito HTTP webhook receiver + +#### 문제 + +`services/core/internal/http/router.go:23`에는 healthz와 Plane webhook만 public route로 등록되어 있다. Gito HTTP webhook callback route가 없어서 signed `branch.updated` delivery를 받을 수 없다. + +Before: + +```go +// services/core/internal/http/router.go:23 +r.Get("/healthz", handler.Healthz) +r.Post("/api/integrations/plane/webhook", handler.ReceivePlaneWebhook) +``` + +#### 해결 방법 + +- `services/core/internal/http/gito_webhook.go`를 추가한다. +- public route `POST /api/integrations/gito/webhook`를 등록한다. +- `ReceiveGitoWebhook`는: + - `GitoWebhookConfig.Secret` 없으면 `503`. + - body max size는 Plane과 같은 1MiB 기준을 쓰거나 `maxGitoWebhookBodyBytes`를 둔다. + - `X-Gito-Signature`를 HMAC-SHA256으로 검증하고 `sha256=` prefix와 raw hex를 모두 허용한다. + - `X-Gito-Event`가 `branch.updated`가 아니면 `400`으로 reject하고 handler를 호출하지 않는다. + - body는 `gitoevents.DecodeBranchUpdatedPayload`로 decode한다. + - target repo/branch가 아니면 `202 {"status":"ignored"}`로 side effect 없이 ack한다. + - valid on-target event는 configured `GitoBranchEventHandler.Handle`에 전달한다. + +After outline: + +```go +r.Post("/api/integrations/plane/webhook", handler.ReceivePlaneWebhook) +r.Post("/api/integrations/gito/webhook", handler.ReceiveGitoWebhook) +``` + +#### 수정 파일 및 체크리스트 + +- [ ] `services/core/internal/http/router.go`: Gito webhook public route 추가. +- [ ] `services/core/internal/http/handlers.go`: Handler fields와 setter seam 추가. +- [ ] `services/core/internal/http/gito_webhook.go`: receiver, signature validation, response mapping. +- [ ] `services/core/internal/http/gito_webhook_test.go`: accepted/wrong event/invalid signature/route bypass tests. + +#### 테스트 작성 + +- 작성한다. +- `TestReceiveGitoWebhookAcceptsSignedBranchUpdatedDelivery`: valid signature and header event calls fake handler once. +- `TestReceiveGitoWebhookRejectsWrongEventWithoutHandlerCall`: wrong header event returns 400 and no handler call. +- `TestReceiveGitoWebhookRejectsInvalidSignatureWithoutHandlerCall`: invalid signature returns 401 and no handler call. +- `TestGitoWebhookRouteBypassesBasicAuth`: route mounted outside protected router. + +#### 중간 검증 + +```bash +cd services/core && go test -count=1 ./internal/http +``` + +예상: PASS. Go test cache output은 허용하지 않는다. + +### [API-2] Delivery/revision duplicate guard + +#### 문제 + +`services/core/internal/gitosync/bridge.go:29`의 duplicate guard는 `(repo, branch, after)` revision만 처리한다. Milestone Task는 `X-Gito-Delivery`와 revision key 양쪽 duplicate를 중복 enqueue 없이 처리하라고 요구한다. + +Before: + +```go +// services/core/internal/gitosync/bridge.go:29 +// ProcessedRevisionStore is the duplicate guard seam: it reports whether a +// (repo, branch, after) revision has already been processed and records it. +// MarkProcessed must only be called after the revision's enqueue work +// succeeded, so a failed run is retried rather than silently swallowed. +type ProcessedRevisionStore interface { + Seen(repoID, branch, after string) bool + MarkProcessed(repoID, branch, after string) +} +``` + +#### 해결 방법 + +- HTTP layer에 process-lifetime dedupe store를 둔다. Bridge의 revision guard는 유지한다. +- duplicate keys: + - `delivery:` when header exists. + - `revision:::` always when `after` exists. +- valid on-target delivery가 handler `Handle`에서 nil을 반환한 뒤 keys를 mark한다. +- sequential duplicate request는 handler를 다시 호출하지 않고 `202 {"status":"duplicate"}` 또는 `{"status":"accepted","duplicate":"true"}` 중 하나로 응답한다. 응답 shape는 tests와 docs 없이 내부 contract로만 고정한다. +- handler error에서는 mark하지 않아 retry 가능성을 보존한다. + +#### 수정 파일 및 체크리스트 + +- [ ] `services/core/internal/http/gito_webhook.go`: dedupe store, key construction, mark policy. +- [ ] `services/core/internal/http/gito_webhook_test.go`: duplicate delivery id and duplicate revision tests. + +#### 테스트 작성 + +- 작성한다. +- `TestReceiveGitoWebhookDeduplicatesSameDeliveryID`: same header twice -> fake handler call count 1. +- `TestReceiveGitoWebhookDeduplicatesSameRevisionWithoutDeliveryID`: no delivery header, same repo/branch/after twice -> fake handler call count 1. + +#### 중간 검증 + +```bash +cd services/core && go test -count=1 ./internal/http +``` + +예상: PASS. Go test cache output은 허용하지 않는다. + +### [API-3] Config/server wiring + +#### 문제 + +`services/core/internal/config/config.go:43`는 Gito proto-socket URL 중심 config만 갖고 있고, `services/core/cmd/server/main.go:120`은 proto-socket runner만 시작한다. HTTP receiver가 `gitosync.Bridge`로 들어갈 server wiring이 없다. + +Before: + +```go +// services/core/internal/config/config.go:43 +GitoProtoSocketURL string +GitoRepoID string +GitoBranch string +GitoDevelopRepoPath string +GitoRemoteName string +RoadmapCreationTodoStateID string +``` + +```go +// services/core/cmd/server/main.go:120 +// Gito branch event consumer: drives Plane-origin Milestone creation sync +// from a develop push. Started only when the endpoint, repo, local develop +// checkout, and Todo state id are all configured; otherwise the Core server +// behaves exactly as before. Cancelled with the root context on shutdown. +if cfg.GitoBranchEventsEnabled() { + gitoRunner, err := newGitoRunner(cfg, planeClient, taskScheduler, logger) +``` + +#### 해결 방법 + +- Config에 `GitoWebhookSecret string`를 추가하고 `GITO_WEBHOOK_SECRET`에서 로드한다. +- `GitoWebhookConsumerEnabled()`를 추가한다. Required fields: secret, repo id, develop repo path, roadmap todo state id. Branch defaults to develop. +- `newGitoBridge(cfg, reader, enqueuer, logger) (*gitosync.Bridge, error)` helper를 만들어 scanner/bridge assembly를 proto runner와 HTTP receiver가 공유할 수 있게 한다. +- `newGitoRunner`는 bridge를 받아 `gitosync.NewRunner(cfg.GitoProtoSocketURL, ...)`만 담당하게 축소한다. +- server run에서 handler 생성 후 `handler.SetGitoWebhookConfig(...)`와 `handler.SetGitoBranchEventHandler(gitoBridge)`를 설정한다. +- 기존 proto-socket runner는 제거하지 않는다. `config-docs` Task에서 docs/env 교체를 다룬다. + +#### 수정 파일 및 체크리스트 + +- [ ] `services/core/internal/config/config.go`: env field/load/enabled method. +- [ ] `services/core/internal/config/config_test.go`: defaults and enabled requirements. +- [ ] `services/core/cmd/server/main.go`: bridge factoring and handler wiring. + +#### 테스트 작성 + +- 작성한다. +- `TestConfigGitoWebhookDefaultsAndDisabled`: default secret empty and disabled. +- `TestConfigGitoWebhookConsumerEnabledRequiresInputs`: secret/repo/develop/todo required, branch default accepted. +- `cmd/server`는 helper unit seam이 작으면 test 추가, 아니면 `go test ./cmd/server` compile check로 검증한다. + +#### 중간 검증 + +```bash +cd services/core && go test -count=1 ./internal/config ./cmd/server +``` + +예상: PASS. Go test cache output은 허용하지 않는다. + +## 수정 파일 요약 + +| 파일 | 항목 | +|------|------| +| `services/core/internal/http/router.go` | API-1 | +| `services/core/internal/http/handlers.go` | API-1, API-2 | +| `services/core/internal/http/gito_webhook.go` | API-1, API-2 | +| `services/core/internal/http/gito_webhook_test.go` | API-1, API-2 | +| `services/core/internal/config/config.go` | API-3 | +| `services/core/internal/config/config_test.go` | API-3 | +| `services/core/cmd/server/main.go` | API-3 | + +## 최종 검증 + +```bash +cd services/core && go test -count=1 ./internal/http ./internal/config ./cmd/server +cd services/core && go test -count=1 ./... +``` + +예상: 두 명령 모두 PASS. Go test cache output은 허용하지 않는다. + +모든 코드 변경 완료 후 반드시 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 채운다. 이 파일 작성이 구현의 마지막 단계다. diff --git a/agent-task/archive/2026/06/m-gito-http-webhook-consumer-readiness/02+01_http_receiver_idempotency/plan_local_G06_1.log b/agent-task/archive/2026/06/m-gito-http-webhook-consumer-readiness/02+01_http_receiver_idempotency/plan_local_G06_1.log new file mode 100644 index 0000000..18ec0c7 --- /dev/null +++ b/agent-task/archive/2026/06/m-gito-http-webhook-consumer-readiness/02+01_http_receiver_idempotency/plan_local_G06_1.log @@ -0,0 +1,108 @@ + + +# PLAN local-G06 - REVIEW_API + +## 이 파일을 읽는 구현 에이전트에게 + +이 작업은 직전 code-review FAIL의 Required issue만 좁게 해결한다. 구현 후 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 실제 변경 내용과 검증 출력으로 채워야 완료된다. 구현 에이전트는 검증을 실행하고 active 파일을 유지한 채 리뷰 준비를 보고한다. 종결, 로그 rename, `complete.log`, task directory archive는 code-review 스킬 전용이다. 선택된 SDD 결정이나 Milestone `구현 잠금 > 결정 필요` 항목이 실구현을 막을 때만 review stub의 `사용자 리뷰 요청` 섹션을 채우고 멈춘다. 구현 중 사용자에게 직접 묻거나 선택지를 제시하거나 `request_user_input`을 호출하지 않는다. + +## Roadmap Targets + +- Milestone: `agent-roadmap/phase/agent-ops-mcp-control-plane/milestones/gito-http-webhook-consumer-readiness.md` +- Task ids: + - `receiver`: Gito `branch.updated` HTTP callback endpoint를 추가한다. + - `idempotency`: `X-Gito-Delivery`와 revision key 기준으로 duplicate delivery를 중복 enqueue 없이 처리한다. +- Completion mode: check-on-pass + +## Spec Targets + +- SDD: `agent-roadmap/sdd/agent-ops-mcp-control-plane/gito-http-webhook-consumer-readiness/SDD.md` +- Acceptance scenarios: + - `S01`: task=`receiver`; evidence=`HTTP handler unit/integration test, invalid signature test` + - `S03`: task=`idempotency`; evidence=`duplicate delivery/revision idempotency test` +- Completion mode: spec-check-on-pass + +## Archive Evidence Snapshot + +- Archived plan: `agent-task/m-gito-http-webhook-consumer-readiness/02+01_http_receiver_idempotency/plan_local_G06_0.log` +- Archived review: `agent-task/m-gito-http-webhook-consumer-readiness/02+01_http_receiver_idempotency/code_review_local_G06_0.log` +- Verdict: FAIL +- Issue summary: + - Required: `services/core/internal/http/gito_webhook.go:97` local JSON decode lost `gitoevents.DecodeBranchUpdatedPayload` validation for body `type` and malformed `changed_files`, allowing contract-invalid `branch.updated` payloads into accepted/handler path. + - Suggested: 없음 + - Nit: 없음 +- Affected files: + - `services/core/internal/http/gito_webhook.go` + - `services/core/internal/http/gito_webhook_test.go` +- Verification evidence: + - `git diff --check` - PASS, no output. + - `cd services/core && go test -count=1 ./internal/http ./internal/config ./cmd/server` - PASS. + - `cd services/core && go test -count=1 ./...` - PASS. + - Existing tests are insufficient because they do not cover wrong body `type` or malformed `changed_files` through the HTTP receiver. +- Roadmap/spec carryover: + - Roadmap Targets remain `receiver` and `idempotency`. + - Spec Targets remain S01 and S03. This follow-up directly repairs S01; S03 should remain green. +- Narrow reread allowed if needed: + - `agent-task/m-gito-http-webhook-consumer-readiness/02+01_http_receiver_idempotency/plan_local_G06_0.log` + - `agent-task/m-gito-http-webhook-consumer-readiness/02+01_http_receiver_idempotency/code_review_local_G06_0.log` + +## 범위 결정 근거 + +- 포함: HTTP receiver body payload validation and receiver tests for contract-invalid payloads. +- 제외: Gito outbound producer/subscription, full scan-enqueue proof, docs/env samples, and unrelated idempotency behavior changes. +- 새 package는 가능하면 만들지 말고 `services/core/internal/http/gito_webhook.go` 안에서 dependency-light local validation을 보강한다. import cycle 없이 공통화가 더 단순하다고 판단되는 경우에만 최소 파일 추가를 선택하고 이유를 review stub에 남긴다. + +## 구현 체크리스트 + +- [ ] Gito HTTP webhook body validation이 `repo_id`/`branch` 필수 검증을 유지하면서 body `type`이 있으면 `branch.updated`만 허용하고, malformed `changed_files`를 400으로 reject하도록 보강한다. +- [ ] wrong body `type`과 malformed `changed_files`가 handler를 호출하지 않음을 검증하는 HTTP receiver tests를 추가하고 기존 accepted/duplicate tests가 계속 통과하게 한다. +- [ ] `cd services/core && go test -count=1 ./internal/http ./internal/config ./cmd/server`를 실행한다. +- [ ] `cd services/core && go test -count=1 ./...`를 실행한다. +- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다. + +## [REVIEW_API-1] HTTP payload contract validation 복구 + +### 문제 + +직전 구현은 import cycle을 피하려고 HTTP receiver에서 로컬 `gitoWebhookPayload` struct를 사용했다. 하지만 현재 struct는 `repo_id`, `branch`, `before`, `after`만 decode하므로, 이전 subtask가 만든 `gitoevents.DecodeBranchUpdatedPayload`의 다음 검증이 HTTP path에서 빠졌다. + +- body `type`이 present이면 `branch.updated`여야 한다. +- `changed_files`가 present이면 list of objects여야 한다. + +이 때문에 header `X-Gito-Event: branch.updated`가 맞는 malformed body가 accepted path로 들어갈 수 있다. + +### 해결 방법 + +- `services/core/internal/http/gito_webhook.go`의 local decoder를 보강한다. +- `gitoWebhookPayload`에 `Type`과 `ChangedFiles` validation path를 추가하거나, 별도 local `decodeGitoWebhookPayload` helper를 만든다. +- 검증 의미: + - invalid JSON: 400, handler call 없음. + - missing `repo_id` or `branch`: 400, handler call 없음. + - body `type` present and not `branch.updated`: 400, handler call 없음. + - `changed_files` present but not a list of objects: 400, handler call 없음. + - valid minimal base payload without NomadCode-specific fields: accepted behavior 유지. +- import cycle을 다시 만들지 않는다. + +### 테스트 결정 + +- `services/core/internal/http/gito_webhook_test.go`에 다음 테스트를 추가한다. + - `TestReceiveGitoWebhookRejectsWrongBodyTypeWithoutHandlerCall` + - `TestReceiveGitoWebhookRejectsMalformedChangedFilesWithoutHandlerCall` +- 기존 accepted, wrong event, invalid signature, delivery duplicate, revision duplicate tests가 계속 통과해야 한다. + +### 중간 검증 + +```bash +cd services/core && go test -count=1 ./internal/http +``` + +예상: PASS. Go test cache output은 허용하지 않는다. + +## 최종 검증 + +```bash +cd services/core && go test -count=1 ./internal/http ./internal/config ./cmd/server +cd services/core && go test -count=1 ./... +``` + +예상: 두 명령 모두 PASS. Go test cache output은 허용하지 않는다. diff --git a/agent-task/archive/2026/06/m-gito-http-webhook-consumer-readiness/02+01_http_receiver_idempotency/plan_local_G06_2.log b/agent-task/archive/2026/06/m-gito-http-webhook-consumer-readiness/02+01_http_receiver_idempotency/plan_local_G06_2.log new file mode 100644 index 0000000..9240af5 --- /dev/null +++ b/agent-task/archive/2026/06/m-gito-http-webhook-consumer-readiness/02+01_http_receiver_idempotency/plan_local_G06_2.log @@ -0,0 +1,101 @@ + + +# PLAN local-G06 - REVIEW_REVIEW_API + +## 이 파일을 읽는 구현 에이전트에게 + +이 작업은 직전 code-review FAIL의 Required issue만 좁게 해결한다. 구현 후 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 실제 변경 내용과 검증 출력으로 채워야 완료된다. 구현 에이전트는 검증을 실행하고 active 파일을 유지한 채 리뷰 준비를 보고한다. 종결, 로그 rename, `complete.log`, task directory archive는 code-review 스킬 전용이다. 선택된 SDD 결정이나 Milestone `구현 잠금 > 결정 필요` 항목이 실구현을 막을 때만 review stub의 `사용자 리뷰 요청` 섹션을 채우고 멈춘다. 구현 중 사용자에게 직접 묻거나 선택지를 제시하거나 `request_user_input`을 호출하지 않는다. + +## Roadmap Targets + +- Milestone: `agent-roadmap/phase/agent-ops-mcp-control-plane/milestones/gito-http-webhook-consumer-readiness.md` +- Task ids: + - `receiver`: Gito `branch.updated` HTTP callback endpoint를 추가한다. + - `idempotency`: `X-Gito-Delivery`와 revision key 기준으로 duplicate delivery를 중복 enqueue 없이 처리한다. +- Completion mode: check-on-pass + +## Spec Targets + +- SDD: `agent-roadmap/sdd/agent-ops-mcp-control-plane/gito-http-webhook-consumer-readiness/SDD.md` +- Acceptance scenarios: + - `S01`: task=`receiver`; evidence=`HTTP handler unit/integration test, invalid signature test` + - `S03`: task=`idempotency`; evidence=`duplicate delivery/revision idempotency test` +- Completion mode: spec-check-on-pass + +## Archive Evidence Snapshot + +- Archived plan: `agent-task/m-gito-http-webhook-consumer-readiness/02+01_http_receiver_idempotency/plan_local_G06_1.log` +- Archived review: `agent-task/m-gito-http-webhook-consumer-readiness/02+01_http_receiver_idempotency/code_review_local_G06_1.log` +- Verdict: FAIL +- Issue summary: + - Required: `services/core/internal/http/gito_webhook.go:96` validates `changed_files` only as an array, not as a list of objects, so `{"changed_files":["not-an-object"]}` can still enter the accepted/handler path even though the SDD and `gitoevents.DecodeBranchUpdatedPayload` contract reject non-object items. + - Suggested: 없음 + - Nit: 없음 +- Affected files: + - `services/core/internal/http/gito_webhook.go` + - `services/core/internal/http/gito_webhook_test.go` +- Verification evidence: + - `cd services/core && go test -count=1 ./internal/http ./internal/config ./cmd/server` - PASS. + - `cd services/core && go test -count=1 ./...` - PASS. + - Existing HTTP receiver tests are insufficient because they cover `changed_files` as a non-list string but not a list containing non-object elements. +- Roadmap/spec carryover: + - Roadmap Targets remain `receiver` and `idempotency`. + - Spec Targets remain S01 and S03. This follow-up directly repairs S01; S03 should remain green. +- Narrow reread allowed if needed: + - `agent-task/m-gito-http-webhook-consumer-readiness/02+01_http_receiver_idempotency/plan_local_G06_1.log` + - `agent-task/m-gito-http-webhook-consumer-readiness/02+01_http_receiver_idempotency/code_review_local_G06_1.log` + +## 범위 결정 근거 + +- 포함: HTTP receiver `changed_files` item-type validation and the matching receiver test proving handler is not called. +- 제외: Gito outbound producer/subscription, scan/enqueue bridge behavior, config/docs, delivery/revision idempotency semantics, and unrelated roadmap/SDD edits. +- 새 package는 만들지 않는다. 기존 local validator 안에서 `gitoevents.decodeChangedFiles`의 item object check와 같은 의미를 최소 구현한다. + +## 구현 체크리스트 + +- [ ] `validateBranchUpdatedPayload`가 `changed_files` present 시 배열 여부뿐 아니라 각 element가 JSON object인지 검증하고, non-object element를 400 reject하도록 보강한다. +- [ ] `changed_files` 배열 안의 non-object item이 400이고 handler를 호출하지 않음을 검증하는 HTTP receiver test를 추가하며 기존 wrong body type, malformed string, accepted/duplicate tests가 계속 통과하게 한다. +- [ ] `cd services/core && go test -count=1 ./internal/http`를 실행한다. +- [ ] `cd services/core && go test -count=1 ./internal/http ./internal/config ./cmd/server`를 실행한다. +- [ ] `cd services/core && go test -count=1 ./...`를 실행한다. +- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다. + +## [REVIEW_REVIEW_API-1] `changed_files` element contract validation + +### 문제 + +현재 HTTP receiver validator는 `changed_files`가 JSON array인지까지만 확인한다. 하지만 SDD와 `gitoevents.DecodeBranchUpdatedPayload` 기준은 `changed_files`가 present이면 list of objects여야 한다. 따라서 아래 payload는 현재 HTTP path에서 invalid로 거부되어야 하지만 통과할 수 있다. + +```json +{"repo_id":"target-repo","branch":"develop","changed_files":["not-an-object"]} +``` + +### 해결 방법 + +- `services/core/internal/http/gito_webhook.go`의 `validateBranchUpdatedPayload`에서 `p.ChangedFiles.([]interface{})`를 얻은 뒤 각 item이 `map[string]interface{}`인지 확인한다. +- non-object item 발견 시 `payload changed_files[] is not an object`처럼 원인을 알 수 있는 error를 반환한다. +- 검증 실패는 기존 path처럼 HTTP 400과 handler call 없음으로 종료한다. +- `gitoevents` package import cycle을 만들지 않는다. + +### 테스트 결정 + +- `services/core/internal/http/gito_webhook_test.go`에 `TestReceiveGitoWebhookRejectsMalformedChangedFileItemWithoutHandlerCall`을 추가한다. +- payload는 signed `branch.updated` 요청으로 만들고 `changed_files`에 string element를 넣는다. +- 기대값은 HTTP 400, fake handler call count 0이다. + +### 중간 검증 + +```bash +cd services/core && go test -count=1 ./internal/http +``` + +예상: PASS. Go test cache output은 허용하지 않는다. + +## 최종 검증 + +```bash +cd services/core && go test -count=1 ./internal/http ./internal/config ./cmd/server +cd services/core && go test -count=1 ./... +``` + +예상: 두 명령 모두 PASS. Go test cache output은 허용하지 않는다. diff --git a/services/core/cmd/server/main.go b/services/core/cmd/server/main.go index 5a7d2fb..53ff39c 100644 --- a/services/core/cmd/server/main.go +++ b/services/core/cmd/server/main.go @@ -117,12 +117,23 @@ func run(logger *slog.Logger) error { workflowService.SetEnqueuer(taskScheduler) - // Gito branch event consumer: drives Plane-origin Milestone creation sync - // from a develop push. Started only when the endpoint, repo, local develop - // checkout, and Todo state id are all configured; otherwise the Core server - // behaves exactly as before. Cancelled with the root context on shutdown. + // Build a shared Gito bridge when either the proto-socket runner or the HTTP + // webhook consumer is enabled. Both share the same scanner/bridge assembly. + var gitoBridge *gitosync.Bridge + if cfg.GitoBranchEventsEnabled() || cfg.GitoWebhookConsumerEnabled() { + var err error + gitoBridge, err = newGitoBridge(cfg, planeClient, taskScheduler, logger) + if err != nil { + return err + } + } + + // Gito branch event consumer (proto-socket): drives Plane-origin Milestone + // creation sync from a develop push. Started only when the endpoint, repo, + // local develop checkout, and Todo state id are all configured; otherwise the + // Core server behaves exactly as before. Cancelled with the root context on shutdown. if cfg.GitoBranchEventsEnabled() { - gitoRunner, err := newGitoRunner(cfg, planeClient, taskScheduler, logger) + gitoRunner, err := newGitoRunner(cfg, gitoBridge, logger) if err != nil { return err } @@ -152,6 +163,16 @@ func run(logger *slog.Logger) error { AgentAssigneeID: cfg.PlaneAgentAssigneeID, SelfActorID: cfg.PlaneSelfActorID, }) + if cfg.GitoWebhookConsumerEnabled() { + handler.SetGitoWebhookConfig(apphttp.GitoWebhookConfig{ + Secret: cfg.GitoWebhookSecret, + RepoID: cfg.GitoRepoID, + Branch: cfg.GitoBranch, + }) + handler.SetGitoBranchEventHandler(&gitoBridgeHTTPAdapter{bridge: gitoBridge}) + logger.Info("gito http webhook consumer enabled", + "repo_id", cfg.GitoRepoID, "branch", cfg.GitoBranch) + } protosocket.NewTaskChannels(workflowService).Register(protoSocketServer.Dispatcher()) @@ -195,10 +216,11 @@ func run(logger *slog.Logger) error { return nil } -// newGitoRunner assembles the Gito branch event consumer: an exec-backed develop -// scanner, the Plane work item reader, the scheduler enqueuer, and an in-memory -// duplicate-revision guard, wired into a proto-socket runner. -func newGitoRunner(cfg config.Config, reader *plane.Client, enqueuer *scheduler.Client, logger *slog.Logger) (*gitoevents.Client, error) { +// newGitoBridge assembles the shared Gito bridge: an exec-backed develop scanner, +// the Plane work item reader, the scheduler enqueuer, and an in-memory +// duplicate-revision guard. Both the proto-socket runner and the HTTP webhook +// consumer call this and share the resulting bridge. +func newGitoBridge(cfg config.Config, reader *plane.Client, enqueuer *scheduler.Client, logger *slog.Logger) (*gitosync.Bridge, error) { scanner, err := gitosync.NewBranchRevisionScanner(gitosync.ExecCommandRunner{}, gitosync.ScannerConfig{ DevelopRepoPath: cfg.GitoDevelopRepoPath, RemoteName: cfg.GitoRemoteName, @@ -207,13 +229,30 @@ func newGitoRunner(cfg config.Config, reader *plane.Client, enqueuer *scheduler. if err != nil { return nil, err } - bridge, err := gitosync.NewBridge(scanner, reader, enqueuer, nil, gitosync.BridgeConfig{ + return gitosync.NewBridge(scanner, reader, enqueuer, nil, gitosync.BridgeConfig{ RepoID: cfg.GitoRepoID, Branch: cfg.GitoBranch, TodoStateID: cfg.RoadmapCreationTodoStateID, }, logger) - if err != nil { - return nil, err - } +} + +// newGitoRunner wires a pre-built bridge into a proto-socket runner. +func newGitoRunner(cfg config.Config, bridge *gitosync.Bridge, logger *slog.Logger) (*gitoevents.Client, error) { return gitosync.NewRunner(cfg.GitoProtoSocketURL, cfg.GitoRepoID, cfg.GitoBranch, bridge, logger) } + +// gitoBridgeHTTPAdapter adapts *gitosync.Bridge to apphttp.GitoBranchEventHandler. +// The http package defines its own event struct to avoid an import cycle between +// internal/http → internal/gitoevents → internal/protosocket ← protosocket tests → internal/http. +type gitoBridgeHTTPAdapter struct { + bridge *gitosync.Bridge +} + +func (a *gitoBridgeHTTPAdapter) Handle(ctx context.Context, ev apphttp.GitoBranchUpdatedEvent) error { + return a.bridge.Handle(ctx, gitoevents.BranchUpdatedEvent{ + RepoID: ev.RepoID, + Branch: ev.Branch, + Before: ev.Before, + After: ev.After, + }) +} diff --git a/services/core/internal/config/config.go b/services/core/internal/config/config.go index da02c9c..c0c4d77 100644 --- a/services/core/internal/config/config.go +++ b/services/core/internal/config/config.go @@ -41,6 +41,7 @@ type Config struct { ProtoSocketHeartbeatIntervalSec int ProtoSocketHeartbeatWaitSec int GitoProtoSocketURL string + GitoWebhookSecret string GitoRepoID string GitoBranch string GitoDevelopRepoPath string @@ -59,6 +60,16 @@ func (c Config) GitoBranchEventsEnabled() bool { c.RoadmapCreationTodoStateID != "" } +// GitoWebhookConsumerEnabled reports whether the Gito HTTP webhook receiver +// should be wired. Requires secret, repo id, develop repo path, and todo state +// id. Branch defaults to "develop" and is not required. +func (c Config) GitoWebhookConsumerEnabled() bool { + return c.GitoWebhookSecret != "" && + c.GitoRepoID != "" && + c.GitoDevelopRepoPath != "" && + c.RoadmapCreationTodoStateID != "" +} + func Load() Config { return Config{ AppEnv: getEnv("APP_ENV", "local"), @@ -96,6 +107,7 @@ func Load() Config { ProtoSocketHeartbeatIntervalSec: getEnvInt("PROTO_SOCKET_HEARTBEAT_INTERVAL_SEC", 30), ProtoSocketHeartbeatWaitSec: getEnvInt("PROTO_SOCKET_HEARTBEAT_WAIT_SEC", 10), GitoProtoSocketURL: os.Getenv("GITO_PROTO_SOCKET_URL"), + GitoWebhookSecret: os.Getenv("GITO_WEBHOOK_SECRET"), GitoRepoID: os.Getenv("GITO_REPO_ID"), GitoBranch: getEnv("GITO_BRANCH", "develop"), GitoDevelopRepoPath: os.Getenv("GITO_DEVELOP_REPO_PATH"), diff --git a/services/core/internal/config/config_test.go b/services/core/internal/config/config_test.go index 3797dea..33b5e87 100644 --- a/services/core/internal/config/config_test.go +++ b/services/core/internal/config/config_test.go @@ -274,6 +274,44 @@ func TestConfigLoadsPlaneWebhookDispatchEnv(t *testing.T) { } } +func TestConfigGitoWebhookDefaultsAndDisabled(t *testing.T) { + cfg := Load() + + if cfg.GitoWebhookSecret != "" { + t.Fatalf("GitoWebhookSecret default: got %q, want empty", cfg.GitoWebhookSecret) + } + if cfg.GitoWebhookConsumerEnabled() { + t.Fatal("GitoWebhookConsumerEnabled: want false when secret is unset") + } +} + +func TestConfigGitoWebhookConsumerEnabledRequiresInputs(t *testing.T) { + t.Setenv("GITO_WEBHOOK_SECRET", "webhook-secret") + if Load().GitoWebhookConsumerEnabled() { + t.Fatal("GitoWebhookConsumerEnabled: want false when repo id is unset") + } + + t.Setenv("GITO_REPO_ID", "nomadcode") + if Load().GitoWebhookConsumerEnabled() { + t.Fatal("GitoWebhookConsumerEnabled: want false when develop repo path is unset") + } + + t.Setenv("GITO_DEVELOP_REPO_PATH", "/srv/develop") + if Load().GitoWebhookConsumerEnabled() { + t.Fatal("GitoWebhookConsumerEnabled: want false when todo state id is unset") + } + + t.Setenv("ROADMAP_CREATION_TODO_STATE_ID", "state-todo") + cfg := Load() + + if cfg.GitoWebhookSecret != "webhook-secret" { + t.Fatalf("GitoWebhookSecret: got %q", cfg.GitoWebhookSecret) + } + if !cfg.GitoWebhookConsumerEnabled() { + t.Fatal("GitoWebhookConsumerEnabled: want true when all inputs set") + } +} + func TestConfigJiraFieldsAreEmptyByDefault(t *testing.T) { cfg := Load() diff --git a/services/core/internal/gitoevents/events.go b/services/core/internal/gitoevents/events.go index 064bf37..ff7b982 100644 --- a/services/core/internal/gitoevents/events.go +++ b/services/core/internal/gitoevents/events.go @@ -73,42 +73,50 @@ func BuildSubscribeEnvelope(id, repoID, branch string) protosocket.Envelope { } } -// DecodeBranchUpdatedEnvelope validates the envelope channel/action and decodes -// its payload into a BranchUpdatedEvent. It rejects wrong channel/action and -// malformed payloads with a descriptive error instead of returning a partial -// event. -func DecodeBranchUpdatedEnvelope(env protosocket.Envelope) (BranchUpdatedEvent, error) { - var ev BranchUpdatedEvent - - if env.Channel != EventChannel { - return ev, fmt.Errorf("gitoevents: unexpected channel %q, want %q", env.Channel, EventChannel) - } - if env.Action != BranchUpdatedAction { - return ev, fmt.Errorf("gitoevents: unexpected action %q, want %q", env.Action, BranchUpdatedAction) - } - if env.Payload == nil { - return ev, fmt.Errorf("gitoevents: missing payload") +// DecodeBranchUpdatedPayload decodes a `branch.updated` base payload into a +// BranchUpdatedEvent. It validates required fields (repo_id, branch) and the +// type field, and decodes optional fields without requiring them. This function +// has no envelope knowledge so it can be shared between proto-socket envelope +// and raw HTTP JSON body consumers. +// +// Constraints: +// +// - type must be "branch.updated" if present +// - repo_id and branch are required +// - changed_files must be a list of objects if present +func DecodeBranchUpdatedPayload(payload map[string]any) (BranchUpdatedEvent, error) { + if payload == nil { + return BranchUpdatedEvent{}, fmt.Errorf("gitoevents: nil payload") } - ev.ID = stringField(env.Payload, "id") - ev.Type = stringField(env.Payload, "type") - ev.Provider = stringField(env.Payload, "provider") - ev.DeliveryID = stringField(env.Payload, "delivery_id") - ev.RepoID = stringField(env.Payload, "repo_id") - ev.Branch = stringField(env.Payload, "branch") - ev.Before = stringField(env.Payload, "before") - ev.After = stringField(env.Payload, "after") - ev.ObservedAt = stringField(env.Payload, "observed_at") - ev.CreatedAt = stringField(env.Payload, "created_at") + ev := BranchUpdatedEvent{} + ev.ID = stringField(payload, "id") + ev.Type = stringField(payload, "type") + ev.Provider = stringField(payload, "provider") + ev.DeliveryID = stringField(payload, "delivery_id") + ev.RepoID = stringField(payload, "repo_id") + ev.Branch = stringField(payload, "branch") + ev.Before = stringField(payload, "before") + ev.After = stringField(payload, "after") + ev.ObservedAt = stringField(payload, "observed_at") + ev.CreatedAt = stringField(payload, "created_at") + + // Validate required fields if ev.RepoID == "" { - return BranchUpdatedEvent{}, fmt.Errorf("gitoevents: payload missing repo_id") + return BranchUpdatedEvent{}, fmt.Errorf("gitoevents: payload missing required field repo_id") } if ev.Branch == "" { - return BranchUpdatedEvent{}, fmt.Errorf("gitoevents: payload missing branch") + return BranchUpdatedEvent{}, fmt.Errorf("gitoevents: payload missing required field branch") } - files, err := decodeChangedFiles(env.Payload["changed_files"]) + // Validate type field if present + if ev.Type != "" && ev.Type != BranchUpdatedAction { + return BranchUpdatedEvent{}, fmt.Errorf("gitoevents: payload type %q, want %q", ev.Type, BranchUpdatedAction) + } + + // Decode changed_files (optional but must be valid if present) + files, err := decodeChangedFiles(payload["changed_files"]) if err != nil { return BranchUpdatedEvent{}, err } @@ -117,6 +125,24 @@ func DecodeBranchUpdatedEnvelope(env protosocket.Envelope) (BranchUpdatedEvent, return ev, nil } +// DecodeBranchUpdatedEnvelope validates the envelope channel/action and decodes +// its payload into a BranchUpdatedEvent. It rejects wrong channel/action and +// malformed payloads with a descriptive error instead of returning a partial +// event. +func DecodeBranchUpdatedEnvelope(env protosocket.Envelope) (BranchUpdatedEvent, error) { + if env.Channel != EventChannel { + return BranchUpdatedEvent{}, fmt.Errorf("gitoevents: unexpected channel %q, want %q", env.Channel, EventChannel) + } + if env.Action != BranchUpdatedAction { + return BranchUpdatedEvent{}, fmt.Errorf("gitoevents: unexpected action %q, want %q", env.Action, BranchUpdatedAction) + } + if env.Payload == nil { + return BranchUpdatedEvent{}, fmt.Errorf("gitoevents: missing payload") + } + + return DecodeBranchUpdatedPayload(env.Payload) +} + // IsTarget reports whether the event matches the watched repo and branch. func (e BranchUpdatedEvent) IsTarget(repoID, branch string) bool { if branch == "" { diff --git a/services/core/internal/gitoevents/events_test.go b/services/core/internal/gitoevents/events_test.go index a700726..95d8f0d 100644 --- a/services/core/internal/gitoevents/events_test.go +++ b/services/core/internal/gitoevents/events_test.go @@ -1,6 +1,7 @@ package gitoevents import ( + "strings" "testing" "github.com/nomadcode/nomadcode-core/internal/protosocket" @@ -183,6 +184,202 @@ func TestDecodeBranchUpdatedEnvelopeRejectsMissingRepoID(t *testing.T) { } } +func TestDecodeBranchUpdatedPayloadAcceptsBasePayloadWithoutNomadCodeFields(t *testing.T) { + // Gito base JSON shape: minimal fields without NomadCode-specific extensions. + payload := map[string]any{ + "id": "event-123", + "type": "branch.updated", + "provider": "forgejo", + "repo_id": "nomadcode", + "branch": "develop", + "before": "old-sha", + "after": "new-sha", + "changed_files": []any{ + map[string]any{ + "path": "agent-roadmap/phase/example/milestones/example.md", + "change_type": "modified", + }, + map[string]any{ + "path": "README.md", + "change_type": "modified", + }, + }, + "observed_at": "2026-06-13T00:00:00Z", + "created_at": "2026-06-13T00:00:00Z", + } + + ev, err := DecodeBranchUpdatedPayload(payload) + if err != nil { + t.Fatalf("decode base payload: unexpected error: %v", err) + } + if ev.RepoID != "nomadcode" || ev.Branch != "develop" || ev.After != "new-sha" { + t.Fatalf("decoded event: got %+v", ev) + } + if ev.ID != "event-123" || ev.Provider != "forgejo" { + t.Fatalf("id/provider: got %+v", ev) + } + if len(ev.ChangedFiles) != 2 { + t.Fatalf("changed_files: got %d, want 2", len(ev.ChangedFiles)) + } + if ev.DeliveryID != "" { + t.Fatalf("delivery_id should be empty for minimal payload, got %q", ev.DeliveryID) + } +} + +func TestDecodeBranchUpdatedPayloadRejectsWrongType(t *testing.T) { + payload := map[string]any{ + "id": "event-456", + "type": "task.status.changed", + "repo_id": "nomadcode", + "branch": "develop", + "after": "new-sha", + } + + _, err := DecodeBranchUpdatedPayload(payload) + if err == nil { + t.Fatal("expected error for wrong type") + } + if !strings.Contains(err.Error(), "type") { + t.Fatalf("error should mention type validation: %v", err) + } +} + +func TestDecodeBranchUpdatedPayloadRejectsMalformedChangedFiles(t *testing.T) { + payload := map[string]any{ + "id": "event-789", + "type": "branch.updated", + "repo_id": "nomadcode", + "branch": "develop", + "after": "new-sha", + "changed_files": "not-a-list", + } + + _, err := DecodeBranchUpdatedPayload(payload) + if err == nil { + t.Fatal("expected error for malformed changed_files") + } + if !strings.Contains(err.Error(), "changed_files") { + t.Fatalf("error should mention changed_files: %v", err) + } +} + +func TestDecodeBranchUpdatedPayloadRejectsMissingRepoID(t *testing.T) { + payload := map[string]any{ + "type": "branch.updated", + "branch": "develop", + "after": "new-sha", + } + + _, err := DecodeBranchUpdatedPayload(payload) + if err == nil { + t.Fatal("expected error for missing repo_id") + } +} + +func TestDecodeBranchUpdatedPayloadRejectsMissingBranch(t *testing.T) { + payload := map[string]any{ + "type": "branch.updated", + "repo_id": "nomadcode", + "after": "new-sha", + } + + _, err := DecodeBranchUpdatedPayload(payload) + if err == nil { + t.Fatal("expected error for missing branch") + } +} + +func TestDecodeBranchUpdatedPayloadEmptyChangedFiles(t *testing.T) { + payload := map[string]any{ + "id": "event-empty", + "type": "branch.updated", + "repo_id": "nomadcode", + "branch": "develop", + "after": "new-sha", + "changed_files": []any{}, + } + + ev, err := DecodeBranchUpdatedPayload(payload) + if err != nil { + t.Fatalf("decode empty changed_files: unexpected error: %v", err) + } + if ev.ChangedFiles == nil { + t.Fatalf("changed_files should be empty slice, got nil") + } + if len(ev.ChangedFiles) != 0 { + t.Fatalf("changed_files: got %d, want 0", len(ev.ChangedFiles)) + } +} + +func TestDecodeBranchUpdatedPayloadNilPayload(t *testing.T) { + _, err := DecodeBranchUpdatedPayload(nil) + if err == nil { + t.Fatal("expected error for nil payload") + } +} + +func TestDecodeBranchUpdatedPayloadAllowsNilChangedFiles(t *testing.T) { + payload := map[string]any{ + "id": "event-nil-cf", + "type": "branch.updated", + "repo_id": "nomadcode", + "branch": "develop", + "after": "new-sha", + "provider": "forgejo", + } + + ev, err := DecodeBranchUpdatedPayload(payload) + if err != nil { + t.Fatalf("decode with nil changed_files: unexpected error: %v", err) + } + if ev.ChangedFiles != nil { + t.Fatalf("changed_files should be nil, got %+v", ev.ChangedFiles) + } +} + +func TestDecodeBranchUpdatedPayloadHandlesMalformedChangedFileItem(t *testing.T) { + payload := map[string]any{ + "id": "event-bad-item", + "type": "branch.updated", + "repo_id": "nomadcode", + "branch": "develop", + "after": "new-sha", + "changed_files": []any{ + "not-an-object", + }, + } + + _, err := DecodeBranchUpdatedPayload(payload) + if err == nil { + t.Fatal("expected error for malformed changed_files item") + } + if !strings.Contains(err.Error(), "changed_files") { + t.Fatalf("error should mention changed_files: %v", err) + } +} + +func TestDecodeBranchUpdatedPayloadWithoutTypeField(t *testing.T) { + // type field is optional; when absent it should be accepted. + payload := map[string]any{ + "id": "event-no-type", + "repo_id": "nomadcode", + "branch": "develop", + "after": "new-sha", + "provider": "forgejo", + } + + ev, err := DecodeBranchUpdatedPayload(payload) + if err != nil { + t.Fatalf("decode without type: unexpected error: %v", err) + } + if ev.Type != "" { + t.Fatalf("type should be empty when absent, got %q", ev.Type) + } + if ev.RepoID != "nomadcode" || ev.Branch != "develop" { + t.Fatalf("repo/branch: got %+v", ev) + } +} + func TestIsTarget(t *testing.T) { ev := BranchUpdatedEvent{RepoID: "nomadcode", Branch: "develop"} diff --git a/services/core/internal/http/gito_webhook.go b/services/core/internal/http/gito_webhook.go index 6f18af6..15a6487 100644 --- a/services/core/internal/http/gito_webhook.go +++ b/services/core/internal/http/gito_webhook.go @@ -93,10 +93,15 @@ func validateBranchUpdatedPayload(p *gitoWebhookPayload) error { // changed_files must be a list of objects if present if p.ChangedFiles != nil { - _, ok := p.ChangedFiles.([]interface{}) + changedFiles, ok := p.ChangedFiles.([]interface{}) if !ok { return errors.New("payload changed_files is not a list") } + for i, item := range changedFiles { + if _, ok := item.(map[string]interface{}); !ok { + return fmt.Errorf("payload changed_files[%d] is not an object", i) + } + } } return nil @@ -216,13 +221,3 @@ func validGitoWebhookSignature(secret string, body []byte, signature string) boo } return hmac.Equal(actual, expected) } - -func missingGitoFields(p gitoWebhookPayload) string { - if p.RepoID == "" && p.Branch == "" { - return "repo_id and branch are required" - } - if p.RepoID == "" { - return "repo_id is required" - } - return "branch is required" -} \ No newline at end of file diff --git a/services/core/internal/http/gito_webhook_test.go b/services/core/internal/http/gito_webhook_test.go index ed6b3b2..10b335d 100644 --- a/services/core/internal/http/gito_webhook_test.go +++ b/services/core/internal/http/gito_webhook_test.go @@ -206,3 +206,25 @@ func TestReceiveGitoWebhookRejectsMalformedChangedFilesWithoutHandlerCall(t *tes t.Fatalf("expected handler not called, got %d", fake.callCount) } } + +func TestReceiveGitoWebhookRejectsMalformedChangedFileItemWithoutHandlerCall(t *testing.T) { + fake := &fakeGitoBranchEventHandler{} + h := newGitoHandlerForTest() + h.SetGitoBranchEventHandler(fake) + + // changed_files 가 list 이지만 element 가 object 가 아닌 경우 + malformedItemBody := `{"repo_id":"target-repo","branch":"develop","changed_files":["not-an-object"]}` + 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") + rec := httptest.NewRecorder() + + 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) + } +} diff --git a/services/core/internal/http/handlers.go b/services/core/internal/http/handlers.go index ff32eee..d34e44e 100644 --- a/services/core/internal/http/handlers.go +++ b/services/core/internal/http/handlers.go @@ -26,6 +26,24 @@ type WorkItemTaskCreator interface { CreateTaskFromWorkItem(ctx context.Context, input workitempipeline.CreateTaskInput) (storage.Task, error) } +// GitoWebhookConfig carries the non-secret gate identifiers for the Gito +// HTTP webhook receiver. +type GitoWebhookConfig struct { + Secret string + RepoID string + Branch string +} + +type gitoWebhookConfig struct { + secret string + repoID string + branch string +} + +func (c gitoWebhookConfig) ready() bool { + return c.secret != "" +} + type Handler struct { db *pgxpool.Pool workflow *workflow.Service @@ -34,6 +52,9 @@ type Handler struct { logger *slog.Logger planeWebhookSecret string planeDispatch planeWebhookDispatch + gitoConfig gitoWebhookConfig + gitoEventHandler GitoBranchEventHandler + gitoDelivery *gitoDeliveryStore } type webhookTaskEnqueuer interface { @@ -81,6 +102,7 @@ func NewHandler(pool *pgxpool.Pool, workflowService *workflow.Service, binder wo webhookEnqueuer: workflowService, workItemProviders: make(map[workitem.ProviderID]WorkItemTaskCreator), logger: logger, + gitoDelivery: newGitoDeliveryStore(), } if workflowService != nil { for _, provider := range providers { @@ -175,6 +197,18 @@ func (b projectSyncBinder) ReserveWorkspaceSlot(ctx context.Context, projectSync return projectsync.SlotFromDBRecord(slot), nil } +func (h *Handler) SetGitoWebhookConfig(cfg GitoWebhookConfig) { + h.gitoConfig = gitoWebhookConfig{ + secret: strings.TrimSpace(cfg.Secret), + repoID: strings.TrimSpace(cfg.RepoID), + branch: strings.TrimSpace(cfg.Branch), + } +} + +func (h *Handler) SetGitoBranchEventHandler(handler GitoBranchEventHandler) { + h.gitoEventHandler = handler +} + func (h *Handler) registerWorkItemProvider(id workitem.ProviderID, creator WorkItemTaskCreator) { trimmed := strings.TrimSpace(string(id)) if trimmed == "" || creator == nil { diff --git a/services/core/internal/http/handlers_test.go b/services/core/internal/http/handlers_test.go index 87df5d3..10d4d9b 100644 --- a/services/core/internal/http/handlers_test.go +++ b/services/core/internal/http/handlers_test.go @@ -63,6 +63,7 @@ func withWebhookEnqueuer(enqueuer webhookTaskEnqueuer) handlerOpt { func newHandlerForTest(opts ...handlerOpt) *Handler { h := &Handler{ workItemProviders: make(map[workitem.ProviderID]WorkItemTaskCreator), + gitoDelivery: newGitoDeliveryStore(), } for _, opt := range opts { opt(h) diff --git a/services/core/internal/http/router.go b/services/core/internal/http/router.go index 921d627..1be287e 100644 --- a/services/core/internal/http/router.go +++ b/services/core/internal/http/router.go @@ -22,6 +22,7 @@ func NewRouter(handler *Handler, logger *slog.Logger, auth AuthConfig, protoSock r.Get("/healthz", handler.Healthz) r.Post("/api/integrations/plane/webhook", handler.ReceivePlaneWebhook) + r.Post("/api/integrations/gito/webhook", handler.ReceiveGitoWebhook) protected := chi.NewRouter() protected.Use(basicAuthMiddleware(auth))