site infoHacknerd | Tech Blog
blog cover

📠 [inquirer] Jenkins CLi开发到发布

JavaScriptWebpack

背景

觉得在Jenkins构建时需要手动打开浏览器,输入账号密码,手动触发构建的操作比较繁琐。于是想开发一套ClI简化操作。同时CLI可以嵌入一些hook,比如在执行构建之前合并主干到当前分支,或在正式发布时将当前分支合并到主干,并删除该分支等。


核心代码

入口

  • 1.hashbang 注解用于指定Javascript解释器
  • 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()
    

    控制台输出

  • 1.通过装饰器给各类log函数的入参,添加日期。
  • 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))
      }
    }

    用户模块

  • 1.获取用户账号密码
  • 2.base64加密储存
  • 3.生成token 通过 head 中的 Authorization 传递给Jenkins API进行登录。
  • 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模块

  • 1.选择要构建的应用。获取Jenkins指定 view下的应用。
  • 2.生成构建参数。获取build 中的param 生成表单供用户输入。
  • 3.触发远程构建。
  • 4.通过触发构建后生成的queneId 定时查询build id ,并输出构建日志。
  • 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配置

  • 1.通过babel-loader 将ES6+代码编译为ES5
  • 2.通过BannerPlugin 插件在main.js 中添加hashbang注释,这样就可以直接用文件名执行脚本。
  • 3.打包生成commonJS模块
  • 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

  • 1.通过bin。指定构建后的可执行文件目录
  • 2.通过npm link 软连接,可以本地调试jb 命令。
  • 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

    Contents

    • 背景
    • 核心代码
    • 入口
    • 控制台输出
    • 用户模块
    • Jenkins模块
    • Webpack配置
    • pacakge.json
    • 发布

    2024/05/31 05:40