Featured image of post 『 Hugo 』Hugo Deploy

『 Hugo 』Hugo Deploy

安全分发你的 Hugo 站点

摘要

本文介绍一种利用 GitHub Actions / Workflows 以及 GitHub Private Repository 特性实现的 Hugo 站点安全分发策略。通过创建公有仓库部署 GitHub Pages ,创建私有仓库存储 Hugo 站点源码,通过私有仓库中的工作流以及 Deploy Key 机制桥接两个仓库的数据,达到私有化源码信息的同时部署个人站点的目的。

相关工作

目前传统的 Hugo + GitHub Pages 部署思路主要分为以下 3 种(以 username.github.io 公有库为例):

  1. Pages Path = main/(root)hugo publishDir = "./public",以 ./public 文件夹为项目根推送站点。

  1. Pages Path = main/docshugo publishDir= "./docs",以 Hugo 站点文件夹为项目根推送。

  1. Pages Path = gh-pages/(root),仍将 Hugo 站点文件夹作为项目根推送至 main 主分支,但通过引入 GitHub Pages action 以及 Hugo setup 工作流,编译你推送的 Hugo 源码并将渲染出来的站点文件(如默认的 ./public)拷贝到(当前仓库)子分支 gh-pages,实现站点部署。

因 GitHub Pages 的部署仓库必须为 Public,如上 3 种方案优劣划分明显。

方案一 是新手入门此技术栈最为常见的路线之一,优势显著——门槛低,快速部署。参照着网络上大部分的 Quick Start 教程都可以成功部署自己的站点。但其劣势也足够明显——无法有效管控 Hugo 源码

我们需要知道,Hugo + GitHub Pages 技术栈中,两套代码是相互独立的。Hugo 通过我们编写的「博客内容」以及「主题样式」渲染出站点;而 GitHub Pages 相当于提供了一个实体来托管我们渲染出来的站点。换句话说,除了 GitHub Pages 我们还有不下 10 种 托管方案。此处举个不恰当例子,Hugo 相当于是一个写了 print("Hello World") Python 代码的程序,我们可以用 Pycharm 打印它,也可以用 Spyder 打印,只是换了个壳,但如果我们「代码」没了,我只能有当前打印出来的东西,我们博客运营至今的工作进度都没法继承并且「丢失」了。

显然,本地存储文件的损坏或丢失将引发不可预估的灾难。

方案二 是一种能够有效处理上文所述痛点的解决方案。该方案的关键操作是将整个 Hugo 站点文件托管,指定 GitHub Pages 挂载子目录(既编译输出目录),有效存储了此技术栈的「内脏信息」,但同时它也暴露出了一些无法应对的问题——我们并不希望自己暂未发表的内容被公开。显然这是矛盾的,这与 GitHub 要求挂载博客的仓库必须「可见」有关(Free Plan)。我们暂未完成的工作可能会以 draf: true 形式标记,防止其被编译渲染进而呈现到公开可见的站点上,但如果我们将整个 Hugo 代码都上传到公有库中, 包括 content/post 在内的所有文件都是可见的。

有的人提出将敏感信息加入到 .gitignore ,待文章编写工作完成后再一次性上传。显然这治标不治本,甚至有些方案一的影子。

方案三 是在方案二上的提升与拓展。该方案主要解决提交历史紊乱的问题。在此方案中,我们一般会人为 ignore publishDir,意味着我们提交 Hugo 代码时,不会携带编译输出目录中大量繁杂的变更信息。如此一来,我们每次在本地完成编写工作后,无需运行 Hugo 编译指令,只需正常提交改动(仅变更 Markdown 文章或主题样式信息),而编译、推送、部署的工作由工作流自动完成。

如上所述,如果你的需求是:需要一个「不可见仓库」存储源码,同时又不得不依赖「可见仓库」托管博客站点,那你也许会对本文介绍的解决方案感兴趣。

解决方案

预备知识

若您阅读了上文所述信息,您应该了解本方案并不是一个 Hello World 操作指南,其具备一定的上手难度,需要您至少掌握如下技术以解决本文暂未提及的偶发性 BUG。

  1. 了解 Git 基本指令(如:remote, pull, add, commit, push, checkout…);

    另外,读者需要知道如何设定全局配置用以绑定个人 GitHub 账号,否则你需要在每次提交代码时输出账号密码。

  2. 了解 Hugo 基本指令,知道如何用 Hugo 写文章;

  3. 了解 Bash(shell) 基本指令;

  4. 了解怎么添加 Actions;

  5. 已有成功部署 Hugo + GitHub Pages 个人博客站点的经历;

    意味着本文默认读者已有 GitHub 账号且知道如何新建公有/私有仓库。

  6. [Optional] CNAME 以及 GitHub Pages 自定义域名相关知识;

    你需要知道 CNAME 文件需要填什么,CNAME 文件命名与存储位置,以及如何在 Pages 中指定 custom domain。

创建必要仓库

创建 GitHub 公有仓库

若您仅想测试本方案是否可行,大可新建一个 test repo,而无需直接在 username.github.io 上开刀。此处我们新建一个公有仓库 test-demo-repo。建议在创建仓库时不初始化任何文件,让其保持空置状态。

image-20210930183711805

创建 GitHub 私有仓库

同样,我们创建一个不初始化任何文件的私有仓库 test-demo-actions。注意,此处为了演示对比,作者将仓库权限设为「可见」。

配置 DEPLOY_KEY

参考 GitHub 官方文档查看如何 生成 SSH 密钥。注意替换 "[email protected]" 即可,生产的密钥默认保存在 ~/.ssh/目录下。

  1. 分配公钥 DEPLOY KEY

    以你喜欢的方式打开 id_rsa.pub(以使用 RSA 算法为例,根据你所选的算法文件名有所不同),复制其内容,打开 公有仓库SettingsDeploy KeysAdd deploy key,添加密钥。

    分配公钥 DEPLOY KEY

    此处你可能会疑惑 GitHub SSH Key 和 GitHub Deploy Key 的区别 是什么,可以简要理解为 root 用户和 user 用户的区别,前者配置在你的账号设置里,可以操作你的所有仓库,后者配置在仓库里,只能对此仓库具备操作权限。

  2. 分配私钥 SECRET KEY

    以你喜欢的方式打开 id_rsa,复制其内容,打开 私有仓库 也即 源码库SettingsSecretsNew repository secret 添加仓库密钥(给予工作流中被临时分发的虚拟机操作你的某个仓库的权限)。

生产 Hugo 站点源码

  1. 在一个你喜欢的本机位置打开 Git Bash 并创建 hugo 站点:
1
hugo new site blog && cd blog
  1. 此处我们需要用 git 指令拉取主题代码,并将其作为子模块导入,故需先初始化 git 运行环境:
1
git init
  1. 此处使用 Stack 主题 构建演示站点:
1
git submodule add https://github.com/CaiJimmy/hugo-theme-stack/ themes/hugo-theme-stack
  1. 拷贝 Stack example site 演示站点特性:
1
cp -a themes/hugo-theme-stack/exampleSite/. .
  1. 删除默认站点配置文件(防止冲突):
1
rm config.toml
  1. 启动 Hugo 本地服务,查看站点是否正常运行:
1
hugo server
  1. 访问 Hugo 本地服务器(默认1313端口)如果一切安好,你可以看到如下画面:

demo-site

  1. Press Ctrl + C to stop hugo server 我们要开始后续步骤了~

连接 Hugo 与 私有仓库

  1. 源码库 (私有库)作为远程仓库链接(请替换为你自己的 URL):
1
git remote add origin https://github.com/QIN2DIM/test-demo-actions.git
  1. 我们的仓库处于空置状态,需要先进行一些预处理:
1
git branch -M main
  1. 添加变动并强制推送:

    这也解释了上文为什么不推荐在创建仓库时添加初始化文件,如果你那样做了,此处需要多一步历史对齐的操作。链接远程仓库后需要先把远程仓库中的初始化文件拉到本地,否则强制推送会出现一些遗留问题。

1
git add --all && git commit -m "migrate" && git push -f origin main
  1. 查看仓库盛况

    如果一切安好,你会在你的私有源码库中看到如下目录结构的信息,如果你足够了解 Git 默认的文件比对机制,应该知道并未被上传的资源是「空文件」。

Init private repository

此时,诸如此目录结构的「Hugo 源码」已能渲染出成型的博客站点,就如同你在本地启动服务那样。而我们需要思考的是,这个私有仓库并不能挂载博客,我们需要将渲染出来的站点代码放到可见的公有仓库下面(如 username.github.io),这个工作交由 workflows 进行。

创建工作流

就算您不了解 GitHub Actions / Workflows也无大碍,我会在后文详细注释「workflows 代码」中关键步骤的具体含义,让你足够了解自己在做什么。你也可以阅览作者临时总结的关于 workflows 的运行逻辑,来了解它是如何工作的。

  1. 打开 私有源码库 ,创建工作流Set up a workflow yourself

  2. 将以下代码覆盖 Edit new file 窗格中的内容,并按照提示修改env环境变量

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    
    # 工作流的名称
    name: hugo-deploy
    
    # 触发事件
    on:
      # 当主分支 main 文件变更时触发任务
      push:
        branches: [main]
    
    # jobs 此工作流执行的任务
    # 在 Workflows 中,各个 job 是并行执行的,此处仅有 1 个 job
    jobs:
      # job-id 在一个工作流中唯一区分,此 job-id 为 build
      build:
        # 此 job 运行的虚拟系统
        runs-on: ubuntu-latest
        # 此 job 的任务执行步骤,默认顺序执行
        steps:
    
          # step1: 检查运行环境是否正常
          - uses: actions/checkout@v2
            # 若您的代码中携带子模块,请务必书写如下内容
            with:
              submodules: true
              fetch-depth: 0
    
          # step2: 开始执行核心逻辑
          # name 是步骤的名称,相当于步骤的简明注释,但可有可无
          - name: "Building..."
            # uses FORMAT: 用户/仓库@版本信息
            # - uses 是 workflows 的精髓,其作用可概述为:预加载并运行指定资源
            # 它可以指向 workflows-image,甚至可以具体到某个仓库中的一个可执行文件
            # - 此处使用 reuixiy/hugo-deploy@v1 桥接两个仓库
            uses: reuixiy/hugo-deploy@v1
    
            # env 环境变量
            # - job 中的每一个 step 都是独立的进程,其内创建的变量互不共享
            # - 通过 env 创建的环境变量(键值对)可以被 step 读取并使用
            # - 此处需要设定4个环境变量
            env:
              # DEPLOY_REPO 部署 GitHub Pages 的可见代码库(如 username/username.github.io)
              DEPLOY_REPO: username/test-demo-repo
    
              # DEPLOY_BRANCH 部署 GitHub Pages 的可见代码库的分支
              # - 若上一步选择的是 username.github.io,请不要在不熟悉本解决方案的情况下填写 main,master 等主分支名词;
              # - 工作流会自动创建原先不存在的分支
              DEPLOY_BRANCH: build
    
              # DEPLOY_KEY 操作权限(非对称)密钥
              # - 源码仓库中的工作流读取私钥,获取 GitHub Pages 所在仓库的(读写)权限
              # - workflows 读取的是 Secrets Key,只是此处的变量名叫 DEPLOY_KEY
              DEPLOY_KEY: ${{ secrets.DEPLOY_KEY }}
    
              # TZ 时区信息,主要为了 git commit -m "xxx" 提供备注信息
              # https://en.wikipedia.org/wiki/List_of_tz_database_time_zones
              TZ: Asia/Shanghai
    
    Commit workflows yourself
  3. 为了防止冲突以及提高容错,我们选择先将远程仓库的代码改动(add main.yml)同步到本地:

    顺便等一下 workflows 运行。

    1
    
    git pull origin main
    
  4. 查看仓库盛况

    当你看到 workflows 运行完成的标志后,访问你部署 GitHub Pages 的仓库页面,如果一切安好,你讲看到如下内容:

image-20210930204344604
  1. 分析盛况

    若你严格按照本方案提供的步骤操作, workflows 配置文件的提交也算是一次触发动机,Actions 会自动进行首次工作。若您的顺序有点颠倒,比如先加了工作流,才想起来自己没有配置密钥,那么你仅需要做任何改动再次向主分支提交代码触发 actions 的工作流即可,比如修改一下 workflows 配置文件的注释之类的。

    你可以看到,在你的公有库(指定分支)下的内容就是你 「Hugo 源码」编译输出的内容。以此公有库分支(如 build 或你指定的分支)为根(/root)即可生成 Pages。

    是的你没听错,我们进行到这个步骤都还没配置 GitHub Pages,就如同上文所说,Hugo 是「内脏」,而 GitHub Pages 仅是「壳」。

验收

  1. 打开公有库配置 GitHub Pages。

    SettingsPages ,指定 Source,Branch 就是你指定的分支,路径选择 /(root),完事 Save。

  1. Page Build

    等待 GitHub Pages 部署完成,访问部署站点。

  1. Hello Man

    部分同学到这一步会遇到如下图所示的问题(相对路径索引异常),不要慌。

    1)你需要确保 Hugo Config 中的 baseUrl 是否填写正确。如公有仓库名为 superman/blog,则 baseUrl: https://supermain.github.io/blog,若仓库名为 superman/superman.github.io,则 baseUrl: https://supermain.github.io。若您的仓库名包含英文大小写,请确保 baseUrl 中统一小写的写法,这与 Linux 文件系统命名有关。

    2)如果你正在 test 仓库上进行本方案的实验任务,且其他仓库已经部署了(自定义)根域名站点,那你需要在测试站点上也配置 CNAME(具体如何配置需要您自行掌握,这不在本篇博客的介绍范围之内),同时需要检查上一步的提议。这并不是什么难事,作者几乎每一个有价值的 GitHub Project 都会使用 Hugo + Github Pages 的技术栈部署技术文档,而这些站点都使用同一个自定义域名,而作者的 QIN2DIM/QIN2DIM.github.io 仓库则部署了 Blog Pages。

  1. Hello World

    本站目前采用的就是这种部署方案。

关联问题

Workflows: How does it work?

Uses: reuixiy/hugo-deploy@v1

我们在 workflows 配置文件的任务步骤中拉取并执行了 reuixiy/hugo-deploy@v1 指向的代码。这个脚本文件主要做了 3 件事:

  1. 读取我们配置的 env 环境变量

    • 定位我们挂载 GitHub Pages 站点的仓库地址;
    • 配置 GitHub SSH 用户名以及邮箱(用于鉴权);
    • 拉取 DEPLOY_KEY 并赋予操作权限;
  2. 将我们的源码库克隆到虚拟容器中,执行 Hugo 编译指令生产静态站点

    划重点,如果你的 Hugo 站点配置文件中没有指定 publishDir 参数,那么编译输出默认是./public。换句话说,如果你之前手动指定了 publishDir 为其他文件,那你需要注释掉这个参数,否则工作流的运行会出现异常。

  3. 进入编译输出文件夹(./public),并在此文件路径下执行代码提交指令

此外,我们并不需要在 workflows 配置文件中编写 Hugo setup 等配置 Hugo 编译环境的步骤,因为我们引用的 reuixiy/hugo-deploy@v1 已经写了一个拉取最新拓展版 Hugo 的 Dockerfile。

脚本源码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
#!/bin/bash

# Required environment variables:
#
#   DEPLOY_KEY          SSH private key
#
#   DEPLOY_REPO         GitHub Pages repository
#   DEPLOY_BRANCH       GitHub Pages publishing branch
#
#   GITHUB_ACTOR        GitHub username
#   GITHUB_REPOSITORY   GitHub repository (source code)
#
#   TZ                  Timezone

set -e

REMOTE_REPO="[email protected]:${DEPLOY_REPO}.git"
REMOTE_BRANCH="${DEPLOY_BRANCH}"

git config --global user.name "${GITHUB_ACTOR}"
git config --global user.email "${GITHUB_ACTOR}@users.noreply.github.com"

# https://github.com/reuixiy/hugo-theme-meme/issues/27
git config --global core.quotePath false

ln -s /usr/share/zoneinfo/${TZ} /etc/localtime

mkdir /root/.ssh
ssh-keyscan -t rsa github.com > /root/.ssh/known_hosts && \
echo "${DEPLOY_KEY}" > /root/.ssh/id_rsa && \
chmod 400 /root/.ssh/id_rsa

git clone --recurse-submodules "[email protected]:${GITHUB_REPOSITORY}.git" site && \
cd site

hugo --gc --minify --cleanDestinationDir

pushd public \
&& git init \
&& git remote add origin $REMOTE_REPO \
&& git add -A \
&& git checkout -b $REMOTE_BRANCH \
&& git commit -m "Automated deployment @ $(date '+%Y-%m-%d %H:%M:%S') ${TZ}" \
&& git push -f origin $REMOTE_BRANCH \
&& popd

rm -rf /root/.ssh

总结

下图所示为本文介绍方案的总流程图。核心环节是创建在 「私有仓库」 中的 Workflows hugo-deploy

  1. 工作流将你的 Hugo 私有代码克隆进虚拟容器中;
  2. 执行 Hugo 编译指令生成静态站点;
  3. 通过 Secreat Key / Deploy Key 权限密钥获取 GitHub Pages 公有仓库的读写权限;
  4. 将编译输出文件 ./publishDir 推送至公有仓库。

最后部署了 GitHub Pages 的公有仓库拥有自己的网页渲染工作流,执行完毕后将自动更新你的博客站点。

Hugo 个人博客安全分发架构

下表为本文涉及四种方案在部署速度,隐私性、上手难度等维度的横向比较:

属性/方案方案一方案二方案三安全分发
部署速度☀️☀️☀️☀️☀️☀️☀️☀️
隐私性☀️☀️☀️☀️☀️☀️☀️☀️
持久运营☀️☀️☀️☀️☀️☀️☀️☀️☀️☀️
上手难度☀️☀️☀️☀️☀️☀️

不难看出,本文所介绍的“安全分发”方案优势在于利用 GitHub Private Repository 不可见特性在提高了隐私性的同时解决了方案一无法持久运营的问题,但其所依赖的 Workflows 相关技术栈提升了方案整体的上手难度。

此外,本文介绍的方案使用了reuixiy/hugo-deploy@v1 提供的 Hugo 站点“分发”脚本,其流程上使用 DockerFile 进行 Hugo 编译环境的创建,而非使用 GitHub Actions 提供的预加载环境,这极大拖累了“整机性能”,导致“安全分发”的部署速度在此次横评中垫底。

Workflows hugo-deploy details

不可否认的是,你完全可以选择使用诸如 方案一 + 坚果云 双路复用的解决方案,既保证了部署速度和隐私性,又具备代码实时上云的持久运营能力;也可以使用 GitHub bot 来替代“安全分发”中的某些环节;也可以魔改“安全分发”的工作流来拔升部署速度;甚至你可以使用多级 NAS 存储你的项目源码。毕竟折腾技术总是有趣上头,根据你的具体需求和环境选择适合你的方案即可!

参考资料

You will to enjoy grander sight / By climing to a greater height.