birchholt 직접 해보고 남기는 기록

push 한 번으로 배포되게 만들면서 걸린 것들

이 사이트는 main에 push하면 알아서 배포됩니다. 빌드해서 Cloudflare Pages에 올리고, 올라간 게 실제로 열리는지까지 확인합니다.

만드는 데 오래 걸리진 않았는데, 당연히 될 거라고 믿었던 것 하나가 안 되고 있었습니다. 그것부터 적습니다.

1. 설정 파일이 막아줄 거라 믿었는데, 경고만 냈습니다

Hugo에는 최소 버전을 적어두는 자리가 있습니다. 저는 이렇게 적어뒀습니다.

[module]
  [module.hugoVersion]
    min = "0.165.0"
    extended = true

이러면 낮은 버전에서는 빌드가 막힐 거라고 생각했습니다. 아니었습니다. 확인해보니 경고(WARN)만 찍고 빌드는 그대로 통과합니다.

문제는 이게 조용하다는 겁니다. 로컬에서는 최신 Hugo가 깔려 있으니 아무 일도 없고, 빌드 로그에 경고 한 줄이 지나가는 걸 볼 일도 없습니다. 깨지는 건 몇 달 뒤 다른 환경에서입니다.

그래서 실제로 막는 건 스크립트로 따로 만들었습니다.

MIN="0.165.0"

RAW="$(hugo version)"
VER="$(printf '%s' "$RAW" | sed -E 's/^hugo v([0-9]+\.[0-9]+\.[0-9]+).*/\1/')"

# extended 빌드인지 — assets 파이프라인에 필요합니다
if ! printf '%s' "$RAW" | grep -q 'extended'; then
  echo "🔴 extended 빌드가 아니다: $RAW" >&2
  exit 1
fi

# 버전 비교는 sort -V 로
LOWEST="$(printf '%s\n%s\n' "$MIN" "$VER" | sort -V | head -1)"
if [ "$LOWEST" != "$MIN" ]; then
  echo "🔴 hugo $VER 은 최소 요구 버전 $MIN 보다 낮다." >&2
  exit 1
fi

extended를 따로 확인하는 이유: 버전이 맞아도 extended 빌드가 아니면 CSS 파이프라인(fingerprint 등)이 동작하지 않습니다. 버전 숫자만 봐서는 이걸 알 수 없어서 문자열을 직접 봅니다.

2. 같은 버전을 두 군데 적지 않기

여기서 새 문제가 생겼습니다. 이제 Hugo 버전이 두 곳에 있습니다 — hugo.toml과 빌드 스크립트. 그리고 배포 워크플로도 설치할 버전을 알아야 하니 세 곳입니다.

세 곳에 적어두면 언젠가 어긋납니다. 어긋나도 한동안 모릅니다.

그래서 워크플로가 스크립트에서 직접 읽어가게 했습니다.

- name: build.sh 에서 Hugo 버전 읽기
  run: |
    VER="$(grep -E '^MIN=' build.sh | cut -d'"' -f2)"
    if [ -z "$VER" ]; then
      echo "🔴 build.sh 에서 MIN 을 못 읽었다" >&2
      exit 1
    fi
    echo "HUGO_VERSION=$VER" >> "$GITHUB_ENV"

못 읽으면 거기서 멈추게 한 것이 중요합니다. 빈 값으로 넘어가면 이상한 버전을 설치하고 나서 엉뚱한 데서 실패합니다.

버전을 올릴 때는 build.sh 한 줄만 고치면 됩니다.

3. 연속으로 push하면 배포 순서가 뒤집힙니다

오타를 고치고 바로 또 push하는 일이 잦습니다. 그러면 워크플로 두 개가 동시에 돕니다. 먼저 시작한 게 나중에 끝나면 옛날 내용이 라이브에 남습니다.

이 사이트는 빌드 결과물을 통째로 업로드하는 방식이라, 늦게 도착한 쪽이 그대로 덮어씁니다.

concurrency:
  group: deploy-pages
  cancel-in-progress: true

앞선 실행을 취소하고 마지막 것만 배포합니다.

4. 배포가 끝났다고 열리는 건 아닙니다

배포 단계가 초록불이어도 사이트가 깨져 있을 수 있습니다. 빌드가 성공했는데 결과물이 비어 있는 경우가 있기 때문입니다.

그래서 두 단계를 붙였습니다. 올리기 전에 파일이 있는지,

- name: 산출물 확인
  run: |
    test -f public/index.html      || { echo "🔴 index.html 이 없다" >&2; exit 1; }
    test -f public/ads.txt         || { echo "🔴 ads.txt 가 없다" >&2; exit 1; }
    test -f public/privacy/index.html || { echo "🔴 privacy 페이지가 없다" >&2; exit 1; }

올린 다음에 진짜 열리는지를 봅니다.

- name: 라이브 검증
  run: |
    sleep 10
    for p in / /privacy/ /ads.txt; do
      code="$(curl -s -o /dev/null -w '%{http_code}' --max-time 20 "https://birchholt.com$p")"
      echo "$p → $code"
      [ "$code" = "200" ] || { echo "🔴 $p 가 200 이 아니다" >&2; exit 1; }
    done

sleep 10은 배포 반영에 시간이 걸려서 넣었습니다. 넉넉하게 잡은 값입니다.

5. 토큰에 IP 제한을 걸지 마세요

이건 겪은 게 아니라 걸기 전에 알아본 것입니다.

Cloudflare API 토큰은 호출 IP를 제한할 수 있습니다. 보안 습관대로면 걸고 싶어집니다. 그런데 GitHub Actions 러너는 실행할 때마다 IP가 바뀝니다. 걸어두면 배포가 통째로 막힙니다.

대신 권한 범위를 좁히는 쪽으로 갔습니다 — Cloudflare Pages 편집 권한 하나만 준 토큰입니다.

토큰에 만료일(TTL)을 설정했다면 갱신 알림을 같이 걸어두세요. 만료되면 배포가 조용히 실패합니다. 글을 올렸는데 안 올라가고 있는 상태를 며칠 뒤에 발견하게 됩니다.

6. 예약 발행은 정적 사이트에서 그냥은 안 됩니다

글을 미리 써두고 날짜를 벌려 올리고 싶었습니다. Hugo에는 미래 날짜를 적어두면 되는 걸로 알고 있었는데, 반만 맞았습니다.

직접 확인해봤습니다. 미래 날짜 글을 하나 넣고 두 번 빌드했습니다.

$ hugo                 → posts/ 에 그 글 없음
$ hugo --buildFuture   → posts/ 에 그 글 있음

미래 날짜 글이 빌드에서 빠지는 건 맞습니다. 문제는 그 다음입니다.

정적 사이트는 빌드한 결과물이 그대로 올라가 있는 것이라, 날짜가 됐다고 저절로 나타나지 않습니다. 빌드가 한 번 더 돌아야 합니다. 그런데 워크플로 트리거가 push밖에 없으면 그걸 돌릴 사람이 없습니다.

트리거를 하나 더 붙여서 해결했습니다.

on:
  push:
    branches: [main]
  schedule:
    - cron: '0 1 * * *'   # 매일 10:00 KST
  workflow_dispatch:

이제 오늘 세 편을 다 써서 한 번에 push해도, 날짜가 다르면 사흘에 걸쳐 하나씩 열립니다.

정리