背景
在之前的微信小程序版本开发中,受累于小程序版本发布以下几个痛点:
- 上传代码都要依赖开发者工具,且版本信息和描述都要手动填写
- 构建-上传-设置体验版-发布二维码通知测试这一套流程过于繁琐
- 当开发者不一致时,需要去小程序后台频繁设置体验版,容易造成操作失误
刚好看到了前端CI/CD思考 这篇文章,就思考如何基于微信小程序ci的api构建小程序自动化部署流程。
Taro官方推荐的CI插件,也只是对于miniprogram-ci 官方api的封装,并不适用自定义场景。
目标
构建一套自动部署流程/工具,具体流程如下图所示:
就这个流程来说,实现方式有三种:
- 自定义cli工具,通过命令部署
- gitlab ci,通过runner监听代码提交自动化部署
- jenkins,可以拉取git仓库指定分支,可手动部署
本文采用第一、二种方式先完成研发的自动化部署流程。
实现
- 微信公众号配置 参考官方文档,在”微信公众平台-开发-开发设置“,下载代码上传密钥并放到项目根目录,设置相应的ip白名单
- 配置钉钉群机器人 参考钉钉官方文档,钉钉群-设置-智能群助手-添加机器人-自定义来添加机器人, 注意保存得到的Webhook地址,后续用到
- 在微信小程序项目安装几个依赖包
yarn add miniprogram-ci node-fetch ora@5.0.0
并在根目录创建wxci.config.js
module.exports = {
appid: '', // 微信小程序appid
privateKeyPath: '', // 第一步的密钥地址
qrcodeImageUrl:'', // 微信体验版二维码图片网络地址,因为体验版图片地址是固定的,建议转存到cdn上,不然无法在钉钉显示
dingTalkUrl:'', // 第二步webhook的url
}
- 在根目录建立script文件夹,写入执行逻辑
// index.js
#!/usr/bin/env node
const path = require('path')
const { checkConfigFile, info } = require('./util')
async function main() {
// 1. 读取配置文件,合并配置
const filePath = 'wxci.config.js'
checkConfigFile(filePath)
const absConfigDir = process.cwd()
info(`absConfigDir: ${absConfigDir}`)
const config = require(path.resolve(`${absConfigDir}`, filePath))
const baseConfig = require(path.resolve(__dirname, './defaultConfig.js'))
const fullConfig = { ...baseConfig, ...config }
const {
appid,
type,
projectPath,
privateKeyPath,
version,
desc,
robot,
qrcodeImageUrl,
uploadImage,
dingTalkUrl,
} = fullConfig
const WxCi = require('./wxCi')
const DingCi = require('./dingCi')
// 2. 上传/预览小程序
const wxCi = new WxCi({
appid,
type,
projectPath,
privateKeyPath,
outDir,
uploadImage,
qrcodeImageUrl,
version,
desc,
robot,
setting,
qrcodeFormat,
})
const weappQRImgUrl = await wxCi.run()
// 3. 发送钉钉提醒
const dingCi = new DingCi({
absConfigDir,
weappQRImgUrl,
dingTalkUrl,
isExperience,
})
await dingCi.run()
}
main()
// wxCi.js
const path = require('path')
const fs = require('fs')
const ci = require('miniprogram-ci')
const { execSync } = require('child_process')
const { checkConfigFile, fail, info, success } = require('./util')
class WxCi {
constructor(
options = {
appid: '',
privateKeyPath: '',
projectPath: '',
qrcodeImageUrl: '',
version: '',
}
) {
this.options = options
this.QRImgUrl = ''
}
async run() {
const {
projectPath,
privateKeyPath,
appid,
type,
version,
desc,
robot,
qrcodeImageUrl,
} = this.options
// 校验密钥
if (fs.existsSync()) {
fail(`${privateKeyPath}密钥文件不存在`)
process.exit(1)
}
info('正在上传...')
try {
const project = new ci.Project({ appid, type, projectPath, privateKeyPath })
info('上传体验版...')
if(!version){
const branchName = execSync('git rev-parse --abbrev-ref HEAD', options).toString().trim()
}
git rev-parse --abbrev-ref HEAD
await ci.upload({ project, version, desc, robot })
// 微信体验版地址不会变,直接写死
this.QRImgUrl = qrcodeImageUrl
success('上传成功')
} catch (error) {
fail(`上传失败: ${error}`)
process.exit(1)
}
return this.QRImgUrl
}
}
module.exports = WxCi
// 处理钉钉消息
const { execSync } = require('child_process')
const HOSTNAME = require('os').hostname()
const fetch = require('node-fetch')
const ora = require('ora')
const { info } = require('./util')
class DingCi {
constructor(
options = {
weappQRImgUrl: '',
dingTalkUrl: '',
}
) {
this.options = options
this.uploadType = '体验版'
this.gitInfo = ''
this.template = ''
}
/**
* 入口
*/
async run() {
// 1.获取git分支及最近的提交记录
this.getGitInfo()
// 2.构造消息数据结构
this.buildTemplate()
// 3.推送钉钉消息
await this.sendDingTalk()
}
async sendDingTalk() {
const { isExperience, dingTalkUrl } = this.options
const postBody = {
msgtype: 'markdown',
markdown: {
title: '小程序构建测试已完成',
text: this.template,
},
at: {
isAtAll: isExperience,
},
}
const spinner = ora({
text: `正在推送钉钉消息...\n`,
spinner: 'moon',
}).start()
try {
await fetch(dingTalkUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(postBody),
})
spinner.succeed(`推送钉钉消息成功\n`)
} catch (error) {
console.error('推送钉钉消息error', error)
}
}
getGitInfo() {
try {
const options = { cwd : this.options.absConfigDir }
const branchName = execSync('git rev-parse --abbrev-ref HEAD', options).toString().trim()
this.gitInfo = `\n当前分支: **${branchName}** \n`
} catch (error) {
console.error('获取git日志失败', error)
this.gitInfo = ''
}
}
buildTemplate() {
const { weappQRImgUrl, isExperience } = this.options
const { uploadType, gitInfo } = this
const wechatPart = weappQRImgUrl && `## 微信${uploadType}:![](${weappQRImgUrl})`
this.template = `# ${uploadType}小程序构建完成\n---\n 构建机器:${HOSTNAME} \n ${gitInfo} \n---\n${wechatPart || ''}`
}
}
module.exports = DingCi
在package.json增加脚本
"ci": "yarn && yarn run build && node ./script/index.js",
执行yarn ci测试一下
- gitlab 配置 参考使用小程序CI自动上传代码,在一台机器上安装runner并启动注册
在项目根目录创建.gitlab-ci.yml
cache:
key: modules
paths:
- npm_cache
- node_modules/
stages:
- deploy
# 打包项目
deploy_job:
stage: deploy
tags: # ci runner标签
- test-ci
only: # 指定分支名称
- test-ci
# variables:
# - $CI_COMMIT_MESSAGE =~ /^build/
cache:
key: modules
paths:
- node_modules/
before_script:
- node -v && npm -v
- yarn global add @tarojs/cli
script:
- echo "开始构建🔥🔥🔥"
# - npm ci
- npm install --registry=https://registry.npm.taobao.org
- npm run build
- echo " 完成构建🔥🔥🔥"
- echo "开始部署🚀🚀🚀"
- npm run ci
- echo " 完成部署🚀🚀🚀"
修改下刚才的脚本
"ci": "node ./script/index.js",
这样提交代码到指定分支就会自动部署,这里指定的是test-ci分支
到此为止,部署流水线的工作基本就完成了🎉🎉🎉
下一步,就要独立出脚本,作为单独的工具包,并结合jenkins更完善部署方案。