📠 [inquirer] Jenkins CLi开发到发布
背景
觉得在Jenkins构建时需要手动打开浏览器,输入账号密码,手动触发构建的操作比较繁琐。于是想开发一套ClI简化操作。同时CLI可以嵌入一些hook,比如在执行构建之前合并主干到当前分支,或在正式发布时将当前分支合并到主干,并删除该分支等。
核心代码
入口
typescriptCopy
#!/usr/bin/env node
import { GitModule } from "./package/git/core";
import { JekninsModule } from "./package/jekins/core";
import { LogModule } from "./package/log/core";
async function main() {
LogModule.normal('流水线构建脚本...')
GitModule.checkIfGitExit()
await JekninsModule.main()
}
main()
控制台输出
typescriptCopy
import chalk from 'chalk';
import moment from "moment";
// 生成日期
export function logGenerateDate(): string {
return `${chalk.white('[')}${chalk.gray(moment().format('HH:mm:ss'))}${chalk.white(']')}`
}
// 输出日志装饰器
export function logDate(target: any, _descriptor: ClassMethodDecoratorContext) {
return function (...args: any) {
target(logGenerateDate(), ...args)
}
}
// 日志模块
export class LogModule {
@logDate
static normal(...args: string[]) {
console.log(...args)
}
@logDate
static info(...args: string[]) {
console.log(chalk.blueBright(...args))
}
@logDate
static warning(...args: string[]) {
console.log(chalk.yellow(...args))
}
@logDate
static error(...args: string[]) {
console.log(chalk.red(...args))
}
@logDate
static success(...args: string[]) {
console.log(chalk.green(...args))
}
}用户模块
typescriptCopy
import inquirer from "inquirer"
import { jenkinsHelperGetTokenFromLocal, jenkinsHelperSetTokenFromLocal } from "../../package/jekins/helper"
import { LogModule } from "../log/core"
export class UserModule {
static _token: string
static async getAuthToken() {
if (!this._token) {
const localToken = jenkinsHelperGetTokenFromLocal()
if (localToken) {
this._token = localToken
} else {
LogModule.normal('请登录')
const { username } = await inquirer.prompt({ type: 'input', message: '用户名', name: 'username' })
const { password } = await inquirer.prompt({ type: 'password', message: '密码', name: 'password' })
const token = `Basic ${btoa(`${username}:${password}`)}`
this._token = token
jenkinsHelperSetTokenFromLocal(token)
}
}
return this._token
}
}Jenkins模块
javascriptCopy
import inquirer from "inquirer";
import { JEKNKINS_URL, JENKINS_FRONT_END_VIEW_NAME } from "../../config/global-config";
import { LogModule } from "../log/core";
import Jenkins from "jenkins";
import { UserModule } from "../user/core";
import { JenkinsParam } from "./define";
import { jenkinsHelperGenerateParamForm } from "./helper";
export class JekninsModule {
static _jenkins: any
static get jenkins(): Jenkins {
if (!this._jenkins) {
this._jenkins = new Jenkins({
baseUrl: ''
})
}
return this._jenkins
}
// jenkins实例
static async getJenkinsInstance(): Promise<Jenkins> {
if (!this._jenkins) {
this._jenkins = new Jenkins({
baseUrl: JEKNKINS_URL,
headers: { Authorization: await UserModule.getAuthToken() }
})
}
return this._jenkins
};
// 主入口
static async main() {
const application = await this.chooseApplication()
await this.build(application)
}
// 选择要构建的应用
static async chooseApplication() {
const jenkins = await this.getJenkinsInstance()
const exist = await jenkins.view.exists(JENKINS_FRONT_END_VIEW_NAME)
if (!exist) {
LogModule.error(`不存在 ${JENKINS_FRONT_END_VIEW_NAME}`)
process.exit(1)
}
const { jobs } = await jenkins.view.get(JENKINS_FRONT_END_VIEW_NAME)
const jobChooseList = jobs.map((ele: any) => ({ key: ele.name, value: ele.name }))
LogModule.normal('选择要构建的应用...')
const { application } = await inquirer.prompt({ type: 'list', message: '选择应用', name: 'application', choices: jobChooseList })
return application
}
// 参数
static async getBuildParam(name: string): Promise<any> {
LogModule.normal('正在获取任务信息...')
const jenkins = await this.getJenkinsInstance()
const { actions } = await jenkins.job.get(name)
const paramList: JenkinsParam[] = actions[0]?.parameterDefinitions ?? []
const param: any = {}
for (let i = 0; i < paramList.length; i++) {
const ele = paramList[i];
param[ele.name] = await jenkinsHelperGenerateParamForm(ele)
}
return param
}
// 构建
static async build(name: string) {
const jenkins = await this.getJenkinsInstance()
const parameters = await this.getBuildParam(name)
// token 为jenkins 流水线中配置的身份验证令牌
const queneId = await jenkins.job.build({ name, parameters, token: '...' })
const id = setInterval(async () => {
const { executable, why } = await jenkins.queue.item(queneId)
if (executable) {
clearInterval(id)
this.logStream(name, executable.number)
} else {
LogModule.normal(why)
}
}, 1000)
}
// 输出日志
static async logStream(name: string, id: number) {
const jenkins = await this.getJenkinsInstance()
const log = await jenkins.build.logStream(name, id)
log.on("data", (text: any) => {
process.stdout.write(text);
});
log.on("error", (err: any) => {
LogModule.error(`构建失败`)
LogModule.error(err)
LogModule.normal(`详细日志: ${JEKNKINS_URL}/job/${name}/${id}/console`)
process.exit(1)
});
log.on('end', () => {
LogModule.normal(`详细日志: ${JEKNKINS_URL}/job/${name}/${id}/console`)
})
}Webpack配置
javascriptCopy
import path from "path";
import TerserPlugin from "terser-webpack-plugin";
import webpack from "webpack";
import { fileURLToPath } from "url";
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const config = {
entry: "./src/main.ts",
output: {
path: path.resolve(__dirname, "bin"),
filename: "[name].cjs",
clean: true,
},
target: "node",
mode: "production",
module: {
rules: [
{
test: /\.ts$/,
use: ["babel-loader"],
exclude: /node_modules/,
},
],
},
resolve: {
extensions: [".ts", ".js"],
},
optimization: {
splitChunks: {
chunks: "all",
},
minimizer: [
new TerserPlugin({
terserOptions: {
format: {
comments: false,
},
},
extractComments: false,
}),
],
},
plugins: [new webpack.BannerPlugin({ banner: "#!/usr/bin/env node", raw: true })],
};
export default config;
pacakge.json
jsonCopy
{
"name": "jenkins-build",
"version": "1.0.0",
"bin": {
"jb": "./bin/main.cjs"
},
"type": "module",
"main": "./src/main.ts",
"scripts": {
"build": "webpack --config ./webpack.config.js",
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC",
"dependencies": {
"chalk": "^5.3.0",
"core-js": "^3.37.1",
"inquirer": "^9.2.17",
"jenkins": "^1.1.0",
"moment": "^2.30.1",
"typescript": "^5.4.5",
"zx": "^7.2.3"
},
"devDependencies": {
"@babel/core": "^7.24.5",
"@babel/plugin-proposal-class-properties": "^7.18.6",
"@babel/plugin-proposal-decorators": "^7.24.1",
"@babel/preset-env": "^7.24.5",
"@babel/preset-typescript": "^7.24.1",
"@types/inquirer": "^9.0.7",
"@types/jenkins": "^1.0.2",
"@types/lodash": "^4.17.0",
"babel-loader": "^9.1.3",
"color-name": "^2.0.0",
"terser-webpack-plugin": "^5.3.10",
"webpack": "^5.91.0",
"webpack-cli": "^5.1.4"
},
}
发布
shellCopy
> npm publish