微信,支付宝小程序中的CI/CD,不要再让别人打断你的开发思路了!

954 阅读6分钟

本文内容均只用于支付宝和微信小程序

背景

先说一下为什么要做这个东西把。

  • 基于提审
    • 原生小程序中漫长的提审流程,合PR,切到主分支,pull代码,点击上传,后台提审
    • 原生小程序基于config文件决定抛出环境,提审时项目环境完全靠人工校验
  • 基于测试
    • 测试bugfix校验或者后端同学需要测试环境时,通常都是直接跟我们要二维码,这时我们可能在开发别的项目,本地代码不纯净,所以普遍我们都是commit本地代码,切到干净的分支,然后切好测试环境,在生成二维码给到他们,然后再切回开发分支。这也太麻烦了吧。所以我们开发了一个生成小程序二维码的平台,可以让他们完成脱离我们
    • 测试同学可能对git不熟悉,而且对怎么切换环境也不熟悉

就是基于上面这几点原因让我们基于支付宝和微信提供的SDK开发了属于我们小程序的CI/CD,尽最大可能解放一些繁琐且打断我们思路的工作

功能介绍

  • 自动上传小程序后台,PR合并后触发
    • 校验环境,version自增1(根据自己的项目自行补充需要的校验和功能)
    • 上传完成后叮叮通知,通知信息包括version,体验版二维码,包信息
  • 小程序预览平台
    • 用户体系:普通(可构建),管理员(可上传),超管(可添加用户)
    • 获取所有分支,可切分支
    • 获取当前环境,可切换环境
    • 定义入口页
    • 定义入口页参数
    • 构建预览二维码
    • 管理员以上可手动上传

image.png
image.png
本篇文章先说一下自动上传是如何实现的

gitee webHook配置

Gitee WebHook 功能是帮助用户 push 代码后,自动回调一个您设定的 http 地址。 这是一个通用的解决方案,用户可以自己根据不同的需求,来编写自己的脚本程序(比如发邮件,自动部署等)

添加webhook

image.png

勾选PR

image.png

添加完成

image.png

完成如上步骤后发起PR和更新PR后,码云会发起一个post请求到我们的承接接口。

接口实现

注册之前码云上填写的路由来接收WebHook

router.post('/upload/:appKey', async ctx => {
  // ...log
}

启动node服务,这时我们去刚刚添加完成的页面点一下测试按钮,接口就能接收到码云发来的请求啦~

既然第一步已经完成,那么我们梳理一下这个接口需要实现的内容:

  • 校验PR的目标分支和PR的状态
  • clone或者pull目标分支的代码至本地
  • 校验是否需要上传,version是否需要+1,是否为正式环境(与自己项目耦合的功能)
  • 叮叮通知开始上传
  • 上传
  • 叮叮通知上传成功
  • 结束

梳理完之后是不是非常的清晰,接下来让我们一步步来实现它

校验目标分支和PR状态

因为我们所有的PR目标分支均是master,并且需要提审的版本也是master,所以在接收到请求之后,需要判断一下目标分支是不是master并且PR的状态是不是合并完成。
WebHook推送数据的数据类型文档
PS:PR分为新建,更新,合并,关闭,每次状态更新都会发起一次请求,所以需要过滤不是合并的请求

router.post('/upload/:appKey', async ctx => {
    const { body } = ctx.request
    if (body.state === 'merged' && body.target_branch === "master") {
    	// ...
    }
}

拉取代码

校验完之后就需要拉取代码,如果项目目录存在就去pull,不存在就去clone(偷个懒,确保你的目录是存在的,不然会报错)

if (fs.existsSync(xcxpath)) {
  child_process.execSync('git pull', {
    cwd: xcxPath
  })
} else {
  child_process.execSync(`git clone ${git地址}`, {
    cwd: path.join(xcxPath, '..'))
  })
}

自定义校验

接下来就需要去做一些与自己项目相关的上传前的校验,比如我们有些时候可能存在多个PR,不想每次合并后都上传一次,这时我们利用PR中的标签来判断是否需要上传,当我们选中aNotUpload标签时,这个PR将不会上传
image.png

let arr = body.pull_request.labels.filter(item => item.name === 'aNotUpload')
if( arr.length ) {
	// ...存在,所以不上传
  ctx.body = {
    code: 200,
    msg: '自主选择不上传',
    success: true
  }
  return 
}

然后我们也维护了一套自己的version用来区分每次的版本,但是每次都需要手动去+1,非常不合理。为了完成这个自动化,在合并PR之后(合并了所有commit)偷偷在master增加version并且push(暂时没有想到更合理的方法)

const parser = require("@babel/parser");
const generator = require("@babel/generator").default;
const traverse = require("@babel/traverse").default;
// 获取ast
function readConfig(root) {
    const configFile = path.join(root, '/config/config.js')
    const context = fs.readFileSync(configFile, "utf-8")
    const ast = parser.parse(context)
    return {
        ast,
        file: configFile
    }
} 
// version是否+1
function prodHooks({
    root,
    appKey
}) {
    let {
        ast,
        file
    } = readConfig(root)
    let isVersionUp = false
    // 是否上升了一个版本号
    const diffStr = child_process.execSync(`git diff HEAD^ config/config.js`, {
        cwd: '项目路径',
        encoding: 'utf-8'
    })
    // 获取不同的version,大概得到['-  version: "10.0.0"', '+  version: "10.0.1"']
    let diffArr = diffStr.match(/(\+|-)(.*)version:[^\r\n]*[\r\n]/g)
    if (diffStr && diffArr.length) {
        // 提取数字
        diffArr = diffArr.map(i => i.replace(/\D*/g, ''))
        // +的version是否比-的version大,是的话就完成了version的提升
        isVersionUp = diffArr[1] - diffArr[0] > 0
    }
  	// 没有增加version,自动+1
    if(!isVersionUp) {
      	traverse(ast, {
            enter(p) {
                if (p.isObjectProperty()) {
                    let name = p.node.key.name
                    if (name === 'version' || name === 'versionCode') {
                        let val = p.node.value.value
                        if (typeof val === 'string') {
                            val = versionUpdate(val)
                        } else {
                            val += 1
                        }
                        p.node.value.value = val
                    }
                }
            }
        })
        const {
            code
        } = generator(ast)
        fs.writeFileSync(file, code, "utf-8")
        child_process.execSync(`git add . && git commit -m 'version up' && git pull && git push`, {
            cwd: '项目目录'
        })
    }
    return true
}
/**
 * 升级版本号
 * @param {string} val '2.09.09' | '99.99.99'
 * @return {string} '2.09.10' | '100.00.00'
 *  
*/ 
function versionUpdate(val) {
    let num = [...(1 + Number(val.replace(/\D/g, '')) + '')]
    for(let i=val.length-1; i>=0; i--){
        if(val[i] === '.') {
            num.splice(i - val.length + 1 , 0, '.')
        }
    }
    return num.join('')
}

到这里我们完成了所有上传前的校验和自定义处理,接下来就到了最重要的通知和上传阶段啦~

钉钉群通知

平常上班主要使用钉钉,所以接入钉钉群机器人通知上传进度
PS:钉钉群机器人SDK
先创建一个钉钉群并设置自定义机器人获取accessToken和secret
image.png
image.png
image.png
点击完成后打开机器人
image.png
就可以得到accessToken和secret
然后利用sdk向钉钉群发送一个推送吧

const Robot = require('dingtalk-robot-sdk');
const robot = new Robot({
  accessToken: 'xxxx',
  secret: 'xxxx'
});
const text = new Robot.Text('走起~')
robot.send(text)

上传

支付宝和微信小程序利用sdk上传前的准备工作这边就不说啦,大家跟着官方文档去配置吧
PS:微信上传文档支付宝上传文档
支付宝

alipaydev.setConfig({
  toolId,
  privateKey,
})
const uploadResult = await alipaydev.miniUpload({
  appId: '支付宝appid',
  clientType: 'alipay',
  project: '项目地址',
  experience: true // 设置为体验版
})

微信

const project = new ci.Project({
  appid,
  type: 'miniProgram',
  projectPath,
  privateKeyPath
})
const year = new Date().getFullYear() - 2000
let month = new Date().getMonth() + 1
const day = new Date().getDate()
// 根据年月日当版本号
const version = '2.5.' + year + (month < 10 ? '0' + month : month) + (day < 10 ? '0' + day : day);
// 上传
const previewResult = await ci.upload({
  project,
  version,
  desc: body.title,
  setting: {
    es6: true,
    minify: true,
    autoPrefixWXSS: true,
    minifyWXML: true,
    minifyWXSS: true,
    minifyJS: true
  }
})