关于前端:一起编写个多用途-Github-Action-吧

37次阅读

共计 7071 个字符,预计需要花费 18 分钟才能阅读完成。

  • 一起编写个多用途 Github Action 吧

    • 前言
    • 疾速开始

      • 0. 从模板初始化我的项目
      • 1. 在根目录增加 action.yml
      • 2. 创立入口 index.ts
      • 3. 获取参数以及 github 上下文
      • 4. 在你的 main 函数填入逻辑
      • 5. 把后果打包输入到指定目录
      • 6. 公布到 github marketplace
    • 开始进阶之旅

      • 0. 条件编译
      • 1. 代码宰割
      • 2. 增加条件变量,并兼顾 actionnpm 包的写法

        • 3. 重载获取参数
        • 4. 重载获取 Octokit 实例
      • 5. 更改打包配置
      • 6. 公布到 npm
    • 单元测试
    • 结尾
    • 参考文档
    • 源代码

一起编写个多用途 Github Action 吧

前言

Github Actions 想必大家或多或少都理解,并应用过相似的产品。

这篇文章就从开发,测试,构建的角度来设计一个 Github Action,让它能够便捷的复用代码逻辑,并同时公布到 Github Marketplace, npm 等平台。

疾速开始

0. 从模板初始化我的项目

疾速创立一个 ts rollup lib 我的项目,自己个别应用本人的模板(sonofmagic/npm-lib-rollup-template),当然这无所谓,本人 npm init -y 也是能够的。

1. 在根目录增加 action.yml

这个文件是用来通知 Github 这个仓库是一个 ActionGithub 指南中给的示例如下:

name: 'Hello World' # 必填 Required GitHub Action 名称
description: 'Greet someone and record the time' # 必填 Required 形容
inputs: # 输出
  who-to-greet:  # id of input
    description: 'Who to greet' # 参数形容
    required: true # 是否必填
    default: 'World' # 此参数是一个字符串,文档中没有注明其余的类型
outputs: # 输入
  time: # id of output
    description: 'The time we greeted you'
runs:
  using: 'node16' # 运行时
  main: 'index.js' # 执行入口

从这个配置文件中,咱们大体能够分为 5 类元数据:

  1. 形容类: nameauthordescription 这些字段来形容这个 action 是什么
  2. 入参: inputs 下的字段,用来给 action 传参
  3. 出参: outputs 下的字段,用于定义出参字段
  4. runs: 用于定义运行时相干的配置,JavaScript actionDocker container action 有不同的配置。这篇文章次要介绍的是 JavaScript action
  5. 款式相干: branding 字段次要用于上架到 Github Marketplace 上的 icon 和色彩。

这样咱们就能够定义本人的元数据 action.yml:

name: 'github-repository-distributor'
description: 'github-repository-distributor'
inputs:
  token: # id of input
    description: 'the repo PAT or GITHUB_TOKEN'
    required: true
  username:
    description: 'github username to generate markdown files'
    required: true
  motto:
    description: 'whether add powered by footer (boolean)'
    default: 'true' # 留神这里是字符串
  # ....
  title:
    description: 'main markdown h1 title'
  onlyPrivate:
    description: 'only include private repos (boolean)'
    default: 'false'
runs:
  using: 'node16'
  main: 'lib/index.js'
branding:
  icon: 'arrow-up-circle'
  color: 'green'

2. 创立入口 index.ts

async function main(){// do something}
main()

3. 获取参数以及 github 上下文

这里就须要介绍 @actions/core@actions/github

@actions/core 外面蕴含了大量 action 的外围办法,咱们获取参数,导出变量,或者获取秘钥等等都得靠它。

@actions/github 则次要蕴含了 Github 的上下文和一个 @octokit/core,它可能间接帮忙咱们调用 Githubrest api 接口们。

这样咱们获取 inputs 里的参数就能够这么写:

import core from '@actions/core'
import type {UserDefinedOptions} from './type'

export function getActionOptions (): UserDefinedOptions {const token = core.getInput('token')
  const username = core.getInput('username')
  // getBooleanInput 其实实质上就是一种 parseBoolean(core.getInput('key'))
  const motto = core.getBooleanInput('motto')
  const filepath = core.getInput('filepath')
  const title = core.getInput('title')
  const includeFork = core.getBooleanInput('includeFork')
  const includeArchived = core.getBooleanInput('includeArchived')
  const onlyPrivate = core.getBooleanInput('onlyPrivate')
  return {
    token,
    username,
    motto,
    filepath,
    title,
    includeFork,
    includeArchived,
    onlyPrivate
  }
}

当然咱们也能够轻而易举的获取到上下文里的信息和 octokit 实例:

import github from '@actions/github'
// 应用 action 的仓库名
github.context.repo.repo
// token 为 the repo PAT or GITHUB_TOKEN
octokit = github.getOctokit(token)
// 获取一个人的仓库
const res = await octokit.rest.repos.listForUser({
  username: 'sonofmagic',
  per_page: 20,
  page: 1,
  sort: 'updated'
})

4. 在你的 main 函数填入逻辑

咱们回到入口点,在代码中填充逻辑

async function main(){const options = getActionOptions()
  // do something
}
main()

5. 把后果打包输入到指定目录

这里我把打包后果输入到了 lib 文件中,值得注意的是,官网文档中是应用 @vercel/ncc(webpack),同时还把 node_modules/* 也提交到 Github 上。这里咱们优化一下,采纳了 rollup 打包,间接把依赖项打入构建产物中。

import typescript from '@rollup/plugin-typescript'
import {nodeResolve} from '@rollup/plugin-node-resolve'
import commonjs from '@rollup/plugin-commonjs'
import json from '@rollup/plugin-json'
import pkg from './package.json'
import {terser} from 'rollup-plugin-terser'

const isDev = process.env.NODE_ENV === 'development'

/** @type {import('rollup').RollupOptions} */
const config = {
  input: 'src/index.ts',
  output: {
    dir: 'lib',
    format: 'cjs',
    exports: 'auto'
  },
  plugins: [
    // 厌弃 lib 太大能够压缩一下
    terser(),
    json(),
    nodeResolve({preferBuiltins: true}),
    commonjs(),
    typescript({
      tsconfig: './tsconfig.build.json',
      sourceMap: isDev
    })
  ],
  external: [...(pkg.dependencies ? Object.keys(pkg.dependencies) : []),
    'fs/promises'
  ]
}

export default config

而后再 git add lib/* 增加构建产物,提交。这样,lib 中大量的 "无用" 代码也被提交到了 Github

6. 公布到 github marketplace

在手机上下载微软的 Authenticator 软件,而后扫描 GithubTwo factor 绑定的二维码,这样你的 Github Action 就被顺利的公布到了 插件市场 里了。

庆贺一下你的胜利吧!

开始进阶之旅

当然笔者远不止想介绍这么多,不然题目的 多用途 三个字就没提现进去。

接下来咱们同时要把这个包的主逻辑抽离进去,公布成 npm 包,再通过 mock 的上下文,构建单元测试用例。具体怎么做呢?

外围其实很简略:代码宰割 条件编译

0. 条件编译

咱们开发者对这个再相熟不过了,通过条件编译能够间接去除一些 unreachable code,比方咱们公布成 npm 包给用户用,天然是不须要 @actions/core@actions/github 的。那么就能够在打包时间接把它们干掉。

实现它的伎俩很多,比方 webpack.DefinePlugin@rollup/plugin-replaceesbuild#define 等等。

1. 代码宰割

这个借助打包工具也很容易实现,比方咱们原先引入是用动态写法:

import {getActionOptions} from './action'

接下来咱们改为 async/await动静引入

async function mian() {const { getActionOptions} = await import('./action')
}

通过这种形式,打包工具除了默认的 output 配置,会生成 [name].jsentryFile 外,还会生成一些 [name]-[hash].jschunkFile,来交给运行时动静加载。

2. 增加条件变量,并兼顾 actionnpm 包的写法

这里咱们增加一个 __isAction__ 的布尔值变量

declare var __isAction__: boolean

对于 actionnpm 的不同,次要在于它们的入参出参形式不同,还有上下文不同。

那么咱们就能够依据这 2 点,进行编译时重载:

3. 重载获取参数

咱们获取参数就能够这么写:

export async function getOptions (options?: UserDefinedOptions): Promise<UserDefinedOptions> {
  let opt: Partial<UserDefinedOptions>

  if (__isAction__) {const { getActionOptions} = await import('./action')
    opt = getActionOptions()} else {opt = options}
  return defu<Partial<UserDefinedOptions>, UserDefinedOptions>(
    opt,
    getDefaults()) as UserDefinedOptions
}

这样在打包时就能确定代码的走向。

4. 重载获取 Octokit 实例

咱们获取 Octokit 实例就能够这么写:

const {token} = options
let octokit
if (__isAction__) {const { github} = await import('./action')
  octokit = github.getOctokit(token)
} else {const { Octokit} = await import('@octokit/rest') // require()
  octokit = new Octokit({auth: token})
}

这样 action@actions/github,默认状况下走 @octokit/rest,取得的 Octokit 也是统一的。

5. 更改打包配置

咱们增加 BUILD_TARGET 环境变量,当值为 action 打包 Action,默认为 npm 包。

这样咱们很容易能够编写出这样的 rollup.config.js:

import typescript from '@rollup/plugin-typescript'
import {nodeResolve} from '@rollup/plugin-node-resolve'
import commonjs from '@rollup/plugin-commonjs'
import json from '@rollup/plugin-json'
import pkg from './package.json'
import replace from '@rollup/plugin-replace'
import {terser} from 'rollup-plugin-terser'

const isDev = process.env.NODE_ENV === 'development'
const isAction = process.env.BUILD_TARGET === 'action'

/** @type {import('rollup').OutputOptions} */
const npmOutput = {
  file: pkg.main,
  format: 'cjs',
  sourcemap: isDev,
  exports: 'auto'
}

/** @type {import('rollup').OutputOptions} */
const actionOutput = {
  dir: 'lib',
  format: 'cjs',
  exports: 'auto'
}

/** @type {import('rollup').RollupOptions} */
const config = {
  input: 'src/index.ts',
  output: isAction ? actionOutput : npmOutput,
  plugins: [isAction ? terser() : undefined,
    replace({
      preventAssignment: true,
      values: {__isAction__: JSON.stringify(isAction)
      }
    }),
    json(),
    nodeResolve({preferBuiltins: true}),
    commonjs(),
    typescript({
      tsconfig: isAction ? './tsconfig.action.json' : './tsconfig.build.json',
      sourceMap: isDev
    })
  ],
  external: [...(pkg.dependencies ? Object.keys(pkg.dependencies) : []),
    'fs/promises'
  ]
}

export default config

其中能够看到,打包的配置也随着构建指标不同,应用了不同的配置。比方:

  • npmOutputactionOutput2rollup#OutputOptions
  • tsconfig.action.jsontsconfig.build.json2ts 配置。

6. 公布到 npm

package.json 中增加打包指令和 npm 包含文件吧!

{
    "scripts":{
        "build": "yarn clean && yarn dts && cross-env NODE_ENV=production rollup -c",
        "build:action": "yarn clean lib && cross-env NODE_ENV=production BUILD_TARGET=action rollup -c",
    },
    "files": ["dist"]
}

构建实现后,执行 yarn publish,功败垂成!

单元测试

其实测试也是同样的情理,在单元测试用例执行之前,能够劫持获取参数的办法和获取 github 上下文的办法,通过这样来进行单元测试。

结尾

出于篇幅限度,本篇文章并未就细节过多介绍。次要给大家编写 Github Action 一个思路,如果各位有趣味能够一起探讨。

参考文档

Debug your GitHub Actions by using tmate

上架 github marketplace 地址

GitHub Actions / Creating actions (指南)

Metadata syntax for GitHub Actions

源代码

github-repository-distributor

正文完
 0