## 入门

### 介绍

GitHub Actions 的仓库中自动化、自定义和执行软件开发工作流程，有四个基本的概念，如下：

:- | :-
--- | ---
`workflow` (工作流程) | 持续集成一次运行的过程，就是一个 `workflow`
`job` (任务) | 一个 `workflow` 由一个或多个 `jobs` 构成，含义是一次持续集成的运行，可以完成多个任务
`step` (步骤) | 每个 `job` 由多个 `step` 构成，一步步完成
`action` (动作) | 每个 `step` 可以依次执行一个或多个命令(`action`)

- 采用 YAML 格式定义配置文件
- 存放在代码仓库的 `.github/workflows` 目录中
- 后缀名统一为 `.yml`，比如 `ci.yml`
- 一个库可以有多个 `workflow` 文件
- 根据配置事件自动运行配置文件

### 配置文件

    name: GitHub Actions Demo
    on:
      push:
        branches:
          - main

    # 任务
    jobs:
      build:
        runs-on: ubuntu-latest
        # 步骤 根据步骤执行任务
        steps:
          - uses: actions/checkout@v3
          - uses: actions/setup-node@v3
            with:
              node-version: 16

          - run: npm install
          - run: npm run build

存放到 `.github/workflows` 目录中，命名为 `ci.yml`，当 `push` 代码到仓库 `main` 分支中，该配置自动运行配置。

### 指定触发

`push` 事件触发 `workflow`

    on: push

`push` 事件或 `pull_request` 事件都可以触发 `workflow`

    on: [push, pull_request]

只有在 `main` 分支 `push` 事件触发 `workflow`

    on:
      push:
        branches:
          - main

`push` 事件触发 `workflow`，`docs` 目录下的更改 `push` 事件不触发 `workflow`

    on:
      push:
        paths-ignore:
          - 'docs/**'

push 事件触发 workflow，包括 sub-project 目录或其子目录中的文件触发 workflow，除非该文件在 sub-project/docs 目录中，不触发 workflow

    on:
      push:
        paths:
          - 'sub-project/**'
          - '!sub-project/docs/**'

版本发布为 `published` 时运行工作流程。

    on:
      release:
        types: [published]

### 多项任务

    jobs:
      my_first_job:  # 第一个任务
        name: My first job

      my_second_job: # 第二个任务
        name: My second job

通过 jobs `(jobs.<job_id>.name)`字段，配置一项或多项需要执行的任务

### 多项任务依赖关系

通过 needs `(jobs.<job_id>.needs)`字段，指定当前任务的依赖关系

    jobs:
      job1:
      job2:
        needs: job1
      job3:
        needs: [job1, job2]

上面配置中，`job1` 必须先于 `job2` 完成，而 `job3` 等待 `job1` 和 `job2` 的完成才能运行。因此，这个 `workflow` 的运行顺序依次为：`job1`、`job2`、`job3`

### 多项任务传递参数

    jobs:
      job1:
        runs-on: ubuntu-latest
        # 将步骤输出映射到作业输出
        outputs:
          output1: ${{ steps.step1.outputs.test }}
          output2: ${{ steps.step2.outputs.test }}
        steps:
          - id: step1
            run: echo "::set-output name=test::hello"
          - id: step2
            run: echo "::set-output name=test::world"
      job2:
        runs-on: ubuntu-latest
        needs: job1
        steps:
          - run: echo ${{needs.job1.outputs.output1}} ${{needs.job1.outputs.output2}}

### 指定每项任务的虚拟机环境

    runs-on: ubuntu-latest

指定运行所需要的虚拟机环境，⚠️ 它是必填字段

    jobs:
      build: # 任务名称
        runs-on: ubuntu-latest # 虚拟机环境配置

- `Windows Server 2022` (windows-latest) 或 (windows-2022)
- `Ubuntu 20.04` (ubuntu-latest) 或 (ubuntu-20.04)
- `macOS Monterey 12` (macos-12)
- `macOS Big Sur 11` (macos-latest),(macos-11)

另见: 选择 GitHub 托管的运行器

### 指定每项任务的步骤

每个步骤都可以指定以下三个字段

    jobs.<job_id>.steps.name # 步骤名称
    # 该步骤运行的命令或者 action
    jobs.<job_id>.steps.run
    # 该步骤所需的环境变量
    jobs.<job_id>.steps.env

`steps` 字段指定每个 `Job` 的运行步骤，可以包含一个或多个步骤(`steps`)

    jobs:
      build:
        runs-on: ubuntu-latest
        steps:
          - uses: actions/checkout@v3
          - uses: actions/setup-node@v3
            with:
              node-version: 16

          - run: npm install
          - run: npm run build

### 环境变量

    jobs.<job_id>.environment

使用单一环境名称的示例

    environment: staging_environment

使用环境名称和 URL 的示例

    environment:
      name: production_environment
      url: https://github.com

#### 自定义环境变量

`GitHub` 会保留 `GITHUB_` 环境变量前缀供 `GitHub` 内部使用。设置有 `GITHUB_` 前缀的环境变量或密码将导致错误。

    - name: 测试 nodejs 获取环境变量
      env:
        API_TOKEN: ${{ secrets.API_TOKEN }}

在 `https://github.com/<用户名>/<项目名称>/settings/secrets` 中添加 `secrets` `API_TOKEN`，在工作流中设置环境变量 `API_TOKEN`

### 表达式

在 `if` 条件下使用表达式时，可以省略表达式语法 (`${{ }}`)，因为 GitHub 会自动将 `if` 条件作为表达式求值

    steps:
      - uses: actions/hello-world-action@v1.1
        if: github.repository == 'uiw/uiw-repo'
        # if: ${{ <expression> }}

设置环境变量的示例

    env:
      MY_ENV_VAR: ${{ <expression> }}

#### 操作符

- `( )` (逻辑分组)
- `[ ]` (指数)
- `.` (属性取消引用)
- `!` (不是)
- `<` (少于)
- `<=` (小于或等于)
- `>` (比...更棒)
- `>=` (大于或等于)
- `==` (平等的)
- `!=` (不相等)
- `&&` (和)
- `||` (或者)

### Github 上下文

属性名称 | 类型 | 描述
--- | --- | ---
`github` (object) | 工作流程中任何作业或步骤期间可用的顶层上下文。 |
`github.event` (object) | 完整事件 web 挂钩有效负载。 更多信息请参阅“触发工作流程的事件”。 |
`github.event_path` (string) | 运行器上完整事件 web 挂钩有效负载的路径。 |
`github.workflow` (string) | 工作流程的名称。 如果工作流程文件未指定 name，此属性的值将是仓库中工作流程文件的完整路径。 |
`github.job` (string) | 当前作业的 job_id。 |
`github.run_id` (string) | 仓库中每个运行的唯一编号。 如果您重新执行工作流程运行，此编号不变。 |
`github.run_number` (string) | 仓库中特定工作流程每个运行的唯一编号。 此编号从 1（对应于工作流程的第一个运行）开始，然后随着每个新的运行而递增。 如果您重新执行工作流程运行，此编号不变。 |
`github.actor` (string) | 发起工作流程运行的用户的登录名。 |
`github.repository` (string) | 所有者和仓库名称。 例如 Codertocat/Hello-World。 |
`github.repository_owner` (string) | 仓库所有者的名称。 例如 Codertocat。 |
`github.event_name` (string) | 触发工作流程运行的事件的名称。 |
`github.sha` (string) | 触发工作流程的提交 SHA。 |
`github.ref` (string) | 触发工作流程的分支或标记参考。 |
`github.head_ref` (string) | 工作流程运行中拉取请求的 head_ref 或来源分支。 此属性仅在触发工作流程运行的事件为 pull_request 时才可用。 |
`github.base_ref` (string) | 工作流程运行中拉取请求的 base_ref 或目标分支。 此属性仅在触发工作流程运行的事件为 pull_request 时才可用。 |
`github.token` (string) | 代表仓库上安装的 GitHub 应用程序进行身份验证的令牌。 这在功能上等同于 GITHUB_TOKEN 密码。 更多信息请参阅“使用 GITHUB_TOKEN 验证身份”。 |
`github.workspace` (string) | 使用 checkout 操作时步骤的默认工作目录和仓库的默认位置。 |
`github.action` (string) | 正在运行的操作的名称。 在当前步骤运行脚本时，GitHub 删除特殊字符或使用名称 run。 如果在同一作业中多次使用相同的操作，则名称将包括带有序列号的后缀。 例如，运行的第一个脚本名称为 run1，则第二个脚本将命名为 run2。 同样，actions/checkout 第二次调用时将变成 actionscheckout2。 |

Github 上下文是访问有关工作流运行、运行器环境、作业和步骤的信息的一种方式

### 默认环境变量

环境变量 | 描述
--- | ---
`CI` | 始终设置为 `true`
`HOME` | 用于存储用户数据的 GitHub 主目录路径。 例如 `/github/home`
`GITHUB_WORKFLOW` | 工作流程的名称。
`GITHUB_RUN_ID` | 仓库中每个运行的唯一编号。 如果您重新执行工作流程运行，此编号不变。
`GITHUB_RUN_NUMBER` | 仓库中特定工作流程每个运行的唯一编号。 此编号从 1（对应于工作流程的第一个运行）开始，然后随着每个新的运行而递增。 如果您重新执行工作流程运行，此编号不变。
`GITHUB_ACTION` | 操作唯一的标识符 (id)。
`GITHUB_ACTIONS` | 当 GitHub 操作 运行工作流程时，始终设置为 true。 您可以使用此变量来区分测试是在本地运行还是通过 GitHub 操作 运行。
`GITHUB_ACTION_PATH` | GitHub 操作所在的路径
`GITHUB_ACTOR` | 发起工作流程的个人或应用程序的名称。 例如 octocat
`GITHUB_API_URL` | 返回 `API URL`。例如：`https://api.github.com`
`GITHUB_REPOSITORY` | 所有者和仓库名称。 例如 octocat/Hello-World
`GITHUB_EVENT_NAME` | 触发工作流程的 web 挂钩事件的名称
`GITHUB_EVENT_PATH` | 具有完整 web 挂钩事件有效负载的文件路径。 例如 /github/workflow/event.json
`GITHUB_WORKSPACE` | GitHub 工作空间目录路径。 如果您的工作流程使用 actions/checkout 操作，工作空间目录将包含存储仓库副本的子目录。 如果不使用 actions/checkout 操作，该目录将为空。 例如 /home
`GITHUB_SHA` | 触发工作流程的提交 SHA。 例如 ffac537e6cbbf9
`GITHUB_REF` | 触发工作流程的分支或标记参考。 例如 refs/heads/feature-branch-1。 如果分支或标记都不适用于事件类型，则变量不会存在
`GITHUB_HEAD_REF` | 仅为复刻的仓库设置。头部仓库的分支
`GITHUB_BASE_REF` | 仅为复刻的仓库设置。基础仓库的分支

另见: 默认环境变量

### 直接常量

作为表达式的一部分，可以使用 `boolean`, `null`, `number` 或 `string`数据类型

    env:
      myNull: ${{ null }}
      myBoolean: ${{ false }}
      myIntegerNumber: ${{ 711 }}
      myFloatNumber: ${{ -9.2 }}
      myHexNumber: ${{ 0xff }}
      myExponentialNumber: ${{ -2.99e-2 }}
      myString: Mona the Octocat
      myStringInBraces: ${{ 'It''s source!' }}

### 函数 contains

使用字符串的示例

    contains('Hello world', 'llo') // 返回 true

使用对象过滤器的示例返回 true

    contains(github.event.issue.labels.*.name, 'bug')

另见: 函数 contains

### 函数 startsWith

    startsWith('Hello world', 'He') // 返回 true

另见: 函数 startsWith，此函数不区分大小写

### 函数 format

    format('{{Hello {0} {1} {2}!}}', 'Mona', 'the', 'Octocat')
    // 返回 '{Hello Mona the Octocat!}'.

另见: 函数 format

### 函数 join

    join(github.event.issue.labels.*.name, ', ')
    // 也许返回 'bug, help wanted'.

另见: 函数 join

### 函数 toJSON

    toJSON(job)
    // 也许返回 { "status": "Success" }.

另见: 函数 toJSON

### 函数

:- | :-
--- | ---
`fromJSON` | 返回 JSON 对象或 JSON 数据类型的值 #
`hashFiles` | 返回与路径模式匹配的文件集的单个哈希 #
`success` | 当前面的步骤都没失败或被取消时返回 true #
`always` | 使步骤始终执行，返回 `true` 即使取消也是如此 #
`cancelled` | 如果工作流被取消，则返回 true #
`failure` | 当作业的任何先前步骤失败时返回 true #

### 函数 success()

    steps:
      ...
      - name: The job has succeeded
        if: ${{ success() }}

### 函数 failure()

    steps:
      ...
      - name: The job has failed
        if: ${{ failure() }}

## 常用实例

### 获取版本信息

    - name: Test
      run: |
        # Strip git ref prefix from version
        echo "${{ github.ref }}"
        # VERSION=$(echo "${{ github.ref }}" | sed -e 's,.*/\(.*\),\1,')

        # # Strip "v" prefix from tag name
        # [[ "${{ github.ref }}" == "refs/tags/"* ]] && VERSION=$(echo $VERSION | sed -e 's/^v//')
        echo "$VERSION"

### 提交到 gh-pages 分支

    - name: Deploy
      uses: peaceiris/actions-gh-pages@v3
      with:
        github_token: ${{secrets.GITHUB_TOKEN}}
        publish_dir: ./build

### 修改 package.json

    - name: Modify Version
      shell: bash
      run: |
        node -e 'var pkg = require("./package.json"); pkg.version= (new Date().getFullYear().toString().substr(2)) + "." + (new Date().getMonth() + 1) + "." + (new Date().getDate()); require("fs").writeFileSync("./package.json", JSON.stringify(pkg, null, 2))'

使用 github-action-package 修改 `name` 字段

    - name: package.json info
      uses: jaywcjlove/github-action-package@main
      with:
        rename: '@wcj/github-package-test'

### 克隆带有 Submodule 的仓库

    - name: Checkout
      uses: actions/checkout@v3
      with:
        path: main
        submodules: true

`submodules`：`true` 检出子模块或 `recursive` 递归检出子模块

    - name: Clone sub repository
      shell: bash
      run: |
        auth_header="$(git config --local --get http.https://github.com/.extraheader)"
        # git submodule sync --recursive
        # git -c "http.extraheader=$auth_header" -c protocol.version=2 submodule update --init --remote --force --recursive --checkout ant.design

### 步骤依赖作业

使用 `jobs.<job_id>.needs` 识别在此作业运行之前必须成功完成的任何作业。它可以是一个字符串，也可以是字符串数组。 如果某个作业失败，则所有需要它的作业都会被跳过，除非这些作业使用让该作业继续的条件表达式。

    jobs:
      job1:
      job2:
        needs: job1
      job3:
        needs: [job1, job2]

在此示例中，`job1` 必须在 `job2` 开始之前成功完成，而 `job3` 要等待 `job1` 和 `job2` 完成。此示例中的作业按顺序运行：

    ❶ job1
    ❷ job2
    ❸ job3

配置如下

    jobs:
      job1:
      job2:
        needs: job1
      job3:
        if: ${{ always() }}
        needs: [job1, job2]

在此示例中，`job3` 使用 `always()` 条件表达式，因此它始终在 `job1` 和 `job2` 完成后运行，不管它们是否成功。

### 同步 Gitee

    - name: Sync to Gitee
      run: |
        mirror() {
          git clone "https://github.com/$1/$2"
          cd "$2"
          git remote add gitee "https://jaywcjlove:${{ secrets.GITEE_TOKEN }}@gitee.com/uiw/$2"
          git remote set-head origin -d
          git push gitee --prune +refs/remotes/origin/*:refs/heads/* +refs/tags/*:refs/tags/*
          cd ..
        }
        mirror uiwjs uiw

### 提交 NPM 包

    - run: npm publish --access public
      env:
        NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}}

获取 `NPM_TOKEN`，可以通过 npm 账号创建 `token`

    npm token list [--json|--parseable] # 查看
    npm token create [--read-only] [--cidr=1.1.1.1/24,2.2.2.2/16] # 创建
    npm token revoke <id|token> # 撤销

可以使用 JS-DevTools/npm-publish 提交

    - name:  📦 @province-city-china/data
      uses: JS-DevTools/npm-publish@v1
      with:
        token: ${{ secrets.NPM_TOKEN }}
        package: packages/data/package.json

它有个好处，检测 `package.json` 中版本号是否发生变更，来决定是否提交版本，不会引发流程错误。

### 步骤作业文件共享

Artifacts 是 GitHub Actions 为您提供持久文件并在运行完成后使用它们或在作业（文档）之间共享的一种方式。

要创建工件并使用它，您将需要不同的操作：上传和下载。 要上传文件或目录，您只需像这样使用它：

    steps:
      - uses: actions/checkout@v2
      - run: mkdir -p path/to/artifact
      - run: echo hello > path/to/artifact/a.txt
      - uses: actions/upload-artifact@v2
        with:
          name: my-artifact
          path: path/to/artifact/a.txt

然后下载 `artifact` 以使用它：

    steps:
      - uses: actions/checkout@v2
      - uses: actions/download-artifact@v2
        with:
          name: my-artifact

### Node.js

    - name: Setup Node
      uses: actions/setup-node@v2
      with:
        node-version: 14

使用矩阵策略 在 nodejs 不同版本中运行

    strategy:
      matrix:
        node-version: [10.x, 12.x, 14.x]

    steps:
      - uses: actions/checkout@v2
      - name: 使用 Node ${{ matrix.node-version }}
        uses: actions/setup-node@v1
        with:
          node-version: ${{ matrix.node-version }}
      - run: npm ci
      - run: npm run build --if-present
      - run: npm test

### 提交 docker 镜像

    # https://www.basefactor.com/github-actions-docker
    - name: Docker login
      run: docker login -u ${{ secrets.DOCKER_USER }} -p ${{ secrets.DOCKER_PASSWORD }}

    - name: Build ant.design image
      run: |
        cd ./ant\.design
        docker build -t ant.design .
    - name: Tags & Push docs
      run: |
        # Strip git ref prefix from version
        VERSION=$(echo "${{ github.ref }}" | sed -e 's,.*/\(.*\),\1,')

        # Strip "v" prefix from tag name
        [[ "${{ github.ref }}" == "refs/tags/"* ]] && VERSION=$(echo $VERSION | sed -e 's/^v//')

        docker tag ant.design ${{ secrets.DOCKER_USER }}/ant.design:$VERSION
        docker tag ant.design ${{ secrets.DOCKER_USER }}/ant.design:latest
        docker push ${{ secrets.DOCKER_USER }}/ant.design:$VERSION
        docker push ${{ secrets.DOCKER_USER }}/ant.design:latest

### 创建一个 tag

    - name: Create Tag
      id: create_tag
      uses: jaywcjlove/create-tag-action@main
      with:
        package-path: ./package.json

根据 `package-path` 指定的 `package.json` 检测 `version` 是否发生变化来创建 `tag`

### 生成 git 提交日志

    - name: Generate Changelog
      id: changelog
      uses: jaywcjlove/changelog-generator@main
      with:
        filter-author: (小弟调调™)

    - name: Get the changelog
      run: echo "${{ steps.changelog.outputs.changelog }}"

### 提交到 GitHub docker 镜像仓库

    - name: '登录到 GitHub 注册表'
      run: echo ${{ github.token }} | docker login ghcr.io -u ${{ github.actor }} --password-stdin

    - name: '编译 docker image'
      run: docker build -t ghcr.io/jaywcjlove/reference:latest .

    - name: '推送到 GitHub 注册表中'
      run: docker push ghcr.io/jaywcjlove/reference:latest

    - name: '标记 docker 镜像并发布到 GitHub 注册表'
      if: steps.create_tag.outputs.successful
      run: |
        echo "version: v${{ steps.changelog.outputs.version }}"
        docker tag ghcr.io/jaywcjlove/reference:latest ghcr.io/jaywcjlove/reference:${{steps.changelog.outputs.version}}
        docker push ghcr.io/jaywcjlove/reference:${{steps.changelog.outputs.version}}

### 提交 commit 到 master 分支

    - name: 生成一个文件，并将它提交到 master 分支
      run: |
        # Strip git ref prefix from version
        VERSION=$(echo "${{ github.ref }}" | sed -e 's,.*/\(.*\),\1,')
        COMMIT=released-${VERSION}
        # Strip "v" prefix from tag name
        [[ "${{ github.ref }}" == "refs/tags/"* ]] && VERSION=$(echo $VERSION | sed -e 's/^v//')
        echo "输出版本号：$VERSION"
        # 将版本输出到当前 VERSION 文件中
        echo "$VERSION" > VERSION
        echo "1. 输出Commit：$commit"
        echo "2. Released $VERSION"
        git fetch
        git config --local user.email "action@github.com"
        git config --local user.name "GitHub Action"
        git add .
        git commit -am $COMMIT
        git branch -av
        git pull origin master

    - name: 将上面的提交 push 到 master 分支
      uses: ad-m/github-push-action@master
      with:
        github_token: ${{ secrets.GITHUB_TOKEN }}

### 作业之间共享数据

创建一个文件，然后将其作为构件上传

    jobs:
      example-job:
        name: Save output
        steps:
          - shell: bash
            run: |
              expr 1 + 1 > output.log
          - name: Upload output file
            uses: actions/upload-artifact@v3
            with:
              name: output-log-file
              path: output.log

可以下载名为 `output-log-file` 的工件

    jobs:
      example-job:
        steps:
          - name: Download a single artifact
            uses: actions/download-artifact@v3
            with:
              name: output-log-file

### 指定运行命令的工作目录

    - name: Clean temp directory
      run: rm -rf *
      working-directory: ./temp

使用 `working-directory` 关键字，您可以指定运行命令的工作目录(`./temp`)

#### defaults.run

    jobs:
      job1:
        runs-on: ubuntu-latest
        defaults:
          run:
            shell: bash
            working-directory: scripts

作业中的所有 `run` 步骤提供默认的 `shell` 和 `working-directory`

### jobs.<job_id>.steps[*].shell

使用 `bash` 运行脚本

    steps:
      - name: Display the path
        run: echo $PATH
        shell: bash

运行 `python` 脚本

    steps:
      - name: Display the path
        run: |
          import os
          print(os.environ['PATH'])
        shell: python

您可以使用 `shell` 关键字覆盖运行器操作系统中的默认 `shell` 设置

### 一些 actions 推荐

:- | :-
--- | ---
`create-tag-action` | 根据 package.json 创建 `Tag` / `Release`
`changelog-generator` | 生成 `changelog` 日志
`github-action-modify-file-content` | 修改仓库文件内容
`github-action-contributors` | 生成贡献(contributors.svg)图片
`generated-badges` | 生成徽章(Badges)图片
`coverage-badges-cli` | 生成覆盖率徽章(Badges)图片
`action-ejs` | 基于 ejs 生成 HTML
`github-action-package` | 修改 JSON 文件内容
`markdown-to-html-cli` | Markdown 转换成 HTML
`ncipollo/release-action` | 创建 `Release`
`peaceiris/actions-gh-pages` | 将文件或文件夹内容提交到 `gh-pages` 分支

## 另见

- Github Actions 学习笔记 (jaywcjlove.github.io)
- 了解 GitHub Actions (docs.github.com)