birchholt 직접 해보고 남기는 기록

예약 발행이 또 안 떴습니다 — 빈 커밋은 밀기 전에 접었습니다

2026-09-03 오전, 공개일이 된 글이 라이브에 없었습니다. 예약 발행 스케줄은 이틀 전에 한 번 손봐둔 참이었는데도 그랬습니다.

미래 날짜 글이 빌드에서 빠진다는 것, 날짜가 됐다고 저절로 나타나지 않고 빌드가 한 번 더 돌아야 한다는 것은 먼저 확인해서 적어뒀습니다. 그래서 워크플로에 schedule 트리거를 달아둔 상태였습니다. 이 글은 그 설계가 또 실패했을 때 어떻게 되살리는가에 대한 기록입니다.

빠진 것이 글 하나인지 배포 전체인지

splitting-the-repodate: 2026-09-03 이고 publishDate 가 없어 date 로 폴백한 글입니다. 09:00 KST 에 공개 자격을 얻고 1차 cron 이 10:23 KST 에 도는데, 11:44 KST 에 봤을 때 없었습니다.

$ curl -o /dev/null -w '%{http_code}' https://birchholt.com/posts/splitting-the-repo/
404

404 하나로는 “배포는 돌았는데 그 글만 빠졌다"와 “배포가 아예 안 돌았다"가 갈리지 않습니다. 그래서 라이브 sitemap.xml<loc> 목록을 같이 봤습니다. privacy 까지 포함해 9건인데 그 주소가 없었고, 올라가 있는 결과물 자체가 그 글을 모르는 상태였습니다. 한 주소만 찍는 것이 왜 확인이 아닌지는 먼저 적어뒀습니다.

푸시는 돼 있었고(git rev-parse HEAD origin/main 두 값이 같았습니다) 로컬 빌드에도 그 글이 들어 있었습니다. 자격도 커밋도 다 됐는데 라이브만 옛날 결과물이었습니다.

정각을 피해도 건너뜁니다

이게 처음이 아니었습니다. 2026-09-01 에 '0 1 * * *' 가 발화하지 않아 예약 글이 안 올라갔고, 그때 근거로 삼은 것은 GitHub 공식 문서의 이 문장이었습니다.

“The schedule event can be delayed during periods of high loads of GitHub Actions workflow runs. High load times include the start of every hour.”

그래서 정각을 피해 '23 1 * * *' 로 옮겼고, 그것만으로는 부족하다고 보고 하루 3회로 늘려뒀습니다.

cronKST역할
23 1 * * *10:231차입니다. 미래 글은 09:00 KST 부터 자격이 생깁니다
23 4 * * *13:231차가 안 떴을 때 잡습니다
23 7 * * *16:232차도 안 떴을 때 잡습니다

그 상태에서 2026-09-03 에 1차가 또 안 떴습니다. cron 이 안 뜬 두 번째 사례이고, 정각을 피한 뒤로는 첫 사례입니다. 정각 회피는 확률을 낮출 뿐이고, GitHub 스케줄은 보장이 아니라 best-effort 입니다. 백업 cron 두 개를 둔 판단 자체는 옳았습니다. 다만 이번에 그것들이 일한 건 아닙니다 — 13:23 까지 기다릴 생각이 없어서 2차가 돌기 전에 직접 되살렸습니다.

1차가 「건너뛴」 것인지 「돌다가 실패한」 것인지는 구분하지 못했습니다. 저장소가 비공개이고 gh CLI 가 다른 계정으로 로그인돼 있어 실행 이력을 못 봤습니다.

paths-ignore 가 걸린 워크플로의 복구 경로

먼저 떠오른 건 git commit --allow-empty 였습니다. 내용은 안 건드리고 push 만 발생시키면 될 것 같았습니다. 밀기 전에 워크플로를 열어봤고, 거기서 접었습니다.

on:
  push:
    branches: [main]
    paths-ignore:
      - 'README.md'
      - '.gitignore'

paths-ignore 가 있으면 GitHub 은 그 push 의 변경 파일 목록을 필터에 겁니다. 변경 파일이 0건이면 필터를 통과하는 파일도 0건이라 워크플로가 뜨지 않습니다. 다만 이건 설정을 읽고 내린 판단이지, 밀어보고 확인한 것이 아닙니다.

그래서 되살리는 경로를 둘로 봤습니다.

방법조건
Actions 탭에서 workflow_dispatch 수동 실행워크플로에 그 트리거가 있어야 합니다
변경이 든 커밋을 pushpaths-ignore 에 없는 파일이 하나는 바뀌어야 합니다

저는 두 번째를 골랐습니다. 마침 이 사건 자체를 워크플로 주석에 남길 참이어서, deploy.yml 에 경위를 적은 커밋이 그대로 트리거가 됐습니다. 푸시하고 라이브를 15초 간격으로 찍었습니다.

[11:49:09] try 3 → 404
[11:49:25] try 4 → 200

푸시부터 반영까지 1분 남짓이었습니다.

타임존 탓인 줄 알았는데 아니었습니다

저는 로컬(KST)과 Actions(UTC)의 판정이 9시간 어긋나서 생긴 일이라고 봤습니다. 로컬에서 공개일이 지난 글이 UTC 기준으로는 아직 미래라면 증상이 그대로 설명되니까요. 같은 트리를 타임존만 바꿔 세 번 빌드했습니다(02:47 UTC).

TZ빌드 결과그 타임존이 적용됐다면
UTC7편, 그 글 포함입니다포함이 맞습니다
Asia/Seoul (UTC+9)7편, 그 글 포함입니다포함이 맞습니다
Pacific/Midway (UTC-11)7편, 그 글 포함입니다빠져야 합니다

셋째 줄이 답을 줬습니다. UTC-11 에서 시각 없는 date: 2026-09-03 은 11:00 UTC 가 되어 측정 시점에는 아직 미래입니다. 빠져야 하는데 빠지지 않았습니다.

Hugo 는 TZ 환경변수를 보지 않습니다. hugo.tomltimeZone 이 없으면 시각 없는 날짜를 UTC 로 고정해서 읽습니다. 로컬과 CI 가 애초에 어긋나지 않으니, 이번 원인은 타임존이 섞인 것이 아니라 순수한 cron 미발화였습니다.

판정을 실제로 움직일 수 있는 값은 셸의 TZ 가 아니라 hugo.tomltimeZone 인데, 그건 넣어보고 재보지 않았습니다. timeZone 을 추가할 이유가 없다는 결론은 「양쪽이 어긋나지 않는다」까지만 근거가 있습니다.

이 실측을 워크플로 주석에 남길 때는 처음에 Pacific/Midway 한 건만 적었습니다. 나중에 세 건을 다 적어뒀습니다 — 한 건만 남겨두면 이 결론을 나중에 다시 의심할 때 재현 범위를 알 수가 없습니다.

빌드 제외를 결정하는 것은 publishDate 이고, 없으면 date 로 폴백합니다. 시각이 없으면 양쪽 다 00:00 UTC 로 읽히고, 그게 KST 로 09:00 입니다. 1차 cron 을 10:23 KST 에 둔 것이 그 때문입니다 — 자격이 생기고 나서 도는 시각이어야 합니다.

정리