diff --git a/docs/GITEA_ACTIONS_API_GUIDE.md b/docs/GITEA_ACTIONS_API_GUIDE.md index cb2ebd0f..2a046958 100644 --- a/docs/GITEA_ACTIONS_API_GUIDE.md +++ b/docs/GITEA_ACTIONS_API_GUIDE.md @@ -240,13 +240,144 @@ Write-Host "$($latest.display_title): $($latest.conclusion)" --- +## Workflow 트리거 + 모니터링 하네스 (PowerShell) + +Gitea Actions API에는 `/actions/runs/{id}/jobs/{job_id}/logs` 엔드포인트가 **없다** (404). +따라서 워크플로우를 API로 트리거하고 완료까지 폴링한 뒤, 실패 시 **SSH로 서버에 직접 접속해 +로그 파일을 읽는 2단계 하네스**가 필요하다. 아래 스크립트가 그 표준 패턴이다. + +### 1단계: workflow_dispatch 트리거 + 완료까지 폴링 + +```powershell +$token = $env:GITEA_TOKEN_TAXBAIK +$repo = "kjh2064/QuantEngineByItz" +$api = "https://gitea.taxbaik.com/api/v1" + +# 트리거 (workflow 파일명을 그대로 ID로 사용 가능) +$body = @{ ref = "main" } | ConvertTo-Json +$response = Invoke-WebRequest -Method POST ` + -Uri "$api/repos/$repo/actions/workflows/prepare-release.yml/dispatches" ` + -Headers @{ "Authorization" = "token $token" } ` + -ContentType "application/json" -Body $body +# 성공 시 Status: 204 (No Content) 반환 -- 이것이 정상 응답이다 + +Start-Sleep -Seconds 3 # run이 목록에 나타날 때까지 약간의 지연 필요 + +# 방금 생성된 run 조회 (limit=1이 항상 최신순) +$runs = Invoke-WebRequest -Uri "$api/repos/$repo/actions/runs?limit=1" ` + -Headers @{ "Authorization" = "token $token" } | ConvertFrom-Json +$run = $runs.workflow_runs[0] +$runId = $run.id + +# 완료까지 폴링 (8초 간격, 최대 5분) +$elapsed = 0 +while ($run.status -ne "completed" -and $elapsed -lt 300) { + Start-Sleep -Seconds 8 + $elapsed += 8 + $run = Invoke-WebRequest -Uri "$api/repos/$repo/actions/runs/$runId" ` + -Headers @{ "Authorization" = "token $token" } | ConvertFrom-Json +} + +Write-Host "Conclusion: $($run.conclusion)" + +# Job별 결과 확인 +$jobs = Invoke-WebRequest -Uri "$api/repos/$repo/actions/runs/$runId/jobs" ` + -Headers @{ "Authorization" = "token $token" } | ConvertFrom-Json +$jobs.jobs | ForEach-Object { + $icon = if ($_.conclusion -eq "success") { "OK" } elseif ($_.conclusion -eq "failure") { "FAIL" } else { "SKIP" } + Write-Host " [$icon] $($_.name)" +} +``` + +**주의사항**: +- `Invoke-WebRequest`의 에러 응답 본문은 `$_.Exception.Response.Content`로 읽으려 하면 + `HttpResponseMessage`에 `GetResponseStream()`이 없어서 실패한다 (PowerShell 7 / .NET + `HttpClient` 기반이기 때문). 상태 코드(`$_.Exception.Response.StatusCode`)만 신뢰하고, + 본문이 필요하면 애초에 `-ErrorAction Stop` 없이 시도하거나 SSH 로그 쪽으로 넘어가는 게 빠르다. +- workflow ID는 파일명(`prepare-release.yml`)을 그대로 쓸 수 있다 — 매번 + `/actions/workflows` 목록을 조회해서 숫자 ID를 찾을 필요 없음. + +### 2단계: 실패 시 SSH로 실제 로그 읽기 (API 로그 엔드포인트 우회) + +Job이 `failure`면, 어떤 step에서 실패했는지 API로는 알 수 없다. 실제 stdout/stderr는 +프로덕션 서버의 압축된 로그 파일에만 존재한다. + +```bash +# 1. 어떤 act_runner가 이 run을 처리했는지, task ID가 몇 번인지 확인 +# (run 트리거 직후 곧바로 실행 — 여러 runner에 로드밸런싱되므로 3개 다 확인) +ssh kjh2064@178.104.200.7 \ + 'for r in gitea-runner gitea-runner-2 gitea-runner-3; do + echo "=== $r ==="; docker logs --since 3m $r 2>&1 | grep "task 2" + done' +# 출력 예: task 2326 repo is kjh2064/QuantEngineByItz ... +# → task ID 2326이 방금 트리거한 run에 해당 + +# 2. task ID로 실제 로그 파일 위치 찾기 (디렉토리는 ID 기반 샤딩됨: XX/task_id.log.zst) +ssh kjh2064@178.104.200.7 \ + 'find /opt/stacks/gitea/gitea/gitea/actions_log/kjh2064/QuantEngineByItz \ + -name "2326.log.zst"' +# → .../16/2326.log.zst + +# 3. zstd로 압축 해제하며 바로 읽기 (파일로 풀 필요 없음) +ssh kjh2064@178.104.200.7 \ + 'zstd -dc /opt/stacks/gitea/gitea/gitea/actions_log/kjh2064/QuantEngineByItz/16/2326.log.zst' \ + | grep -A 15 "Failure\|exitcode" +``` + +**핵심 포인트**: +- 로그 경로 규칙: `actions_log/{owner}/{repo}/{taskId 앞 또는 뒤 hex 2자리}/{taskId}.log.zst` + (샤딩 방식은 taskId를 hex로 표현한 문자열의 접두 디렉토리 — `find`로 찾는 게 가장 안전함) +- 압축 해제 없이 `zstd -dc`로 스트리밍 읽기 가능. `.zst` 확장자를 보고 `cat`으로 읽으면 + 바이너리가 그대로 출력되니 반드시 `zstd -dc`를 거칠 것. +- 로그 안에서 실패 지점은 `❌ Failure - Main `과 `exitcode 'N': ...` 패턴으로 + 검색하면 즉시 찾아짐 (grep -A 15로 앞뒤 문맥 함께 확인). +- taxbaik 프로젝트의 로그도 같은 서버, 같은 `actions_log` 루트 아래 `kjh2064/taxbaik/`에 + 섞여 있으니 repo 이름으로 경로를 좁혀야 함. + +### 네트워크/인프라 디버깅 (dispatch가 500을 반환하거나 job이 안 뜰 때) + +```bash +# Runner 컨테이너들이 올바른 네트워크에 붙어 있는지 확인 +ssh kjh2064@178.104.200.7 \ + 'docker network inspect gitea_default --format "{{range .Containers}}{{.Name}} {{.IPv4Address}}{{println}}{{end}}"' +# gitea-runner, gitea-runner-2, gitea-runner-3 만 여기 있어야 정상. +# (과거 실험적으로 띄웠던 이름 없는 컨테이너들이 default bridge에 남아있는 경우가 +# 있는데, 이들은 gitea:3000에 도달 못해 "connection refused"로 무한 재시도만 함 — +# 실제 job 처리에는 영향 없지만 리소스 낭비이므로 발견 시 정리 대상) + +# gitea 컨테이너가 재시작된 시점 확인 (재시작 직후 몇 초는 runner가 접속 실패할 수 있음) +ssh kjh2064@178.104.200.7 \ + 'docker inspect gitea --format "RestartCount: {{.RestartCount}}\nStartedAt: {{.State.StartedAt}}"' + +# 실제 러너 → gitea 연결 테스트 (컨테이너 내부에서) +ssh kjh2064@178.104.200.7 \ + 'docker exec gitea-runner sh -c "wget -O- -T 5 http://gitea:3000/ 2>&1 | head -3"' +``` + +`dispatch` API가 500을 반환하는 흔한 원인 두 가지: +1. **workflow YAML 문법 오류** — `--notes "여러줄\n텍스트"`처럼 멀티라인 문자열에 콜론(`:`)이 + 포함되면 YAML 파서가 `mapping values are not allowed here`로 깨짐. 로컬에서 + `python3 -c "import yaml; yaml.safe_load(open('file.yml'))"`로 먼저 검증할 것. +2. **Gitea 컨테이너 재시작 타이밍과 겹침** — 일시적이며 몇 초 후 재시도하면 해결. + +### 실제로 겪은 실패 패턴 모음 + +| 증상 (API/로그) | 원인 | 해결 | +|---|---|---| +| dispatch 500, "mapping values are not allowed here" | YAML 멀티라인 문자열에 `:` 포함 | 단일 라인 `--notes`로 축약, 또는 `env:` + heredoc 사용 | +| job은 뜨는데 특정 step에서 `exitcode '1'` + 그 직전 줄이 `git config user.name` | 러너 컨테이너에 git 전역 identity 미설정 (`set -e`라 즉시 중단) | 태그/커밋 전에 `git config user.name "Gitea Actions"` 명시적으로 설정 | +| `exitcode '127': command not found` | act_runner 기본 이미지에 `gh` CLI 없음 | `gh release create` 대신 `curl` + Gitea REST API (`POST /repos/{r}/releases`, `POST /repos/{r}/releases/{id}/assets`) 직접 호출 | +| runner 로그에 `dial tcp 172.18.0.2:3000: connect: connection refused` | gitea 컨테이너 재시작 타이밍과 겹친 일시적 현상, 또는 잘못된 네트워크(bridge)에 붙은 유령 러너 | 몇 초 후 재시도; `docker network inspect gitea_default`로 정상 러너 3개만 있는지 확인 | + +--- + ## 관련 문서 - [CLAUDE.md - Deployment Gates](https://gitea.taxbaik.com/kjh2064/QuantEngineByItz/src/branch/main/CLAUDE.md) -- [deploy-prod.yml - 4-Stage Pipeline](.gitea/workflows/deploy-prod.yml) +- [deploy-prod.yml / prepare-release.yml](.gitea/workflows/) - [Gitea Official API Docs](https://docs.gitea.io/en-us/api-usage/) --- -**마지막 업데이트**: 2026-07-11 -**상태**: 운영 중 - Act Runner 연결 불안정 이슈 진행 중 +**마지막 업데이트**: 2026-07-12 +**상태**: prepare-release.yml 운영 검증 완료 (Run #2000 성공, 릴리즈 `quant_20260711.1.6ab270f` 생성)