Github action 这东西是好东西, 但我看了一下, 很多朋友都是停留在用的阶段, 其实偶尔也要换换口味, 自己开发一个 action, 而不是仅仅是用

简介

github actions 是 github 推出的一个工作流的工具, 目的是为了帮助我们在某些情况下主动触发仓库的动作, 从而完成 单元测试/CI/CD, 甚至包括 release,发布包管理工具等等

官方关于 actions 有关的一些仓库都在这里: https://github.com/actions , 文档在这里

github 的主语言是 js, 当然也肯定也支持 ts

另外如果对于速度需求并不高的朋友, 也可以使用 docker, 但因为 docker 安装的过程会根据镜像大小有一定的耗时, 所以不一定适用于所有朋友

如果,你对于本文章不是很感兴趣,可以参考创建 action 的文档

新建

因为我对于 js 比较不喜欢, 所以使用 ts(虽然也不是很感冒, 但是会好一点)

进入这个仓库, 然后使用image-20200907165856148按钮, 完成初始化的过程.

这里我们创建一个仓库, 这个仓库的目的是自动给 issue 打上 label

初始化后的仓库

简单介绍一下这个仓库, 有一些文件和注意事项

  • action.yml 是 action 本身的配置文件(别的项目实际就是读取这个东西来确定入口在哪里), 包括参数的配置都是这东西
  • 一个标准的 npm 项目, 指定了入口
  • src 内是主要的 ts 代码
  • ts 代码需要被编译为 js 才能使用
  • dist 内就是编译产物, git 的版本控制需要包含 dist 下的所有文件, 不然运行的时候会是老代码
  • 项目本身自带 action, 主要是 CI 这个项目的

入门

开发环境

  • vscode, 我这里是使用 vscode 进行编辑, 你请根据自己的情况
  • npm(node), 我是使用 nvm 管理的

如果你的 node 大于 12.0, 理论上不用动

clone 项目

git clone https://github.com/CaiJingLong/action_auto_label.git
cd action_auto_label
npm i

官方支持库

toolkit包含了 github 官方支持的一些库, 就不一一介绍了

  • @actions/core actions 的核心库, 会被默认包含
  • @actions/exec 如果你需要执行 cli 工具, 比如 ls, mkdir, 之类的操作, 可以用这个, 可以便利的封装过程和日志输出之类的东西
  • @actions/io glob 匹配文件, 我们都知道 ls *.sh 这样的东西, 这个*就是 glob, 而不是正则
  • @actions/github github 的封装, 这东西就包含了操作 github 本身的操作

因为本篇要操作 github, 所以我们把这个东西加入以下

npm i @actions/github

Hello world

这里要注意, ts 中不建议我们使用console.log来输出日志, 所以我们这里使用core.info方法来输出

老规矩, 先 hello world 一下.

src/main.ts

import * as core from "@actions/core";

async function run(): Promise<void> {
  try {
    core.info(`Hello world`);
  } catch (error) {
    core.setFailed(error.message);
  }
}

run();

.github/workflows/issue.yml

name: "On issue"
on:
  issue:
    types: [opened, reopened, edited]
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2
      - uses: ./

npm run all 打包, 这一步很重要, 不然 dist 不会生效, 可以考虑使用 git hooks 来做

然后是 push 代码, 接着 新建一个 issue 来触发一下

issue 报错了, 说不是合法的 event name. 好吧, 这里需要修改为 issues, 我们重新提交一下, 然后再触发它. 因为这里有 edited 可以触发, 我们修改一下 issue 的内容, 然后重新 commit

name: "On issue"
on:
  issues:
    types: [opened, reopened, edited]
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2
      - uses: ./

这次, 成功触发了 action, 并且输出了 Hello world.

action.yml 配置

前面说过, 这个文件是 action 的配置文件(或者可以叫清单文件), 其中有一些配置选项

在 actions 中可以配置参数, 以便于从外部传入, 默认的

默认的文件内容如下:

name: "Your name here" # 顾名思义, action的名字
description: "Provide a description here" # 对于action的说明
author: "Your name or organization here" # 作者名/组织名/email 之类的信息
inputs: # 参数的字典
  milliseconds: # change this # 参数名,
    required: true # 是否是必填
    description: "input description here" # 参数的说明
    default: "default value if applicable" # 默认值
runs: # 运行的环境
  using: "node12" # 运行环境为 node12
  main: "dist/index.js" # 入口文件, 就是这个东西要求我们必须编译ts为js后才可用

看过了默认文件内容后, 我们要开始尝试修改了(文档在这里), 我们通过文档得知, 有如下的配置参数

在配置中没有出现的 2 个参数

  1. outputs: 输出参数, 因为各个 action 之间其实互相是不知道的, 用这个, 可以做到约定式输出, 比如我在 actions 1 里执行了某个东西, 并将其中计算的结果放到这个参数内, 后面就可以用了, 可以简单理解为 action 的返回值
  2. branding: action 对应的徽章样式, 是在GitHub Marketplace里的样子

我们知道 runs 支持三种形式

  1. js(本篇就用的这个)
  2. composite: 复合式, 其实就是使用 linux 命令(当然如果是 macos 设备, 理论上也支持), shell 脚本
  3. Docker: 使用 docker 环境,优点就不多说了, 配置方便, 普适性较强, 缺点是没有 js 和 composite 快, 毕竟加载 docker 需要时间, 镜像越大速度越慢

inputs 有一个需要注意的点: 在 js 代码里获取的时候, 使用原名称即可, 但如果你是在 shell 里使用(composite, 或其他语言, 比如 docker 使用 c 语言或者 java 等等), 则需要通过 INPUT_<VARIABLE_NAME>的名称在环境变量里获取

简单的概念完成了, 接着我们就来实战一下

环境变量

环境变量就是你在配置自己的工作流时, 可以使用 $ENV_VAR这种方式来使用环境变量, 至于来源, 看github 默认的环境变量, 包括但不仅限于$HOME,$GITHUB_WORKSPACE之类的, 具体看官方文档

配置敏感信息的问题

我们都知道, 很多情况下, 项目有一些隐秘信息, 不能直接配置在项目内, 包括但不仅限于:

  • github token
  • 各种账号的用户名密码
  • 私钥信息
  • 各种网站的 api key,app key, secret key 等等

这时候, 就需要有一些技巧来配置它们, 并在代码中读取, 官方文档

配置

这一步是在 github 仓库的 setting 里完成的

image-20200908083801557

这里看到, 我们虽然用的是小写, 但是实际上写入的时候会是大写, 这里需要注意一下

读取

这个读取的过程并不是在 js 代码中, 而是在 yml 中配置, 配置成 inputs 的值,既然需要值, 就需要对于的预配置, 然后通过 ${{secrets.<VAR_NAME> }}的方式来获取

  1. 先定义一个选项以便于外部知道, 我们需要这个, 反应到项目中就是action.yml
name: "Auto label"
description: "Automation generate label for issues."
author: "Caijinglong"
inputs:
  user_name:
    required: true
    description: "User name"
runs:
  using: "node12"
  main: "dist/index.js"
  1. 配置 workflow: .github/workflows/issue.yml

    name: "On issue"
    on:
      issues:
        types: [opened, reopened, edited]
    jobs:
      build:
        runs-on: ubuntu-latest
        steps:
          - uses: actions/checkout@v2
          - uses: ./
            with:
              user_name: ${{ secrets.USER_NAME }}
    

测试下

import * as core from "@actions/core";

async function run(): Promise<void> {
  try {
    core.info(`Hello world`);
    const username = core.getInput("user_name");
    core.info(`Hello ${username}`);

    core.info(`username === admin : ${username === "admin"}`);
  } catch (error) {
    core.setFailed(error.message);
  }
}

run();

经常 push, 老要修改东西, 很麻烦, 简单些个推送脚本

touch push.sh
chmod +x push.sh
echo "npm run all && git add . && git commit -m 'push with shell' && git push" > push.sh

./push.sh

然后就是使用 open issue 的方式触发了

image-20200908101226112

然后, 嗯, 结果是这样的, 这里的*** 就是被’安全化’过的, 鉴于我们 admin 是手输入的, 但是'‘碰巧'‘和 secret 里配置的一样, 所以一起被打码了, 然后, 结果是 true, 说明吧, 虽然这里被打码了, 但是并不影响真实的运行结果

前面简单的入门配置都完成了, 接下来简单的实战一下

实战

本篇的 action 项目是自动根据 issue 标题决定添加 issue label

使用 github api

学习下如何使用 api, 这里使用@actions/github提供的能力

import * as github from '@actions/github'

...
core.info(`event name = ${github.context.eventName}`)

image-20200908102023621

结果就是这样

github 配置 label

先思考步骤

  1. 获取所有的 label
  2. 匹配 issue 标题, 使用正则获取开头的[]内的内容如[bug] 标题的, 自动标注 bug label, feature/feature request 之类的自动标注 feature, 有就创建, 没有就不管

核心代码:

import * as core from "@actions/core";
import * as github from "@actions/github";
import * as Webhooks from "@octokit/webhooks";

export async function run(githubToken: string): Promise<void> {
  try {
    if (github.context.eventName !== "issues") {
      core.info(
        `目前仅支持 issues 触发, 你的类型是${github.context.eventName}`
      );
      return;
    }
    core.info(`The run token = '${githubToken}'`);

    const payload = github.context
      .payload as Webhooks.EventPayloads.WebhookPayloadIssues;

    core.info(`Hello world`);
    const username = core.getInput("user_name");
    core.info(`Hello ${username}`);

    core.info(`username === admin : ${username === "admin"}`);

    core.info(`event name = ${github.context.eventName}`);

    const octokit = github.getOctokit(githubToken);

    const { owner, repo } = github.context.repo;
    const issue_number = payload.issue.number;
    const regex = /\[([^\]]+)\]/g;
    const array = regex.exec(payload.issue.title);

    core.info(
      `触发的issue : owner: ${owner}, repo = ${repo}, issue_number = ${issue_number}`
    );

    if (array == null) {
      core.info(`没有找到标签, 回复一下`);
      await octokit.issues.createComment({
        owner,
        repo,
        issue_number,
        body: `没有找到[xxx]类型的标签`,
      });
      return;
    }

    const labelName = array[1];
    core.info(`预计的标签名: labelname is = ${labelName}`);

    const allLabels = await octokit.issues.listLabelsForRepo({
      owner,
      repo,
    });

    const labelText = allLabels.data
      .map<string>((data) => {
        return data.name;
      })
      .join(",");

    core.info(`找到了一堆标签 ${labelText}`);

    let haveResult = false;

    for (const label of allLabels.data) {
      const labels = [label.name];
      if (labelName.toUpperCase() === label.name.toUpperCase()) {
        core.info("找到了标签, 标上");
        await octokit.issues.addLabels({
          owner,
          repo,
          issue_number,
          labels,
        });
        haveResult = true;
        break;
      }
    }

    if (!haveResult) {
      core.info(
        `没找到标签 ${labelName}, 回复下, 可能是新问题, 现在先短暂回复一下`
      );
      await octokit.issues.createComment({
        owner,
        repo,
        issue_number,
        body: `没有找到 ${labelName}`,
      });
    }

    core.info("run success");
  } catch (error) {
    core.error("The action run error:");
    core.error(error);
    core.setFailed(error.message);
  }
}

配置文件

name: "On issue"
on:
  issues:
    types: [opened, reopened, edited]
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2
      - uses: ./
        with:
          user_name: ${{ secrets.USER_NAME }}
          github-token: ${{ secrets.GITHUB_TOKEN }}

在编译上传后看一下

在经过调试后, 达到了预期的效果, 找到了就标记上, 没有就不标


也就是说, 在经历过这些以后, 就可以简单的达到我们的目的,后续的话, 可以根据需求扩展功能, 目前的瑕疵是, 部分功能调试起来并不方便

在实际使用时为了单元测试的方便, 可以封装的更加细一些. 比如: 把,github token, issue, repo, owner, title 等参数全部抽出去, 以便于本地测试是否真的有用

发布

写完了, 要发布了, 也就是让别人可以在 action 商店 里搜到你的作品

一般来讲有如下三个步骤

  1. 写 README
  2. 打 tag/release
  3. 发布到 action 商店里

最终的文件样式

.
├── LICENSE
├── README.md
├── __tests__/
│   └── main.test.ts
├── action.yml
├── dist/
│   ├── index.js
│   ├── index.js.map
│   ├── licenses.txt
│   └── sourcemap-register.js
├── jest.config.js
├── lib/
│   ├── handle.js
│   ├── main.js
│   └── wait.js
├── package-lock.json
├── package.json
├── push.sh*
├── src/
│   ├── handle.ts
│   ├── main.ts
│   └── wait.ts
└── tsconfig.json

编写 README

这个就不展开说了, 抄一下别人的, 然后自己随便搞搞

打 tag

直接使用 github web 端的 release 功能, 这样可以同时完成 tag 和 release 的, 一般来说, action 比较常见的是 1 位长度的 action, 我们直接打一个 v1.0.0, 然后使用者的话, 一般使用 xxx@v1 就可以了

比如最常用的 actions/checkout, 目前最新 release 版本是v2.3.2, 但是你可以直接使用@v2 来使用一样

官方说明, 使用时可以接受诸如v1 v1.0.0 commitHash, master 这样的标记, 但, 一般不建议使用@master

发布吧

网址在这, 选中你的 action, 这个名字是你定义在action.yml里的

image-20200909111541661

image-20200909111804111

提示, 需要 release, 这里就来一个 v1.0.0 吧

当公开仓库后, 就可以看到这里多了一个 release action 的选项

image-20200909120200386

然后, 如果你是第一次使用, 可能有两个额外步骤

  1. 发布的协议
  2. 要求必须开启两步验证, 我这里使用authy , 你可以使用别的任何 github 支持的工具, 具体的过程可以百度一下

image-20200909120530155

提示重名了, 我们修改一下 action.yml , 接着就可以用了

后记

本篇结合了 github 文档和模板完成了 github action 的创建, 使用, 调用的过程 仓库