持续集成 每日优鲜便利购微信小程序集成 Jenkins 生成二维码发版

一个便利购QA · 2018年07月02日 · 最后由 phobal 回复于 2020年04月26日 · 5025 次阅读
本帖已被设为精华帖!


每日优鲜便利购微信小程序集成 Jenkins 生成二维码发版
微信小程序的测试发版在没有 CI/CD 等相关工具的情况下,存在诸多问题,为了让前端小程序的测试发版过程更加严谨可控化,我们采用了 Jenkins 集成微信开发者工具,来解决当前遇到的问题,首先给大家展示一下集成 jenkins 前后带来的差异。

下面我们给大家演示一下具体的实现过程:
1、安装 jenkins
Jenkins 依赖于 Java 运行环境,因此需要首先安装 Java。
安装 Jenkins 的方式有多种,可以运行对应系统类型的安装包,可以通过 docker 获取镜像,也可以直接运行 war 包。
个人倾向于直接运行 war 包的形式,只需下载 jenkins.war 后,运行如下命令即可启动 Jenkins。如果不指定 httpPort,Jenkins 的默认端口为 8080。

2、Jenkins 主要涉及的插件:
GIT plugin
SSH Credentials Plugin
Git Changelog Plugin: 获取仓库提交的 commit log
build-name-setter:用于修改 Build 名称
description setter plugin:用于在修改 Build 描述信息,在描述信息中增加显示 QRCode(二维码)
Post-Build Script Plug-in:在编译完成后通过执行脚本实现一些额外功能
NodeJS:小程序构建需要,在更改提交状态时使用 node 技术
AnsiColor:日志输出有颜色,脚本中有设置 log 格式类似:33[字符背景颜色;字符颜色 m{String}33[0m
安装方式也比较简单,直接在 Jenkins 的插件管理页面搜索上述插件,点击安装即可。
3、创建项目(job),第一次创建自由风格的 job 即可,后续可作为模板,针对具体的项目复制该模板,修改少量配置信息,即可将这一套持续集成发版平台运行起来
(1)在 jenkins 中创建一个自由风格类型的 job

(2)参数的传递
在构建脚本中,我们已经对常用的可配置参数实现了可传参机制,在 jenkins 的配置如下:
参数一:选项参数类型的,build_type 用来区分是线上后台域还是测试后台域

参数二:字符参数类型,build_branch 用于区分分支的版本

参数三:字符参数类型:work_palt 用于配置项目所在路径

参数四:文本参数类型:desc 用于描述上传代码时的备注,默认设置备注为发布人员信息

参数五:选项参数类型:feature 用于命令上传代码时指定上传版本的自增
refactor 为主版本,update 为次版本,debug 为修改版,根据选项来对版本自增

最终展示状态为:

(3)源码管理,git 配置:配置 git 路径,分支可配,支持在每次拉取代码时先清理工作空间

(4)构建环境:
Add timestamps to the Console Output:在控制台添加时间戳
Color ANSI Console Output 对输出的文本颜色进行控制
Provide Node & npm bin/ folder to PATH 配置 node.js
Build User Vars Plugin 是 jenkins 用户相关变量插件,使得在构建过程中可以使用用户相关环境变量,例如代码提交时设置的用户信息
(5)构建:
执行 shell:

mp_deploy.sh

#!/bin/bash
msg() {
    printf '%b\n' "$1" >&2
}

info()
{
    msg "[INFO] $1"
}

success() {
    msg "\e[1;32m[✔] ${1}${2} \33[0m "
}

notice() {
    msg "\e[1;33m ${1} \e[0m"
}

error_exit() {
    msg "\e[1;31m[✘] ${1}${2} \33[0m"
    exit 1
}

exec_cmd()
{
  echo "[执行命令] $1"
  $1
  if [ "$?" != "0" ]; then
    error_exit "命令执行失败: 错误码为 $?"
  fi
}

# sed匹配hosts.js内容,替换服务端环境
change_hosts() 
{
    if [ -f "hosts.js" ]; then
        case $build_type in 
            "dev")
                target_env="dev"
                echo "${work_palt}"/hosts.js
                sed -i "" "s/^const curr_env = .*/const curr_env = '$target_env'/" ${work_palt}"/hosts.js"
                info "切换到 ${target_env} 环境"
            ;;
            "prod")
                target_env="prod"
                echo "${work_palt}"/hosts.js
                sed -i "" "s/^const curr_env = .*/const curr_env = '$target_env'/" ${work_palt}"/hosts.js"
                info "切换到 ${target_env} 环境"
            ;;
        esac
        if [ "$?" != "0" ]; then
            error_exit "切换环境失败!"
        fi
    else
        error_exit "没有找到hosts.js文件!"
    fi
}

# 根据feature参数增加版本号,上传到微信小程序后台准备提审
# 用node程序直接读取version.js修改版本号
upload_for_release() 
{
    if [ -f ${work_palt}"/version.js" ]; then
        exec_cmd "node /Users/qa/.jenkins/workspace/ops-server/tmp/mp/upload.js ${work_palt} $feature $desc"
    else
        error_exit "没有找到version.js文件!"
    fi
}

# 生成开发版二维码
# 这里直接执行小程序cli的命令
uplaod_for_preview()
{
    exec_cmd "/Applications/wechatwebdevtools.app/Contents/Resources/app.nw/bin/cli -o"
    port=$(cat "/Users/qa/Library/Application Support/微信web开发者工具/Default/.ide")
    echo "微信开发者工具运行在${port}端口"
    echo "调用http://127.0.0.1:${port}/open"
    return_code=$(curl -sL -w %{http_code} http://127.0.0.1:${port} -o /open)
    if [ $return_code == 200 ]; then
        echo "返回状态码200,devtool启动成功!"
    else
    echo "返回状态码${return_code},devtool启动失败"
        exit 1
    fi
  exec_cmd "pwd"
    exec_cmd "wget  -O  $BUILD_ID.png http://127.0.0.1:${port}/preview?projectpath=${work_palt}"
}

if [ "$build_type" == "dev" ]; then
  info "发布开发版!"
  change_hosts
  #生成二维码
  uplaod_for_preview
  success "预览成功!请扫描二维码进入开发版!"
elif [ "$build_type" == 'prod' ]; then
  info "准备上传!"
  change_hosts
  #提交代码微信公众平台
  upload_for_release
  success "上传成功!请到微信小程序后台设置体验版并提交审核!"
else
    error_exit "需要设置合法的build_type!"
fi

uploda.js:

var fs = require('fs');
var cp = require('child_process');
var args = process.argv;  // 获取命令行带入的参数 args[0]是程序执行的目录 args[1]是文件名

const succ_code = 0;
const err_code = 1;

function err_msg(str) {
    console.log('[NODE ERR] ', str);
}

function info_msg(str) {
    console.log('[NODE INFO] ', str);
}

function exec_cmd(cmd, cb) {
    info_msg('执行命令: ' + cmd)
    // 使用child_process在终端执行命令
    cp.exec(cmd, function(err, stdout, stderr) {
        var succ = true;
        if (err) {
            // 命令执行异常,退出程序
            err_msg(err);
            if (stderr) {
                err_msg(stderr);
            }
            succ = false;
        }   else {
            // 打印所执行命令的标准输出
            info_msg(stdout);
            if (stderr) {
                err_msg(stderr);
            }
        }
        typeof cb === 'function' && cb(succ);
    })
}

try {
    var path = args[2];
    var feature = args[3];
    var desc = '【RELEASE】' + args[4];

    var ver_path = path + '/version.js';
    var version_conf = require(ver_path);

    // 根据feature参数确定版本号增加的逻辑
    switch(feature) {
        case 'debug': {
            version_conf.debug++;
            break;
        }
        case 'update': {
            version_conf.update++;
            version_conf.debug = 0;
            break;
        }
        case 'refactor': {
            version_conf.refactor++;
            version_conf.update = 0;
            version_conf.debug = 0;
            break;
        }
        default:
            err_msg('未设置正确的版本特性!');
            process.exit(err_code);
    }
    // 生成version.js所需的文件内容,方便小程序代码直接读取
    var str = 'module.exports = {' + '\n\trefactor: ' + version_conf.refactor + ',\n\tupdate: ' + version_conf.update + ',\n\tdebug: ' + version_conf.debug + '\n}';
    // 写入文件(fs.writeFile() 以w模式打开文件,写入的内容会覆盖原有内容)
    fs.writeFile(ver_path, str, function(err) {
        if (err) {
            err_msg(err);
            process.exit(err_code);
        } else {
            info_msg('UPDATE VERSION SUCCESS!');
            info_msg('Build feature is: ' + feature);
            info_msg('Current version is: ' + version_conf.refactor + '.' + version_conf.update + '.' + version_conf.debug);
            var ver = version_conf.refactor + '.' + version_conf.update + '.' + version_conf.debug;
            // 写入正常,执行小程序cli命令上传
            var upload_cmd = '/Applications/wechatwebdevtools.app/Contents/Resources/app.nw/bin/cli -u ' + ver + '@' + path + ' --upload-desc ' + desc;
            exec_cmd(upload_cmd, function(ok) {
                process.exit(ok ? succ_code : err_code);
            });
        }
    });
} catch(error) {
    // 捕获异常,退出程序
    err_msg(error);
    process.exit(err_code);
}

(6)构建后操作
description setter plugin 插件,构建完成后设置当次 build 的二维码采用 HTML 的 img 标签,将<img src='二维码png路径'><a href='二维码png路径'>写入到build描述信息中。例如:<img src="http://机器ip:端口/job/项目名称/ws/${BUILD_ID}.png" alt="二维码${BUILD_ID}" width="200" height="200" /><a href="http://机器ip:端口/job/项目名称/ws/${BUILD_ID}.png">二维码${BUILD_ID}</a>`

展示二维码图片:
Jenkins 出于安全的考虑,所有描述信息的 Markup Formatter 默认都是采用 Plain text 模式,在这种模式下是不会对 build 描述信息中的 HTML 编码进行解析的。
Manage Jenkins -> Configure Global Security,将 Markup Formatter 的设置更改为 Safe HTML 即可。更改配置后,我们就可以在 build 描述信息中看到采用 HTML 的 img 标签插入的图片了。

对于一个持续集成打包平台,每次打包都由 4 步组成:触发构建、拉取代码、执行构建、构建后处理。对应的,在每个 Job 中也对应了这几项的配置。
4、脚本简易说明:
生成二维码操作步骤:1.获取微信开发者工具启动端口:在微信开发者工具的安装目录找到.ide 文件获取;2.使用命令方式打开微信开发者工具;3.通过开发者工具发送 http 请求生成二维码,下载并保存为 png 图片到指定目录
代码上传:配置 version 文件,自动升级版本,通过小程序 cli 命令上传代码到微信平台进行提审。

5、相关文件准备,注意事项说明:
项目代码中需要能提供 hosts.js,upload.js,project.config.json 文件
微信开发者工具需要先登录,且有该项目的开发者权限,以此保证二维码的生成
(1)host.js 文件说明:提供两个节点 dev 和 prod;dev 是测试环境后台域名配置;prod 是线上后台域名配置,例如:
const curr_env = 'dev'
const hosts = {
dev: {
hostSmart: 'http://.具体环境 (如:beta)..com',
hostDc: 'https://.cn' //
},
prod: {
hostSmart: 'http://
.生成环境..com',
hostDc: 'https://
.cn' //
}
}
module.exports = {
getHosts: function () {
return hosts[curr_env]
}
}
(2)version.js 文件说明:发布时小程序的版本说明:版本会在 jenkins 中根据本次发布情况自动升级,version,js 文件每次发布均会自动根据选择升级版本
已有版本的按照 master 已有版本配置,没有版本的主版本 refactor 配置为 1,次版本 update 为 0,修改版 debug 本为 0
module.exports = {
refactor: 0,
update: 0,
debug: 0
}
(3)project.config.json 文件,开发者工具在小程序代码处自动生成,在使用开发者工具的 http 接口生成二维码时需要读该文件的配置。

共收到 14 条回复 时间 点赞
思寒_seveniruby 将本帖设为了精华贴 07月02日 22:37

确实很实用,赞赞赞

@nicesunday 生成的二维码有有效期的吧?

麦子 回复

微信开发者工具生成的有时限,这个原理是一样的,我们都用 tag 保证二维码的唯一性

弱弱的问下楼主:代码用的是 SVN jenkins 配置会更简单不?感觉只配二维码就可以了

这是在 Mac 上做的吧。有没有办法在 Liunx 上做。。。

请问参数三:work_palt 的默认值后面马赛克的部分除了有项目名字还有什么信息?

脚本文件 mp_deploy.sh 和 uploda.js 都是放在哪的?

谢谢,一直也想做这块,看来具有可行性

10楼 已删除

请教下,wget 这步我一直返回 400 错误是为什么,怎么解决需要用户登录的问题呢?

"wget  -O  $BUILD_ID.png http://127.0.0.1:${port}/preview?projectpath=/Users/xxxx/xxxx"

已发出 HTTP 请求,正在等待回应... 400 Bad Request
2018-07-23 19:22:18 错误 400:Bad Request。
非言 回复

因为没有登录 谢谢楼主的方案,我做了一个简化版 可行

@donson 可以放在 windows,因为是基于微信工具搞的。
另外一个 mac 系统的 macmini 其实就够了,无线相关的 app 打包必备。

楼主好,这个构建机器上必须装开发工具吗?是不是只支持 Windows 或 Mac

gaofei 回复

工具就只有 windows 和 Mac 的

simple 专栏文章:[精华帖] 社区历年精华帖分类归总 中提及了此贴 12月13日 14:44
simple [精彩盘点] TesterHome 社区 2018 年 度精华帖 中提及了此贴 01月07日 12:08

微信官方推出了一个命令行工具 https://www.npmjs.com/package/miniprogram-ci,全平台支持,和现有发布系统集成很方便

需要 登录 后方可回复, 如果你还没有账号请点击这里 注册