제품 카탈로그 사이트 하나를 주간 봇에 맡겨두고 있습니다. 매주 월요일에 새 제품을 발견해 데이터로 커밋하고, 화요일엔 이미지 후보를 승인합니다. 두 잡 다 몇 주째 success였습니다.
질문 하나 드리겠습니다. 당신의 CI가 초록불이라는 건, 사용자가 그 결과를 본다는 뜻입니까? 저는 같은 뜻이라고 믿고 있었습니다.
잡은 정상이었습니다
로그를 열어보면 이랬습니다.
Discovered 1312 raw product rows.
Added 60 new product rows.
Imported 60 products. Catalog now has 474 products.
Generated 692 pages
Checked 693 HTML files; no missing internal links.발견도 되고, 임포트도 되고, 빌드도 되고, 링크 체크까지 통과합니다. 커밋도 푸시도 됐습니다. 실행 이력에는 실패가 한 건도 없었습니다.
라이브를 직접 세어보기 전까지는요
그런데 배포 이력이 눈에 걸렸습니다. 마지막 Deploy 실행이 07-31. 그 뒤로 데이터 커밋은 08-03, 08-04, 08-10 세 건인데 배포는 한 번도 안 돌았습니다.
사이트를 직접 세어봤습니다.
$ curl -s https://<site>/sitemap-pages.xml | grep -c "<loc>"
572
$ curl -s https://<site>/sitemap-pages.xml | grep -o "<lastmod>[^<]*" | sort -u | tail -1
<lastmod>2026-07-27레포는 474제품 · 692페이지, 라이브는 572페이지에 lastmod 07-27. 2주치 신제품이 사이트에 존재하지 않았습니다. 봇은 열심히 일했고, 결과물은 저장소에만 쌓이고 있었습니다.
원인은 GitHub의 안전장치였습니다
배포 워크플로의 트리거는 평범합니다.
on:
push:
branches: [main]그리고 데이터 봇은 GITHUB_TOKEN으로 푸시합니다. 여기가 함정입니다. GITHUB_TOKEN이 만든 푸시는 다른 워크플로를 트리거하지 않습니다. 워크플로가 워크플로를 무한히 깨우는 사고를 막으려는 GitHub의 의도된 규칙입니다.
즉 이 파이프라인은 처음부터 이렇게 생겼던 겁니다.
- 사람이 푸시 → 배포됨 (그래서 07-31까지는 멀쩡해 보였습니다)
- 봇이 푸시 → 조용히 아무 일도 안 일어남
제가 마지막으로 손으로 커밋한 게 07-31이었고, 그 뒤로 사람 푸시가 없었습니다. 파이프라인이 고장난 순간은 제가 손을 뗀 순간이었습니다.
당신이라면 어떻게 잇겠습니까
선택지가 있습니다. 개인 액세스 토큰(PAT)을 발급해 봇이 그걸로 푸시하게 하면 트리거가 살아납니다. 간단합니다. 그리고 관리할 비밀이 하나 늘고, 만료되면 또 조용히 멈춥니다. 저는 이미 토큰이 조용히 죽어서 봇이 멈추는 경험을 한 뒤였습니다.
그래서 토큰 없이 잇는 쪽을 골랐습니다. 배포 워크플로에 workflow_call 트리거를 열고, 데이터 워크플로가 그걸 직접 호출합니다.
# deploy.yml
on:
push:
branches: [main]
workflow_dispatch:
workflow_call: # 봇 커밋은 push 트리거가 안 걸리므로 직접 호출받는다
# catalog-audit.yml
deploy:
needs: refresh
if: needs.refresh.outputs.pushed == 'true'
uses: ./.github/workflows/deploy.yml
secrets: inherit한 가지 더 넣었습니다. 커밋 스텝이 실제로 푸시했는지를 $GITHUB_OUTPUT에 pushed=true/false로 내보내고, 그 값이 참일 때만 배포합니다. 변화가 없는 주에 프로덕션을 건드리지 않기 위해서입니다. 배포는 부작용이 있는 행동이고, 부작용은 이유가 있을 때만 일으키는 게 낫습니다.
검증은 라이브에서
푸시하자 배포가 돌았고, 다시 세어봤습니다.
| 항목 | 수정 전 | 수정 후 |
|---|---|---|
| 라이브 페이지 | 572 | 692 |
| sitemap lastmod | 07-27 | 08-10 |
| 신규 제품 페이지 | (없음) | HTTP 200 |
레포 숫자가 아니라 라이브 숫자로 확인했습니다. 이 습관은 배포된 페이지가 아예 다른 앱이었던 편에서 배웠습니다. 빌드 로그의 692는 "내 컴퓨터에서 692"라는 뜻이고, sitemap의 692만 "세상에서 692"라는 뜻입니다.
덤: 침묵이 정상일 때도 있습니다
같은 파이프라인에서 텔레그램 승인 알림도 몇 주째 안 왔습니다. 이것도 고장인 줄 알았는데, 로그의 마지막 줄이 답이었습니다.
Discovered 1 image candidates for 16 products.
Auto-approved 1 images.
No image candidates waiting for Telegram approval.신뢰 도메인에서 온 이미지는 자동 승인되도록 짜여 있었고, 그래서 사람에게 물어볼 게 남지 않았던 겁니다. 알림이 안 오는 건 여기서는 설계대로였습니다. 고장난 침묵과 정상인 침묵을 구분하려면, 결국 그 잡의 마지막 출력 한 줄을 읽어야 합니다.
자가진단 3줄
- 봇이 푸시하는 레포에서, 마지막 배포가 언제였는지 아십니까? Actions 목록에서
Deploy만 필터해 최근 실행 날짜를 보세요. 데이터 커밋 날짜와 어긋나 있으면 그 사이가 전부 미배포입니다. - 라이브 산출물을 숫자로 세고 있습니까?
sitemap.xml의<loc>개수와<lastmod>는 로그인도 필요 없는 무료 배포 검증기입니다. 빌드 로그 말고 이걸 세세요. - 침묵하는 알림 채널의 마지막 정상 출력이 뭔지 아십니까? "안 온다"만으로는 고장인지 조건 미충족인지 알 수 없습니다.
솔직한 부분
이 사이트는 트래픽이 거의 없습니다. 2주 낡아 있었어도 아무도 항의하지 않았고, 그래서 2주가 갔습니다. 아프지 않은 고장은 오래 삽니다. 제가 발견한 것도 사이트를 보다가가 아니라 "수집 잘 되고 있나?"를 확인하다가였습니다.
그리고 이번에도 진짜 교훈은 CI 문법이 아닙니다. 저는 "잡 success"를 배포의 증거로 쓰고 있었습니다. 증거가 아니었습니다. 자동화는 자기가 끝까지 갔다는 걸 스스로 증명하지 않습니다. 증명은 마지막 지점, 즉 사용자가 보는 화면에서 세어야 합니다.
당신의 마지막 배포는 언제였습니까? Actions 탭 말고, 실제 사이트에서 확인해 보시겠습니까?