Jekyll 集成 Travis CI
前段时间,因为本地 git push -f
覆盖远端 master 分支,导致博客在 GitHub Pages 上编译失败。查看 GitHub Help 的文档 “Viewing Jekyll build error messages”,文中提及可以通过第三方平台执行 build,直接观察错误信息细节,这才开始认识了本文主角:Travis CI。
Travis CI 是个提供持续集成的服务平台,对 GitHub 开源项目免费,它可以自定义配置编译、测试到发布全套流程。如果只用来观察编译的日志,实在太浪费了。
在此之前,本站项目推送到 GitHub 之前,都会先干两件事:
- 检查每篇文章的最后更新时间
- 生成所有 Category 和 Tag 的独立页面
第一点「文章更新时间」可以用第三方 Jekyll 插件实现。直接推送 Jekyll 项目源码到仓库根目录,是由 GitHub Pages 执行构建,但它启用了 --safe
选项,第三方的 Jekyll Plugins 是不允许运行的,所以笔者写了 Python 脚本去完成这两个初始化工作,每次写完文章执行 commit 之后,本地运行一下初始化脚本,然后 push 到远端。初始化脚本会对本地硬盘进行 I/O 操作,理论上会损耗硬件寿命:Mac 价格逐年创新,必须加倍疼爱手上的机器。现在看来,将这两任项务交给 Travis CI,那是再合适不过了。
注:Travis CI 只对 GitHub 上的项目提供 CI 服务,其他代码托管平台只能移步。
经过进一步的考究,GitHub Pages 支持静态文件托管,因此可以把站点构建生成的静态站点文件(_site
的内容)放到一个空白仓库中,由它开启 Pages 服务即可。站点构建托付给 Travis CI,绕开 GitHub Pages,这意味着可以任意使用第三方 Jekyll Plugins。
最后,项目源码和发布的静态资源分成两个仓库管理,最初的代码库仓库 USER.github.io
可以不受约束改成自己喜欢的名字,而且代码库的 master
分支也解放出来了(GitHub Pages 强制占用 User and Organization Pages sites 的 master
分支)。
所以改造工程将包含:
- 项目源码和项目部署分仓
- 源码仓库集成 Travis CI
- GitHub 授权
- Travis CI 开启服务
GitHub 分仓
Step 1. 在 GitHub 上把 USER.github.io
仓库重命名为 USER-blog
,用于存放源码。
Step 2. 本地 Git 配置要做更改:
1
2
3
$ cd /path/to/repository
$ git remote set-url origin git@github.com:USER/USER-blog.git
$ git remote set-url --push origin git@github.com:USER/USER-blog.git
上述命令把本地 git 的 fetch
和 push
远程地址修改为新库地址,USER
为 GitHub 用户名。
Step 3. GitHub 新建仓库 USER.github.io
,没有错,替代 Step 1 的旧仓库名称,用于存放构建输出的静态文件。切记为这个仓库开启 Pages 服务。
下面详细介绍源码仓库 USER-blog
接入 Travis CI 的步骤。
Jekyll 配置
Gemfile
因为在 CI 中会用 Bundler
编译项目,所以要确保项目根目录包含一个 Gemfile
文件,没有则新建,首行内容如下:
1
source "https://rubygems.org"
项目中依赖的 Jekyll Plugins,在 Gemfile
中添加依赖声明:
1
2
3
4
5
group :jekyll_plugins do
gem "plugin-a"
gem "plugin-b"
# ...
end
注: 如果插件没有声明在
:jekyll_plugins
分组内,则需要在_config.yml
的plugins
数组再次引用,即:
1 2 3 4 5 6 # Gemfile gem "plugin-x" # _config.yml plugins: - plugin-x
笔者 Gemfile 全部配置如下:
1
2
3
4
5
6
7
8
9
10
11
source "https://rubygems.org"
group :jekyll_plugins do
gem "jekyll-paginate"
gem "jekyll-feed", "~> 0.6"
gem "jekyll-redirect-from"
gem "jekyll-last-modified-at" # 3rd-party plugins
end
# Windows does not include zoneinfo files, so bundle the tzinfo-data gem
gem 'tzinfo-data', platforms: [:mingw, :mswin, :x64_mingw, :jruby]
_config.yml
_config.yml 中若没有显式声明 exclude
,以下文件列表会从默认构建中略过:
1
2
3
4
5
6
7
8
exclude:
- Gemfile
- Gemfile.lock
- node_modules
- vendor/bundle/
- vendor/cache/
- vendor/gems/
- vendor/ruby/
但是如果自定义了 exclude
覆盖默认列表,则确保要包含:
1
exclude: vendor
因为 Travis 容器默认构建命令为:
1
$ bundle install --deployment # some other args ...
其中 --deployment
参数会将 gems 包存放在当前路径的 vendor
目录之下。
另外,自定义的 exclude
列表,也应包含对站点无用的其他工程文件:
1
2
3
4
exclude:
- Gemfile
- Gemfile.lock
- README.md
.travis.yml
项目根目录建一个文件,命名为 .travis.yml
,有了它 Travis 才会自动对项目进行抓取编译等工作。笔者配置如下:
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
os: linux
dist: bionic
language: ruby
rvm: 2.6
cache: Bundler # speed up the build by using bundler cache
before_install:
- pip install --upgrade pip # Travis CI asked for it.
- pip install ruamel.yaml
script: bash ./scripts/cibuild.sh
branches:
only: master
notifications:
email:
recipients:
- secure: ENCRYPT_EMAIL_ADDRESS_STRING
on_success: never
env:
global:
- secure: ENCRYPT_KEY_VALUE_PAIRE
上述配置首先 language
和 rvm
描述了 Travis 虚拟机中的主语言环境。
接着执行流程是:install
→ script
。
install
部分安装了 Jekyll 环境和后面脚本需要的 Python 依赖。
script
声明了 Travis 应该执行命令。
特别说明一下 env.global
的内容,里面是一个 Travis 客户端加密后的 GitHub Token 键值对。cibuild.sh
中的向 GitHub 推送代码需要用这个 token 来通过 GitHub 的权限认证。
GitHub Token
GitHub Token 申请可访问设置页面新建,Select scopes 部分选取 repo
的全部权限。
这个 token 十分重要,如果不想让人在自己仓库涂鸦,须谨慎保存。Travis Web 提供Envrionmen 变量的加密存储,为了减少网上泄漏的风险,还是建议自己本地加密,再将加密串写入 .travis.yml
。
本地安装 Travis Client,执行加密:
1
2
$ cd /path/to/repository
$ travis encrypt GITHUB_TOKEN=<your-github-access-token>
注: 上述命令命令末端加
--add
指令,加密结果会自动添加到.travis.yml
文件末尾的env.global
。
将加密结果拷贝到 .travis.yml
的 env.global
下,用 secure
表明是一个加密字符串。笔者用这个方式还加密了 build 结果通知的邮件地址。
Coding Token(可选)
如果想同时部署到 Coding Pages,则还要建立一个 Coding 的 Token,官方称作「访问牌令」,创建入口在 Coding 的个人账户即可找到:
界面几乎是 GitHub 的中文版,选择权限 部分选取 project:depot。
Travis 加密
获得的 token 同样也是安全敏感数据,采用本地 Travis-Cli 加密,存入 env.global
即可:
1
2
3
4
env:
global:
- secure: "yHH2GnrRs6aUmqu4t7dRV8L..." # GitHub token
- secure: "RTg3SeugMzxrkzfLBU8zHpa..." # Coding token
cibuild.sh
script
部分执行脚本 cibuild.sh
,Travis 构建和部署的核心逻辑就定义在此。内容如下:
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
#!/bin/bash
GITHUB_DEPOLY=https://${GITHUB_TOKEN}@github.com/cotes2020/cotes2020.github.io.git
CODING_DEPOLY=https://cotes:${CODING_TOKEN}@git.dev.tencent.com/cotes/cotes.coding.me.git
# skip if build is triggered by pull request
if [ $TRAVIS_PULL_REQUEST != "false" ]; then
echo "this is PR, exiting"
exit 0
fi
# enable error reporting to the console
set -e
if [ -d "_site" ]; then
rm -rf _site
fi
## Check lasmod of posts and the Category, Tag pages.
export TZ='Asia/Shanghai' # the lastmod detection needs this
echo "date: $(date)"
python ./scripts/pages_generator.py
# build Jekyll ouput to directory ./_site
JEKYLL_ENV=production bundle exec jekyll build
## Git settings
git config --global user.email "travis@travis-ci.org"
git config --global user.name "Travis-CI"
DEPOLYS=(${GITHUB_DEPOLY} ${CODING_DEPOLY})
for i in "${!DEPOLYS[@]}"
do
echo "TRAVIS_BUILD_DIR=${TRAVIS_BUILD_DIR}"
cd ${TRAVIS_BUILD_DIR}
if [ -d "../repos_${i}" ]; then
rm -rf ../repos_${i}
fi
git clone --depth=1 ${DEPOLYS[${i}]} ../repos_${i}
rm -rf ../repos_${i}/* && cp -a _site/* ../repos_${i}/
cd ../repos_${i}/
git add -A
git commit -m "Travis-CI automated deployment #${TRAVIS_BUILD_NUMBER}."
git push ${DEPOLYS[${i}]} master:master
echo "Push to ${DEPOLYS[${i}]}"
done
Bash 脚本执行了笔者用 Python 实现的 Category 和 Tag 页面生成工具,然后执行 Jekyll 构建。
要注意的一点是,构建需要声明 Jekyll 环境变量JEKYLL_ENV=production
,因为它关系到<head>
加载 Google Analytic 埋线的逻辑:
1
2
3
{% if jekyll.environment == 'production' %}
{% include google-analytics.html %}
{% endif %}
成功后把输出的静态文件部署到 USER.github.io
和 USER.coding.me
两个远程仓库。每次 commit message 用 Travis CI 自动生成的构建号 ${TRAVIS_BUILD_NUMBER}
标识。
Travis CI 官网配置
Travis CI 官网只需要用 GitHub 账号授权登陆就行了,仓库管理页面开启选定的仓库,Travis CI 就会开始监听仓库。每次往仓库推送代码,都会触发自动构建、部署:
点击 build|passing
的徽章,还可以生成指定格式的状态图标链接,如 Markdown 格式:
生成地址拷贝到项目 README,可以方便的看到项目构建状态。Travis CI 的 Web 界面上有很多配置小细节,根据个人业务需求作个性化选择。
现在,整个接入工作完毕,从今以后,便可尽情地往源码仓库提交更新,Travis CI 会在背后矜矜业业的工作,构建错误会邮件通知,否则,就会自动部署最新的内容到 Pages 服务。一切悄无声息的进行,岂不美哉。
CI 测试
通常在编写 CI 自动化脚本时,免不了经历开发阶段的测试。本地没法模拟 Travis CI 线上虚拟服务的环境,只能把测试阶段的脚本提交到 Travis CI,这会产生过多无意义的 build 记录。Travis CI 只支持清空 build 内部的输出日志,却不能完全删除某个 build 记录,所以这显然是不太稳妥的选择。
其实可以用一个小技巧避开这种烦恼,首先在 GitHub 上创建工作仓库的一个副本,例如正在开发的项目为 AwesomeProj
,那么可以先创建一个副本仓库,命名为 AwesomProj-CI
,然后在这个项目上面集成 Travis CI 服务,集成期间可以尽意地推送到 Travis CI 上测试效果,直至脚本和配置成熟了,再拷贝到原始仓库 AwesomeProj
,然后删除临时仓库 AwesomProj-CI
即可,Travis CI 服务器就会自动清掉临时项目的所有记录,而 AwesomeProj
项目在 Travis CI 上没有任何测试性的 build 记录,纯洁美观。
片尾花絮
修改 Git 远程地址对 Travis 加密的影响
假设你在 Git 仓库调用过 Travis 本地加密命令 travis encrypt
,那么仓库的 .git/config
就会被写入变量 travis.slug
,内容是:
1
username/repository
username
为用户名,repository
为仓库名。
本地修改 Git 远程地址后,上述 travis.slug
的值是不会改变的。若此时再调用 Travis CLI 加密,Travis CI 服务端就不能正确解密。 正确的做法应该是:将 username
和 repository
更改成于新地址一致的内容,再执行 travis encrypt
:
1
$ git config travis.slug new-username/new-repository
Travis 的两个站点
请注意 Travis CI 有两个官网:
org
域名是对开源项目提供服务的,com
域名是对私有项目提供服务。
笔者之前用同一个 GitHub 账号在两个域名都关联了一次。结果悲催了:每次 git push,就会收到 Travis 两个不同 build number 的邮件通知,org
域名的 “passing”,com
域名的 “failling”,因为没发现域名的不同,这个乌龙问题困扰了笔者两三天,后来在 GitHub 取消对 com
域名授权就解决了。