GitHub Actions 배포 자동화 5단계 — 블로그 CI/CD 파이프라인 구축

블로그에 글을 올릴 때마다 수동으로 빌드하고 배포하는 건, 한두 번은 괜찮지만 반복되면 상당한 시간 낭비이다. hugo build 치고, 결과물 확인하고, FTP나 rsync로 올리고, 혹시 잘못된 건 없나 확인하고… 이 루틴을 매번 반복하다 보면, 글 쓰는 시간보다 배포하는 시간이 더 길어지는 날이 온다.

나는 Hugo로 블로그를 운영하면서, 초반에는 로컬에서 빌드하고 Cloudflare Pages에 수동으로 푸시하는 방식을 썼다. 글이 5개일 때는 괜찮았는데, 20개를 넘기면서부터 실수가 나기 시작했다. draft 상태인 글을 배포해버린 적도 있고, 빌드 명령어에 --minify 플래그를 빼먹어서 CSS가 깨진 채로 올라간 적도 있었다. 결국 GitHub Actions를 도입했고, 지금은 git push 한 번이면 빌드-배포-알림까지 전부 자동으로 돌아간다.

아래에서는 내가 실제로 쓰고 있는 GitHub Actions 워크플로우를 기반으로, 블로그 자동 배포 파이프라인을 처음부터 끝까지 구축하는 과정을 정리했다. Hugo를 기준으로 설명하지만, Jekyll이나 Next.js를 쓰더라도 빌드 명령어만 바꾸면 동일하게 적용된다.

왜 블로그에 CI/CD가 필요한가

블로그 배포에 CI/CD가 과하다고 생각할 수 있다. “고작 정적 사이트 하나 올리는 건데 파이프라인까지 필요한가?” 솔직히 나도 처음엔 그렇게 생각했다. 그런데 수동 배포를 3개월쯤 하다 보면, 아래 상황이 반드시 발생한다. 관련 내용은 Cloudflare Workers wrangler deploy 해결법에서도 다루고 있다.

  • 빌드 환경 불일치: 로컬 Hugo 버전이 0.139.0인데, 어느 날 업데이트하고 나서 테마가 깨진다. 빌드 환경이 고정되어 있지 않으면 “내 컴퓨터에선 되는데?”가 반복된다.
  • 배포 실수: draft 글 배포, 빌드 플래그 누락, 잘못된 브랜치에서 배포 등. 사람이 하면 실수가 섞이기 마련이다.
  • 롤백 불가: 수동 배포는 이전 버전으로 되돌리기 어렵다. GitHub Actions를 쓰면 이전 커밋으로 revert하고 push만 하면 자동으로 롤백된다.
  • 멀티 디바이스 문제: 데스크톱에서만 배포할 수 있다면, iPad에서 글을 수정한 뒤 배포가 안 된다. CI/CD가 있으면 어디서든 push만 하면 된다.

결정적으로, 한 번 세팅해두면 유지보수가 거의 필요 없다. 워크플로우 파일 하나 작성하면 끝이다.

GitHub Actions 기본 개념 — workflow, trigger, job, step

GitHub Actions — 설정 및 실행 결과 화면

GitHub Actions의 구조를 간단히 정리하면 다음과 같다. 이미 알고 있다면 다음 섹션으로 넘어가도 된다.

핵심 용어 4가지

용어 설명 비유
Workflow .github/workflows/ 아래의 YAML 파일 하나 레시피 전체
Trigger 워크플로우를 실행하는 이벤트 (push, schedule 등) 조리 시작 신호
Job 독립적으로 실행되는 작업 단위. 각 Job은 별도 VM에서 돌아간다. 조리 단계 (손질, 볶기, 플레이팅)
Step Job 안의 개별 명령어. 쉘 커맨드 또는 Action을 실행한다. 각 단계의 세부 동작

워크플로우 파일 위치와 기본 구조

워크플로우 파일은 반드시 리포지토리 루트의 .github/workflows/ 디렉토리에 위치해야 한다. 파일명은 자유지만, 목적이 드러나는 이름이 좋다.

my-blog/
├── .github/
│   └── workflows/
│       ├── deploy.yml          # 배포 워크플로우
│       └── link-checker.yml    # 링크 검증 (선택)
├── content/
│   └── posts/
├── themes/
├── config.toml
└── hugo.toml

가장 간단한 워크플로우의 뼈대는 다음과 같다.

# .github/workflows/deploy.yml
name: Deploy Blog

on:
  push:
    branches:
      - main

jobs:
  build-and-deploy:
    runs-on: ubuntu-24.04
    steps:
      - name: Checkout repository
        uses: actions/checkout@v4
        with:
          submodules: true  # Hugo 테마가 submodule인 경우
          fetch-depth: 0    # lastmod 등 git 정보 사용 시 필요

      - name: Build
        run: echo "여기에 빌드 명령어"

      - name: Deploy
        run: echo "여기에 배포 명령어"

on.push.branchesmain을 지정했으므로, main 브랜치에 push할 때만 실행된다. runs-on은 GitHub에서 제공하는 가상 머신 이미지인데, 2026년 기준 ubuntu-24.04가 안정적이다. fetch-depth: 0은 전체 커밋 히스토리를 가져오는 옵션으로, Hugo의 .GitInfo.AuthorDate 같은 기능을 쓸 때 필요하다.

Hugo 블로그 배포 워크플로우 전체 YAML

아래는 내가 실제로 사용 중인 워크플로우를 정리한 것이다. Hugo로 빌드하고 Cloudflare Pages에 배포하는 전체 파이프라인이다. 주석으로 각 라인의 역할을 달아뒀으니, 복사해서 자기 환경에 맞게 수정하면 된다.

# .github/workflows/deploy.yml
name: Build and Deploy Hugo Blog

on:
  push:
    branches:
      - main
    paths:
      - 'content/**'
      - 'layouts/**'
      - 'static/**'
      - 'config.toml'
      - 'hugo.toml'
      - 'themes/**'
  workflow_dispatch:  # GitHub UI에서 수동 실행 가능

# 동시 배포 방지: 같은 워크플로우가 중복 실행되면 이전 것을 취소
concurrency:
  group: deploy-${{ github.ref }}
  cancel-in-progress: true

# GITHUB_TOKEN 권한을 최소한으로 제한
permissions:
  contents: read

env:
  HUGO_VERSION: '0.142.0'
  HUGO_ENV: production
  TZ: Asia/Seoul

jobs:
  build:
    runs-on: ubuntu-24.04
    timeout-minutes: 10
    steps:
      # 1. 소스 코드 체크아웃
      - name: Checkout
        uses: actions/checkout@v4
        with:
          submodules: recursive  # 테마가 git submodule인 경우
          fetch-depth: 0

      # 2. Hugo 설치 — extended 버전 (SCSS 지원)
      - name: Setup Hugo
        uses: peaceiris/actions-hugo@v3
        with:
          hugo-version: ${{ env.HUGO_VERSION }}
          extended: true

      # 3. Hugo 모듈 캐시 (go module 기반 테마 사용 시)
      - name: Cache Hugo modules
        uses: actions/cache@v4
        with:
          path: /tmp/hugo_cache
          key: hugo-${{ runner.os }}-${{ hashFiles('**/go.sum') }}
          restore-keys: |
            hugo-${{ runner.os }}-

      # 4. 빌드
      - name: Build site
        run: |
          hugo \
            --gc \
            --minify \
            --baseURL "${{ vars.SITE_URL }}" \
            --cacheDir /tmp/hugo_cache
          echo "✅ Build complete: $(find public -name '*.html' | wc -l) HTML files"

      # 5. 빌드 결과물을 artifact로 업로드 (deploy job에서 사용)
      - name: Upload build artifact
        uses: actions/upload-artifact@v4
        with:
          name: hugo-site
          path: public/
          retention-days: 3

  deploy:
    needs: build
    runs-on: ubuntu-24.04
    timeout-minutes: 5
    environment:
      name: production
      url: ${{ vars.SITE_URL }}
    steps:
      # 1. 빌드 결과물 다운로드
      - name: Download build artifact
        uses: actions/download-artifact@v4
        with:
          name: hugo-site
          path: public/

      # 2. Cloudflare Pages에 배포
      - name: Deploy to Cloudflare Pages
        uses: cloudflare/wrangler-action@v3
        with:
          apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
          accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
          command: pages deploy public/ --project-name=${{ vars.CF_PROJECT_NAME }}

      # 3. 배포 결과 출력
      - name: Print deploy URL
        run: |
          echo "🚀 Deployed to: ${{ vars.SITE_URL }}"
          echo "📅 Deploy time: $(TZ=Asia/Seoul date '+%Y-%m-%d %H:%M:%S KST')"

몇 가지 포인트를 짚어보겠다.

paths 필터: 모든 push에서 빌드가 도는 건 낭비이다. README.md만 수정했는데 빌드가 돌 이유는 없다. paths에 실제 사이트에 영향을 주는 파일만 지정하면, 불필요한 빌드를 방지할 수 있다.

concurrency: 커밋을 빠르게 연속으로 push하면 워크플로우가 중복 실행된다. cancel-in-progress: true를 걸면, 앞선 실행을 자동 취소하고 최신 커밋으로만 배포한다. 이걸 걸지 않으면 동시 배포로 꼬이는 경우가 생긴다.

build/deploy Job 분리: 하나의 Job으로 합칠 수도 있지만, 분리하면 build는 성공했는데 deploy만 실패한 경우 deploy만 재실행할 수 있다. GitHub Actions UI에서 개별 Job 재실행이 가능하기 때문이다.

Secrets 관리 — DEPLOY_KEY, API 토큰 설정

워크플로우에서 ${{ secrets.CLOUDFLARE_API_TOKEN }}처럼 참조하는 값은 GitHub 리포지토리 Settings에서 등록해야 한다. 이 값들은 로그에 마스킹되어 노출되지 않는다.

시크릿 등록 방법

리포지토리 → Settings → Secrets and variables → Actions → New repository secret 순서로 들어간다.

Secret 이름 용도 발급 위치
CLOUDFLARE_API_TOKEN Cloudflare Pages 배포 인증 Cloudflare Dashboard → API Tokens
CLOUDFLARE_ACCOUNT_ID Cloudflare 계정 식별 Cloudflare Dashboard → 우측 사이드바
TELEGRAM_BOT_TOKEN 배포 알림 전송 Telegram BotFather
TELEGRAM_CHAT_ID 알림 받을 채팅방 ID getUpdates API 호출

Secrets vs Variables 차이: Secrets는 로그에 ***로 마스킹되고 한 번 저장하면 다시 볼 수 없다. Variables(${{ vars.SITE_URL }})는 마스킹 없이 노출되므로, URL 같은 비밀이 아닌 설정값에 사용한다.

Cloudflare API 토큰 발급 시, 권한은 Cloudflare Pages: Edit만 있으면 된다. 과도한 권한을 주는 건 보안상 좋지 않는다. 나는 처음에 전체 관리자 토큰을 넣었다가, 나중에 “이 토큰 유출되면 DNS까지 털리겠구나” 싶어서 최소 권한으로 다시 발급했다.

# Cloudflare API 토큰 테스트 — 로컬에서 먼저 확인
curl -X GET "https://api.cloudflare.com/client/v4/user/tokens/verify" \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Content-Type: application/json"

# 정상 응답 예시
# {"result":{"id":"abc123","status":"active"},"success":true}

# GitHub CLI로 Secret 등록 (웹 UI 대신)
gh secret set CLOUDFLARE_API_TOKEN --body "your-token-here"
gh secret set CLOUDFLARE_ACCOUNT_ID --body "your-account-id"
gh secret set TELEGRAM_BOT_TOKEN --body "7123456789:AAH_xxxxx"
gh secret set TELEGRAM_CHAT_ID --body "123456789"

# Variables는 별도 명령어
gh variable set SITE_URL --body "https://yourblog.pages.dev"
gh variable set CF_PROJECT_NAME --body "my-blog"

고급 설정 — 캐싱, 조건부 배포, 멀티 플랫폼

의존성 캐싱으로 빌드 시간 단축

Hugo 자체는 빠르지만, npm 기반 테마를 쓰거나 PostCSS를 돌리면 npm install이 병목이 된다. actions/cachenode_modules를 캐싱하면 빌드 시간을 절반 이하로 줄일 수 있다.

      # npm 의존성 캐싱 (PostCSS, Tailwind 등 사용 시)
      - name: Cache node_modules
        uses: actions/cache@v4
        id: npm-cache
        with:
          path: node_modules
          key: npm-${{ runner.os }}-${{ hashFiles('package-lock.json') }}
          restore-keys: |
            npm-${{ runner.os }}-

      - name: Install dependencies
        if: steps.npm-cache.outputs.cache-hit != 'true'
        run: npm ci

      # 캐시 적중 시 npm ci를 건너뛰므로, 30초 이상 절약된다.

hashFiles('package-lock.json')이 핵심이다. package-lock.json이 바뀌지 않았으면 캐시를 그대로 사용하고, 바뀌었으면 npm ci를 새로 실행한다. npm install이 아니라 npm ci를 쓰는 이유는, ci가 lock 파일 기준으로 정확한 버전을 설치하기 때문이다. CI 환경에서는 항상 npm ci를 사용하는 것이 좋다.

조건부 배포 — 특정 조건에서만 배포

PR 머지가 아니라 단순 push일 때는 빌드만 하고 배포는 건너뛰고 싶을 수 있다. if 조건으로 제어한다.

  deploy:
    needs: build
    # main 브랜치 push일 때만 배포, PR 이벤트는 제외
    if: github.ref == 'refs/heads/main' && github.event_name == 'push'
    runs-on: ubuntu-24.04
    steps:
      # ... 배포 steps

  # preview 배포: PR에서는 프리뷰 URL로 배포
  deploy-preview:
    needs: build
    if: github.event_name == 'pull_request'
    runs-on: ubuntu-24.04
    steps:
      - name: Download build artifact
        uses: actions/download-artifact@v4
        with:
          name: hugo-site
          path: public/

      - name: Deploy preview
        uses: cloudflare/wrangler-action@v3
        with:
          apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
          accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
          command: pages deploy public/ --project-name=${{ vars.CF_PROJECT_NAME }} --branch=${{ github.head_ref }}

Vercel / Netlify 배포 대안

Cloudflare Pages가 아닌 다른 플랫폼을 쓴다면, deploy step만 바꾸면 된다.

      # --- Vercel 배포 ---
      - name: Deploy to Vercel
        uses: amondnet/vercel-action@v25
        with:
          vercel-token: ${{ secrets.VERCEL_TOKEN }}
          vercel-org-id: ${{ secrets.VERCEL_ORG_ID }}
          vercel-project-id: ${{ secrets.VERCEL_PROJECT_ID }}
          vercel-args: '--prod'
          working-directory: public/

      # --- Netlify 배포 ---
      - name: Deploy to Netlify
        uses: nwtgck/actions-netlify@v3
        with:
          publish-dir: public/
          production-deploy: true
        env:
          NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
          NETLIFY_SITE_ID: ${{ secrets.NETLIFY_SITE_ID }}

      # --- GitHub Pages 배포 (가장 단순) ---
      - name: Deploy to GitHub Pages
        uses: peaceiris/actions-gh-pages@v4
        with:
          github_token: ${{ secrets.GITHUB_TOKEN }}
          publish_dir: ./public
          cname: yourblog.com  # 커스텀 도메인 사용 시

GitHub Pages가 가장 설정이 간단하다. GITHUB_TOKEN은 GitHub이 자동으로 제공하므로 별도 시크릿 등록이 필요 없다. 다만 빌드 시간이 10분을 넘기면 GitHub Pages 자체 빌드 제한에 걸릴 수 있으니, Hugo 빌드는 Actions에서 하고 결과물만 Pages에 올리는 방식을 추천한다.

배포 알림 — Telegram과 Slack으로 결과 받기

배포가 끝나면 결과를 알림으로 받는 게 좋다. 특히 실패했을 때 빠르게 알 수 있어야 한다. 나는 Telegram을 쓰는데, Slack을 쓰는 팀이라면 Slack Webhook으로 대체하면 된다.

  notify:
    needs: [build, deploy]
    runs-on: ubuntu-24.04
    if: always()  # 성공이든 실패든 항상 실행
    steps:
      - name: Set deploy status
        id: status
        run: |
          if [ "${{ needs.deploy.result }}" == "success" ]; then
            echo "emoji=✅" >> $GITHUB_OUTPUT
            echo "text=배포 성공" >> $GITHUB_OUTPUT
          elif [ "${{ needs.deploy.result }}" == "failure" ]; then
            echo "emoji=❌" >> $GITHUB_OUTPUT
            echo "text=배포 실패" >> $GITHUB_OUTPUT
          else
            echo "emoji=⚠️" >> $GITHUB_OUTPUT
            echo "text=배포 스킵/취소" >> $GITHUB_OUTPUT
          fi

      # Telegram 알림
      - name: Send Telegram notification
        if: env.TELEGRAM_CONFIGURED == 'true'
        env:
          TELEGRAM_CONFIGURED: ${{ secrets.TELEGRAM_BOT_TOKEN != '' }}
        run: |
          COMMIT_MSG=$(echo "${{ github.event.head_commit.message }}" | head -1)
          COMMIT_SHA="${{ github.sha }}"
          SHORT_SHA="${COMMIT_SHA:0:7}"
          REPO="${{ github.repository }}"
          RUN_URL="https://github.com/${REPO}/actions/runs/${{ github.run_id }}"

          MESSAGE="${{ steps.status.outputs.emoji }} *${{ steps.status.outputs.text }}*
          📝 \`${SHORT_SHA}\` ${COMMIT_MSG}
          🔗 [Actions 로그](${RUN_URL})
          📅 $(TZ=Asia/Seoul date '+%Y-%m-%d %H:%M KST')"

          curl -s -X POST \
            "https://api.telegram.org/bot${{ secrets.TELEGRAM_BOT_TOKEN }}/sendMessage" \
            -d chat_id="${{ secrets.TELEGRAM_CHAT_ID }}" \
            -d parse_mode="Markdown" \
            -d text="${MESSAGE}" \
            -d disable_web_page_preview=true

      # Slack 알림 (Incoming Webhook 방식)
      - name: Send Slack notification
        if: env.SLACK_CONFIGURED == 'true'
        env:
          SLACK_CONFIGURED: ${{ secrets.SLACK_WEBHOOK_URL != '' }}
        run: |
          COMMIT_MSG=$(echo "${{ github.event.head_commit.message }}" | head -1)
          SHORT_SHA="${{ github.sha }}"
          SHORT_SHA="${SHORT_SHA:0:7}"

          curl -s -X POST "${{ secrets.SLACK_WEBHOOK_URL }}" \
            -H "Content-Type: application/json" \
            -d "{
              \"text\": \"${{ steps.status.outputs.emoji }} ${{ steps.status.outputs.text }}\",
              \"blocks\": [
                {
                  \"type\": \"section\",
                  \"text\": {
                    \"type\": \"mrkdwn\",
                    \"text\": \"${{ steps.status.outputs.emoji }} *${{ steps.status.outputs.text }}*\n\`${SHORT_SHA}\` ${COMMIT_MSG}\"
                  }
                }
              ]
            }"

if: always()가 중요하다. 이걸 안 쓰면 deploy Job이 실패했을 때 notify Job이 아예 실행되지 않는다. 실패했을 때야말로 알림이 필요한데, 기본 동작은 이전 Job이 성공해야 다음 Job이 실행되는 구조이기 때문이다. 나는 이걸 몰라서 “배포 실패했는데 왜 알림이 안 오지?” 하고 한참 삽질했다.

트러블슈팅 — 자주 만나는 에러와 해결법

GitHub Actions를 처음 세팅하면 높은 확률로 만나는 에러들을 모아봤다. 전부 내가 실제로 겪은 것들이다.

1. YAML 문법 에러

YAML은 들여쓰기에 극도로 민감하다. 탭을 쓰면 안 되고, 스페이스 2칸이 표준이다.

# ❌ 이렇게 하면 에러
steps:
  - name: Build
    run: hugo --minify
     env:  # 들여쓰기가 한 칸 더 들어가서 파싱 에러
      HUGO_ENV: production

# ✅ 올바른 들여쓰기
steps:
  - name: Build
    run: hugo --minify
    env:
      HUGO_ENV: production

# ❌ 콜론 뒤에 값이 바로 오면 에러 (특수문자 포함 시)
run: echo "deploy: started"  # 이건 OK
run: echo deploy: started     # ⚠️ 따옴표 없으면 파싱이 애매해짐

# ✅ 여러 줄 명령어는 | (literal block) 사용
run: |
  echo "Step 1: Building"
  hugo --minify
  echo "Step 2: Done"

YAML 문법 검증은 로컬에서 먼저 하는 것이 좋다. actionlint라는 도구가 GitHub Actions 전용 린터이다.

# actionlint 설치 (macOS)
brew install actionlint

# 워크플로우 파일 검증
actionlint .github/workflows/deploy.yml

# 전체 워크플로우 한 번에 검증
actionlint

# VS Code를 쓴다면, YAML 확장 + actionlint 확장 설치 추천
# 저장할 때마다 실시간으로 오류를 잡아준다

2. Permission denied 에러

GITHUB_TOKEN의 기본 권한이 2023년 이후 read-only로 변경되었다. GitHub Pages에 배포하려면 write 권한이 필요하다.

# 워크플로우 레벨에서 권한 지정
permissions:
  contents: read
  pages: write       # GitHub Pages 배포 시
  id-token: write    # OIDC 인증 사용 시

# 또는 리포지토리 Settings에서 변경:
# Settings → Actions → General → Workflow permissions
# "Read and write permissions" 선택

3. Action 버전 미고정

Action을 @v4처럼 메이저 버전 태그로 참조하는 것도 괜찮지만, 보안에 민감하다면 커밋 SHA를 고정하는 게 낫다. 태그는 덮어쓸 수 있기 때문이다.

# 일반적인 방식: 메이저 버전 태그
- uses: actions/checkout@v4       # v4의 최신 패치를 자동으로 사용

# 보안 강화: 커밋 SHA 고정 (supply chain attack 방지)
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683  # v4.2.2

# Dependabot으로 Action 버전 자동 업데이트
# .github/dependabot.yml
version: 2
updates:
  - package-ecosystem: "github-actions"
    directory: "/"
    schedule:
      interval: "weekly"
    reviewers:
      - "your-username"

4. 로컬과 CI 빌드 결과가 다를 때

Hugo 버전을 명시적으로 고정하지 않으면 이 문제가 반복된다. HUGO_VERSION을 환경변수로 관리하고, 로컬 Hugo 버전도 맞추는 것이 좋다.

# 현재 로컬 Hugo 버전 확인
hugo version
# hugo v0.142.0-... extended

# CI에서 사용하는 버전과 로컬 버전이 다르면
# brew로 특정 버전 설치
brew install [email protected]

# 또는 .hugo-version 파일로 관리 (일부 호스팅에서 지원)
echo "0.142.0" > .hugo-version

에러 대응 빠른 참조표

증상 원인 해결
워크플로우가 아예 안 뜸 파일 위치가 .github/workflows/가 아님 디렉토리 경로 확인, 오타 체크
push해도 트리거 안 됨 branches 필터 불일치 또는 paths 필터에 안 걸림 브랜치명, 변경 파일 경로 확인
Error: Process completed with exit code 128 Git 인증 실패 (submodule, private repo) token 옵션으로 PAT 전달
Error: Input required and not supplied: token Secret 이름 오타 또는 미등록 Settings → Secrets에서 이름 정확히 확인
캐시가 계속 miss key에 사용한 hashFiles 경로 오류 파일 존재 여부, 경로 패턴 확인

트러블슈팅에서 가장 중요한 건 Actions 탭의 로그를 꼼꼼히 읽는 것이다. 에러 메시지를 그대로 복사해서 검색하면 대부분 해결책이 나온다. 그리고 workflow_dispatch 트리거를 추가해두면, 코드 변경 없이 GitHub UI에서 수동으로 워크플로우를 실행해볼 수 있어서 디버깅이 훨씬 편하다.

여기까지 따라했다면, git push 한 번으로 빌드-배포-알림이 자동으로 돌아가는 파이프라인이 완성된 것이다. 처음 세팅할 때 1시간 정도 투자하면, 이후에는 글 쓰는 것 자체에만 집중할 수 있다. 나는 이 파이프라인 덕분에 “배포 귀찮아서 글 안 써야지” 같은 핑계가 사라졌다. 물론 글 쓰는 건 여전히 귀찮지만, 적어도 배포 때문은 아니다. 자세한 내용은 GitHub Actions 공식 문서를 참고하자.