GitHub Trending 자동화 파이프라인 구축기
요약
GitHub Trending 데이터를 수집하여 개인 사이트, Substack, Dev.to, Supabase에 자동으로 게시하는 AI 에이전트 기반 파이프라인 구축 사례를 소개합니다. SEO 최적화를 위해 Canonical URL을 관리하고, 날짜 불일치 문제를 해결하는 등 자동화 과정에서의 기술적 도전과 해결책을 다룹니다.
핵심 포인트
- AI 에이전트를 활용한 콘텐츠 생성 및 다중 플랫폼 자동 배포
- SEO 소유권 유지를 위한 Canonical URL 전략 적용
- Hermes, Codex 등 역할 분담을 통한 에이전트 아키텍처 설계
- 데이터 수집 시점과 실행 시점 차이로 인한 날짜 드리프트 문제 해결
대부분의 개발자 블로그는 마크다운 (Markdown) 파일로 시작해서 "발행 (Publish)" 버튼으로 끝납니다. 하지만 우리의 블로그는 AI 에이전트가 GitHub Trending을 스크레이핑 (Scraping) 하는 것으로 시작하여, 올바른 표준 URL (Canonical URL)과 적절한 SEO 소유권을 갖추고 Supabase 레코드로 이 모든 것을 연결하여 네 개의 플랫폼에 게시되는 것으로 끝납니다.
우리가 이것을 어떻게 구축했는지, 무엇이 고장 났는지, 그리고 무엇을 배웠는지 소개합니다.
문제점 (The Problem)
우리는 일주일에 세 번 GitHub Trending 요약본을 발행합니다. 각 게시물은 다음 플랫폼들에 게시되어야 합니다:
- hongphuc5497.com — 우리의 개인 사이트 (Next.js, Vercel)
- Dev.to — 개발자 커뮤니티
- Supabase — 사이트의 동적 페이지를 위한 콘텐츠 저장소
이를 수동으로 한다는 것은 다음과 같은 의미였습니다: Substack에 복사-붙여넣기 하고, Dev.to 형식에 맞게 수정하고, 사이트를 위한 YAML 프론트매터 (YAML-frontmatter) 노트를 작성한 다음, 콘텐츠 데이터베이스 업데이트를 잊어버리는 것입니다. 표준 URL (Canonical URLs)은 잘못되었습니다. 날짜가 어긋났습니다. Supabase 저장소는 영구적으로 동기화가 맞지 않았습니다.
우리는 올바른 순서와 올바른 URL을 사용하여 이 네 가지를 모두 생성하는 단일 명령어를 원했습니다.
아키텍처 (The Architecture)
파이프라인은 엄격한 순서로 실행되는 5단계로 구성됩니다:
1단계: 생성 (Create) → LLM 에이전트가 게시물 작성 (Hermes cron)
2단계: 사이트 (Site) → hongphuc5497.com (표준 진실의 원천/canonical source of truth)
3단계: Substack → 표준 URL (canonical URL) → 개인 사이트
...
개인 사이트가 항상 첫 번째입니다. Substack과 Dev.to는 모두 자신의 canonical_url을 개인 사이트로 가리킵니다. 이는 Google이 플랫폼이 아닌 개인 사이트를 인덱싱 (Indexing) 함을 의미합니다. SEO 소유권은 우리에게 유지됩니다.
우리가 자동화한 것 (What We Automated)
세 개의 AI 에이전트가 서로 다른 부분을 처리합니다:
- Hermes: 크론 스케줄러 (cron scheduler)를 실행합니다 — 트렌딩 데이터를 가져오고, 파이프라인을 조율하며, Telegram으로 전달합니다.
- Codex: 리포지토리 (Repo) 인프라를 구축합니다 — 스테이징 스크립트 (staging scripts), 아카이브 로직 (archive logic), 에이전트 운영 (Agent Ops) 프로토콜을 담당합니다.
- Hermes 에이전트 자체: 요약 분석을 구성합니다 — 원시 JSON을 읽고, 리포지토리당 2~3문장의 요약을 작성합니다.
Fetcher 스크립트는 github.com/trending을 직접 스크래핑하며 (API 키가 필요 없음), 리포지토리 이름, 스타 수, 언어 및 설명을 추출한 다음 로컬 시간대 필드가 포함된 구조화된 JSON을 출력합니다. 에이전트는 게시물 제목과 파일 이름을 정할 때 UTC나 시스템 시계가 아닌, 해당 필드들을 사용합니다.
발생한 문제
1. 날짜 드리프트 (Date Drift) 문제
가장 미묘한 버그였습니다. 트렌딩 데이터는 6월 4일 오후 10:36에 가져왔지만, cron 작업은 6월 5일 오전 9:00에 실행되었습니다. 에이전트는 Fetcher 데이터에는 "6월 4일"이라고 되어 있음에도 불구하고 요약본의 제목을 "6월 05일"로 붙였습니다.
해결책: 이중 계층 강제 적용을 도입했습니다. 이제 스테이징 (Staging) 스크립트는 제목 날짜가 Fetcher의 local_date_long 필드와 일치하지 않을 경우 종료 코드 1(exit code 1)과 함께 종료됩니다. Cron 프롬프트에는 다음과 같이 명시적으로 경고를 추가했습니다: "제목 날짜가 Fetcher 날짜와 일치하지 않으면 스테이징이 종료 코드 1로 실패합니다."
이를 통해 조용히 지나가던 경고를 파이프라인의 명확한 실패(hard failure)로 전환했습니다. 이제 에이전트는 이를 무시할 수 없습니다.
2. Supabase 네임스페이스 섀도잉 (Namespace Shadow)
markdown-content-store 리포지토리에는 supabase/migrations/ 디렉토리가 있습니다 (표준 Supabase CLI 레이아웃). Python의 임포트 (import) 시스템이 이를 네임스페이스 패키지 (namespace package)로 취급하면서, 실제 supabase pip 라이브러리를 섀도잉 (shadowing) 했습니다.
어떤 게시 스크립트라도 from supabase import Client를 실행하면, Python은 설치된 라이브러리 대신 비어 있는 migrations 디렉토리를 찾아냈습니다. 이로 인해 모든 콘텐츠 스토어 저장 작업이 조용히 실패했습니다.
해결책: src/db.py에서 임포트하기 전에 sys.path의 맨 앞에 .venv/lib/python*/site-packages를 추가했습니다. 이렇게 하면 실제 라이브러리를 가장 먼저 찾게 됩니다. 파일 하나, 14줄의 코드로 외부 변경 없이 해결했습니다.
3. 다중 행 리스트 번호 매기기 (Multi-Line List Numbering)
Hermes 에이전트는 다음과 같이 번호가 매겨진 리스트를 작성합니다:
1. ***chopratejas/headroom*** · Python · 12.6K⭐ +3.1K LLM에 전달되기 전의 압축 도구 출력물...
하지만 Substack의 from_markdown() 파서 (parser)가 이를 인식할 때, 이어지는 줄을 헤딩 (heading)과 병합해 버려 번호 매기기가 완전히 망가집니다. 동일한 문제가 Dev.to에서도 발생했습니다.
해결책: 우리는 헤딩 (heading), 인라인 포맷팅 (inline formatting), 그리고 여러 줄로 구성된 콘텐츠 (multi-line content)를 올바르게 처리하는 커스텀 ProseMirror JSON 빌더 (pm_builder.py)를 구축했습니다. Dev.to의 경우에는 API에 전달하기 전에 연속되는 줄을 단일 행 항목으로 병합하는 _reformat_ordered_lists()를 추가했습니다.
Canonical URL 체인
hongphuc5497.com/notes/{slug} ← SOURCE OF TRUTH
│
├── Substack ← canonical → personal site
...
Substack과 Dev.to 모두 개인 사이트를 자신들의 canonical 소스로 선언합니다. 검색 엔진은 개인 사이트를 인덱싱(indexing)합니다. 플랫폼들은 콘텐츠를 가져가지만 SEO 권한은 양보합니다.
Supabase 콘텐츠 저장소는 published_urls 테이블을 통해 세 가지 URL을 모두 기록합니다. 즉, 하나의 포스트에 대해 여러 플랫폼 엔트리가 생성됩니다. 사이트는 Supabase에서 데이터를 읽어와 노트 페이지를 동적으로 채웁니다.
상태 머신 (State Machine)
trending.json → digest.md → stage → archive → deliver
│
├── run.json (date_consistent, warnings)
...
스테이징 (staging) 스크립트가 게이트키퍼 (gatekeeper) 역할을 합니다. 이 스크립트는 날짜 일관성을 확인하고, 아티팩트 (artifacts)를 canonical 상태 디렉터리로 복사하며, run.json에 메타데이터를 기록합니다. 그리고 만약 --auto-archive가 설정되어 있다면, 업데이트된 index.json과 함께 digest를 docs/로 아카이브 (archive) 합니다.
무언가 실패하면 파이프라인은 중단됩니다. 부분적인 게시(publish)는 없습니다. 오래된 상태(stale state)도 남지 않습니다.
우리가 배운 점
-
Python 네임스페이스 패키지(namespace packages)는 소리 없는 살인자입니다.
__init__.py파일이 없는supabase/디렉토리는 아무런 에러 메시지 없이 pip 설치 결과물을 가려버릴(shadow) 수 있습니다. 임포트(import)가 알 수 없는 이유로 실패할 때는 항상import supabase; print(supabase.__path__)를 통해 확인하십시오. -
프롬프트 강제(Prompt enforcement)만으로는 충분하지 않습니다. 에이전트(agent)에게 "반드시 fetcher의 날짜만 사용하라"고 지시했음에도 불구하고, 에이전트는 여전히 시스템 시계(system clock)를 통해 날짜를 추론했습니다. 강제 종료(Hard exits)만이 유일하게 신뢰할 수 있는 강제 수단입니다.
-
표준 URL(Canonical URLs)은 게시(publish) 시점에 설정해야 하며, 그 이후에는 안 됩니다. 만약 Substack이 표준 URL 없이 게시된다면, 게시물을 삭제하고 다시 게시하지 않는 한 소급하여 추가할 수 없습니다.
-
개인 사이트가 우선되어야 합니다. 만약 Substack에 먼저 게시하면, 해당 URL이 사실상의 표준(de facto canonical)이 되어버리며, 여러분은 자신의 플랫폼에 대한 SEO 소유권을 잃게 됩니다.
-
콘텐츠 저장소(Content stores)는 수 시간을 아껴주는 지루하지만 필수적인 인프라입니다. Supabase의
upsert_by_slug+add_platform_url패턴을 사용하면 재실행 시 중복 데이터가 생성되지 않으며, 사이트는 항상 모든 게시물이 어디에 있는지 알 수 있습니다.
결과
일주일에 세 번, AI 에이전트가 GitHub Trending을 스크래핑(scrape)하여 10개의 리포지토리(repo) 요약본을 작성하고, 2분 이내에 4개의 플랫폼에 게시합니다. 개인 사이트가 표준 URL(canonical URL)을 소유합니다. 콘텐츠 저장소는 모든 것을 추적합니다. 그리고 날짜가 일치하지 않으면 파이프라인은 진행을 거부합니다.
수동 복사-붙여넣기도 없습니다. 오래된 Supabase 레코드도 없습니다. 잘못된 표준 URL도 없습니다.
오직 에이전트, fetcher, 그리고 매우 확고한 원칙을 가진 스테이징 스크립트(staging script)만 있을 뿐입니다.
게시 파이프라인은 github-digest에서 오픈 소스로 공개되어 있습니다. 콘텐츠 저장소는 markdown-content-store에 있습니다. 두 리포지토리 모두 오케스트레이션(orchestration)을 위해 Hermes Agent를 사용합니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기