一起编写个多用途 Github Action 吧
- 前言
疾速开始
- 0. 从模板初始化我的项目
- 1. 在根目录增加
action.yml
- 2. 创立入口
index.ts
- 3. 获取参数以及
github
上下文 - 4. 在你的 main 函数填入逻辑
- 5. 把后果打包输入到指定目录
- 6. 公布到 github marketplace
开始进阶之旅
- 0. 条件编译
- 1. 代码宰割
2. 增加条件变量,并兼顾
action
和npm
包的写法- 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
这个仓库是一个 Action
,Github
指南中给的示例如下:
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
类元数据:
形容类
:name
,author
,description
这些字段来形容这个action
是什么入参
:inputs
下的字段,用来给action
传参出参
:outputs
下的字段,用于定义出参字段runs
: 用于定义运行时相干的配置,JavaScript action
和Docker container action
有不同的配置。这篇文章次要介绍的是JavaScript action
款式相干
: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
,它可能间接帮忙咱们调用 Github
的 rest 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_TOKENoctokit = 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
软件,而后扫描 Github
的 Two factor
绑定的二维码,这样你的 Github Action
就被顺利的公布到了 插件市场
里了。
庆贺一下你的胜利吧!
开始进阶之旅
当然笔者远不止想介绍这么多,不然题目的 多用途
三个字就没提现进去。
接下来咱们同时要把这个包的主逻辑抽离进去,公布成 npm
包,再通过 mock
的上下文,构建单元测试用例。具体怎么做呢?
外围其实很简略:代码宰割
和 条件编译
0. 条件编译
咱们开发者对这个再相熟不过了,通过条件编译能够间接去除一些 unreachable code
,比方咱们公布成 npm
包给用户用,天然是不须要 @actions/core
和 @actions/github
的。 那么就能够在打包时间接把它们干掉。
实现它的伎俩很多,比方 webpack.DefinePlugin
,@rollup/plugin-replace
,esbuild#define
等等。
1. 代码宰割
这个借助打包工具也很容易实现,比方咱们原先引入是用动态写法:
import { getActionOptions } from './action'
接下来咱们改为 async/await
动静引入
async function mian() { const { getActionOptions } = await import('./action')}
通过这种形式,打包工具除了默认的 output
配置,会生成 [name].js
的 entryFile
外,还会生成一些 [name]-[hash].js
的 chunkFile
,来交给运行时动静加载。
2. 增加条件变量,并兼顾 action
和 npm
包的写法
这里咱们增加一个 __isAction__
的布尔值变量
declare var __isAction__: boolean
对于 action
和 npm
的不同,次要在于它们的入参出参形式不同,还有上下文不同。
那么咱们就能够依据这 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 } = optionslet octokitif (__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
其中能够看到,打包的配置也随着构建指标不同,应用了不同的配置。比方:
npmOutput
与actionOutput
这2
个rollup#OutputOptions
tsconfig.action.json
和tsconfig.build.json
这2
个ts
配置。
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