一起编写个多用途|一起编写个多用途 Github Action 吧
- 一起编写个多用途 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 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_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
软件,然后扫描 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 {
let opt: Partialif (__isAction__) {
const { getActionOptions } = await import('./action')
opt = getActionOptions()
} else {
opt = options
}
return defu, 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
其中可以看到,打包的配置也随着构建目标不同,使用了不同的配置。比如:
npmOutput
与actionOutput
这2
个rollup#OutputOptions
tsconfig.action.json
和tsconfig.build.json
这2
个ts
配置。
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 Action 吧】github-repository-distributor
推荐阅读
- 编程|//设计算法,将某个大于1的自然数n分解为其素因子的乘积,如6=2*3,7=7,8=2*2*2。
- 能直接调试的开放API(这个API|能直接调试的开放API?这个API Hub绝了)
- 鸿蒙OS如何开发一个前端应用详解
- 三、多态
- 利用|利用 onnxruntime 库同时推理多个模型的效率研究
- 又到了「金三银四」跳槽季,大家都是在哪个平台找到工作的呢()
- #|Spark优化总结(二)——代码编写
- 一起来作画吧「GitHub|一起来作画吧「GitHub 热点速览 v.22.14」
- 面试官(MySQL 中的 varchar 最多能存储多少个字符(大部分人都会答错。。。))
- java|看看人家那物流系统,那叫一个优雅(附源码)