定时发布:静态网站的遗憾

定时发布顾名思义就是在特定时间发布文章或内容,是CMS系统的基础功能。目前,几乎所有动态博客框架都能实现,例如Ghost、WordPress等,然而,对于静态类型的博客来说,由于缺乏能够后台定时的服务端,使得无法将定时发布变为原生功能,需要站长各凭本事实现,较为繁琐。

为什么研究这个问题?因为本博客就是一个用 Zola SSG 工具构建的静态网站,从开始移植主题建站到现在,我已经构思并调研了好几种方案,最终决定基于 GitHub Action 来实现定时发布。

几种实现方式对比

这里我先把可以实现静态网站定时发布的几种方式列举出来,并对比关键指标。

方法 描述 心智负担 可靠性 运维难度 费用

自己干 通过手机闹钟、手机日历提醒自己该发布啦 高 中,节假日不保证 低 无

别人干 安排一个工具人帮我发布 低 中,取决于报酬,节假日不可用 低 高

自建发布服务 预先构建网站,然后在本地或云端自建定时服务部署网站 中 高 中 取决于是否使用付费云资源

自建CI触发服务 在本地或云端自建定时服务触发CI接口构建网站,然后自动发布 低 高 低 取决于是否使用付费云资源

使用平台原生功能 依托平台能力定时触发CI实现定时发布 低 高 低 取决于是否使用付费云资源

通过对比我们可以发现:

靠人力(自己干、别人干)实现定时发布在可靠性上难以保证,因为定时发布的一个重要使用场景就是在节日等重要节点发布信息,其稳定性至关重要。

依托外部服务(自建发布服务、自建 CI 触发服务)是符合直觉的,但是会引入额外的复杂度,同事可能产生一定的费用。

最理想的情况是使用的工具或平台自带定时发布的功能(使用平台原生功能),但是遗憾的是,大部分平台并不具备这类功能。

为了实现最好的效果,我决定尽可能通过平台自身实现定时发布的功能。

通过 GitHub Action 实现定时发布

本博客使用 GitHub 私有仓库存储代码和文章,也就是 Git-Based 静态博客,那么接下来的工作势必要围绕 GitHub 生态进行。

在此之前,我已经通过 GitHub Action (GitHub 的持续集成 CI 服务) 实现了主分支的自动部署,若在此之上添加定时发布,必然是围绕分支合并和 CI 流水线做文章。

比较典型的方案是用新分支存放内容 + 定时合并 PR。将要发布的内容存在新分支中,到达特定时间后,自动合并,然后自动触发发布流水线,以此实现定时发布。

沿着该技术路线,我搜集到一些能够定时合并 PR 的 GitHub 扩展工具,然而,这类工具基本都需要使用付费高级版。比如有一款工具的免费版额度是每月 4 次定时合并,我想这很难满足博主的需求,毕竟定时发布除了用于发文章,也可用于发版。

最终,我找到了 Merge Schedule,可以完全基于 GitHub Action 实现定时合并 PR 进而实现自动发布。

前提条件

本方案需要具备以下条件,我想大部分静态网站应该都能轻易达成。

网站存放在 GitHub,并且是 Git-Based 静态博客。

代码仓库主分支已经实现自动构建、部署的流水线,能做到更新代码后自动部署到生产环境。

重要更新(如文章发布、重大改版等)采用分支 + PR 的工作流,能做到通过合并 PR 即可完成所有发布内容的更新工作。

基本原理

经过深入研究,现将技术原理梳理了大概:

设置专门用于定时合并 PR 的 GitHub Action 流水线,并设置定时执行,如每小时cron: '0 * * * *'。

在计划定时合并的 PR 描述中添加定时器指令,如/schedule 2022-06-08T09:00:00.000Z。

每次运行定时合并流水线时,检查所有打开的 PR,如果时间匹配,则调用 API 完成 PR 合并。

这种方式巧妙的利用了 GitHub Action 的定时触发机制,实现了原生的定时合并 PR,进而做到定时发布。

部署过程

这个方案的部署十分简单,只需新增一个 GitHub Action 定义文件即可。

在代码库新建文件.GitHub/workflows/merge-schedule.yml,用于存放流水线定义。

在文件中粘贴以下内容,这里设置了时区为国内Asia/Shanghai,合并方式为merge,可以根据需要自行修改。

name: Merge Schedule

# see https://GitHub.com/gr2m/merge-schedule-action

on:

pull_request:

types:

- opened

- edited

- synchronize

schedule:

# https://crontab.guru/every-hour

- cron: '0 * * * *'

jobs:

merge_schedule:

runs-on: ubuntu-latest

steps:

- id: merge-schedule

uses: gr2m/merge-schedule-action@v2

with:

# Merge method to use. Possible values are merge, squash or

# rebase. Default is merge.

merge_method: merge

# Time zone to use. Default is UTC.

time_zone: 'Asia/Shanghai'

# Require all pull request statuses to be successful before

# merging. Default is false.

require_statuses_success: 'true'

# Label to apply to the pull request if the merge fails. Default is

# automerge-fail.

automerge_fail_label: 'merge-schedule-failed'

env:

GitHub_TOKEN: ${{ secrets.GitHub_TOKEN }}

可以在官方文档中找到更高级的用法,例如如何绕过存储库安全规则等。

最后,将代码提交并推送到 GitHub 的主分支就大功告成了。

下面分享一种更高级的流水线配置,可以在定时发布之后推送通知。

这里我以 Bark 推送服务为例,需要提前配置好 GitHub Action 的 Secret 变量Bark_KEY,将推送服务的 API KEY 放进去。

name: Merge Schedule

# see https://GitHub.com/gr2m/merge-schedule-action

on:

pull_request:

types:

- opened

- edited

- synchronize

schedule:

# https://crontab.guru/every-hour

- cron: '0 * * * *'

jobs:

merge_schedule:

runs-on: ubuntu-latest

steps:

- id: merge-schedule

uses: gr2m/merge-schedule-action@v2

with:

# Merge method to use. Possible values are merge, squash or

# rebase. Default is merge.

merge_method: merge

# Time zone to use. Default is UTC.

time_zone: 'Asia/Shanghai'

# Require all pull request statuses to be successful before

# merging. Default is false.

require_statuses_success: 'true'

# Label to apply to the pull request if the merge fails. Default is

# automerge-fail.

automerge_fail_label: 'merge-schedule-failed'

env:

GitHub_TOKEN: ${{ secrets.GitHub_TOKEN }}

# run if there is any merged pull request

- name: notification

uses: shink/bark-action@v2

if: ${{ fromJson(steps.merge-schedule.outputs.merged_pull_requests)[0] != null }}

with:

key: ${{ secrets.Bark_KEY }}

title: GitHub PR 已自动合并

body: 稍后请检查部署结果

sound: alarm

isArchive: 0

automaticallyCopy: 0

这个流水线会在成功合并 PR 后,给用户推送通知,这样用户可以等待自动部署完成,然后访问网站看看结果。

使用流程

至此,我们可以按照直观的逻辑步骤来完成定时发布。

基于主分支创建用于定时发布的子分支,一般是文章分支。

完成内容更新,并推送到 GitHub。

在 GitHub 创建 PR 以合并到主分支,并在 PR 的描述内容最末尾,添加指明时间的以下内容。

特定日期,如/schedule 2022-06-08

特定日期的整点时间,如/schedule 2022-06-08T09:00:00.000Z代表2022年6月8日的上午9点,时间格式遵循ISO 8601规范

下一次整点时间/schedule

等待自动发布~

如果需要修改或者取消,直接删除或编辑 PR 的描述内容即可,你甚至可以简单粗暴地删除 PR ~

方案的局限性与解决措施

虽然我们实现了不出 GitHub 就能定时发布,但是仍存在一些局限性:

可选时间颗粒度较粗,默认只能按照整点时间进行配置,如8点、9点等。虽然可以满足大部分应用场景了,但存在用户希望半点发布,如8点30分,可以通过自行调整流水线定时触发频率来结婚,如每半小时。目前另外尚未测试是否存在时间的模糊匹配或近似匹配,

定时器的设置仍然较为繁琐,在 PR 中需要输入较长的指令,缺少正确性校验,可能导致失败。针对该问题,可以通过接下来在定时合并流水线中添加新增通知来解决。

缺少发布结果的预览,用户对最终效果缺少掌控。针对该问题,需要进一步定制 CI 流水线,增加子分支的自动部署,预览最终效果,还可以为项目增加预生产分支,确保合并后没有 Bug,不过这些实现起来就比较繁琐了。

总结与展望

这个方案本质是持续集成流水线的高级应用,依托 GitHub Action 的定时功能实现定时发布。围绕流水线可以添加许多有意思且实用的功能,例如构建预览环境、提前的错误检查等,这就需要发挥大家的想象力了。

其实,我最初的构想是基于免费的函数服务(如 Cloudflare Worker)实现定时触发功能,但是函数服务的冷启动问题可能会影响定时触发的稳定性,所以这个方案暂时搁置了,如果大家有解决方案欢迎在评论区分分享~

这篇文章提供了一种基于 GitHub Action 的静态网站定时发布方法,目标是为了让静态网站能享受到动态网站的功能与便利。接下来,我会继续研究如何提升静态网站的维护者体验,敬请期待!

参考资料

Merge Schedule · Actions · GitHub Marketplace