# Contract: gito.forgejo-branch-events.v1

## 범위

이 계약은 Gito가 Forgejo push webhook을 받아 watched branch revision 변화로 정규화하고, NomadCode 같은 외부/인접 소비자에게 HTTP webhook delivery로 전달하는 경계를 정의한다. Gito는 여러 Git 플랫폼의 공통 control plane을 지향하므로 외부 소비자에게 Gito 전용 binary transport 구현을 요구하지 않는다.

## Transport

- Forgejo -> Gito: REST webhook callback.
- Gito -> registered consumers: signed HTTP webhook delivery.
- REST는 provider callback, external webhook delivery, bootstrap/smoke 표면에 사용한다.
- proto-socket은 Gito 내부 control/runtime transport이며 이 외부 계약의 일부가 아니다.
- gRPC는 이 계약 범위가 아니다.

## Environment Port Profiles

포트 profile은 원격 host에서 local용 환경과 dev용 환경을 동시에 분리 운영하기 위한 기본값이다. API path, headers, request/response schema는 profile과 무관하게 동일하다.

| Profile | Gito base URL | NomadCode webhook target | Preview | Optional wire | DB/cache 후보 |
| --- | --- | --- | ---: | ---: | ---: |
| remote local | `http://127.0.0.1:18060` | `http://127.0.0.1:18010/api/integrations/gito/webhook` | `13060` | `19060` | `15460/16360` |
| remote dev | `http://127.0.0.1:18061` | `http://127.0.0.1:18011/api/integrations/gito/webhook` | `13061` | `19061` | `15461/16361` |

Gito Core process/container 내부 listen 포트는 제품 기본값 `8080`을 유지할 수 있다. workspace 충돌 관리는 host publish 또는 direct smoke 포트(`18060`/`18061`)에서 처리한다. IOP dev model endpoint `http://toki-labs.com:18083/v1`은 NomadCode 실행 의존성이지 Gito-owned port가 아니다.

## REST Callback

### `POST /callbacks/forgejo/push`

Forgejo push webhook을 수신한다.

Headers:

| Field | Required | Meaning |
| --- | --- | --- |
| `X-Forgejo-Event` | 권장 | `push`일 때만 branch update 후보로 처리한다. |
| `X-Forgejo-Delivery` | 선택 | provider delivery id. event payload의 `delivery_id`로 전달한다. |
| `X-Forgejo-Signature` | `FORGEJO_WEBHOOK_SECRET` 설정 시 필수 | request body HMAC-SHA256 hex digest. `sha256=` prefix도 허용한다. |

Query:

| Field | Required | Meaning |
| --- | --- | --- |
| `repo_id` | 선택 | Gito local repo id override. 없으면 `repository.full_name`, 그 다음 `repository.name`을 사용한다. |

Request body minimum shape:

```json
{
  "ref": "refs/heads/develop",
  "before": "old-sha",
  "after": "new-sha",
  "repository": {
    "name": "nomadcode",
    "full_name": "owner/nomadcode",
    "default_branch": "develop"
  },
  "commits": [
    {
      "id": "new-sha",
      "added": ["agent-roadmap/phase/example/milestones/example.md"],
      "modified": ["README.md"],
      "removed": []
    }
  ]
}
```

Response shape:

```json
{
  "accepted": true,
  "matched": true,
  "event": {
    "id": "event-id",
    "type": "branch.updated",
    "provider": "forgejo",
    "delivery_id": "delivery-id",
    "revision": {
      "repo_id": "nomadcode",
      "branch": "develop",
      "before": "old-sha",
      "after": "new-sha",
      "changed_files": [
        {
          "path": "agent-roadmap/phase/example/milestones/example.md",
          "change_type": "added"
        }
      ],
      "observed_at": "2026-06-13T00:00:00Z"
    },
    "created_at": "2026-06-13T00:00:00Z"
  }
}
```

Duplicate delivery response (additive field, only present when `true`):

```json
{
  "accepted": true,
  "matched": true,
  "duplicate": true,
  "event": { "..." : "same event shape as first delivery" }
}
```

Idempotency: when a durable store is configured, Gito deduplicates push deliveries by `X-Forgejo-Delivery` header (dedupe key `delivery:<id>`) or by `revision:<repo_id>:<branch>:<before>:<after>` when the header is absent. A duplicate delivery returns the same `accepted=true, matched=true` response with an additional `duplicate=true` field and does not create a new event or outbound webhook delivery.

Status:

| Status | Meaning |
| --- | --- |
| `202` | Payload was accepted. `matched=false` means no watched branch event was emitted. |
| `400` | Malformed webhook payload. |
| `401` | Signature verification failed. |
| `405` | Method is not `POST`. |
| `500` | Event publish failed. |

## Branch Watch Bootstrap

### `POST /api/listeners/branches`

Registers a watched branch for the in-memory MVP runtime.

Request:

```json
{
  "repo_id": "nomadcode",
  "branch": "develop",
  "provider": "forgejo"
}
```

Response:

```json
{
  "listener": {
    "id": "watch-forgejo-nomadcode-develop",
    "repo_id": "nomadcode",
    "branch": "develop",
    "provider": "forgejo",
    "created_at": "2026-06-13T00:00:00Z"
  }
}
```

### `GET /api/listeners/branches`

Lists active in-memory branch watches.

## Webhook Subscription Bootstrap

This is the target external subscription surface for the outbound webhook
delivery Milestone. The current MVP may still expose only listener and event
inspection helpers until that Milestone is implemented.

### `POST /api/webhook-subscriptions`

Registers an HTTP endpoint that receives normalized Gito events.

Request:

```json
{
  "name": "nomadcode-develop-wakeup",
  "target_url": "http://127.0.0.1:18010/api/integrations/gito/webhook",
  "events": ["branch.updated"],
  "repo_id": "nomadcode",
  "branch": "develop",
  "secret_ref": "secret://gito/webhooks/nomadcode"
}
```

Response:

```json
{
  "subscription": {
    "id": "webhook-sub-nomadcode-develop",
    "name": "nomadcode-develop-wakeup",
    "events": ["branch.updated"],
    "repo_id": "nomadcode",
    "branch": "develop",
    "created_at": "2026-06-13T00:00:00Z"
  }
}
```

`target_url`은 remote local profile 예시다. remote dev profile에서는 `http://127.0.0.1:18011/api/integrations/gito/webhook`을 사용한다. `secret_ref`는 예시 참조값이며 raw signing secrets, tokens, or private endpoint values must not be stored in tracked docs.

## Event Inspection

### `GET /api/events`

Lists recent in-memory events. This is a smoke/debug surface, not the primary internal consumption path.

Response:

```json
{
  "events": [
    {
      "id": "event-id",
      "type": "branch.updated",
      "provider": "forgejo",
      "delivery_id": "delivery-id",
      "revision": {
        "repo_id": "nomadcode",
        "branch": "develop",
        "before": "old-sha",
        "after": "new-sha",
        "changed_files": [
          {
            "path": "agent-roadmap/phase/example/milestones/example.md",
            "change_type": "modified"
          }
        ],
        "observed_at": "2026-06-13T00:00:00Z"
      },
      "created_at": "2026-06-13T00:00:00Z"
    }
  ]
}
```

## HTTP Webhook Delivery

Gito delivers matched `branch.updated` events to registered HTTP endpoints with a
GitHub-style webhook model. The delivery body is JSON.

Headers:

| Field | Required | Meaning |
| --- | --- | --- |
| `X-Gito-Event` | 필수 | Normalized event type, for example `branch.updated`. |
| `X-Gito-Delivery` | 필수 | Stable delivery id. Consumers use this for idempotency. |
| `X-Gito-Signature` | `secret_ref` 설정 시 필수 | HMAC-SHA256 digest over the request body. `sha256=` prefix is allowed. |

Request body:

```json
{
  "id": "event-id",
  "type": "branch.updated",
  "provider": "forgejo",
  "delivery_id": "forgejo-delivery-id",
  "repo_id": "nomadcode",
  "branch": "develop",
  "before": "old-sha",
  "after": "new-sha",
  "changed_files": [
    {
      "path": "README.md",
      "change_type": "modified"
    }
  ],
  "observed_at": "2026-06-13T00:00:00Z",
  "created_at": "2026-06-13T00:00:00Z"
}
```

Delivery response handling:

| Status | Meaning |
| --- | --- |
| `2xx` | Delivery accepted. |
| `400` | Consumer rejected malformed payload; retry is not useful unless config changed. |
| `401` or `403` | Signature/auth failure; disable or pause delivery until config is fixed. |
| `404` or `410` | Endpoint no longer exists; delivery should be disabled or marked failed. |
| `429` or `5xx` | Retry with backoff. |

Delivery idempotency is consumer-visible through `X-Gito-Delivery`. Gito records
attempt state in the durable outbox/delivery store and must not treat webhook
delivery success as proof that the consumer has applied source-of-truth changes.

## Base Schema 및 Extension 경계

`branch.updated` base payload 필드:

| 필드 | 설명 |
| --- | --- |
| `id` | Gito event id |
| `type` | 고정값 `branch.updated` |
| `provider` | `forgejo` 등 provider 이름 |
| `delivery_id` | provider delivery id (있을 때만) |
| `repo_id` | Gito local repo id |
| `branch` | branch 이름 |
| `before` | 이전 revision SHA |
| `after` | 새 revision SHA |
| `changed_files` | `[{path, change_type}]` 목록 |
| `observed_at` | Gito가 payload를 정규화한 시각 (RFC3339) |
| `created_at` | Gito가 event를 기록/발행한 시각 (RFC3339) |

위 필드는 모두 consumer-neutral한 범용 Git event 정보다. NomadCode 전용 필수 필드는 base payload에 포함하지 않는다. consumer-specific 확장이 필요하면 optional `payload.metadata` 또는 별도 optional field로만 허용한다.

## Field Semantics

| Field | Meaning |
| --- | --- |
| `repo_id` | Gito-local repo id used by watches and downstream sync. |
| `branch` | Branch name without `refs/heads/`. |
| `before` | Previous revision SHA from Forgejo push payload. |
| `after` | New revision SHA from Forgejo push payload. |
| `changed_files[].path` | Path changed by the push payload or future revision scan. |
| `changed_files[].change_type` | `added`, `modified`, `deleted`, `renamed`, or `copied`. |
| `delivery_id` | Provider delivery id when supplied. |
| `observed_at` | Time Gito normalized the provider payload. |
| `created_at` | Time Gito recorded/emitted the event. |

## Consumer Responsibilities

- NomadCode treats `branch.updated` as a wakeup signal.
- NomadCode must verify the target branch revision before mutating Plane, agent-roadmap, or other source-of-truth state.
- NomadCode should use `repo_id`, `branch`, `before`, `after`, and `changed_files` to decide whether roadmap sync is relevant.
- NomadCode should not treat webhook file lists as the only proof of state; for critical updates it should fetch/scan the target branch.

## Consumer Interop Procedure

NomadCode 또는 다른 consumer가 Gito branch event webhook delivery를 연동할 때 따르는 최소 절차다.

1. `POST /api/listeners/branches`로 watched branch를 등록한다. branch watch가 없으면 이벤트가 발행되지 않는다.
2. Consumer는 HTTP endpoint와 event/repo/branch filter를 Gito webhook subscription으로 등록한다.
3. Gito는 Forgejo push webhook 수신 후 matched branch에 대해 durable `branch.updated` event를 기록한다.
4. Gito는 matching subscription의 `target_url`로 signed HTTP POST delivery를 보낸다.
5. Consumer는 base payload(`repo_id`, `branch`, `before`, `after`, `changed_files`)를 자기 내부 wakeup 로직에 매핑한다.
6. 중요 동작 전에 `before`/`after` SHA와 `changed_files`를 참고하되, provider file list나 webhook delivery를 유일한 상태 증거로 쓰지 않는다.

**NomadCode interop 확인 기준**: NomadCode dev consumer가 generic `branch.updated` HTTP webhook delivery를 수신해 내부 wakeup으로 매핑한다. base payload에 NomadCode 전용 필수 필드가 없다. 다른 consumer도 같은 schema를 재사용할 수 있다.

## Provider Responsibilities

- Gito must not store raw provider secrets in tracked docs or event payloads.
- Gito must reject invalid signatures when `FORGEJO_WEBHOOK_SECRET` is configured.
- Gito should accept unsupported or unwatched provider events without side effects when safe, returning `matched=false`.
- Gito should eventually persist branch watches, revision cursors, and event outbox records in PostgreSQL; the MVP keeps them in memory.

## 운영 확인 절차

listener bootstrap, webhook subscription, `/api/listeners/branches`, `/api/events`를 secret 없이 재현 가능하게 확인하는 절차다.

예시는 remote local profile 기준이다. remote dev profile에서는 `GITO_BASE_URL=http://127.0.0.1:18061`, `GITO_NOMADCODE_TARGET_URL=http://127.0.0.1:18011/api/integrations/gito/webhook`으로 바꾼다.

### 필요한 환경 변수

| 변수 | 필수 | 기본값 | 설명 |
| --- | --- | --- | --- |
| `APP_ENV` | 아니오 | `development` | 앱 환경 |
| `FORGEJO_WEBHOOK_SECRET` | 아니오 | (없으면 서명 검증 생략) | Forgejo webhook 서명 키 |
| `DATABASE_URL` | 아니오 | (없으면 in-memory) | PostgreSQL DSN |
| `GITO_BASE_URL` | 아니오 | `http://127.0.0.1:18060` | Gito profile별 base URL |
| `GITO_NOMADCODE_TARGET_URL` | 아니오 | `http://127.0.0.1:18010/api/integrations/gito/webhook` | NomadCode consumer endpoint |

기본 smoke는 `FORGEJO_WEBHOOK_SECRET`과 `DATABASE_URL` 없이 실행할 수 있다.

```bash
GITO_BASE_URL="${GITO_BASE_URL:-http://127.0.0.1:18060}"
GITO_NOMADCODE_TARGET_URL="${GITO_NOMADCODE_TARGET_URL:-http://127.0.0.1:18010/api/integrations/gito/webhook}"
```

### Step 1 — Branch watch 등록

```bash
curl -s -X POST "$GITO_BASE_URL/api/listeners/branches" \
  -H "Content-Type: application/json" \
  -d '{"repo_id":"nomadcode","branch":"develop","provider":"forgejo"}'
```

기대 응답: `201 Created`, `{"listener": {...}}`

### Step 2 — 등록된 watch 목록 확인

```bash
curl -s "$GITO_BASE_URL/api/listeners/branches"
```

### Step 3 — webhook subscription 등록

```bash
curl -s -X POST "$GITO_BASE_URL/api/webhook-subscriptions" \
  -H "Content-Type: application/json" \
  -d "{\"name\":\"nomadcode-develop-wakeup\",\"target_url\":\"$GITO_NOMADCODE_TARGET_URL\",\"events\":[\"branch.updated\"],\"repo_id\":\"nomadcode\",\"branch\":\"develop\",\"secret_ref\":\"secret://gito/webhooks/nomadcode\"}"
```

기대 응답: `201 Created`, `{"subscription": {...}}`
이 endpoint는 outbound webhook delivery 구현 Milestone에서 추가한다.

### Step 4 — webhook delivery worker 통합 테스트로 연결 확인

```bash
cd services/core && go test -run TestWebhookDeliveryBranchUpdated ./internal/controlplane/ -v -count=1
```

이 테스트는 outbound webhook delivery 구현 Milestone에서 추가한다.

### Step 5 — Forgejo push 트리거 (서명 없는 경우)

```bash
curl -s -X POST "$GITO_BASE_URL/callbacks/forgejo/push?repo_id=nomadcode" \
  -H "Content-Type: application/json" \
  -H "X-Forgejo-Event: push" \
  -H "X-Forgejo-Delivery: smoke-delivery-1" \
  -d '{"ref":"refs/heads/develop","before":"old-sha","after":"new-sha","repository":{"name":"nomadcode","full_name":"owner/nomadcode"},"commits":[{"id":"new-sha","modified":["README.md"],"added":[],"removed":[]}]}'
```

기대 응답: `{"accepted":true,"matched":true,"event":{...}}`

### Step 6 — 발행된 event 확인

```bash
curl -s "$GITO_BASE_URL/api/events"
```

기대 응답에 `"type":"branch.updated"`, `"before":"old-sha"`, `"after":"new-sha"`, `changed_files`가 포함된다.

## 금지 사항

- Raw webhook secret, token, password, or credential value must not appear in tracked docs, event payloads, logs, or contract examples.
- Provider webhook callbacks must not directly mutate NomadCode or Plane state.
- Gito -> external consumer branch event delivery must not require proto-socket.
- Consumer webhook delivery must not be treated as final source-of-truth state.
