一、概括
整个实现流程大致如下:
1. 创建 CODING DevOps 项目。
2. 创建构建计划,配置微信小程序代码上传白名单。
3. 配置微信小程序代码上传私钥到环境变量中。
4. 配置企业微信的 webhook 地址到环境变量中。
5. 配置构建计划,分为 4 个步骤(检出、编译、上传新版本、发送通知)。
下面我们来一步步实现它。
二、前置准备
·CODING DevOps 项目。
·具有管理员权限的微信小程序账号。
·企业微信机器人 WebHook 地址。
·将 示例仓库 导入至 CODING 代码仓库中。
三、设置小程序白名单
此过程需要将构建任务的网络出口 IP 添加至小程序开发白名单中,你可以在构建计划的基本信息中获取出口 IP。
# 中国上海 43.143.12.0/2443.142.234.0/2443.142.23.0/24124.220.180.0/2481.68.101.0/24111.231.92.0/24101.33.207.0/2443.136.72.0/24 # 中国香港 124.156.164.25/32119.28.15.65/32 # 美国硅谷 170.106.136.17/32170.106.83.77/32
前往微信小程序的管理后台,单击左侧菜单栏中的开发 > 开发者设置 > 小程序代码上传 > 编辑 IP 白名单,添加需要的出口地址。
四、创建构建计划
在持续集成中新建构建计划,选择自定义构建过程模板。
在配置详情中参考 Jenkinsfile 编写构建过程。
Jenkinsfile:
pipeline { agent any stages { stage('检出') { steps { checkout([ $class: 'GitSCM', branches: [[name: env.GIT_BUILD_REF]], userRemoteConfigs: [[ url: env.GIT_REPO_URL, credentialsId: env.CREDENTIALS_ID ]]]) } } stage('构建') { steps { echo '开始安装依赖' sh 'npm install' echo '开始构建…' sh 'npm run build' echo '构建完成' } } stage('上传新版本') { steps { withCredentials([sshUserPrivateKey(credentialsId: "${env.privatekey}",keyFileVariable: 'identity')]) { sh 'node upload.js -p ${identity}' } } } stage('发送新版本通知') { steps { sh 'node notification.js -u ${WECHAT_WEBHOOK}' } } }}
五、添加环境变量
持续集成过程中,我们总会将一些配置(例如:账号密码/版本号等)信息以环境变量的形式注入到构建过程中。在本实践中需要将以下两个凭据以环境变量的形式添加至构建计划中。
·微信小程序代码上传私钥。
·企业微信机器人 webhook 地址。
六、微信小程序代码上传私钥
前往微信管理后台:开发 > 开发设置 > 小程序代码上传获取上传私钥与 AppID。
将信息导入至 CODING 项目中的项目设置 > 开发者选项 > 凭据管理 > 录入凭据 > 选择 SSH 私钥凭据类型,复制私钥内容粘贴至凭据中。CODING 会对你的私钥进行加密保存,杜绝明文暴露在工程文件中。同时还需要勾选授权所有持续集成构建计划。
创建完成后将生成一串凭据 ID,将其导入至变量与缓存中。
七、上传机器人 webhook
新建 群聊机器人 后,复制机器人的 webhook 地址后,以字符串的形式粘贴至变量与缓存中。
八、构建阶段细节
示例项目的代码是从微信开发者工具中抽离的关于小程序/小游戏项目代码的编译模块。开发者可不打开小程序开发者工具,独立使用已导入的示例仓库进行小程序代码的上传、预览等操作。
在上文中,我们将小程序上传代码的凭证加到环境变量,通过在 Jenkinsfile 定义 withCredentials 参数即可快速提取凭证。
提取到凭证后,调用了一个 upload.js 脚本。此部分代码涉及到了代码的上传和预览二维码的生成。
const ci = require('miniprogram-ci')const path = require('path');const fs = require("fs");const argv = require('minimist')(process.argv.slice(2));const package = require('./package.json')const appDirectory = fs.realpathSync(process.cwd());const ProjectConfig = require('./dist/project.config.json');const previewPath = path.resolve(appDirectory, './preview.jpg'); (async () => { try { const project = new ci.Project({ appid: ProjectConfig.appid, type: "miniProgram", projectPath: path.resolve(appDirectory, './dist'), privateKeyPath: argv.p, ignores: ["node_modules/**/*"], }) await ci.upload({ project, version: package.version, desc: package.versionDesc, setting: { …ProjectConfig.setting }, onProgressUpdate: console.log, }) await ci.preview({ project, version: package.version, desc: package.versionDesc, qrcodeFormat: "image", qrcodeOutputDest: previewPath, setting: { …ProjectConfig.setting }, onProgressUpdate: console.log, }) } catch (e) { console.error(e); process.exit(1); } })()
九、通知阶段
原理为直接发送请求,触发 webhook 后将发送预览二维码。关于企业微信 API 请参见 群机器人配置说明。
const md5File = require('md5-file')const axios = require('axios');const path = require('path');const argv = require('minimist')(process.argv.slice(2));const fs = require("fs");const appDirectory = fs.realpathSync(process.cwd()); const previewPath = path.resolve(appDirectory, './preview.jpg'); function sendQrCode (imageBase64, hash) { return axios({ headers: { "Content-Type": 'application/json' }, method: 'post', url: argv.u, data: { "msgtype": "image", "image": { "base64": imageBase64, "md5": hash } } });} (async () => { try { const imageData = fs.readFileSync(previewPath); const hash = md5File.sync(previewPath) const imageBase64 = imageData.toString("base64"); await sendQrCode(imageBase64, hash); } catch(e) { console.error(e); process.exit(1); }})()
当我们把代码上传,发布新版本之后,就会往企业微信群上发送一个预览二维码,通知群上的同事进行预览体验
十、更多扩展
版本号和版本说明没有集中管理,目前是读取 package.json 文件里的 version 和 versionDesc 参数。若需要进行版本控制,可以尝试通过 CODING 代码仓库的 tag 来管理版本,同时配置通过 tag 来触发构建。