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해도, 날짜가 다르면 사흘에 걸쳐 하나씩 열립니다.
정리
- “설정에 적어뒀으니 막힐 것"은 확인 전까지는 짐작입니다. 경고만 내고 통과하는 설정이 있습니다
- 같은 값을 두 곳에 적게 되면 한쪽이 다른 쪽을 읽어가게 만드는 편이 낫습니다
- 배포가 성공했다는 것과 사이트가 열린다는 것은 다른 사실이라 따로 확인합니다
- 자동화 토큰에 IP 제한은 안 됩니다. 좁힐 곳은 IP가 아니라 권한 범위입니다
- 정적 사이트의 예약 발행은 날짜가 아니라 빌드가 트리거입니다