Post

Jekyll 集成 Travis CI

Jekyll 集成 Travis CI

前段时间,因为本地 git push -f 覆盖远端 master 分支,导致博客在 GitHub Pages 上编译失败。查看 GitHub Help 的文档 “Viewing Jekyll build error messages”,文中提及可以通过第三方平台执行 build,直接观察错误信息细节,这才开始认识了本文主角:Travis CI

Travis CI 是个提供持续集成的服务平台,对 GitHub 开源项目免费,它可以自定义配置编译、测试到发布全套流程。如果只用来观察编译的日志,实在太浪费了。

在此之前,本站项目推送到 GitHub 之前,都会先干两件事:

  1. 检查每篇文章的最后更新时间
  2. 生成所有 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 sitesmaster 分支)。

所以改造工程将包含:

  • 项目源码和项目部署分仓
  • 源码仓库集成 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 的 fetchpush 远程地址修改为新库地址,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.ymlplugins 数组再次引用,即:

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

上述配置首先 languagervm 描述了 Travis 虚拟机中的主语言环境。

接着执行流程是:installscript

install 部分安装了 Jekyll 环境和后面脚本需要的 Python 依赖。

script 声明了 Travis 应该执行命令。

特别说明一下 env.global 的内容,里面是一个 Travis 客户端加密后的 GitHub Token 键值对。cibuild.sh 中的向 GitHub 推送代码需要用这个 token 来通过 GitHub 的权限认证。

GitHub Token

GitHub Token 申请可访问设置页面新建,Select scopes 部分选取 repo 的全部权限。

github-token-scopos

这个 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.ymlenv.global 下,用 secure 表明是一个加密字符串。笔者用这个方式还加密了 build 结果通知的邮件地址。

Coding Token(可选)

如果想同时部署到 Coding Pages,则还要建立一个 Coding 的 Token,官方称作「访问牌令」,创建入口在 Coding 的个人账户即可找到:

Coding-Token

界面几乎是 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.ioUSER.coding.me 两个远程仓库。每次 commit message 用 Travis CI 自动生成的构建号 ${TRAVIS_BUILD_NUMBER} 标识。

Travis CI 官网配置

Travis CI 官网只需要用 GitHub 账号授权登陆就行了,仓库管理页面开启选定的仓库,Travis CI 就会开始监听仓库。每次往仓库推送代码,都会触发自动构建、部署:

travis-builds

点击 build|passing 的徽章,还可以生成指定格式的状态图标链接,如 Markdown 格式:

travis-status-badge

生成地址拷贝到项目 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 服务端就不能正确解密。 正确的做法应该是:将 usernamerepository 更改成于新地址一致的内容,再执行 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 域名授权就解决了。

参考链接

This post is licensed under CC BY 4.0 by the author.