# Gitea Actions API 호출 가이드 **작성일**: 2026-07-11 **대상**: QuantEngine CI/CD 담당자 **목표**: CLI에서 Gitea Actions 상태 조회 및 troubleshooting --- ## 사전 요구사항 ### 환경 변수 설정 ```powershell # PowerShell $env:GITEA_TOKEN_TAXBAIK = "your_gitea_access_token" # 또는 Windows 환경변수 저장 [Environment]::SetEnvironmentVariable("GITEA_TOKEN_TAXBAIK", "your_token", "User") ``` ### 토큰 생성 1. Gitea 웹 UI: https://gitea.taxbaik.com/user/settings/applications 2. "Generate New Token" → 권한: `repo`, `read:actions` 3. 토큰 복사 및 환경 변수 설정 --- ## API Endpoints ### 1. 최근 Workflow Runs 조회 ```powershell $token = $env:GITEA_TOKEN_TAXBAIK $response = Invoke-WebRequest ` -Uri "https://gitea.taxbaik.com/api/v1/repos/kjh2064/QuantEngineByItz/actions/runs" ` -Headers @{ "Accept" = "application/json" "Authorization" = "token $token" } $data = $response.Content | ConvertFrom-Json $data.workflow_runs | ForEach-Object { Write-Host "Run #$($_.id): $($_.display_title) [$($_.status)/$($_.conclusion)]" } ``` **Bash/cURL 버전:** ```bash curl -X GET "https://gitea.taxbaik.com/api/v1/repos/kjh2064/QuantEngineByItz/actions/runs" \ -H "Accept: application/json" \ -H "Authorization: token $GITEA_TOKEN_TAXBAIK" | jq '.workflow_runs[] | {id, display_title, status, conclusion}' ``` --- ### 2. 특정 Run 상세 정보 조회 ```powershell $run_id = 1987 $response = Invoke-WebRequest ` -Uri "https://gitea.taxbaik.com/api/v1/repos/kjh2064/QuantEngineByItz/actions/runs/$run_id" ` -Headers @{ "Authorization" = "token $env:GITEA_TOKEN_TAXBAIK" } $run = $response.Content | ConvertFrom-Json Write-Host "Run #$($run.id)" Write-Host " Title: $($run.display_title)" Write-Host " Status: $($run.status)" Write-Host " Conclusion: $($run.conclusion)" Write-Host " Commit: $($run.head_sha)" Write-Host " Branch: $($run.head_branch)" Write-Host " Created: $($run.created_at)" Write-Host " Updated: $($run.updated_at)" ``` --- ### 3. Run의 Jobs 조회 ```powershell $run_id = 1987 $response = Invoke-WebRequest ` -Uri "https://gitea.taxbaik.com/api/v1/repos/kjh2064/QuantEngineByItz/actions/runs/$run_id/jobs" ` -Headers @{ "Authorization" = "token $env:GITEA_TOKEN_TAXBAIK" } $jobs_data = $response.Content | ConvertFrom-Json $jobs_data.jobs | ForEach-Object { Write-Host "Job #$($_.id): $($_.name)" Write-Host " Status: $($_.status), Conclusion: $($_.conclusion)" Write-Host " Started: $($_.started_at)" Write-Host " Completed: $($_.completed_at)" } ``` --- ## Troubleshooting ### 문제: Run이 failure 상태 **원인 분석:** ```powershell # 1. Jobs 상태 확인 $run_id = 1987 $response = Invoke-WebRequest ` -Uri "https://gitea.taxbaik.com/api/v1/repos/kjh2064/QuantEngineByItz/actions/runs/$run_id/jobs" ` -Headers @{ "Authorization" = "token $env:GITEA_TOKEN_TAXBAIK" } $jobs = ($response.Content | ConvertFrom-Json).jobs # 2. failure 상태인 job 찾기 $failed_jobs = $jobs | Where-Object { $_.conclusion -eq "failure" } $failed_jobs | ForEach-Object { Write-Host "Failed Job: $($_.name) (ID: $($_.id))" Write-Host " Status: $($_.status)" } # 3. Build 로그 확인 (로컬 또는 프로덕션 서버) ssh kjh2064@178.104.200.7 'ls /opt/stacks/gitea/gitea/actions_log/kjh2064/taxbaik/*/*.log.zst' ``` ### 문제: Act Runner 연결 실패 **증상:** ``` error="unavailable: dial tcp 172.18.0.2:3000: connect: connection refused" ``` **해결 방법:** ```bash # 1. Runner 상태 확인 docker ps | grep runner # 2. Runner 로그 확인 docker logs gitea-runner | grep -E "error|failed|connection" | tail -20 # 3. Gitea ↔ Runner 네트워크 확인 docker network ls docker network inspect bridge | grep -E "Name|Containers" # 4. Runner 재시작 (위험: 진행 중인 job 중단) docker restart gitea-runner gitea-runner-2 gitea-runner-3 ``` --- ## 실행 예제 ### 예제 1: 최근 Failed Run 찾기 ```powershell $response = Invoke-WebRequest ` -Uri "https://gitea.taxbaik.com/api/v1/repos/kjh2064/QuantEngineByItz/actions/runs?limit=10" ` -Headers @{ "Authorization" = "token $env:GITEA_TOKEN_TAXBAIK" } ($response.Content | ConvertFrom-Json).workflow_runs ` | Where-Object { $_.conclusion -eq "failure" } ` | ForEach-Object { Write-Host "❌ Run #$($_.id): $($_.display_title)" Write-Host " Commit: $($_.head_sha.Substring(0, 7))" Write-Host " Time: $($_.completed_at)" } ``` ### 예제 2: Run 전체 Job 상태 맵 ```powershell function Show-RunStatus { param($RunId) $run_url = "https://gitea.taxbaik.com/api/v1/repos/kjh2064/QuantEngineByItz/actions/runs/$RunId" $run = (Invoke-WebRequest -Uri $run_url -Headers @{ "Authorization" = "token $env:GITEA_TOKEN_TAXBAIK" }).Content | ConvertFrom-Json Write-Host "Run #$RunId ($($run.display_title))" -ForegroundColor Cyan Write-Host "Status: $($run.status) / Conclusion: $($run.conclusion)" Write-Host "" $jobs_url = "$run_url/jobs" $jobs = (Invoke-WebRequest -Uri $jobs_url -Headers @{ "Authorization" = "token $env:GITEA_TOKEN_TAXBAIK" }).Content | ConvertFrom-Json $jobs.jobs | ForEach-Object { $icon = if ($_.conclusion -eq "success") { "✓" } elseif ($_.conclusion -eq "failure") { "✗" } else { "⊘" } Write-Host " [$icon] $($_.name) ($($_.status))" } } # 사용 Show-RunStatus -RunId 1987 ``` --- ## API 응답 구조 ### Run Object ```json { "id": 1987, "display_title": "CI: Trigger deploy-prod.yml workflow via git push", "head_sha": "5b41423aef4a03398f6b80c55c959563583e4f28", "head_branch": "main", "status": "completed", "conclusion": "failure", "created_at": "2026-07-11T22:33:06+09:00", "updated_at": "2026-07-11T22:33:34+09:00" } ``` ### Job Object ```json { "id": 2375, "name": "Build Release", "status": "completed", "conclusion": "failure", "started_at": "2026-07-11T13:33:06+09:00", "completed_at": "2026-07-11T13:33:34+09:00" } ``` --- ## 자주 묻는 질문 (FAQ) **Q: 토큰 권한이 부족하면?** ``` "message": "invalid username, password or token" ``` A: Gitea 설정에서 토큰 재생성, `repo` + `read:actions` 권한 부여 **Q: Run 로그를 API로 다운로드할 수 없나?** A: 현재 Gitea API는 `/actions/runs/{id}/logs` 지원하지 않음. 프로덕션 서버에서 `/opt/stacks/gitea/gitea/actions_log/` 디렉토리 직접 접근 **Q: 가장 최신 Run 빠르게 확인하는 법?** ```powershell $latest = ((Invoke-WebRequest -Uri "https://gitea.taxbaik.com/api/v1/repos/kjh2064/QuantEngineByItz/actions/runs?limit=1" ` -Headers @{ "Authorization" = "token $env:GITEA_TOKEN_TAXBAIK" }).Content | ConvertFrom-Json).workflow_runs[0] 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 / prepare-release.yml](.gitea/workflows/) - [Gitea Official API Docs](https://docs.gitea.io/en-us/api-usage/) --- **마지막 업데이트**: 2026-07-12 **상태**: prepare-release.yml 운영 검증 완료 (Run #2000 성공, 릴리즈 `quant_20260711.1.6ab270f` 생성)