最近CI/CD用的比较频繁,因为项目在github上开源,自然而然用到github的CI github action。
什么是CI/CD?
字面意思就是持续集成Continuous Intergation/持续分发Continuous Delivery持续部署Continuous Deployment,网上有太多同质的解释都太过于晦涩,我就按照使用的体验来重新解释下这个问题。在回答这个问题之前,先来了解下目前开发面临的问题。
首先开发工作不是一个人完成的任务,ok如果你真的是一个人开发,那也就意味着你无需CI/CD的工具了。所以首先CI/CD的出现是解决团队协作的问题。
那解决团队协作什么问题呢?
提到CI/CD就离不开一个词叫流水线。流水线上每个人的工作是分工明确的,而且工程是有先后顺序,就像造一台车,先造零件,然后组装、喷涂、内饰最后还要测试。软件开发同样如此,有前期的设计,单元的开发,产品的测试,优化迭代等都由不同的人负责。
CI/CD另一个作用就是解决团队代码中的冲突或错误。你提交的代码是否会和其他人的有冲突,如果有就不准你提交,回去再和其他哥们儿商量下。同样这一点也能解决你上传BUG代码的问题,同样禁止你。
如果要做的这几点那就一定需要一个中心化的工厂而且有另一个高效的团队来管理这个工厂的正常运行。CI/CD背后的提供商就是这样的工厂,而为了保证“持续”这个词,任何人都不可能24小时不睡觉帮你在工厂审核你的代码,所以自动化也是CI/CD的一个关键,自动触发机制使得以上所有的操作都是自动化完成的。所以这个工厂其实是无人化的工厂。
说到底CI/CD并不是一项新的技术,而是一种行业的标准,任何人在其上工作都必须遵守的标准,正因有这样的标准化流程,才能让团队的每一个人都能在自己的岗位上有效输出。说白了也就是领导团队想方设法提高员工效率榨干每一分钟的防摸鱼工具。(笑)
确实是这样,CI/CD的标准化工具的出现使得从开发到部署的周期大幅度缩短,再配上云开发的代码仓,那简直就是:云仓CI,法力无边。Github Action出现了。
Github本来就是一个协同工作的平台,而Action在此基础上加强了自动化部分,这正是之前所说的自动化工厂的模型。其他类似的还有如微软的DevOps、GitLab CI、Circle CI、Travis CI等等。
如何实现自动化?
其实自动化也是你自己写的代码,平台提供了规范,自动化代码文件.yml 就必须遵循这样的规定。平台提供触发时机如在你提交时、发PR时或是定时等等方式,当达到触发条件,平台会逐一运行.yml自动化代码文件的内容来检查你提交代码,至于运行什么内容都由你自己定,github才不负责帮你写代码,但他会验证文件内容有没有语法问题是否合乎标准。
背后的工作原理大致就是Github会在运行自动化代码时创建一台VM虚拟机,然后把你的代码下载下来run一遍看看是不是符合你设定的标准或者会不会报错。没有问题一切结束后自动再清理环境。反馈结果是实时的,你可以随时查看代码运行到哪一步了或是查看在哪一步出了问题。
由于我工作的项目是在github上的,以下内容就做github Action相关的介绍。
自动化脚本文件
以下是.yml文件的示例:
name: SCRIPT_NAME
on: # 触发时机
workflow_dispatch: # 手动触发
push: # 代码提交触发
branches: # 在哪个分支
- main # 在main分支
pull_request: # PR提交时触发
jobs: # 工作流
first-job: # 工作名称
runs-on: ubuntu-latest # 运行在哪个环境中
steps: # 步骤
- name: Checkout # 步骤名
uses: actions/checkout@v3 # 使用market上的脚本包
- name: Setup node
uses: actions/setup-node@v3
with: # 设定参数
node-version: 16.13.x
cache: npm
- name: Install
run: npm ci # 运行脚本命令
yaml文件的语法像python一样,是使用缩进来代表从属关系,如像了解更多关于yaml,请访问以下网站:
Github Action
GitHub Actions文档 - GitHub Docs
GitHub Actions 是一个持续集成和持续交付 (CI/CD) 平台,可用于自动执行构建、测试和部署管道。 您可以创建工作流程来构建和测试存储库的每个拉取请求,或将合并的拉取请求部署到生产环境。
GitHub Actions 不仅仅是 DevOps,还允许您在存储库中发生其他事件时运行工作流程。 例如,您可以运行工作流程,以便在有人在您的存储库中创建新问题时自动添加相应的标签。
GitHub 提供 Linux、Windows 和 macOS 虚拟机来运行工作流程,或者您可以在自己的数据中心或云基础架构中托管自己的自托管运行器。
单独一个.yml脚本文件称之为run,run脚本中的流程这里叫jobs,在jobs中有起了名字的每一个小的job,在每一个job中还有steps,steps中还有step,所以step是你能够做的最小的一个action。比如我有个run的文件叫"把大象放入冰箱.yml"
name: 'put elephone in to fridge'
on: [push, pull_request]
jobs:
prepareEle: # first job
runs-on: ubuntu-latest
steps:
- name: go to zoo
run: "echo 'find elephant in the zoo'"
- name: get ele
run: "echo 'elephant is ready'"
prepareFridge: # second job
runs-on: ubuntu-latest
steps:
- name: buy fridge
run: "echo 'go to buy fridge'"
- name: standing by
run: "echo 'fridge is on power, temperature is cool'"
putEleIn: #thirid job
runs-on: ubuntu-latest
steps:
- name: open the fridge
run: "echo 'fridge is open'"
- name: put ele in
run: "echo 'ele in'"
- name: close the door
run: "echo 'door close, finish!'"
文件上传到什么位置呢,这里github其实是有要求的,只有在 .github/workflows/ 文件夹中的yaml文件,github action才会执行。
那创建仓库应该是第一步要做的工作,之后在本地写好代码,使用git同步到github上去。关于git简单的操作和如何从0开始搭建本地环境,可以参考我之前的文章:
为最大化利用重复的代码,在github marketplace 中提供了数不清的Action功能可供你选择,这也是Action的优势所在。这样的脚本文件是你可以自己编写并上传到marketplace的,人人皆可用。即使你没有上传到marketplace,只要是public的repo,只需owner name和repo name也可使用。放入market能让更多人知道。
如果是第一次提交代码,那么就用以下命令上传到云仓中,前提是你已经在github上建了个仓库。
git init
git add .
git commit -m 'first commit'
git remote add origin https://github.com/{owner}/{repo}.git
git push -u origin master
打开Github action,由于以上脚本出发条件是push,所以在我上传代码时,这个脚本会自动运行一次。可以看到以下情况
当状态变成绿色的✅时,就说明脚本运行完成,如果是红色的❌那说明哪里出了问题,可以进一步点击这条run信息查看详细。
在URL上包含这条run的run_id,左侧的job栏中对应你运行的jobs中的每一项任务,右侧还有运行时间。每一条job也可再单独点击打开。
URL中的ID是job_id,和每一个job一一对应,我们可以看到当VM运行脚本的所以详细信息都在里面。
或许我做的以上脚本看起来没意义,但这是最简单的展示了。其实它的能力和你的想象力成正比。另一个常用的方式是把某一个仓库的代码clone下来,然后安装环境,并执行测试代码,如果测试不通过,直接代码报错。这就是CI的代码审核自动化。
获取Github Action job_id
项目需求
以下是我做的一个实际的功能,获取action的job_id
项目在:ayachensiyuan/get-action-job-id (github.com)
起初的原因是我有个unit test 的自动化脚本,在每次PR提交时会run一遍unit test,而结果会自动以邮件的形式告知相关开发人员,前面demo中看到job_id是出现在url中,所以直接拼接并可以点击直接定位到某条job中,提供极大的便利。
很遗憾在开发过程中要拿这个job_id遇到了好多困难,因为github action没有提供一个现有的api获取job_id,上下文中有个github.job,但那其实是string类型的job name,而我想要的是在URL中出现的一串nubmer类型的值。
项目技术
Github Action 提供了很多REST API来解决各种问题,获取各样数据。我使用在Node.js环境下,用@octokit/action来调用API。所以具体项目文件如下:
文件解析
- index.js 文件包含实现的所有逻辑
- action.yml 文件是Github规定的,如果要执行自定义的Action就必须在root目录下包含名字叫action.yml的文件
- runtest.yml是测试脚本,在VM中测试这个脚本是否可行。
初始化环境
最先一定是在Github上新建一个repo,之后在本地进行开发。
mkdir get-action-job-id && cd git-action-job-id
npm init -y
touch index.js action.yml .github/workflows/runtest.yml
npm install @actions/core @actions/github @octokit/action
-
Octokit 这个类是Github官方提供的调用API的包,
-
actions包含了调用者信息的上下文
实现逻辑解析
其实github action挺搞的,我们看以下例子:第一个job指定了name属性,而第二个没有
...
jobs:
firstJob:
- name: foo
...
secondJob:
...
在显示上也有以下的区别:
气人的是这个指定的名字在他的context中是无法获取的,只有个叫job的字段,而这个字段其实是原来的名字。而且并没有叫name的字段,更没有id字段。
GET /repos/${repo.owner}/${repo.repo}/actions/runs/${run_id}/jobs
以上这个API是可以获取一整个run下面所有的jobs,这里的jobs包含了id,和那么。那我如何能获取一串jobs中当前运行的那个呢?答案就是传入当前的name值。
先设计下Action文件
# action.yml
name: 'get action job id'
description: 'get action job id'
inputs: # 输入信息
job-name:
description: 'job name'
required: true
outputs: # 输出信息
jobId:
description: 'job-id'
runs: # 运行环境
using: 'node16'
main: 'index.js' # 执行入口文件
传入name,传出id。
具体Action文件如何规范,请查阅官方文档。
// index.js
const core = require('@actions/core')
const github = require('@actions/github')
const { Octokit } = require("@octokit/action")
const { createActionAuth } = require("@octokit/auth-action")
const run = async () => {
try {
const { repo, runId: run_id } = github.context
// user input job name
const job_name = core.getInput('job-name', { required: true })
const auth = createActionAuth()
const authentication = await auth()
const octokit = new Octokit({
auth: authentication.token
})
const { data } = await octokit.request(`/repos/${repo.owner}/${repo.repo}/actions/runs/${run_id}/jobs`)
let target = ''
let count = 0
for (let job of data.jobs) {
// find current job id from the list of jobs
if (job_name == job.name) {
console.log(job.id)
count++
target = job.id
}
}
if (count == 1)
// set id to output
core.setOutput('jobId', JSON.stringify(target))
else
core.setOutput('jobId', JSON.stringify('notUniqueId'))
} catch (error) {
core.setFailed(error.message)
}
}
run()
逻辑其实很简单,用户需要传入当前job的名字,然后程序会在Jobs的一串job中找到和传入名字相同的job。当用户传入不是唯一的name,这里加了个验证只有当名字是唯一才能使用,不是唯一返回‘notUniqueId’,不然数据会错误。
为什么一定要指定name?
或许有人会说直接传入job进行比对,为什么要额外指定名字,在内部实现就直接比对原来的名字,这在大多情况下是可行的。但我发现当使用matrix时候批量工作传入的名字是一样的,这就不能确定是哪一个。所以必须要起一个唯一的名字。
测试代码
name: get job id run test
on:
push:
branches: [main, dev]
jobs:
get-job-id: # use matrix jobs
runs-on: ubuntu-latest
strategy:
matrix:
cases: ['test1', 'test2', 'test3', 'test4']
name: ${{matrix.cases}} # change SET-A-NEW-NAME
steps:
- name: set id
id: set-job-id
uses: ayachensiyuan/get-action-job-id@v1.5
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
job-name: ${{matrix.cases}} # same as name
# get id
- name: get id
run: echo "The current job id is ${{ steps.set-job-id.outputs.jobId }}"
get-job-id1: # use single job
runs-on: ubuntu-latest
name: SET-A-NEW-NAME1 # change SET-A-NEW-NAME
steps:
- name: set id
id: set-job-id
uses: ayachensiyuan/get-action-job-id@v1.5
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
job-name: SET-A-NEW-NAME1 # same as name
# get id
- name: get id
run: echo "The current job id is ${{ steps.set-job-id.outputs.jobId }}"
调用上下文中的 steps.set-job-id.outputs.jobId 获取前一步输出的id。
以上就完成了所有逻辑。
测试结果
总结
获取ID后可以拼接url可以得到这条job的具体位置如:
https://github.com/${{owner}}/${{repo}}/${{job_id}}
很好奇这个简单的job_id为何Github不在context中能直接获取,使用这种方式把Action和Github API打通从而获取context中无法获取的信息是解决复杂问题的方法。
Action真的是很有创造力的工具,以后随着项目的开展,有机会再分享更深层的有趣的用法。