现在 DevOps 的理念可谓是相当火,其中 CI/CD(持续集成、持续部署)是必不可少的环节。有了它们,我们开发完软件之后,一些测试、构建、部署的环节就可以自动化完成了。

我开发的的这款分布式爬虫管理框架—— Gerapy,代码也是放在了 GitHub 上面,但在之前 GitHub 上面是缺少原生的 CI/CD 功能支持的,可能需要根据第三工具或者 Webhook 等来配合实现项目的自动测试、构建和部署。

比如我可能有这么一些需求:

•每次合并代码到 master 分支时,想测试这个项目能否在各个版本的 Python 环境下正常安装和运行。•我为 Gerapy 新建了一个独立的 Repo,叫做 Gerapy/Gerapy,在 docs 文件夹下存放文档说明,但我还另外新建了一个 Repo 专门用来存放文档,叫做 Gerapy/Docs,希望能把 Gerapy/Gerapy 的 docs 子文件夹下的内容整个自动同步到 Gerapy/Docs 这个 Repo 的根目录。•每次 Gerapy 发布新版本的时候,自动构建 Docker 镜像,并上传到 Docker Hub,打上 latest 标签和版本号标签。•每次 master 分支提交代码的时候,自动构建 Docker 镜像,并上传到 Docker Hub,打上 master 标签,代表当前 master 分支版本。

上面的功能之前有一部分工作是手工操作的,有一部分是借助于第三方工具来自动操作的,感觉并不是一个很好的解决方案

在最近一段时间,GitHub 上面上线了 Actions 功能,它就是为 CI/CD 而生的,和 GitHub 项目原生紧密结合。然而几个月以来一直处于内测阶段。就在 11 月 13 日,GitHub Actions 功能正式上线了。

上线之后,我就开始正式使用这个功能了,是真的香!

上面的四个需求,我用 GitHub Actions 已经完全实现了自动化,非常简单方便。

接下来简单介绍下我的一些实现方式。

GitHub Actions,卧槽!牛批!_GitHub

GitHub Actions

首先简单介绍下 GitHub Actions,其官方介绍页面为:https://github.com/features/actions,介绍语如下:

Automate your workflow from idea to production. GitHub Actions makes it easy to automate all your software workflows, now with world-class CI/CD. Build, test, and deploy your code right from GitHub. Make code reviews, branch management, and issue triaging work the way you want.

简而言之就是提供了一个高效易用的 CI/CD 工作流,帮助我们自动构建、测试、部署我们的代码。

GitHub Actions,卧槽!牛批!_GitHub _02 另外它支持三大平台—— Linux、MacOS、Windows,支持任何编程语言,而且官方提供了许许多多的 Actions 库供我们直接使用,帮助我们更快地搭建工作流。

GitHub Actions 的官方文档可以见:https://help.github.com/en/actions/automating-your-workflow-with-github-actions,如果大家想好好研究下的话,一定要好好看看。

下面我就介绍我使用 GitHub Actions 实现上文所述的四个需求的方法。

自动测试

由于我开发的 Gerapy 是一个 Python Package,因此我看重的是测试它是否可以在各个 Python 平台下安装和正常使用,于是我新建了一个 GitHub Action,它会自动在项目目录下生成一个 .github/workflows/*.yml 文件,内容如下:

name: build
on: 
  push:
    branches: 
    - master
    - dev
jobs:
  test:
    runs-on: 
    - ubuntu-latest
    strategy:
      max-parallel: 3
      matrix:
        python-version: [3.5, 3.6, 3.7]
    steps:
    - uses: actions/checkout@v1
    - name: Set up Python ${{ matrix.python-version }}
      uses: actions/setup-python@v1
      with:
        python-version: ${{ matrix.python-version }}
    - name: Install Dependencies
      run: |
        python -m pip install --upgrade pip
        pip install .
    - name: Run Gerapy
      run: |
        gerapy -v
        gerapy init
        cd gerapy
        gerapy migrate
        gerapy initadmin

其实在这里一个 Action 就是一个 YAML 文件,其后缀为 yml,它规定了一系列语法规则,我们根据它的语法规则写出一些工作流,在符合一定条件时,这些工作流会被触发,自动执行。

比如这里最开头,on 就是监听某个事件,其内容为 push,意思就是当 push 代码的时候,就会触发。再进一步地,这里定义了两个分支 master 和 dev。这什么意思呢?就是当我往 master 或者 dev 分支 push 代码的时候,我们定义的工作流就会执行。

下面的 jobs 就是工作流的定义了,包括在什么平台运行,具体执行什么步骤。

比如这里 runs-on 我就定义了在 ubuntu-latest 版本上运行,另外定义了一些并行策略和参数,比如这里就定义了 Python 的三个版本参数,在 3.5、3.6、3.7 版本上运行。

GitHub Actions,卧槽!牛批!_GitHub _03

下面的 steps 就是具体执行哪些步骤了。第一步和第二步,我们可以看到它都有一个 uses 参数,内容都为 actions 开头,这就说明我们使用了 GitHub 提供的写好的 Action,我们只需要引用它的名字就能使用了。这两步运行完毕之后,Python 环境会被初始化,同时会从 GitHub Clone Gerapy 项目代码到本地。

在第三步和第四步,就是我自定义的 Task 了,这里自持直接写入 Shell 脚本。在这里我分了两步。

第三步 Install Dependencies 就是安装 pip 和 Gerapy 安装包,其中一句 pip install . 就是安装当前 Gerapy 目录下的内容到系统中,安装完成之后,就可以使用 gerapy 命令了。

于是第四步 Run Gerapy 就是测试了 gerapy 命令的一些初始化使用,包括初始化工作环境、数据库迁移、初始化账号等等,当然还有更多,比如运行某些测试,运行服务等等,这里我只把一些必要的内容写进去了。

好,基本内容就是这样。

保存这个 Action,命名为 build.yml,它会保存为 .github/workflows/build.yml 文件。同时在保存的时候,我们就相当于执行了一次 Push 任务,这时候我们就可以看到这个 Action 已经启动了,页面如下:

GitHub Actions,卧槽!牛批!_GitHub _04

我们所定义的每一个步骤以及对应的执行结果都会显示在控制台中,一目了然。

可以看到这里初始化了三个版本的 Python 环境,同时都运行了其中的测试流程。如果测试成功,会打绿色的勾,如果失败,会提示红色的叉,并有邮件提示。

这样以来,一些自动化的测试就完成了!!!

GitHub Actions,卧槽!牛批!_GitHub _05

同步文档到新的 Repo

接下来我这个需求可以说稍微有点奇葩了。

写项目免不了的要写文档,这里文档我是用 Sphinx 来写的,可以借助于 ReadTheDocs 自动构建并分发到 readthedocs.io 上面,类似这样子:

GitHub Actions,卧槽!牛批!_GitHub _06

但文档的源代码我是放在了 Gerapy/Gerapy 这个 Repo 的 docs 文件夹,向 Scrapy 看齐,是这样子的:

GitHub Actions,卧槽!牛批!_GitHub _07

但我想着还新建一个 Repo,来单独存放文档,比如我新建一个 Gerapy/Docs 这个 Repo,我在 Gerapy/Gerapy docs 子文件夹下的内容可以被自动同步到 Gerapy/Docs 根目录下面,这样我只需要往 Gerapy/Gerapy 上面提交代码,docs 子文件夹下面的内容变了,Gerapy/Docs 下面的内容也会跟着变。

那这个能不能做到呢?能!(我问你答,快乐神仙;自问自答,法力无边~~

这个流程可以分为四步:

•下载 Gerapy/Gerapy Repo 的源代码。•利用 git 的 subtree 命令将 docs 文件夹下的内容分离到新的分支。•将新分离的分支推送到 Docs 这个 Repo 下面。•推送 Docs 这个 Repo 到远程 Gerapy/Docs Repo。

这里面就有一个关键地方,那就是怎样无需密码将内容推送到远程 Gerapy/Docs 这个 Repo 下面,当然就是 SSH 了。(啊,超爽der)

那 SSH 的话应该怎么设置呢?我们首先要有一对公钥和私钥,这个我们用 ssh-keygen 命令自己生成就好了。

那接下来 Gerapy/Docs 里面需要存有公钥,怎么办呢?我们可以借助于 GitHub 提供的 Deploy Key 配置好公钥即可:

GitHub Actions,卧槽!牛批!_GitHub _08 然后我们需要将私钥上传到 Action 所运行的虚拟机里面,但我们又不能明文将其放在 yml 文件里面,那这个怎么做到呢? 只需要将其配置到 Secrets 里面即可,Action 是有权限访问到的: GitHub Actions,卧槽!牛批!_GitHub _09

嗯,做好这两部分工作之后,接下来完善一下 yml 文件就好了,内容如下:

name: sync docs
on: 
  push:
    branches: 
    - master
jobs:
  sync:
    runs-on: ubuntu-latest
    steps:
    - name: Set SSH Environment
      env:
        DOCS_DEPLOY_KEY: ${{ secrets.DOCS_DEPLOY_KEY }}
      run: |
        mkdir -p ~/.ssh/
        echo "$DOCS_DEPLOY_KEY" > ~/.ssh/id_rsa
        chmod 600 ~/.ssh/id_rsa
        ssh-keyscan github.com > ~/.ssh/known_hosts
        chmod 700 ~/.ssh && chmod 600 ~/.ssh/*
        git config --global user.email "cqc@cuiqingcai.com"
        git config --global user.name "Germey"
    - name: Sync Docs of Gerapy
      run: |
        cd /tmp
        git clone git@github.com:Gerapy/Docs.git docs
        cd docs
        git branch -D docs || true
        git push origin --delete docs || true
        git clone https://github.com/Gerapy/Gerapy.git gerapy
        cd gerapy
        git subtree split --prefix=docs --squash -b docs
        git checkout docs
        git push /tmp/docs docs:docs
        cd /tmp/docs
        git checkout docs
        git checkout -b master || git checkout master || true
        git reset --hard docs
        git push origin master --force

可以看到,这里主要就分了两步。

第一部分就是设置虚拟机的 SSH 环境,这里 secrets.DOCS_DEPLOY_KEY 就是我们刚才在 Secrets 里面定义的私钥,对应的运行命令就是将私钥添加到 ~/.ssh/id_rsa 里面。

第二部分就是分离 docs 文件夹到新的分支,然后将其上传到新的 Repo 下了。

GitHub Actions,卧槽!牛批!_GitHub _10

那么这里有两条比较关键的命令:

git subtree split --prefix=docs --squash -b docs

这条命令就是将 docs 文件夹的内容分离到一个新的分支的根目录下,新的分支的名称为 docs。

git push /tmp/docs docs:docs

这条命令就是将本地的分支推送到另外一个本地 Repo 下,注意这里 push 的目标不一定是远端的 Repo 地址,也可以是本地的 Repo 地址。

最后,将新的 Repo 内容强制推送到远程即可。

这样我们就可以实现,Gerapy/Gerapy Repo docs 文件夹下内容的变动,会自动更新到 Gerapy/Docs Repo 了。

例如 docs 下是这样的:

GitHub Actions,卧槽!牛批!_GitHub _11

Gerapy/Docs Repo 下和子文件的内容会一直维持同步,并在 master 分支上面:

GitHub Actions,卧槽!牛批!_GitHub _12

自动构建 Docker 镜像

由于 Gerapy 是一个 Web 工程,所以它非常适合于打包一个 Docker 镜像。对于 Docker 的镜像,我期望有三个版本:

•当前 master 分支的版本,比较稳定,但未发布版本。•最新版本,latest,代表最新的发布版本。•每个历史版本,每次发布版本的版本号,都标记一个 tag。

最后我们自动构建的镜像都自动 Push 到 Docker Hub 上面,这样大家都可以使用了。

那这个怎么做到呢,同样借助于 GitHub Action 也可以轻松做到。

首先 master 版本,由于没有发版,所以前端需要自行 build,然后 Python Package 需要安装本地代码。废话不多说了,上代码:

name: build docker image master
on:
  push:
    branches: 
    - master
    paths:
    - .github/workflows/**
    - gerapy/**
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
    - name: Checkout Source
      uses: actions/checkout@v1
    - name: Docker Login
      run: docker login -u germey -p ${{ secrets.DOCKERHUB_LOGIN_PASSWORD }}
    - name: Setup Node.js
      uses: actions/setup-node@v1.1.0
      with:
        version: 10.x
    - name: Build Frontend Source
      run: |
        cd gerapy/client
        npm install
        npm run build
    - name: Build the Docker Image
      run: |
        docker build -t germey/gerapy:master -f ./docker/Dockerfile .
    - name: Push the Docker Image
      run: docker push germey/gerapy:master

可以看到这里,监听了 master 分支的变动,同时限定了路径 workflows 文件夹和 gerapy 文件夹下变动。

流程包括了前端的构建和 Docker 的打包,Docker 打包的时候使用了 -f 命令指定了 Dockerfile 的路径,并将打包完成之后的镜像标记为 gerapy:master,推送到 Docker Hub 即可。

对于发布新版本的时候,则直接监听 tag 的变动即可:

name: build docker image release
on:
  push:
    tags:
      - 'v*.*.*'
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
    - name: Checkout Source
      uses: actions/checkout@v1
    - name: Docker Login
      run: docker login -u germey -p ${{ secrets.DOCKERHUB_LOGIN_PASSWORD }}
    - name: Setup Node.js
      uses: actions/setup-node@v1.1.0
      with:
        version: 10.x
    - name: Build Frontend Source
      run: |
        cd gerapy/client
        npm install
        npm run build
    - name: Build and Push the Docker Image
      run: |
        tag=${GITHUB_REF:11}
        echo "Build Tag '$tag'"
        docker build -t germey/gerapy:$tag -f ./docker/Dockerfile .
        docker push germey/gerapy:$tag
        regex='^([0-9]+\.){0,2}(\*|[0-9]+)$'
        if [[ $tag =~ $regex ]]; then
          echo "Build Stable Version '$tag'"
          docker tag germey/gerapy:$tag germey/gerapy:latest
          docker push germey/gerapy:latest
        fi

可以看到这里监听的配置改成了 tags,tag 也变成了一个变量,可以通过 ${GITHUB_REF:11} 获取到。

同时这里还加了一个正则判断是不是正式的发版,如果是 beta、rc 版本,则不构建正式 latest 的 Docker 镜像。

最后我们看看我再一次发版之后,构建完成之后,Docker Hub 的效果:

GitHub Actions,卧槽!牛批!_GitHub _13

可以看到,我发布了 0.9.2 版本之后,它就自动构建了 0.9.2 版本的镜像,同时将 latest 镜像指向 0.9.2 版本。另外对应 maser 版本也构建了一个版本。

这样,以后妈妈再也不用担心我忘记打 Docker 镜像啦。

以上便是我将 GitHub Actions 应用到我的开源项目上的记录。

GitHub Actions,卧槽!牛批!_GitHub _14

 GitHub 地址为:https://github.com/Gerapy/Gerapy,文档:https://docs.gerapy.com/。


 

GitHub Actions,卧槽!牛批!_GitHub _15


Windows效率神器

变态:来瞧瞧阿里一面都面些什么(笔试+机试)

Python 中各种下划线的骚操作:_、_xx、xx_、__xx、__xx__、_classname_