前言

书接上回,我利用 GitHub Actions 开发了一个 Go 脚本用于为 Markdown 文档添加基于文件夹结构的目录。随即产生了将其单独整合为一个持续集成工具的想法,随即在 Google Keep 写下了这个想法,不过第二天就将其完成并归档了。

目前该工具发布在 Markdown auto catalog · Actions · GitHub Marketplace,使用方法简单可见链接内文档,本文记录创建该工具的过程。

调研 GitHub Action

Creating actions – GitHub Docs

在 GitHub Actions 整个文档中,只有这一章是介绍如何创建并发布自己的 GitHub Action 的,可以创建的 Action 分为三种:Docker、JS、Composite,前两种使用较广,见sdras/awesome-actions: A curated list of awesome actions to use on GitHub,加上我自己找到的一些 Actions,很多人都是使用 Docker、JS,这样他们只用编写很简单的 action.yml 文件,而将大部分操作留给熟悉的 Dockerfile 或 js 文件。

不过这次选择 Composite 方式来完成这个工具。

创建基础文件 action.yml

首先给这个项目创建一个 repo 自然不必多说,然后让这个 repo 能被识别为 GitHub Action 的关键就是在根目录创建一个 action.ymlaction.yaml 文件,并在这个文件中编写工具的基本运行步骤,Docker/JS 的文档一般是选择镜像或脚本运行即可,Composite 就要详细的步骤。

name: 'Markdown auto catalog'
author: 'minoic'
description: 'Github action automatically update folder-based table of contents in documents.'

inputs:
  content-path:
    description: 'Path of the documents to be listed in catalog.'
    required: true
  document-path: 
    description: 'Path of the catalog document.'
    required: true
  filter:
    description: 'Filename Regex filter'
    required: false
    default: '\(.*\).md'

runs:
  using: 'composite'
  steps:
    - uses: actions/checkout@v3

    - name: Set up Go
      uses: actions/setup-go@v3
      with:
        go-version: 1.18

    - name: Run
      shell: bash
      run: go run -v ${{ github.action_path }}/catalog.go -content ${{ inputs.content-path }} -doc ${{ inputs.document-path }} -filter ${{ inputs.filter }}

    - name: Commit files
      shell: bash
      run: |
        git config --local user.name "markdown-auto-catalog[bot]"
        git config --local user.email "markdown-auto-catalog[bot]@users.noreply.github.com"
        git commit -a -m 'Automated catalog update'

    - name: Create Pull Request
      uses: peter-evans/create-pull-request@v3
      with:
        title: '[Bot] Automated catalog update'
        committer: GitHub <noreply@github.com>
        author: markdown-auto-catalog[bot] <markdown-auto-catalog[bot]@users.noreply.github.com>
        branch: t/bot/markdown-auto-catalog

branding:
  icon: 'list'
  color: 'blue'

这里的文档和之前的 catalog.yml 在步骤上几乎完全相同,而又多了不少新的内容。

首先最开头的三行:

  • name:必填,工具的名称
  • author:可选,作者
  • description:必填,工具的描述

其次最后的三行:

  • branding:可选,作为发布在 Marketplace 时的图标
  • icon: 可以在 https://feathericons.com/ 选择图标名称
  • color: 图标的颜色,可选 white, yellow, blue, green, orange, red, purple, 或 gray-dark

然后是 inputs,这部分是将工具分离出来经常需要的步骤,毕竟在具体的项目中直接制作工具就可以因地制宜,而这就像将一个代码块抽象为函数一样,需要为它设置参数。

对于每个 input:

  • input_id:必填,即写在外侧的参数名称如content-path
  • description:必填,该参数的描述
  • required:可选,该参数是否必填
  • default:可选,当留空时的默认值
  • deprecationMessage:可选,当你不再推荐使用这个参数时的提示消息

相应的也有 outputs,但因为我没有用到就不介绍了捏。

最后是最主要的 runs,这部分是整个工具的主要步骤,编写起来与上一章的 workflows 略微有一些区别,且可选的参数和功能较少。我这里主要分为签出代码、安装 Go 编译器、运行脚本、提交代码、发起 PR 这几个步骤。由于我选择 Composite 模式的 Action,这里必须要有 using: "composite",然后介绍 steps 的参数:

  • run:可选,这一步要运行的指令
  • shell:可选(有 run 必填),运行指令的环境,有 bashpwshpythonshcmdpowershell 等,详见 这里
  • if:可选,判断这个步骤执行与否
  • name:可选,步骤名称
  • env:可选,环境变量
  • working-directory:可选,运行目录
  • uses:可选,选择一个 Action 来帮助你完成这个步骤
  • with:可选,这一步骤要传入的参数,一般和 uses 组合使用

以及为项目编写详细的 README.md 来介绍使用方法,选择一个合适的 LICENSE

编写 action.yml 的注意事项

1. 不要改名

虽然 action.yml、action.yaml 都可以使用,但建立这个文件(无论是哪一种),就不要再改为另一个,否则这个 repo 在发布时会找不到 Marketplace 选项!

可能会消失的功能

在处理这个离奇的问题时,我搜索到 这个Discussion,这个 Bug 在 2019 年就被提出了,居然还没有修复。

热心网友发现 Bug

2. 脚本文件前面要有地址

    - name: Run
      shell: bash
      run: go run -v ${{ github.action_path }}/catalog.go -content ${{ inputs.content-path }} -doc ${{ inputs.document-path }} -filter ${{ inputs.filter }}

这里虽然 catalog.go 在工具项目的根目录,但也不能只写一个文件名!若要访问工具项目中的文件要有一个 ${{ github.action_path }} 在前面。

通过这张图可以发现目录其实在 /home/runner/work/_actions/minoic/markdown-auto-catalog/v1.0.0/,如果没写的话会因找不到文件而报错,其它的 context 可以参考 Contexts – GitHub Docs

3. 不能跳过错误

Composite 模式的 steps 没有在 workflow 中的 continue-on-error: true 选项,因此像我的这个工具在代码没有更改时,Commit 步骤会出错而产生一个红色的❌,非常的丑陋。这时候就要通过文档告诉用户可以在 workflow 中添加跳过的选项。

脚本文件

与上次的脚本相比增加了对参数的解析、用深度优先搜索实现了多级目录支持、支持用正则表达式过滤文件,功能和实现还是非常简单的。

package main

import (
    "flag"
    "fmt"
    "io"
    "io/ioutil"
    "os"
    "path"
    "regexp"
    "strings"
)

var (
    DocumentPath string
    ContentPath  string
    Filter       string
)

func init() {
    flag.StringVar(&DocumentPath, "doc", "", "")
    flag.StringVar(&ContentPath, "content", "", "")
    flag.StringVar(&Filter, "filter", "", "")
    flag.Parse()
}

func dfs(ctl *strings.Builder, folderPath string, depth int) {
    fmt.Println(folderPath)
    gap := strings.Repeat("  ", depth)
    files, _ := ioutil.ReadDir(folderPath)
    for _, file := range files {
        if file.IsDir() {
            ctl.WriteString(fmt.Sprintf("%s- %s\n", gap, file.Name()))
            dfs(ctl, path.Join(folderPath, file.Name()), depth+1)
            continue
        }
        if ok, _ := regexp.MatchString(Filter, file.Name()); !ok {
            continue
        }
        ctl.WriteString(fmt.Sprintf("%s- [%s](%s)\n", gap, strings.Trim(file.Name(), "[]"), strings.ReplaceAll(path.Join(folderPath, file.Name()), " ", "%20")))
    }
}

func main() {
    readme, err := os.OpenFile(DocumentPath, os.O_RDWR, os.ModePerm)
    if err != nil {
        panic(err)
    }
    readmeB, err := io.ReadAll(readme)
    if err != nil {
        panic(err)
    }
    readmeS := string(readmeB)
    flag := "<!-- catalog -->"
    splits := strings.Split(readmeS, flag)
    ctl := strings.Builder{}
    ctl.WriteString("\n\n")
    dfs(&ctl, ContentPath, 0)
    readmeS = splits[0] + flag + ctl.String() + "\n" + flag + splits[2]
    err = readme.Truncate(0)
    if err != nil {
        panic(err)
    }
    _, err = readme.Seek(0, 0)
    if err != nil {
        panic(err)
    }
    _, err = readme.WriteString(readmeS)
    if err != nil {
        panic(err)
    }
}

发布

在这之前,GitHub 可能要求你设置二次登录验证来确保安全。

要发布到 Marketplace 只需在 Release 页面点击 “Draft new release” 并选中 “Publish this Action to the GitHub Marketplace” 选项,当所有条目检测通过时即可发布。

应用

minoic/stackoverflow-top-cpp: stackoverflow 上对 C/C++ 问题的整理、总结和翻译。

name: markdown-auto-catalog

on:
  push:
    branches: [ "master" ]
  workflow_dispatch:

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: minoic/markdown-auto-catalog@v1.0.0
        with:
          content-path: 'question'
          document-path: 'README.md'
          filter: '\(.*\).md'
        continue-on-error: true

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注