# task-3033 — GitHub Actions 소비 절감 (3,000분 소진 재발 방지)

- **레벨**: Lv.2 · **팀**: dev5-team (마르둑) · **저장소**: `Jeon-Jonghyuk/InsuRo`
- **PR**: [#261](https://github.com/Jeon-Jonghyuk/InsuRo/pull/261) (draft, OPEN) · 브랜치 `task/task-3033-dev5` · head `882ccf2`
- **범위**: PR 생성까지. **머지는 ANU 가 한다 — 자동 머지 금지 준수**
- **레벨: 코드 수정 없음** 아님 (워크플로우 YAML 3파일 수정)

## S — 상황
2026-08-26 InsuRo 의 GitHub Actions 분이 3,000/3,000 (100%) 소진되어 잡이 **시작조차 못 하고 2초 만에 실패**했다.
Billable $10.00, 저장소별 사용량은 InsuRo $27.72 · InsuWiki $0.29 로 사실상 InsuRo 단독 소비였다.
회장이 지출 한도를 인상해 당장은 풀렸으나 구조를 고치지 않으면 다음 달 재발한다.

## C — 문제
`main` push 마다 워크플로우 3종(`ci.yml` · `e2e.yml` · `diagnostic-pytest.yml`)이 **전량 실행**된다.
`paths` 필터는 `extension-release.yml` 에만 있었다.

## Q — 질문
검증을 약화시키지 않으면서 어디를 얼마나 줄일 수 있는가? 그리고 그 절감량은 실제로 몇 분인가?

## A — 답변
트리거 조건만 조정해 **27일 기준 660분(월 733분, 한도의 24.4%)** 을 절감한다.
테스트 내용·스텝·secrets·permissions 는 **무변경**이며, PR 단계 검증은 **전부 유지**된다.

---

## 1. 변경 내용

| 워크플로우 | 변경 |
|---|---|
| ci.yml | push/PR 양쪽에 paths-ignore 추가 |
| e2e.yml | paths-ignore 추가 + push:main 트리거 **제거** (PR 에서만) |
| diagnostic-pytest.yml | paths-ignore 추가 + push → **schedule 일 1회** (06:00 KST = `0 21 * * *` UTC) + workflow_dispatch 추가. PR 트리거 유지 |

`paths-ignore` 최종 목록 (3파일 공통): `'*.md'`(루트 한정) · `'docs/**'` · `'memory/**'` · `'.gitignore'`

## 2. ★ 명세 전제 반증 3건 (실측 근거)

### 반증 ① — 명세가 지정한 `'**/*.md'` 는 검증 구멍을 만든다 (수용 불가 → 좁힘)
- `server/silson/data/sources/{ocr,case_law}/*.md` **19개는 문서가 아니라 OCR 실데이터**다.
- `server/silson/tools/seal_sources.py:194` 가 `input_dir.glob("*.md")` 로 읽고,
  `server/tests/test_silson_provenance.py::test_manifest_ocr_files_exist_and_hash_matches` 가
  이 파일들의 **sha256 을 재계산해 manifest 와 일치하는지 단언**한다 (파일이 바뀌면 테스트가 깨진다).
- 이 테스트는 CI 의 pytest 제외목록(`test_gdrive_sync`·`test_ingest_fcpa`)에 **없다** = CI 검증 대상이다.
- 따라서 `'**/*.md'` 를 제외하면 **이 데이터만 고친 변경이 CI 없이 통과**한다.
  태스크가 경고한 "조용한 검사 누락"이 정확히 이것이므로 루트 한정 `'*.md'` 로 좁혔다.
  (GitHub path 필터에서 `*` 는 `/` 를 넘지 않으므로 하위 디렉터리 `.md` 는 걸리지 않는다.)

### 반증 ② — `LICENSE` 는 저장소에 존재하지 않는다
`git ls-files LICENSE` = **0건**. 죽은 설정이므로 제외 목록에서 뺐다.

### 반증 ③ — "보고서 .md 한 줄" 때문에 분이 샌 것이 아니다 (절감 귀속의 정정)
명세는 문서 커밋이 소비의 원인이라고 전제했으나 실측은 다르다.
- 2026-08 main 커밋 **95건 중 문서 전용 커밋 0건**. `memory/` 는 추적 파일이 **단 1개**(`memory/reports/task-3023.md`)뿐이다.
- 즉 ①(paths-ignore)이 push:main 에서 절약했을 분은 **0분**이다.
- 실제 소비 구조는 "**PR 에서 검증한 것을 머지 후 다시 검증**"이며, 절감의 실체는 ②③이다.
- ① 의 실질 절감은 **PR 브랜치 중간 push** 에서만 발생했다 (아래 3절, 실측 3건).
- ① 은 그럼에도 유지한다 — 비용 0 이고 앞으로 문서 전용 커밋이 생길 때 방어가 된다.

## 3. 절감 산식 (2026-08-01~08-27, 27일 실측 — 추측 없음)

측정 방법: `actions/runs` 를 `.path` 기준으로 집계(★ `ci.yml` 과 `taskctl-ci.yml` 은 **둘 다 `name: CI`** 라 이름 집계는 오집계된다).
push:main 실행 **285건 전량**에 대해 `/timing` 의 `run_duration_ms` 를 수집해 분 단위 올림 합산했다.
대상 3종은 모두 **단일 잡**(`ci`/`e2e-test`/`diagnostic`)이므로 실행시간이 곧 청구시간이다.
(`billable.UBUNTU.total_ms` 는 전 표본 0 을 반환해 사용 불가 → `run_duration_ms` 로 대체 측정)

```
push:main 실측 청구   ci 699분 · e2e 237분 · diagnostic 513분   (합 1,449분 / 27일)
표본 평균/건          ci 6.90분 · e2e 2.11분 · diagnostic 4.92분

① paths-ignore  : push:main 문서전용 커밋 0건               ->   0분
                  PR 단계 문서전용 head 3건 x (7+3+5)분     ->  45분
② e2e push 제거 : 237분
③ diagnostic    : push 513분 - schedule 27일x5분(135분)     -> 378분
                                                   27일 합계  660분
                                             30일 환산 733분/월 (한도 3,000분의 24.4%)
```

① 의 PR 단계 3건은 전수 조사 결과다: `ci.yml` PR 실행 138건의 head 커밋을 전부 로컬 조회(조회불가 0건)해
문서 전용 3건 / 코드 포함 130건 / 빈 diff 5건으로 분류했다. 문서 전용 3건은 모두 `task-3023` 브랜치의
`memory/reports/task-3023.md` 단독 변경이다.

## 4. 트리거 매트릭스 (변경 후)

| 경우 | ci.yml | e2e.yml | diagnostic-pytest.yml | taskctl-ci.yml |
|---|---|---|---|---|
| (a) src/ 코드 변경 PR | 실행 | 실행 | 실행 | 실행 |
| (b) memory/reports/*.md 만 변경 | 스킵 | 스킵 | 스킵 | 실행 |
| (c) main 에 squash 머지(코드 포함) | 실행 | 스킵(제거) | 스킵(schedule 로 이동) | 해당없음(PR 전용) |
| (d) .github/workflows/ 변경 | 실행 | 실행 | 실행 | 실행 |

## 5. 검증 공백 없음 — 어떤 검사가 언제 사라지는가

| 검사 | 변경 전 | 변경 후 | 공백? |
|---|---|---|---|
| PR 단계 ci (tsc/build/vitest/pytest) | 실행 | 실행 | 없음 |
| PR 단계 e2e (Playwright) | 실행 | 실행 | 없음 |
| PR 단계 diagnostic (full failure matrix) | 실행 | 실행 | 없음 |
| required check 8종 (taskctl-ci) | 실행 | 실행 | 없음 |
| 머지 후 main 재검증 e2e | 실행 | 없음 | **의도된 제거** — 근거 아래 |
| 머지 후 main 재검증 diagnostic | 매 push | 일 1회 + 수동 | **주기 완화** — 최대 24h 지연 |

- **required status check 함정 해소**: required 8종(`cancel-kill-switch`/`qc-check`/`hidden-path-audit`/
  `lock-in-check`/`merge-safety-check`/`gemini-review-gate`/`ci/guard`/`guard`)은 전부 **`taskctl-ci.yml`** 이 만든다.
  이 워크플로우는 `pull_request` 전용이고 **paths 필터가 없다**. 따라서 (b) 문서 전용 PR 에서도 required check 이
  정상 보고되어 **머지가 영구 대기에 빠지지 않는다**. (active ruleset 은 `main-protection` id 15953275 하나뿐이며
  `Insuro 260821` id 21126621 은 `enforcement: disabled` 로 무효)
- **e2e push 제거 안전 근거**: 최근 main 커밋 60건 **전부 PR 경유**(직접 push 0건, 정규식 미스매치 1건도
  `commits/{sha}/pulls` 로 PR#227 연결 확인). ruleset 이 PR 필수 + `non_fast_forward` + `current_user_can_bypass: never`
  로 직접 push 를 **구조적으로 차단**한다. squash 머지라 PR head 와 main 반영 내용이 동일하다.
- **diagnostic PR 트리거 유지 근거**: 목적이 "first-fail 에 가려진 전체 failure matrix 가시성 복구"이므로
  PR 단계 가치가 가장 크다. 머지 후 발견은 늦다. `workflow_dispatch` 로 수동 실행 경로도 확보했다.
- **`.github/workflows/**` 는 제외 목록에 없다** — CI 설정 변경은 반드시 검증된다 (6절 실증).

## 6. 실증 — 이 PR 자체에서 CI 가 실행됨

PR #261 은 `.github/workflows/**` 만 변경하는 PR 이다. 새 `paths-ignore` 가 적용된 상태에서 워크플로우 4종이
**전부 트리거됐다** — `.github/workflows/**` 가 제외되지 않았음의 실행 증거다.

```
.github/workflows/ci.yml                 pull_request  run 33024183520
.github/workflows/e2e.yml                pull_request  run 33024183537
.github/workflows/diagnostic-pytest.yml  pull_request  run 33024183530
.github/workflows/taskctl-ci.yml         pull_request  run 33024183512
```

**최종 결과 — 4종 전부 `success`** (2026-08-27 실측):

```
ci.yml                  completed  success
e2e.yml                 completed  success
diagnostic-pytest.yml   completed  success
taskctl-ci.yml          completed  success   (required check 8종)
```

즉 (d) 케이스가 실행 증거로 확정됐고, 수정된 YAML 이 GitHub 러너에서 **문법 오류 없이 파싱·실행**됨이
실증됐다. 이 태스크의 최대 위험("문법 오류로 CI 가 조용히 안 돔")은 해소됐다.

## L1 스모크테스트
- **서버 재시작**: 해당없음 (런타임 코드 무변경 — CI 트리거 설정만 수정)
- **API 응답 확인**: 해당없음
- **스크린샷**: 해당없음
- **대체 실동작 검증 (이 태스크의 L1 등가물)**: PR #261 에서 GitHub Actions 4종이 **실제로 트리거된 것을
  API 로 확인**(6절). 로컬 pytest/actionlint PASS 가 아니라 **GitHub 러너의 실제 트리거 판정**이므로
  "pytest PASS ≠ 실동작" 요건을 충족한다. **4종 전부 `success` 로 완료**됨을 확인했다. `schedule` 트리거의 실제 발화(21:00 UTC)는 머지 후에만
  확인 가능 — **미확인**으로 남긴다.

## 7. 수정 파일별 검증 상태

| 파일 | 변경 | actionlint | YAML 파서 | 원격 blob | CI 실행 |
|---|---|---|---|---|---|
| .github/workflows/ci.yml | paths-ignore 추가 | PASS | PASS | e34e926 일치 | success |
| .github/workflows/e2e.yml | paths-ignore + push 제거 | PASS | PASS | 0d1a626 일치 | success |
| .github/workflows/diagnostic-pytest.yml | paths-ignore + schedule 전환 | PASS | PASS | 9965e50 일치 | success |

- `actionlint` v1.7.12 exit=0 (오류 0건) — 엔키 1회 + 팀장 독립 재실행 1회 = **2회**
- PyYAML 파싱 정상. ★ PyYAML 은 `on:` 을 **불리언 `True` 키**로 파싱한다 — `d['on']` KeyError 는 문법 오류가 아니다
- blob 동일성 로컬 `git hash-object` ↔ 원격 blob sha **3/3 일치**

## 8. trip-wire 5종 실측

| 항목 | 실측 | 근거 |
|---|---|---|
| Critical7 | 0 | 코드 로직 무변경 (트리거 설정만) |
| PII net-new | 0 | secrets 참조·permissions 무변경, 신규 문자열은 cron/glob 뿐 |
| 회귀 실패 | 0 | PR #261 CI 4종 전부 `success` (ci/e2e/diagnostic/taskctl-ci) |
| forbidden_paths 침범 | 0 | `git diff --name-only origin/main...HEAD` 가 3개 워크플로우 파일만 출력 |
| nonce | task-3033 | 일치 |

## 9. 3 Step Why

- **1st Why — 왜 이 설계가 필요한가?**
  A: main push 마다 3종 전량 실행이 Actions 분의 실질 소비원(1,449분/27일)이고, 그 대부분은
  PR 에서 이미 끝낸 검증의 중복이다.
- **2nd Why — 왜 A 가 최선인가?**
  B: 중복 제거(②③)는 검증을 **하나도 줄이지 않고** 660분/27일을 없앤다. ruleset 이 직접 push 를
  구조적으로 차단하므로 "머지 후 재검증"이 잡을 수 있는 것이 원리적으로 존재하지 않는다.
- **3rd Why — 왜 B 가 대안보다 나은가?**
  C: 대안인 "테스트 축소"는 검증을 실제로 줄이고, "paths 화이트리스트"는 새 디렉터리가 조용히
  검사에서 빠진다. 트리거 중복 제거만이 검증량을 보존한 채 비용을 줄인다. A-B-C 일관.

## 10. ★ ANU 판단 요청 (범위 밖)

1. **`ci.yml` push:main 이 남은 최대 소비원** — 699분/27일(약 776분/월)로 단일 최대다. 명세가 ci 트리거
   유지를 지시해 이번 범위에서 제외했다. ②와 **동일한 근거**(직접 push 0건 + ruleset 구조적 차단)가
   ci 에도 그대로 성립하므로 제거 가능성 판단을 요청한다.
2. **`taskctl-ci.yml` 은 paths 필터가 없고 잡이 8개**다. PR 138회 실행됐다. required check 이라 손대면
   안 되지만 잡 단위 청구라 비용 기여가 있을 수 있다 — **미측정**(잡별 duration 미수집).
3. **taskctl 자동 머지 봇이 ci/e2e/diagnostic 의 conclusion 을 자체 로직으로 기다리는지 미확인.**
   GitHub 레벨 required check 은 안전함을 확인했으나 봇 내부 코드는 조사 범위 밖이었다.
   (b) 문서 전용 PR 에서 봇이 "스킵된 워크플로우"를 무한 대기할 가능성은 배제하지 못했다.
4. **빈 diff 커밋 5건** — 첫 부모와 트리가 동일한 커밋이 8월 main 에 5건 있다. paths 필터가 이런 커밋을
   어떻게 처리하는지는 **미측정**이며 절감 산식(①=3건)에 포함하지 않았다(과대추정 방지).

## 11. 모델 사용 기록

| 팀원 | 모델 | 담당 |
|---|---|---|
| 엔키 (백엔드) | sonnet | 워크플로우 YAML 3파일 수정 + actionlint/파서 검증 (2회 위임) |
| 닌기르수 (테스트/QA) | sonnet | ruleset/required check·직접 push 이력·실행 이력·paths 대상 실재성 조사 (2회 위임) |
| 마르둑 (팀장) | opus | 설계·판단·독립 검증·절감 실측 집계·PR 생성 |

haiku 미사용. 이쉬타르(프론트)·나부(UX/UI)는 해당 작업 없어 미소집.
팀장 직접 수행분은 코딩이 아닌 **판단/검증/집계**에 한정했다(절감 실측 집계는 이름 충돌 오집계 위험이
확인되어 팀장이 직접 `.path` 기준으로 재수행).

## 세션 통계
- 총 도구 호출: 0회


## 세션 통계
- 총 도구 호출: 0회


## 세션 통계
- 총 도구 호출: 0회


## 세션 통계
- 총 도구 호출: 0회


## 세션 통계
- 총 도구 호출: 0회


## 세션 통계
- 총 도구 호출: 0회


## 세션 통계
- 총 도구 호출: 0회


## 세션 통계
- 총 도구 호출: 0회


## 세션 통계
- 총 도구 호출: 0회


## 세션 통계
- 총 도구 호출: 0회


## 세션 통계
- 총 도구 호출: 0회


## 세션 통계
- 총 도구 호출: 0회


## 세션 통계
- 총 도구 호출: 0회


## 세션 통계
- 총 도구 호출: 0회


## 세션 통계
- 총 도구 호출: 0회


## 세션 통계
- 총 도구 호출: 0회


## 세션 통계
- 총 도구 호출: 0회

