CircleCI 配置 Latex 备忘录

摘要:本文记录在配置使用 CircleCI 来自动化部署 Latex 时遇到的问题,希望通过该文,能够避免犯同样的错误,同时也可以快速搭建 CircleCI 的配置部署 。

目标

在使用 Latex 撰写文章的时候,有时候需要将编译好的 PDF 发布到线上,如果需要不断更新文章的版本,就需要手动生成 PDF,然后更新线上 PDF 的版本,这个过程步骤虽然不多,但是十分繁琐,再加上如果有其他的更新操作,就会在发布新版本的时候越来越复杂。那么有什么办法可以自动化完成这个更新部署呢?

那就是持续集成 (CI),如今的持续集成绝对不仅仅是代码的持续集成,任何能使用命令自动化完成的东西都可以考虑用自动化持续集成,这将会极大程度的降低重复劳动。

虽然研究持续集成脚本的编写十分费时,有时候如果不熟悉构建的环境还需要不断的尝试才能最终达到希望的效果。但是这绝对是一劳永逸的工作。

下面本文将会使用 CircleCI 来完成这个持续集成部署的过程,同时也穿插一些 CircleCI 使用中的注意点。

CircleCI 配置基础

首先我们先熟悉一下 CircleCI 的配置方法,之前介绍过使用 Travis-CI 来部署 Hexo 博客Travis-CI 免费版只能支持 PublicRepository,而 CircleCI 是针对构建资源进行限制,所以这一点上 CircleCI 更适用于个人使用。

配置文件

不同于 Travis-CI (在根目录创建配置文件.travis.yml), CircleCI 是需要创建一个 .circleci 的文件夹,然后将 config.yml 文件放入该文件夹中来执行持续集成的。原理大同小异,通过定义 YAML 的语法来执行命令。

基础配置

首先我们可以先纵览一下整个配置文件,简单了了解下配置文件的结构,下面有详细的注释说明配置文件项目的用途。

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
# 版本号,这里推荐使用版本2,版本1近期可能会退役
version: 2
# job 是 CircleCI 的指定单元,我们在 CircleCI 的控制面板的第一个主菜单就是 Jobs。
jobs:
# 我们可以给每个 Job 进行命名, 通过这个名字,我们可以在Workflows 中指定该Job 执行
build:
# 支持使用 Docker 镜像
docker:
- image: koppor/texlive:v1.5.0
# Job 执行的步骤
steps:
# checkout source code
- checkout
# build latex
- run: xelatex cv.tex
# create dir for publish script running
- run: mkdir build && cp -r cv.pdf .circleci/latest-version.txt build/
# 需要参考该链接: https://circleci.com/docs/2.0/artifacts/
# 主要功能就是将 build 目录保存下来供下面的步骤使用,同时我们还可以在面板上看到储存的这些内容。参考下图
- store_artifacts:
path: build/
# only publish the master branch
# 这里我们可以执行 sh 脚本,通过这个脚本,我们可以完成各种各样的工作。
- run: chmod +x .circleci/gh_publish.sh
- run: if [ "$CIRCLE_BRANCH" == "master" ]; then .circleci/gh_publish.sh build/; fi
# test pdflatex compilation
- run: git clean -xdf
# - run: pdflatex cv.tex
# 定义 workflows
workflows:
version: 2
# 定义一个 workflow 并命名
cv-workflow:
# 指定 workflow 需要运行的 Jobs
jobs:
# 这个名字就是我们上面指定的 Job 名字。
- build:
# 为这个 Job 指定一个 Context,参考下一节
context: GitHub
# 指定过滤器,这里主要是过滤 Git 的 Branch,只有 master 才执行。
filters:
branches:
only: master

store_artifacts

Workflow 与 Context

CircleCI 有一个 Workflows 的概念,具体可以参考官方文档,我们这里使用 Workflows 并没有使用太多高级功能,只是用到了一个 Context 概念,个人觉得这个 Context 的理念十分方便,之前在 Travis-CI 中每个项目都要定义若干环境变量 (安全目的,尤其是一些Token),但是 CircleCI 可以在个人配置中配置一个 Context 也就是一组变量,然后这个 Context 可以在多个项目中共享。比如个人 GitHub 的用户名和邮箱这种变量就可以放到这个 Context 里面。具体配置方法如下:

1
2
3
4
5
6
7
8
9
10
workflows:
version: 2
# 定义一个 workflow 并命名
cv-workflow:
# 指定 workflow 需要运行的 Jobs
jobs:
# 这个名字就是我们上面指定的 Job 名字。
- build:
# 为这个 Job 指定一个 Context,参考下一节
context: GitHub

这里需要注意 Context 放置的位置,根据官方文档和自己的实践发现,这个 Context 好像只能放在 Workflow 中的 Job, 不能直接放在 Job 的定义中。

注意点

  1. 执行 shell 脚本之前可能需要为其加入执行权限chmod -x xxx.sh
  2. 有些指令,比如安装指令 apt-get 都只能在 run 中执行,不能在脚本中执行。

部署配置

整体思路

参考该网站的开源程序的配置,它的目标和本问题有些相似。

这里主要有两个 Repository,一个是私有,保存了 Latex 的源文件,另一个是公有,作为PDF外链存储仓库。假设分别 sourcepublish

我们的自动化部署总的来说就是:使用 source 构建 PDF 文件,然后再用一种机制更新到 publish 仓库中。

Latex 编辑

现在云端应用越来越强大,基本能像都的服务都能在云上找到相应的产品。同样 Latex 的编辑器也用,之前有两个比较不错的 ShareLatexOverleaf,现在 Overleaf 收购了 ShareLatex,目前 Overleaf V2 功能十分强大。而且它支持 GitHub 同步,这样 Overleaf 无缝融合到本自动化部署的内容更新端。

版本 Release 控制

一般情况下,我们会使用 Git 来通过 Tag 来管理发布版本,但是 Overleaf 还不支持 Tag 的同步操作,所以我想了一个方案,在 CircleCI 的配置文件夹中加入一个版本的文件 latest-version.txt,同时在发布的仓库中加入一个版本文件 version.txt。这样每次希望发布新版本的 PDF,只需要更新这个 latest-version,这个版本会大于 version,只要 latest-version 大于 version 那就发布新版本,并修改 version 为最新,等待下一次更新 latest-version

流程

  1. 使用 Overleaf 编辑 Latex,同步到 GitHub Master 分支
  2. CircleCI 触发执行
  3. 下载源码,使用 xelatex 编译,这里需要和 Overleaf 的配置一致。生成了 PDF 文件。
  4. Git 检出 public 仓库,latest-version 大于 version,那么开始更新,首先将当前最新的 PDF 替换为新版本,同时将最新的 PDF 另存为对应的版本名称,用于归档。
  5. 将 public 仓库的更新提交并推送到GitHub中。
  6. 发布的网页比如 PDF viewer,使用最新版本链接,这样不用更改网站配置,就可以同步显示最新的 PDF 了。

Bash 脚本遇到的问题

  1. 需要理清楚路径
  2. 在执行脚本的时候,可以指定参数,并且在脚本中可以获取该参数
  3. 有些命令可能需要依赖安装包,这个需要检查,否则会出错。比如一个比较版本的函数就依赖 testsort等,所以无法执行,这里只检测了版本号是不是一样。
  4. if 语句的写法一定要注意,加空格,加中括号
  5. 多定义变量

下面附上脚本源码

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
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
#!/bin/sh
# ideas used from https://gist.github.com/motemen/8595451

# abort the script if there is a non-zero error
set -e

# show where we are on the machine
pwd

# get current remote url
# remote=$(git config remote.origin.url)

# because the config.yml run the command with parameter:path, set it.
siteSource="$1"

if [ ! -d "$siteSource" ]
then
echo "Usage: $0 <site source dir>"
exit 1
fi
cd ${siteSource}
#########
# version manage: actually, we need to use git to manage the version, and use CI automatically
# to retrieve the version. But we want to use the Overleaf to edit Latex, so it cannot make
# make tag. So I decide to use the hand-made or script-made separate txt file to manage
# verion.
#########
# get the latest version from the source code
latest_version=$(cat latest-version.txt)
echo "latest version:"${latest_version}
# make a directory to put the public project
publish_folder=publish
mkdir ${publish_folder}
cd ${publish_folder}
# now lets setup a new repo so we can update the gh-pages branch
git config --global user.email "$GH_EMAIL" > /dev/null 2>&1
git config --global user.name "$GH_NAME" > /dev/null 2>&1
git init
# get current publish
git remote add --fetch origin "$GH_CV_PUBLISH"
git pull origin master
# get the latest version from the source code
current_version=$(cat version.txt)
echo "current version:"$current_version

# firstly, check the version, the latest_version should bigger than current_version.
# function version_gt() { test "$(printf '%s\n' "$@" | sort -V | head -n 1)" != "$1"; }

PDF_NAME_PREFIX="professional_cv_"
PDF_NAME_LATEST=${PDF_NAME_PREFIX}"latest.pdf"
PDF_NAME_NEW_VERSION=${PDF_NAME_PREFIX}"v"${latest_version}".pdf"
PDF_BUILD="../cv.pdf"

# compare=vercomp ${latest_version} ${current_version}
# return 1, v1 > v2
echo $current_version
echo ${latest_version}
if [ "$latest_version" != "$current_version" ] ; then
echo "Start version update"
ls -al
# update the current version
echo ${latest_version} > version.txt
# remove the current latest pdf
rm -f ${PDF_NAME_LATEST}
# update the latest and add new version pdf
cp ${PDF_BUILD} ${PDF_NAME_LATEST}
cp ${PDF_BUILD} ${PDF_NAME_NEW_VERSION}
# push
git add -A
git commit -m "update to version: "${latest_version}
git push --quiet origin master
cd ..
rm -rf ${publish_folder}
echo "Update version finish"
fi