385 lines
18 KiB
Markdown
385 lines
18 KiB
Markdown
---
|
|
test_env: local
|
|
test_env_scope: local_and_dev_profiles
|
|
test_profile: live-cycle-smoke
|
|
domain: core
|
|
verification_type: smoke
|
|
last_rule_updated_at: 2026-07-03
|
|
---
|
|
|
|
# live-cycle-smoke
|
|
|
|
전체 webhook -> authoring -> git develop push -> Forgejo/Gito branch event -> Plane projection -> slot 회수 사이클을 검증한다.
|
|
이 문서는 절차 기술과 runtime-equivalent 근거를 함께 기록한다.
|
|
|
|
## 읽기 조건
|
|
|
|
- `live-webhook-task`, `webhook-auto-enqueue`, `live-creation-projection` 증거가 필요한 경우.
|
|
- dev profile에서 Plane-origin milestone 생성이 git에 등록되고 다시 Plane `Todo`로 이동하는지 검증하는 경우.
|
|
- `agent-task/m-milestone-work-item-creation-sync/14+12,13_live_cycle_smoke` 구현/리뷰 시.
|
|
|
|
## 환경
|
|
|
|
- host: standard remote runner `toki@toki-labs.com`.
|
|
- local profile checkout: `$HOME/agent-work/nomadcode`, Core host port `18010->8080`, Gito source `18060`.
|
|
- dev profile checkout: `$HOME/agent-work/nomadcode-dev`, Core host port `18011->8080`, Gito source `18061`.
|
|
- Core server: Docker Compose (`services/core/docker-compose.yml`).
|
|
- Plane dev: `https://plane.toki-labs.com`, workspace `general`, project `NOMAD`.
|
|
- Secrets: `.env.plane.local` (ignored), SOPS `secrets/nomadcode.dev.sops.yaml` (webhook secret).
|
|
- runner checkout: 검증용 disposable mirror다. dirty worktree, uncommitted 변경, 임시 파일을 이유로 중단하지 않고 현재 대화 workspace의 요청 범위 상태와 같게 reset/clean/reclone 또는 파일 동기화한 뒤 테스트한다.
|
|
|
|
## 전제 조건
|
|
|
|
- Core server는 현재 검증 대상 상태로 빌드·배포되어 있어야 한다. 원격 checkout이나 실행 컨테이너가 낡았으면 테스트 수행자가 reset/clean/reclone, 파일 동기화, compose rebuild/recreate로 최신화한다.
|
|
- `PLANE_WEBHOOK_SECRET`, `PLANE_TOKEN` 등 secret은 `.env.plane.local` 또는 SOPS에서 로드한다.
|
|
- Plane dev 서버가 `https://plane.toki-labs.com`에서 정상 동작 중이어야 한다.
|
|
- workspace slot 행이 DB에 `available` 상태로 최소 1개 이상 있어야 한다.
|
|
- dev profile에서는 `agent-test/dev/rules.md`를 먼저 읽고, Core `18011`, Gito `18061`, Forgejo provider hook 경로가 모두 열려 있어야 한다.
|
|
- Forgejo repository hook이 `host.docker.internal:18061`로 Gito에 전달하는 환경이면 Forgejo `[webhook] ALLOWED_HOST_LIST`가 `host.docker.internal`을 허용해야 한다.
|
|
|
|
## dev profile 프리플라이트
|
|
|
|
아래 확인은 secret 값을 출력하지 않는다. 실패하면 Plane work item을 새로 만들기 전에 환경을 먼저 고친다.
|
|
|
|
```sh
|
|
# 1. Core/Gito readiness
|
|
curl -fsS http://127.0.0.1:18011/readyz
|
|
curl -fsS http://127.0.0.1:18061/readyz
|
|
|
|
# 2. Gito branch watch / subscription
|
|
curl -fsS http://127.0.0.1:18061/api/listeners/branches
|
|
curl -fsS http://127.0.0.1:18061/api/webhook-subscriptions
|
|
# Expected: repo_id=nomadcode, branch=develop, event=branch.updated.
|
|
|
|
# 3. Forgejo container -> Gito host endpoint reachability
|
|
docker exec forgejo curl -fsS --max-time 5 http://host.docker.internal:18061/healthz
|
|
|
|
# 4. Forgejo webhook allowlist
|
|
docker exec forgejo sh -c 'grep -n "^ALLOWED_HOST_LIST" /data/gitea/conf/app.ini'
|
|
# Expected when hook target uses host.docker.internal: value includes host.docker.internal.
|
|
```
|
|
|
|
Gito watch 또는 subscription이 없으면 Gito 제공 계약의 `/api/listeners/branches`와 `/api/webhook-subscriptions` bootstrap 절차를 따른다. raw secret 대신 `secret_ref` 또는 환경 참조만 사용한다.
|
|
|
|
## Fixture IDs (non-secret)
|
|
|
|
- `agent-test/plane-dev.md` 참조. webhook smoke에서는 `Backlog + AGENT assignee` 상태 티켓을 생성하거나 AGENT를 assign한다.
|
|
- work item UUID는 Plane에서 새로 생성하거나 기존 NOMAD-10/NOMAD-11 같은 기존 fixture를 재사용할 수 있다.
|
|
|
|
## 명령 (SECRET_FREE)
|
|
|
|
```sh
|
|
# 1. 환경 로드 (secrets은 ignored local file에서)
|
|
set -a; source .env.plane.local; set +a
|
|
|
|
# 2. Core server 헬스 확인
|
|
CORE_URL="http://127.0.0.1:18010"
|
|
curl -s "$CORE_URL/api/tasks" | python3 -c \
|
|
"import sys,json; d=json.load(sys.stdin); print('task_count:', len(d.get('tasks',[])))"
|
|
|
|
# 3. DB slot 상태 확인 (slot_id=1 기준)
|
|
# - slot available이면 smoke 가능
|
|
# - slot in_use/dirty/error이면 prior run이 미완료 또는 오류임
|
|
curl -s "$CORE_URL/api/workspaces/slots" 2>/dev/null | head -200 || echo "(slot API 없음 - DB 직접 확인 필요)"
|
|
|
|
# 4. Plane: Backlog + AGENT assignee 티켓 생성 또는 AGENT 배정
|
|
# - Plane UI 또는 API로 수행; work_item_uuid만 기록
|
|
WORK_ITEM_UUID="<new-or-existing-uuid>"
|
|
|
|
# 5. Plane webhook 수동 전송 (plane-dev.md 참조)
|
|
# - Plane dev UI → Webhook Settings → Test Delivery
|
|
# - 또는 curl로 직접 (PLANE_WEBHOOK_SECRET 필요, secret 출력 금지)
|
|
# Expected: HTTP 202, Core logs show task created
|
|
|
|
# 6. Core task 생성 확인 (external_id=WORK_ITEM_UUID 로 조회)
|
|
curl -s "$CORE_URL/api/tasks?source=plane" 2>/dev/null | python3 -c "
|
|
import sys, json
|
|
d = json.load(sys.stdin)
|
|
for t in d.get('tasks', []):
|
|
if t.get('external_id') == '$WORK_ITEM_UUID':
|
|
m = t.get('metadata', {})
|
|
c = m.get('checkout', {})
|
|
print('task_id:', t['id'])
|
|
print('status:', t['status'])
|
|
print('slot_id:', c.get('slot_id'))
|
|
print('slot_path:', c.get('slot_path'))
|
|
print('authoring_run_state:', m.get('authoring_run_state'))
|
|
print('wait_type:', m.get('wait_type'))
|
|
"
|
|
# Expected: task found, checkout.slot_id > 0, authoring_run_state=in_progress
|
|
|
|
# 7. develop 브랜치에 push 발생 → gitosync scanner가 pick up (자동)
|
|
# Core log: roadmap_creation_sync job enqueued, SyncCreation 호출
|
|
|
|
# 8. projection 완료 후 task 상태 재확인
|
|
# Expected: authoring_run_state=succeeded, status=completed, slot released
|
|
|
|
# 9. slot 재사용 확인: 두 번째 smoke 티켓 생성 → 같은 slot_id 또는 새 available slot에서 reservation
|
|
```
|
|
|
|
## dev profile 자동 경로 체크리스트
|
|
|
|
dev profile에서는 아래 순서가 모두 이어져야 Plane-origin full-cycle 통과로 본다.
|
|
|
|
1. Plane에서 새 `Backlog + AGENT assignee` work item을 만든다.
|
|
- 제목/본문에는 유일한 timestamp를 넣는다.
|
|
- 요청 범위는 roadmap milestone 문서 생성처럼 작은 산출물로 제한한다.
|
|
- 생성 직후 work item UUID만 기록하고 token, webhook secret, private key는 기록하지 않는다.
|
|
2. Core task 생성과 authoring 진입을 확인한다.
|
|
|
|
```sh
|
|
CORE_URL="http://127.0.0.1:18011"
|
|
WORK_ITEM_UUID="<plane-work-item-uuid>"
|
|
curl -fsS "$CORE_URL/api/tasks" | python3 -c "
|
|
import json, sys
|
|
wid='$WORK_ITEM_UUID'
|
|
for t in json.load(sys.stdin).get('tasks', []):
|
|
if t.get('external_id') == wid:
|
|
m=t.get('metadata') or {}
|
|
print('task_id:', t.get('id'))
|
|
print('status:', t.get('status'))
|
|
print('authoring_run_state:', m.get('authoring_run_state'))
|
|
print('push_request_state:', m.get('push_request_state'))
|
|
print('wait_type:', m.get('wait_type'))
|
|
"
|
|
```
|
|
|
|
3. git remote `develop`에 새 revision이 생겼는지 확인한다.
|
|
|
|
```sh
|
|
cd "$HOME/agent-work/nomadcode-dev/workspaces/nomadcode/branches/develop"
|
|
git fetch -q origin develop
|
|
git log --oneline -1 origin/develop
|
|
git diff-tree --no-commit-id --name-status -r origin/develop
|
|
```
|
|
|
|
4. Forgejo provider hook delivery가 성공했는지 확인한다.
|
|
|
|
```sh
|
|
docker exec -i forgejo-db sh -lc 'psql -U "$POSTGRES_USER" -d "$POSTGRES_DB" -At' <<'SQL'
|
|
select id, hook_id, uuid, event_type, is_delivered, is_succeed, left(request_content, 400), left(response_content, 220)
|
|
from hook_task
|
|
order by id desc
|
|
limit 5;
|
|
SQL
|
|
```
|
|
|
|
기대값은 현재 `AFTER_SHA`나 milestone path가 들어 있는 push task의 `event_type=push`, `is_delivered=t`, `is_succeed=t`, response status `202`다. 동시 push가 있을 수 있으므로 단순 최신 행이 아니라 현재 테스트의 `after` revision 또는 changed file path와 매칭한다.
|
|
|
|
5. Gito가 `branch.updated` event를 만들었는지 확인한다.
|
|
|
|
```sh
|
|
AFTER_SHA="<origin-develop-after-sha>"
|
|
tmp=$(mktemp)
|
|
curl -fsS http://127.0.0.1:18061/api/events > "$tmp"
|
|
python3 - "$tmp" "$AFTER_SHA" <<'PY'
|
|
import json, sys
|
|
with open(sys.argv[1], encoding="utf-8") as f:
|
|
data=json.load(f)
|
|
after=sys.argv[2]
|
|
for event in data.get("events", []):
|
|
rev=event.get("revision") or {}
|
|
if rev.get("after") == after:
|
|
print("event_id:", event.get("id"))
|
|
print("delivery_id:", event.get("delivery_id"))
|
|
print("repo_id:", rev.get("repo_id"))
|
|
print("branch:", rev.get("branch"))
|
|
print("changed_files:", rev.get("changed_files"))
|
|
PY
|
|
rm -f "$tmp"
|
|
```
|
|
|
|
6. Core가 Gito webhook을 받고 Plane projection을 완료했는지 확인한다.
|
|
- Core log에 `POST /api/integrations/gito/webhook` status `202`가 있어야 한다.
|
|
- 같은 revision에서 대상 work item의 `gito creation sync enqueued`와 `roadmap creation sync completed`가 이어져야 한다.
|
|
- Core task result의 `next_step`은 `plane_todo_moved`, `authoring_run_state`는 `succeeded`여야 한다.
|
|
7. Plane API 또는 UI에서 work item state가 `Todo`인지 확인한다.
|
|
|
|
```sh
|
|
cd "$HOME/agent-work/nomadcode-dev"
|
|
set -a; source .env.plane.local; set +a
|
|
python3 - <<'PY'
|
|
import os, json, ssl, urllib.request
|
|
wid="<plane-work-item-uuid>"
|
|
base=os.environ["PLANE_BASE_URL"].rstrip("/")
|
|
ws=os.environ.get("PLANE_WORKSPACE_SLUG", "general")
|
|
project=os.environ["PLANE_PROJECT_ID"]
|
|
token=os.environ["PLANE_TOKEN"]
|
|
todo=os.environ.get("PLANE_TODO_STATE_ID")
|
|
ctx=ssl._create_unverified_context()
|
|
req=urllib.request.Request(
|
|
f"{base}/api/v1/workspaces/{ws}/projects/{project}/work-items/{wid}",
|
|
headers={"X-Api-Key": token, "Accept": "application/json"},
|
|
)
|
|
with urllib.request.urlopen(req, context=ctx, timeout=20) as r:
|
|
item=json.load(r)
|
|
state_id=item.get("state") or item.get("state_id")
|
|
print(json.dumps({"work_item_id": wid, "state_id": state_id, "matches_todo_env": state_id == todo}, ensure_ascii=False))
|
|
PY
|
|
```
|
|
|
|
## 검증 항목
|
|
|
|
| 항목 | 증거 유형 | 단계 |
|
|
|------|----------|------|
|
|
| `live-webhook-task`: webhook 202 + task 생성 + checkout metadata | Live API 응답 | 5→6 |
|
|
| `webhook-auto-enqueue`: task가 별도 API 호출 없이 authoring queue 진입 | Core log 또는 status=running | 6 |
|
|
| `live-creation-projection`: develop 스캔 → ledger steps → Plane Todo | Core log + Plane UI | 7→8 |
|
|
| slot 재사용: 두 번째 smoke에서 slot available | API 응답 | 9 |
|
|
| `forgejo-auto-delivery`: Forgejo hook task success + Gito branch.updated event | Forgejo DB + Gito API | dev 4→5 |
|
|
|
|
## 완료 오인 방지 기준
|
|
|
|
- filename slug canonical id: `roadmap_sync_identities.milestone_id` 또는 Core log가 `agent-roadmap/phase/*/milestones/<milestone-id>.md`의 파일명 slug 기준으로 매칭됨을 보여야 한다. `roadmap_milestone_path`나 Plane work item id만 맞는 증거는 완료 근거가 아니다.
|
|
- archive 제외: active reconcile 후보는 `agent-roadmap/phase/*/milestones/*.md`만 포함해야 한다. `agent-roadmap/archive/**` 문서는 생성/갱신 대상이 아니라 이력 lookup 후보로만 남아야 한다.
|
|
- provider identity 누락/불일치: generated Milestone에 provider identity가 없거나 원 Plane 티켓과 불일치하면 Plane `Todo` projection으로 넘기지 않는다. sync ledger 보강 또는 명시 failure evidence가 필요하다.
|
|
- IOP queue timeout/stale: IOP `/v1/responses` 장시간 대기, queue timeout, stale monitor는 각각 task metadata에 관찰 가능한 상태로 남아야 한다. authoring 시작이나 단일 실패 로그만으로 terminal 완료/실패를 단정하지 않는다.
|
|
- push-only 누락: authoring 응답 성공은 완료 근거가 아니다. push-only 요청, remote `develop` revision 확인, Gito wakeup 이후 reconcile evidence가 이어져야 다음 단계 완료로 본다.
|
|
- Gito delivery-only 성공: signed webhook 202 또는 duplicate 202는 delivery 증거일 뿐 full-cycle 완료 근거가 아니다. active Milestone slug reconcile enqueue, `roadmap_sync_steps`, Plane body/title/comment projection, `Todo` 이동까지 확인한다.
|
|
- Forgejo 자동 경로 누락: Gito `/callbacks/forgejo/push`에 직접 POST해서 만든 event는 진단/복구 근거다. Forgejo hook task의 성공 delivery 또는 Forgejo UI/API redelivery 성공이 없으면 자동 Forgejo -> Gito 경로 통과 근거로 쓰지 않는다.
|
|
- allowlist 복구 후 재검증: `webhook can only call allowed HTTP servers`가 관찰된 run은 환경 문제 발견 근거로만 남긴다. allowlist 수정 후 새 Plane work item으로 한 번 더 자동 경로를 통과시켜야 한다.
|
|
- full-cycle 완료: 새 Plane `Backlog + AGENT assignee` authoring roundtrip과 Git에만 있는 active Milestone missing-create/reconcile roundtrip을 모두 secret redaction 상태로 남긴다.
|
|
|
|
## dev profile 트러블슈팅
|
|
|
|
### git push 후 `develop_match` 대기
|
|
|
|
증상:
|
|
|
|
- Core task metadata에 `push_request_state=succeeded`가 있는데 task가 `wait_type=develop_match` 또는 running 상태로 오래 남는다.
|
|
- Gito `/api/events`에 새 `after` SHA가 없다.
|
|
|
|
확인:
|
|
|
|
```sh
|
|
docker exec -i forgejo-db sh -lc 'psql -U "$POSTGRES_USER" -d "$POSTGRES_DB" -At' <<'SQL'
|
|
select id, uuid, event_type, is_delivered, is_succeed, left(request_content, 400), left(response_content, 300)
|
|
from hook_task
|
|
order by id desc
|
|
limit 5;
|
|
SQL
|
|
```
|
|
|
|
`response_content`에 `webhook can only call allowed HTTP servers`가 있으면 Forgejo allowlist 문제다.
|
|
|
|
복구:
|
|
|
|
```sh
|
|
cfg="$HOME/docker/services/forgejo/data/volume/gitea/conf/app.ini"
|
|
cp "$cfg" "$cfg.bak-$(date +%Y%m%d%H%M%S)"
|
|
# [webhook] ALLOWED_HOST_LIST에 host.docker.internal을 추가한다.
|
|
grep -n "^ALLOWED_HOST_LIST" "$cfg"
|
|
cd "$HOME/docker/services/forgejo/compose"
|
|
docker compose restart forgejo || docker compose up -d --force-recreate forgejo
|
|
docker inspect forgejo --format '{{.State.Health.Status}}'
|
|
docker exec forgejo curl -fsS --max-time 5 http://host.docker.internal:18061/healthz
|
|
```
|
|
|
|
복구 후에는 기존 run을 수동 redelivery로 닫을 수 있지만, 자동 경로 판정은 새 Plane work item으로 다시 수행한다.
|
|
|
|
### Gito watch 또는 subscription 누락
|
|
|
|
증상:
|
|
|
|
- Forgejo hook task는 `202`를 받았는데 Gito `/api/events`에 event가 없다.
|
|
- `curl http://127.0.0.1:18061/api/listeners/branches` 또는 `/api/webhook-subscriptions`가 빈 목록이다.
|
|
|
|
복구:
|
|
|
|
- Gito 제공 계약의 branch watch와 webhook subscription bootstrap을 따른다.
|
|
- dev target은 `http://127.0.0.1:18011/api/integrations/gito/webhook`이다.
|
|
- signing secret 값은 직접 쓰지 말고 `secret_ref` 또는 Gito runtime의 env resolver를 사용한다.
|
|
|
|
### Plane Todo 미전환
|
|
|
|
증상:
|
|
|
|
- Forgejo hook task 성공, Gito event 생성, Core Gito webhook `202`까지 확인되었지만 Plane state가 Todo가 아니다.
|
|
|
|
확인:
|
|
|
|
- Core log에서 대상 work item의 `gito creation sync enqueued`, `roadmap creation sync completed`, `action`, `reason`을 확인한다.
|
|
- 생성된 milestone의 provider identity와 filename slug가 Plane work item과 맞는지 확인한다.
|
|
- Core task result의 `next_step=plane_todo_moved`가 없으면 완료로 보지 않는다.
|
|
|
|
### Git-origin identity backfill 실패
|
|
|
|
증상:
|
|
|
|
- Git-origin missing-create에서 Plane work item은 생성되었지만 provider identity backfill commit 또는 push가 실패한다.
|
|
- Core log에 `roadmapsyncpipeline: git commit ... Author identity unknown` 또는 `git push origin HEAD:develop ... could not read Username`가 남는다.
|
|
- Plane item은 생성됐지만 Milestone 파일에 `## Provider identity`가 backfill되지 않고 Todo projection self-loop가 이어지지 않는다.
|
|
|
|
확인:
|
|
|
|
```sh
|
|
repo="$HOME/agent-work/nomadcode-dev/workspaces/nomadcode/branches/develop"
|
|
git -C "$repo" config --get user.name || echo "missing git user.name"
|
|
git -C "$repo" config --get user.email || echo "missing git user.email"
|
|
|
|
docker exec nomadcode-core-dev-nomadcode-core-1 sh -lc \
|
|
'git config --get credential.helper || true; test -r /Users/toki/.git-credentials && echo readable || echo not-readable'
|
|
```
|
|
|
|
복구:
|
|
|
|
```sh
|
|
repo="$HOME/agent-work/nomadcode-dev/workspaces/nomadcode/branches/develop"
|
|
git -C "$repo" config user.name "NomadCode Core"
|
|
git -C "$repo" config user.email "nomadcode-core@toki-labs.local"
|
|
|
|
cd "$HOME/agent-work/nomadcode-dev"
|
|
set -a; source .env.plane.local; set +a
|
|
export COMPOSE_PROJECT_NAME=nomadcode-core-dev
|
|
export NOMADCODE_CORE_HOST_PORT=18011
|
|
export GIT_CREDENTIALS_FILE="$HOME/.git-credentials"
|
|
cd services/core
|
|
docker compose up -d --force-recreate --no-deps nomadcode-core
|
|
```
|
|
|
|
복구 후 같은 disposable Milestone에 작은 follow-up edit을 push하면 기존 missing-create identity row를 재사용해 backfill을 재개해야 한다. 이때 새 Plane work item이 중복 생성되면 실패로 본다.
|
|
|
|
## 대체 근거 (런타임 에퀄리번트)
|
|
|
|
reviewed commit을 현재 실행 중인 서버에 배포하지 않은 경우 아래 단계로 대체 근거를 수집한다.
|
|
|
|
### 단계 A: unit test
|
|
|
|
```sh
|
|
cd services/core && go test -count=1 \
|
|
./cmd/plane-smoke/... \
|
|
./internal/http/... \
|
|
./internal/scheduler/... \
|
|
./internal/roadmapsyncpipeline/...
|
|
```
|
|
|
|
커버 범위:
|
|
- `TestRoadmapCreationSyncWorkerFinalizesExternalTaskOnProjected` → projection + slot available 전환
|
|
- `TestMarkFailedWithTaskAuthoringRecordsState` → failure + slot error 전환
|
|
- `TestMarkFailedWithTaskAuthoringMissingSlotIDSkipsSlotUpdate` → missing slot_id skip
|
|
- `TestSyncCreationReadyRunsStepsAndMarksLedger` → ledger steps all marked
|
|
- webhook HTTP handler tests → dispatch gate logic
|
|
|
|
### 단계 B: 기존 baseline 확인
|
|
|
|
```sh
|
|
# 2026-06-15 smoke baseline (commit 55e0139 기준 서버)
|
|
curl -s http://127.0.0.1:18010/api/tasks/e3c92411-4056-4b76-a879-66f7904125d0
|
|
# Expected: status=completed, checkout.slot_id=1, source=plane
|
|
```
|
|
|
|
`live-webhook-task`와 `webhook-auto-enqueue`의 일부 증거는 이 baseline이 커버한다.
|
|
`live-creation-projection` (Plane Todo 이동)과 slot terminal state 회수는 reviewed commit 배포 후만 검증 가능.
|
|
|
|
## 차단 기준
|
|
|
|
- Core server를 현재 검증 대상 상태로 재배포할 수 없으면 `live-creation-projection`과 slot 회수 증거를 수집할 수 없다.
|
|
- 원격 runner checkout의 dirty 상태는 차단 기준이 아니다. reset/clean/reclone, 파일 동기화, Docker image 재빌드·재배포를 먼저 수행하고, 권한·secret·외부 서비스 장애처럼 에이전트가 해소할 수 없는 사유만 차단으로 보고한다.
|
|
|
|
## 보고 항목
|
|
|
|
- 실행한 명령:
|
|
- 성공한 검증:
|
|
- 실패/차단된 검증:
|
|
- 생략 사유:
|
|
- 남은 위험:
|