Skip to main content

Command Palette

Search for a command to run...

GitHub Actions로 Hashnode 블로그 자동 포스팅 구축하기 (feat. 첫 자동 발행 성공!)

Published
•4 min read•View as Markdown
U
Keep learning toward wellbeing life with co-livers

안녕하세요, Hashnode! 👋

이 글은 저의 GitHub TIL(Today I Learned) 레포지토리에 마크다운(.md) 파일을 푸시하면, GitHub Actions가 이를 감지하여 자동으로 Hashnode 블로그에 글을 발행해 주는 시스템을 통해 작성되었습니다.

원래는 티스토리(Tistory) API를 활용해 자동화를 하려고 했으나, 최근 티스토리 오픈 API가 완전히 종료됨에 따라 훌륭한 대안인 Hashnode를 선택하게 되었습니다. Hashnode는 개발자 친화적이며 강력한 GraphQL API를 제공하여 자동화 구축이 매우 수월합니다.

저처럼 GitHub 레포지토리를 블로그와 연동하여 자동으로 글을 업로드하고 싶으신 분들을 위해, 제가 방금 성공한 따끈따끈한 구축 방법을 단계별로 공유합니다!


🛠️ 1단계: Hashnode API 키 및 블로그 ID 발급

외부(GitHub)에서 내 Hashnode 블로그에 글을 쓰려면 권한이 필요합니다.

  1. Personal Access Token (PAT) 발급

    • Hashnode 우측 상단 프로필 > Account Settings > Developer 메뉴로 이동합니다.
    • Generate New Token을 클릭하고 이름을 지정한 뒤 발급합니다.
    • (중요) 발급된 토큰은 다시 볼 수 없으니 반드시 복사해 둡니다.
  2. 내 블로그의 Publication ID 확인

    • Hashnode GraphQL Playground에 접속합니다.
    • 좌측 쿼리창에 아래 코드를 입력하고 your-blog.hashnode.dev 부분을 본인의 블로그 주소로 변경합니다.
      query {
        publication(host: "your-blog.hashnode.dev") {
          id
        }
      }
      
    • 상단의 재생(▶) 버튼을 누르면 우측에 id 값(예: 625d...)이 나옵니다. 이 값을 복사합니다.

🔒 2단계: GitHub 레포지토리 Secrets 설정

발급받은 토큰과 ID를 코드에 직접 노출하면 안 되므로 GitHub Secrets에 안전하게 보관합니다.

  1. 포스트를 관리할 GitHub 레포지토리의 Settings > Secrets and variables > Actions로 이동합니다.
  2. New repository secret을 클릭하여 다음 두 가지를 추가합니다.
    • HASHNODE_PAT: 1단계에서 발급받은 PAT 토큰 값
    • HASHNODE_PUBLICATION_ID: 1단계에서 확인한 Publication ID 값

📁 3단계: 폴더 구조 설정

로컬 컴퓨터에 레포지토리를 클론하고 다음과 같은 폴더 구조를 만듭니다.

my-til-blog/
├── .github/
│   ├── workflows/    # GitHub Actions 설정 파일이 들어갈 곳
│   └── scripts/      # 파이썬 자동화 스크립트가 들어갈 곳
└── _posts/           # 마크다운 포스트(.md)를 작성할 곳

🐍 4단계: 포스팅 파이썬 스크립트 작성

.github/scripts/ 폴더 안에 publish_to_hashnode.py 파일을 생성하고 아래 코드를 작성합니다. 이 스크립트는 마크다운 파일의 메타데이터(Frontmatter)와 본문을 읽어 Hashnode API로 전송하는 역할을 합니다.

import os
import sys
import requests
import frontmatter

HASHNODE_API_URL = "https://gql.hashnode.com"
HASHNODE_PAT = os.getenv('HASHNODE_PAT')
PUBLICATION_ID = os.getenv('HASHNODE_PUBLICATION_ID')

CREATE_POST_MUTATION = """
mutation publishPost($input: PublishPostInput!) {
  publishPost(input: $input) {
    post {
      id
      title
      url
    }
  }
}
"""

def main():
    filepath = sys.argv[1]

    with open(filepath, 'r', encoding='utf-8') as f:
        post = frontmatter.load(f)

    title = post.get('title')
    content = post.content
    tags = post.get('tags', [])

    if not title:
        print(f"Error: 'title' not found in frontmatter of {filepath}")
        sys.exit(1)

    tag_objects = [{"slug": tag.lower().replace(" ", "-"), "name": tag} for tag in tags]

    variables = {
        "input": {
            "title": title,
            "contentMarkdown": content,
            "publicationId": PUBLICATION_ID,
            "tags": tag_objects
        }
    }

    headers = {
        "Authorization": HASHNODE_PAT
    }

    response = requests.post(
        HASHNODE_API_URL,
        json={'query': CREATE_POST_MUTATION, 'variables': variables},
        headers=headers
    )
    response.raise_for_status()

    result = response.json()
    if 'errors' in result:
        print(f"GraphQL Error: {result['errors']}")
        sys.exit(1)

    print("✅ Successfully published post to Hashnode!")

if __name__ == "__main__":
    main()

⚙️ 5단계: GitHub Actions 워크플로우 작성

언제 위의 파이썬 스크립트를 실행할지 정의하는 파일입니다. .github/workflows/ 폴더 안에 publish_article.yml 파일을 만들고 아래 코드를 넣습니다.

name: Publish to Hashnode

on:
  push:
    branches:
      - main
    paths:
      - "_posts/**.md"

jobs:
  publish:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout Repository
        uses: actions/checkout@v3

      - name: Find changed markdown files
        id: changed_files
        uses: tj-actions/changed-files@v35
        with:
          files: _posts/**.md

      - name: Set up Python
        if: steps.changed_files.outputs.any_changed == 'true'
        uses: actions/setup-python@v4
        with:
          python-version: "3.10"

      - name: Install Python Dependencies
        if: steps.changed_files.outputs.any_changed == 'true'
        run: |
          python -m pip install --upgrade pip
          pip install python-frontmatter requests

      - name: Run publisher script for each changed file
        if: steps.changed_files.outputs.any_changed == 'true'
        env:
          HASHNODE_PAT: ${{ secrets.HASHNODE_PAT }}
          HASHNODE_PUBLICATION_ID: ${{ secrets.HASHNODE_PUBLICATION_ID }}
        run: |
          for file in ${{ steps.changed_files.outputs.all_changed_files }}; do
            python .github/scripts/publish_to_hashnode.py "$file"
          done

📝 6단계: 글 작성 및 푸시

이제 \_posts/ 폴더 안에 마크다운 파일을 만들고 글을 작성합니다. 파일의 최상단에는 반드시 아래와 같이 --- 로 감싼 Frontmatter를 넣어주어야 스크립트가 제목과 태그를 인식합니다.

---
title: "포스트 제목입니다"
tags:
  - "태그1"
  - "태그2"
---

본문 내용...

모두 작성했다면 GitHub에 커밋하고 푸시합니다!


💡 주의사항 (제가 겪은 시행착오!)

모든 설정을 마치고 "첫 번째 커밋"으로 모든 파일(스크립트 포함)을 올리면 워크플로우가 정상 완료되었다고 뜨는데 글은 안 올라가는 현상이 발생할 수 있습니다. 이는 우리가 사용한 tj-actions/changed-files 액션이 '이전 커밋'과 '현재 커밋'을 비교해서 변경된 파일을 찾기 때문입니다. 첫 커밋은 비교할 대상이 없어서 변경된 파일이 '0개'라고 판단하고 스크립트 실행을 건너뜁니다. 해결책: 마크다운 파일에 띄어쓰기 하나라도 수정한 뒤 두 번째 커밋을 푸시해 보세요. 그때부터는 정상적으로 변경 파일을 감지하고 블로그에 글이 촥촥 올라갑니다! 이제 잔디(Commit)도 심고 블로그 관리도 하는 일석이조의 효과를 누려보세요! 🚀

---

이제 터미널에서 아래 명령어를 순서대로 실행하시면 업데이트가 완료됩니다.

```bash
git add .
git commit -m "docs: Update first post with full tutorial"
git push origin main

More from this blog

카메라 영상 $\rarr$ 유속 추정(A to Z) 프로젝트 소개

프로젝트 목적 말 그대로 카메라 영상에서 하천 흐름의 유속값과 유속의 분포 데이터를 안정적으로 추정하기 위한 파이프 라인을 설계하기 위해 제로 베이스에서 시작해서 이해도를 높여보기 위한 프로젝트 수식에 대한 이해와 함께 opencv, python 기반으로 실제 구현 코드를 작성해보고 및 소스코드의 현장 적용성도 함께 검토하고자 한다. 프로젝트 수행 방법 기존에 사용하고 있는 태블릿의 코틀린 코드와 UI를 최대한 그대로 사용하고자 하며, 1...

May 1, 20265 min read2

print(obj.__secret) # 오류 발생! (직접 접근 불가)

객체 지향 프로그래밍 클래스와 인스턴스 클래스 메서드들은 중첩된 def이며, 특수한 첫번째 인자는 묵시적으로 대상 인스턴스를 받는다. 네, 정확합니다! self를 설명하는 내용이 맞습니다. 이 문장을 조금 더 자세히 풀어드리면 다음과 같습니다. 1. "클래스 메서드들은 중첩된 def이며" 파이썬에서 클래스 내부의 함수(메서드)는 클래스라는 '객체 내부'에 정의되어 있습니다. def는 함수를 만드는 키워드인데, 클래스라는 범위(scope) ...

Apr 28, 20264 min read2

9장 함수(Python)

9.3 인수와 매개변수 자바와 동일하게? 함수 외부에서는 인수라고 하지만 내부에서는 매개변수라고 한다. 함수로 전달한 값을 인수라고 부른다. 인수의 값은 함수 내에서 해당하는 매개변수에 복사된다. 함수가 명시적으로 return 을 호출하지 않으면, 호출자는 반환값으로 None을 얻는다. 9.3.1 유용한 None None은 아무것도 없다는 것을 뜻하는 파이썬의 특별한 값이다. 빠뜨린 빈 값을 구분하기 위해 None을 사용한다. 불리언 값 ...

Apr 20, 202613 min read

[ITSantiago]의 개발 블로그 | TIL & Tech Log

36 posts