k3s-msa-infrastructure, target-tracking-service, threat-intel-ai-service 세 리포의 docs/**.md를 Notion으로 옮기고, 이후로는 문서가 바뀔 때마다 자동으로 Notion도 갱신되게 만든 기록. (2026-09-22에 c4i-dashboard-frontend도 동일한 방식으로 추가 — 10절 참고.)
세 리포에 쌓인 docs/ 문서 48개를 Claude Code(Notion 커넥터)로 한 번에 Notion에 옮겼다. 문제는 그다음이었다 — "앞으로 문서를 추가할 때마다 Notion도 같이 최신 상태로 유지되게 하고 싶다"는 요구였는데, Claude Code 세션 안에서 "문서 추가해줘"라고 부탁할 때만 Notion을 같이 갱신하는 방식은 커밋이 다른 경로로 일어나면(예: 직접 git push, 나중에 팀원이 추가) 무력화된다. 그래서 "누가 어떤 방식으로 커밋하든" 항상 동작하는, git push 자체를 트리거로 삼는 자동화(GitHub Actions)로 만들기로 했다.
docs/**.md 변경 → git push (main)
│
▼
GitHub Actions: Notion Doc Sync
│
├─ 이전 커밋과 diff해서 바뀐 .md 파일 목록 추출
├─ 매핑 파일(.notion-sync-map.json)에 있으면 → 그 Notion 페이지 내용 통째 교체
├─ 매핑에 없는 새 파일이면 → 적절한 부모 페이지 아래 새 페이지 생성 후 매핑에 추가
└─ 매핑 파일이 바뀌었으면 → 그 변경분을 [skip ci]로 다시 커밋 + push
Notion API로 페이지를 읽고 쓰려면 notion.so/my-integrations에서 Internal Integration을 만들고 토큰(secret)을 발급받아야 한다. 발급받은 것만으로는 아무 페이지에도 접근이 안 되고, 연결하려는 페이지 쪽에서 그 integration을 명시적으로 "Connections"에 추가해야 한다.
이번엔 k3s-doc-sync라는 이름으로 만들고, 최상위 페이지("Defense C4I 인프라 문서") 하나에만 연결했다 — Notion의 페이지 공유/연결은 하위 페이지로 상속되므로, 그 아래 리포별 페이지·concepts 하위 페이지까지 전부 별도 연결 없이 같은 integration으로 접근 가능하다.
각 리포 루트에 .notion-sync-map.json을 커밋해서 "이 로컬 파일 경로가 이 Notion 페이지 ID에 대응한다"를 기록한다.
{
"docs/architecture.md": "3e250b17-ecd1-810d-8cfe-f275b601a630",
"docs/concepts/13-reranker.md": "3e250b17-ecd1-81fa-b5fd-ce15f1b48170"
}
최초 49개 페이지(48개 문서 + 마이그레이션 문서 하나가 분량 때문에 2페이지로 쪼개진 것 1개 추가)의 ID는 Claude Code의 Notion 커넥터로 각 부모 페이지를 순회 조회(fetch)해서 자식 페이지 목록을 뽑아 수작업으로 매핑했다. 이후로는 스크립트가 새 페이지를 만들 때마다 이 파일에 자동으로 추가한다.
분량 때문에 페이지를 둘로 쪼갰던 문서(VMware-to-Multipass-Cluster-Migration.md → 1/2, 2/2)는 매핑에 1/2 페이지만 등록해뒀다 — 원본 파일이 하나뿐이라 앞으로 그 파일이 바뀌면 1/2 페이지만 갱신되고 2/2 페이지는 그대로 남는다(고아 페이지). Claude Code로 옮길 때는 대화 단위 분량 제한 때문에 나눈 것이었지 Notion API 자체의 제약은 아니라서, 자동화 스크립트는 애초에 파일 하나당 페이지 하나로만 다룬다.
Node.js + Notion 공식 SDK(@notionhq/client) + 마크다운→Notion 블록 변환 라이브러리(@tryfabric/martian, GFM 표·코드블록·목록을 Notion 블록으로 변환)로 짰다. 저장 위치는 프로젝트 루트가 아니라 .github/scripts/notion-sync/에 별도 package.json을 뒀다 — 이 세 리포는 각각 YAML/Java/Python 프로젝트라, Node 의존성을 리포 루트에 섞으면 기존 빌드 도구(Gradle, pip 등)와 무관한 잡동사니가 생긴다. Node 모듈 해석은 스크립트 파일 위치부터 상위로 찾아 올라가므로, .github/scripts/notion-sync/ 안에서만 npm install해도 그 스크립트를 실행할 땐 정상적으로 잡힌다.
Notion API에는 "이 페이지 내용을 통째로 이걸로 바꿔라" 같은 단일 엔드포인트가 없다. 그래서 기존 페이지를 갱신할 땐 blocks.children.list로 현재 자식 블록을 전부 가져와 blocks.children.delete로 지운 뒤, 새로 변환한 블록을 blocks.children.append로 다시 채우는 2단계로 구현했다. append는 한 번에 최대 100개 블록만 받아서, 문서가 길면 90개씩 나눠서 여러 번 호출한다.
const base = process.env.GITHUB_EVENT_BEFORE; // 이 push 직전 커밋 SHA
execSync(`git diff --name-status ${base} HEAD -- "docs/**/*.md"`);
GitHub Actions의 push 이벤트가 주는 github.event.before를 기준으로 diff를 떠서, 그 push에 실제로 포함된 docs/**.md 변경분만 골라낸다. 이 값이 없거나(수동 트리거 등) 커밋이 로컬에 없는 상황(얕은 클론)이면 HEAD~1로 폴백한다.