# 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 <step name>`과 `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` 생성)
