Git 서브모듈 + GitHub Actions로 블로그 자동 배포 파이프라인 구성하기

Git 서브모듈 + GitHub Actions로 블로그 자동 배포 파이프라인 구성하기

배경

이 블로그는 두 개의 레포로 구성되어 있습니다.

레포 역할
blog-content 콘텐츠 (마크다운 글)
blog-site 블로그 빌드 + 배포 (11ty 등)

blog-contentblog-site의 Git 서브모듈로 연결되어 있습니다. 동시에 Obsidian에서 볼트로 직접 열어 글을 작성하는 용도로도 쓰입니다.

문제는 글을 쓰고 blog-content에 푸시해도 블로그가 자동으로 배포되지 않는다는 점이었습니다. 서브모듈은 부모 레포에서 명시적으로 커밋해야 반영되기 때문입니다.

목표

Obsidian도 잘 쓰고싶은데, blog-site 쪽에 매번 문서내용이 추가되는 것도 싫었고, 기왕 이전도 잘 했으니 Obsidian화 하면 더 낫겠단 생각이 들었습니다. 정리해보니 아래 과정을 자동화하는 게 목표였어요.

  1. Obsidian으로 글을 쓰고 blog-content에 푸시
  2. blog-site 서브모듈 자동 업데이트
  3. 트리거를 받으면 블로그에서 재배포

구조 개요

구현을 위해 깃헙 액션 트리거를 쓰기로 했고, 이를 구성해보면 아래 플로우였습니다.

blog-content (push to main)

    │  repository_dispatch: notes-updated

blog-site
    │  1. git submodule update --remote
    │  2. commit & push

    Deploy workflow (build → S3 등)

핵심은 repository_dispatch 이벤트입니다. GitHub Actions에서 다른 레포의 워크플로우를 트리거할 수 있는 공식 메커니즘이죠.

1. 콘텐츠 레포: dispatch 이벤트 발송

blog-content에 아래 워크플로우를 추가합니다.

# .github/workflows/notify-base-blog.yml
name: Notify base blog

on:
  push:
    branches: [main]

jobs:
  dispatch:
    runs-on: ubuntu-latest
    steps:
      - name: Trigger submodule sync
        run: |
          gh api repos/<owner>/blog-site/dispatches \
            -f event_type=notes-updated
        env:
          GH_TOKEN: ${{ secrets.BASE_BLOG_PAT }}

BASE_BLOG_PAT는 대상 레포(blog-site)에 contents: write 권한이 있는 Fine-grained Personal Access Token입니다. 콘텐츠 레포의 GitHub Secrets에 등록합니다.

왜 Fine-grained PAT인가?

이런 장점이 있다보니 생각이상으로 깔끔하게 잘 떨어지고 아래 스크린샷처럼 잘 나와서 너무 좋았습니다.

PAT가 정상등록 되었어요

2. 블로그 레포: 서브모듈 동기화

blog-site에서 repository_dispatch 이벤트를 받아 서브모듈을 업데이트합니다.

# .github/workflows/sync-notes.yml
name: Sync notes submodule

on:
  repository_dispatch:
    types: [notes-updated]
  workflow_dispatch:

permissions:
  contents: write

jobs:
  sync:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v6

      - name: Checkout and update submodule
        env:
          GIT_SSH_COMMAND: >-
            ssh -i ~/.ssh/deploy_key
            -o StrictHostKeyChecking=yes
            -o UserKnownHostsFile=~/.ssh/known_hosts
            -o IdentitiesOnly=yes
        run: |
          set -euo pipefail
          umask 077
          mkdir -p ~/.ssh && chmod 700 ~/.ssh
          printf '%s\n' "${{ secrets.NOTES_DEPLOY_KEY }}" > ~/.ssh/deploy_key
          chmod 600 ~/.ssh/deploy_key
          printf '%s\n' \
            "github.com ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOMqqnkVzrm0SdG6UOoqKLsabgH5C9okWi0dh2l9GKJl" \
            >> ~/.ssh/known_hosts
          chmod 644 ~/.ssh/known_hosts

          git submodule sync --recursive
          git submodule update --init --recursive
          git submodule update --remote _vault

          rm -f ~/.ssh/deploy_key

      - name: Commit and push if changed
        run: |
          git diff --quiet && exit 0
          git config user.name "github-actions[bot]"
          git config user.email "github-actions[bot]@users.noreply.github.com"
          git add _vault
          git commit -m "chore(notes): update submodule ref"
          git push

보안 포인트

왜 Deploy Key인가?

서브모듈의 URL이 SSH(git@github.com:...)인 경우, GitHub Actions의 기본 GITHUB_TOKEN으로는 다른 레포의 서브모듈을 체크아웃할 수 없습니다. Deploy Key는 특정 레포에만 접근 가능하므로 PAT보다 안전합니다.

3. 전체 흐름

그럼 이걸로 자동화한 구성을 살펴보시죠. 이제는 이게 됩니다.

  1. Obsidian에서 글 작성 → blog-content에 push
  2. Notify base blog 워크플로우가 실행 → repository_dispatch 이벤트 발송
  3. blog-siteSync notes submodule 워크플로우가 트리거
  4. 서브모듈 최신 커밋으로 업데이트 → 자동 커밋 & 푸시
  5. 푸시로 인해 기존 배포 워크플로우(Build → Deploy)가 트리거
  6. 블로그 배포 완료

삽질 기록

한번에 되면 그게 이상하죠. 뭐가 이상했던게 있었나, 어떻게 해결했나 공유합니다.

repository_dispatch가 404를 반환한다

PAT의 권한 문제입니다. Fine-grained PAT를 만들 때:

workflow_dispatch도 추가하면 좋다

sync-notes.ymlworkflow_dispatch 트리거를 함께 넣으면 GitHub UI나 gh workflow run으로 수동 실행할 수 있습니다. 디버깅할 때 유용합니다.