Repository navigation
fix(ci): Android Lint 리포터가 매 run reviewdog 을 네트워크 설치 — 실패 시 required check 가 red, 재시도로는 못 푼다 #1093
Description
현황
lint.yml 의 Report Android Lint (Reviewdog) 스텝이 lint 위반이 0건인 PR 에서도 job 을 실패시킨다.
Check Project Issues (Android Lint) 는 develop ruleset 의 required status check 다. 즉 이 실패는 로그 노이즈가 아니라 머지 차단이다.
실측 — run 32742963935 / job 97481566012 (PR #1080):
| 스텝 | 결과 | 소요 |
|---|---|---|
Build dvdandroid/action-android-lint@master |
성공 | 7초 |
Run Android Lint |
성공 — BUILD SUCCESSFUL |
287초 |
Merge module lint XMLs into one |
성공 — merged 0 issues from 26 modules |
0초 |
Report Android Lint (Reviewdog) |
실패 — /entrypoint.sh: line 6: reviewdog: not found |
1초 |
원인 구조
dvdandroid/action-android-lint 는 도커 액션이고, Dockerfile 이 매 run 마다 reviewdog 을 네트워크에서 설치한다.
FROM alpine:3.11
RUN apk --no-cache --update add git python3 && rm -rf /var/cache/apk/*
RUN wget -O - -q https://raw.githubusercontent.com/reviewdog/reviewdog/fd59714416d6d9a1c0692d872e38e7f8448df4fc/install.sh| sh -s
COPY entrypoint.sh /entrypoint.sh
COPY converter.py /usr/local/bin/converter.pywget -O - -q ... | sh -s — 파이프라인의 exit status 는 마지막 sh 의 것이다. wget 이 실패해도 sh 는 빈 입력을 받고 exit 0 을 낸다. -q 라 경고조차 안 나온다. 그래서 이미지 빌드는 성공하고, 몇 분 뒤 entrypoint 에서 reviewdog: not found 로 죽는다.
실측 빈도
리포터 스텝이 존재한 전 기간 — lint.yml run 1173건 (2026-03-23 ~ 2026-08-24) 전수 조사:
| 분류 | 건수 | 비고 |
|---|---|---|
| 리포터 단독 실패 (lint 는 성공) | 2건 | 2026-03-27 fix/43, 2026-08-24 refactor/941 |
| lint + 리포터 동시 실패 | 95건 | 전부 2026-03 ~ 2026-05. 아래 참조 |
Run Android Lint 단독 실패 |
73건 | 실제 lint 위반 — 정상 동작 |
동시 실패 95건은 다른 원인이고 이미 해소됐다. 2026-05-24 이전에는 lint_xml_file 이 app/build/reports/lint-results-debug.xml (:app 하나)을 직접 가리켜, lint 가 실패하면 그 파일이 없어 리포터도 따라 죽었다. #279 의 merge 스텝(if: always())이 항상 파일을 만들면서 2026-06 이후 0건이다.
빈도가 낮은데 왜 다루는가
- 0.17% 지만 상관 실패다. raw.githubusercontent.com 이 흔들리면 그 시간대 모든 PR 이 동시에 required check red 가 된다. 독립 시행이 아니다.
- 실패가 lint 결과와 무관하다. 위반 0건인 PR 이 막히므로 작업자는 원인을 로그에서 찾아야 한다.
- 이 스텝이 만들어내는 산출물이 실측상 없다. 최근 PR 12건(refactor(afternote): 저장 흐름 예외 3종을 AfternoteFailure sealed 루트로 압축 (#934 4-B) [base: develop] #1056~refactor(afternote): AlbumCover 를 core:model 에서 feature:afternote:presentation 으로 이관 (closes 942) [base: develop] #1091)에서 봇 인라인 코멘트·리뷰 0건, 위 run 도
merged 0 issues. 즉 리스크만 지고 있고 대가가 없다.
공급망 상태
- 베이스 이미지
alpine:3.11— 2021-11 EOL. - 저장소 최근 커밋 2025-08-26, 그 전은 2023-08 / 2022-07. star 22. 사실상 방치.
- 현재
@master는21c9fa65=v1.3— PR security(ci): 외부 Actions 를 전부 commit SHA 로 고정한다 (closes 847) [base: ci/848-single-gate] #1039 가 고정하려는 SHA 와 같은 커밋이다. 고정해도 install.sh 를 매 run 받아오는 구조는 그대로라 이 flake 는 남는다.
재시도(안 a)가 원리적으로 듣지 않는 이유
도커 액션 이미지는 job 시작 시 1회만 빌드된다. 위 job 에서 Build dvdandroid/... 는 15:08:04 에 이미지 c97262:3363d02d… 를 만들고, 스텝 9 의 docker run 은 15:13:01 에 그 태그를 그대로 실행한다.
reviewdog 부재는 빌드 시점에 이미 확정돼 있다. 같은 job 안에서 스텝을 몇 번 다시 돌려도 같은 이미지가 뜨므로 결과가 바뀌지 않는다. 게다가 uses: 스텝에는 네이티브 재시도 옵션이 없다(retry 계열 액션은 run: 만 감싼다). 회복 경로는 job 전체 재실행뿐이고, 그건 lint 287초를 다시 태운다.
대응안 비교
| 안 | 하는 일 | flake 제거 | 인라인 코멘트 | 외부 의존 | 판정 |
|---|---|---|---|---|---|
| (a) 리포터 재시도 | 스텝 재시도 | ❌ 무효 — 같은 이미지 재사용 | 유지 | 그대로 | 기각 |
(b) reviewdog/action-setup + 자체 스텝 |
도커 액션 제거, 러너에 바이너리 설치 | 창구 축소(도커 빌드 함정은 사라지나 설치는 여전히 네트워크) | 유지 | 액션 1개 교체 + checkstyle 변환 자체 유지보수 | 차선 |
(c) continue-on-error |
리포터 실패를 job 실패에서 분리 | 증상만 차단 | 실패 시 조용히 소실 | 그대로(EOL 이미지 잔존) | 부분 |
| (d) 리포터 제거 + job summary | 스텝 삭제, merged XML 을 summary 로 렌더 | ✅ 원인 제거 | 대체(summary) | 0 | 채택 |
(d) 를 고른 근거
- 원인 제거: (a)는 무효, (c)는 EOL 도커 액션을 그대로 둔 채 증상만 덮는다. (b)는 개선이지만 "네트워크에서 리포터를 받아 실행한다"는 실패 구조를 유지하고, entrypoint 의
converter.py(lint XML→checkstyle) 몫을 우리가 떠안는다. - 선례: perf(ci): ktlint 를 PR 당 두 번 검사 — reviewdog 경로 32초·버전 이원화 정리 #1012 에서 같은 저장소가 ktlint 의 reviewdog 경로를 같은 논리로 걷어냈다 — "인라인 코멘트의 원래 수요자는 로그를 뒤지기 싫은 사람인데, 이 저장소는 작업자 전원이 에이전트이고 에이전트는 로그를 직접 읽는다." 재사용할 구현·테스트 패턴(
render-ktlint-summary.mjs+.test.mjs)이 이미 있다. - 효용 실측 0: 위 참조. 인라인이 실제로 달린 적이 없다.
- 가시성은 오히려 개선: 인라인은 PR diff 에 걸린 줄만 표시하지만, summary 는 merged XML 전량(26개 모듈)을 보여준다. chore(ci): Reviewdog 가 :feature/* · :core/* 모듈 lint 도 PR 인라인 표시하도록 multi-module 지원 #279 가 풀려던 문제를 더 넓게 커버한다.
- 공급망: EOL 베이스 + 방치된
@master액션이 통째로 사라진다. Actions 참조 SHA 고정 · secret scanning · push protection 적용 #847 의 고정 대상에서도 빠진다.
#1012 는 "Android Lint 는 대상이 아니다 — 검사를 다시 하지 않으므로 추가 비용이 거의 없다" 로 유지를 택했다. 그 판단을 뒤집는 게 아니라, 그때 계산에 없던 비용을 더하는 것이다: 이 스텝은 시간(1초)이 아니라 required check 를 red 로 만들 확률을 비용으로 갖는다.
범위
lint.yml의Report Android Lint (Reviewdog)스텝을 삭제한다.Merge module lint XMLs into one도 함께 삭제한다 — 모듈별 XML 을 한 파일로 합치던 유일한 소비자가 reviewdog 이었다. summary 스크립트가 모듈별 XML 을 직접 읽으므로 셸+xmllint로 XML 을 문자열 조작하는 단계가 사라지고, 합치면서 잃던 위반의 모듈 귀속(:core:common등)도 보존된다..github/scripts/render-android-lint-summary.mjs를 추가해 모듈별 lint XML 을 job summary 로 렌더한다. 모듈 · 파일:라인 · 규칙 ID · severity 를 표로.render-ktlint-summary.mjs형식을 따른다.- 단위 테스트(
.test.mjs)를 붙인다 — 위반 0건, 다중 모듈, 잘린 XML 픽스처. Run Android Lint는 그대로 게이트로 남긴다(--continue유지).
완료 조건
lint.yml에dvdandroid/action-android-lintuses 가 0개다.- lint 위반이 있을 때 job summary 상단에서 위치(모듈·파일:라인·규칙 ID)가 바로 보인다.
- chore(ci): Reviewdog 가 :feature/* · :core/* 모듈 lint 도 PR 인라인 표시하도록 multi-module 지원 #279 회귀 없음:
:feature/*·:core/*모듈 위반이 summary 에 나온다 — 픽스처로 확인한다. Run Android Lint실패는 여전히Check Project Issues (Android Lint)를 red 로 만든다(게이트 보존).- 위반 0건인 PR 에서 이 job 이 green 이다.
관련
- chore(ci): Reviewdog 가 :feature/* · :core/* 모듈 lint 도 PR 인라인 표시하도록 multi-module 지원 #279 — 이 스텝의 multi-module 지원(CLOSED). 이 이슈는 그 뒤에 남은 결함이다.
- perf(ci): ktlint 를 PR 당 두 번 검사 — reviewdog 경로 32초·버전 이원화 정리 #1012 — ktlint reviewdog 제거(CLOSED). 같은 판단의 선례.
- Actions 참조 SHA 고정 · secret scanning · push protection 적용 #847 / PR security(ci): 외부 Actions 를 전부 commit SHA 로 고정한다 (closes 847) [base: ci/848-single-gate] #1039 — Actions SHA 고정. 별건이다:
@master와 고정 대상21c9fa65가 같은 커밋이라 pin 은 이 flake 를 바꾸지 않는다. 다만 순서상 security(ci): 외부 Actions 를 전부 commit SHA 로 고정한다 (closes 847) [base: ci/848-single-gate] #1039 가 먼저 머지되면 이 작업이 그 pin 줄을 삭제하게 된다(충돌 아님). - chore(ci): dependency-review 가 head 스냅샷 제출을 못 기다림 — 2단 파이프라인 경쟁 조건으로 산발 실패 #1081 — dependency-review 산발 실패. 같은 계열이나 다른 워크플로.
Activity