feat: gito http webhook consumer readiness milestone completion
- Update milestone and SDD documents - Add webhook HTTP receiver with idempotency checks - Add config for webhook endpoints - Implement gito events processing - Add HTTP handlers and router updates - Archive completed task files
This commit is contained in:
parent
5d2acc1afb
commit
9258f9de54
22 changed files with 2072 additions and 62 deletions
|
|
@ -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 확인 후 구현 잠금을 해제한다.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,297 @@
|
|||
<!-- task=m-gito-http-webhook-consumer-readiness/01_payload_boundary plan=0 tag=API -->
|
||||
|
||||
# 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-<milestone-slug>`이면 완료 이벤트 메타데이터를 보고한다. 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-<milestone-slug>`이면 런타임이 읽을 완료 이벤트 메타데이터를 보고하고, 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
|
||||
|
|
@ -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
|
||||
|
||||
- 없음
|
||||
|
||||
## 후속 작업
|
||||
|
||||
- 없음
|
||||
|
|
@ -0,0 +1,224 @@
|
|||
<!-- task=m-gito-http-webhook-consumer-readiness/01_payload_boundary plan=0 tag=API -->
|
||||
|
||||
# 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`의 구현 에이전트 소유 섹션을 채운다. 이 파일 작성이 구현의 마지막 단계다.
|
||||
|
|
@ -0,0 +1,266 @@
|
|||
<!-- task=m-gito-http-webhook-consumer-readiness/02+01_http_receiver_idempotency plan=0 tag=API -->
|
||||
|
||||
# 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-<milestone-slug>`이면 완료 이벤트 메타데이터를 보고한다. 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-<milestone-slug>`이면 런타임이 읽을 완료 이벤트 메타데이터를 보고하고, 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
|
||||
```
|
||||
|
|
@ -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-<milestone-slug>`이면 런타임이 읽을 완료 이벤트 메타데이터를 보고하고, 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만 좁게 수정한다.
|
||||
|
|
@ -0,0 +1,205 @@
|
|||
<!-- task=m-gito-http-webhook-consumer-readiness/02+01_http_receiver_idempotency plan=2 tag=REVIEW_REVIEW_API -->
|
||||
|
||||
# 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-<milestone-slug>`이면 완료 이벤트 메타데이터를 보고한다. 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-<milestone-slug>`이면 런타임이 읽을 완료 이벤트 메타데이터를 보고하고, 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[<index>] 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 종결 처리한다.
|
||||
|
|
@ -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
|
||||
|
||||
- 없음
|
||||
|
||||
## 후속 작업
|
||||
|
||||
- 없음
|
||||
|
|
@ -0,0 +1,325 @@
|
|||
<!-- task=m-gito-http-webhook-consumer-readiness/02+01_http_receiver_idempotency plan=0 tag=API -->
|
||||
|
||||
# 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:<X-Gito-Delivery>` when header exists.
|
||||
- `revision:<repo_id>:<branch>:<after>` 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`의 구현 에이전트 소유 섹션을 채운다. 이 파일 작성이 구현의 마지막 단계다.
|
||||
|
|
@ -0,0 +1,108 @@
|
|||
<!-- task=m-gito-http-webhook-consumer-readiness/02+01_http_receiver_idempotency plan=1 tag=REVIEW_API -->
|
||||
|
||||
# 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은 허용하지 않는다.
|
||||
|
|
@ -0,0 +1,101 @@
|
|||
<!-- task=m-gito-http-webhook-consumer-readiness/02+01_http_receiver_idempotency plan=2 tag=REVIEW_REVIEW_API -->
|
||||
|
||||
# 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[<index>] 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은 허용하지 않는다.
|
||||
|
|
@ -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,
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"),
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
||||
|
|
|
|||
|
|
@ -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 == "" {
|
||||
|
|
|
|||
|
|
@ -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"}
|
||||
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
Loading…
Reference in a new issue