miniprogram-ci 实战指南:把小程序上传做成一条命令

Posted on Sat 14 November 2026 in 技术 • Sucre Liu

本文基于 miniprogram-ci 2.1.31 编写。它是微信官方从开发者工具中抽离出来的编译模块,让我们可以在没有图形界面的服务器上完成小程序代码的编译、预览与上传。

一、它解决什么问题

日常开发中,「上传体验版」这件事一直绑在微信开发者工具上:打开工具、点上传、填版本号、等进度条。一旦团队要做自动化——合并到主干自动出包、提测自动生成二维码、每晚自动跑包体积检查——这条路就走不通了。

miniprogram-ci 把这部分能力做成了 Node 模块,同时提供命令行版本。它能做:

  • 上传代码(对应开发者工具的「上传」)
  • 预览代码(生成体验二维码)
  • 构建 npm(对应「工具 → 构建 npm」)
  • 获取本地编译后的代码包,排查体积异常
  • 拉取最近上传版本的 sourceMap
  • 代码质量扫描、静态依赖分析
  • 上传云函数、静态网站、云存储,创建云托管版本
  • 小游戏 jsserver 上传、多端 Apk/Ipa 构建

它不能做的是:提交审核、发布上线、灰度、回退版本。这些仍然要去公众平台人工操作。所以定位要清楚——CI 负责把包送到微信后台,人负责发布决策。

二、准备工作

2.1 安装

全局安装用于命令行调用:

npm install -g miniprogram-ci
miniprogram-ci --help

项目内安装用于脚本调用:

npm install miniprogram-ci --save-dev

Node 版本建议 18 及以上。包体积较大(解压后约 20MB),首次安装稍慢属正常。

2.2 获取代码上传密钥

用小程序管理员账号登录「微信公众平台 → 开发 → 开发设置 → 小程序代码上传」,生成并下载密钥,文件名形如 private.wx1234567890.key。

三个要点:

  1. 密钥拥有预览和上传权限,等同于上传能力,不要提交到代码仓库。
  2. 平台不保存明文,遗失只能重置。
  3. 第三方平台代开发场景从 1.0.28 起同样支持,调用方式不变。

2.3 配置 IP 白名单

仍在「小程序代码上传」页面,把 CI 机器的公网出口 IP 加进去。没有白名单,preview 和 upload 一律失败,报错通常是权限或 IP 不合法。

云厂商的 NAT 出口 IP 可能漂移,建议固定弹性 IP,或把出口网段一次性加全。

2.4 规划 robot 编号

robot 取值 1~30,与开发者工具共用。它的作用是在后台「版本管理」里区分是谁上传的。实践建议:

robot 用途
1 测试环境自动构建
2 预发环境
3 生产发布
4~30 各开发者本地调试

编号冲突不会报错,但排查问题时会很痛苦。

三、核心对象 Project

所有 API 的第一个参数都是 Project 实例:

const ci = require('miniprogram-ci')

const project = new ci.Project({
  appid: 'wx1234567890abcdef',
  type: 'miniProgram',
  projectPath: './dist',
  privateKeyPath: './private.key',
  ignores: ['node_modules/**/*'],
})
字段 必填 说明
appid 是 小程序 / 小游戏 appid
projectPath 是 project.config.json 所在目录
privateKey / privateKeyPath 二选一 密钥内容或密钥文件路径
type 否 miniProgram / miniProgramPlugin / miniGame / miniGamePlugin,默认 miniProgram
ignores 否 排除规则,glob 写法

三个容易踩的点:

  • projectPath 不是源码根目录,而是构建产物里 project.config.json 所在的目录。用 Taro / uni-app 时通常是 dist/ 或 dist/dev/mp-weixin/。
  • 创建时机:Project 在实例化时扫描文件树。必须等 npm run build 结束后再 new,否则会报「找不到文件」。若文件在实例化后才变化,调用 project.updateFiles() 刷新。
  • ignores 只影响打包,不影响本地编译。node_modules 一定要排除,否则包体积会莫名其妙涨几 MB。

四、上传代码

const result = await ci.upload({
  project,
  version: '1.2.0',
  desc: 'feat: 新增订单详情页',
  robot: 1,
  setting: { useProjectConfig: true },
  threads: 4,
  onProgressUpdate: console.log,
})

console.log(result.subPackageInfo)

参数说明:

参数 说明
version 必填,自定义版本号
desc 备注,会显示在后台版本列表
setting 编译设置,见第五节
robot 1~30
threads 本地编译线程数
useCOS 异步上传;代码包超过 5MB 时默认开启
onProgressUpdate 进度回调,适合打日志

返回值里 subPackageInfo 最有用:数组元素 name 为 __FULL__ 表示整包,__APP__ 表示主包,其余为分包名,size 是字节数。把它打印进 CI 日志,包体积变化一目了然。

版本号建议用 v1.2.0+构建号 或 git tag。微信允许同版本号重复上传,但后台会堆出多条同名记录,回溯困难。

五、编译设置 setting

setting 不传就按默认;传 useProjectConfig: true 表示继承 project.config.json,下列字段可单独覆盖:

键 说明
useProjectConfig true 时以项目配置为基准
es7 对应「将 JS 编译为 ES5」,es6 已废弃
minify 上传时压缩全部代码
minifyJS / minifyWXML / minifyWXSS 分项压缩,优先级高于 minify
codeProtect 上传时进行代码保护
autoPrefixWXSS 上传时样式自动补全
compileWorklet 编译 worklet 代码
disableUseStrict 增强编译下禁用严格模式

推荐组合:

// 生产包
setting: { useProjectConfig: true, es7: true, minify: true, codeProtect: true }

// 调试包:关压缩,保留可读堆栈
setting: { useProjectConfig: true, es7: true, minify: false, codeProtect: false }

注意 es6 只是历史遗留,效果等同 es7,不必再写。

六、预览代码

预览生成一个体验二维码,扫码即可在真机打开:

await ci.preview({
  project,
  desc: 'PR #128 预览',
  robot: 1,
  qrcodeFormat: 'image',
  qrcodeOutputDest: './qrcode.png',
  pagePath: 'pages/order/detail',
  searchQuery: 'id=10086&from=ci',
  scene: 1011,
})
参数 说明
qrcodeFormat image / base64 / terminal,默认 terminal
qrcodeOutputDest 二维码保存路径,必填
pagePath 启动页面路径
searchQuery 启动参数,命令行里 & 要写成 \&
scene 场景值,默认 1011
bigPackageSizeSupport 预览时主包、分包上限放宽到 4M

CI 中的典型用法:在 PR 评论里贴二维码图片,测试同学直接扫码验收,不必等打包同事。

七、构建 npm

对应开发者工具的「构建 npm」:

const warnings = await ci.packNpm(project, {
  ignores: ['**/__tests__/**'],
  reporter: (infos) => console.log(infos),
})

返回的 warnings 是数组,每项含 msg、code、jsPath、startLine、endLine,可以格式化成易读的告警列表。

当 npm 依赖不在小程序项目内(monorepo 常见),用 packNpmManually:

await ci.packNpmManually({
  packageJsonPath: '../lib/package.json',
  miniprogramNpmDistDir: './dist/',
})

它会读取指定 package.json 对应的 node_modules,把 miniprogram_npm 构建到目标目录。

流水线里的顺序是固定的:

npm ci → npm run build → pack-npm → preview / upload

顺序错了,上传的包里就没有 miniprogram_npm。

八、排查包体积与质量问题

8.1 拿到最终代码包

当后台提示「代码包超出限制」,但你在源码里怎么找都找不到大文件时:

const compiled = await ci.getCompiledResult({
  project,
  version: '1.2.0',
  setting: { useProjectConfig: true },
}, './compiled.zip')

第二个参数是 zip 保存路径,返回值是 Record<string, string | Buffer>。解开 zip 直接看最终上传的文件清单,比猜快得多。

8.2 静态依赖分析

const report = await ci.analyseCode(project)

输出 files 与 modules 两张表。每个模块有唯一 id(形如 Page:pages/index/index),通过 parentDeps 和 deps 描述依赖关系。适合定位「谁引用了这个 300KB 的库」。

8.3 代码质量扫描

const result = await ci.checkCodeQuality(project)

这个接口不读取私钥,可以放心在本地或任意 CI 里跑。返回数组,每项包含 name、success、text、docURL、detail。

11 个检查项:

检查项 关注点
PACKAGE_SIZE_LIMIT 主包 / 分包体积
JS_COMPRESS_OPEN JS 是否压缩
WXML_COMPRESS_OPEN WXML 是否压缩
WXSS_COMPRESS_OPEN WXSS 是否压缩
LAZYCODE_LOADING_OPEN 是否开启按需注入
IMAGE_AND_AUDIO_LIMIT 图片音频体积
CONTAINS_OTHER_PKG_JS 主包里只被单个分包引用的 JS
CONTAINS_OTHER_PKG_COMPONENTS 主包里只被单个分包引用的组件
CONTAINS_UNUSED_PLUGINS 未使用的插件
CONTAINS_UNUSED_COMPONENTS 未使用的组件
CONTAINS_UNUSED_CODES 无依赖文件

把它接成 CI 闸门:任意一项 success === false 就失败,能挡住绝大多数「包越做越大」的回归。

8.4 拉取 sourceMap

后台错误监控里的堆栈是压缩后的,需要 sourceMap 还原:

await ci.getDevSourceMap({
  project,
  robot: 1,
  sourceMapSavePath: './sourcemap.zip',
})

它取的是该 robot 最近一次上传的版本,所以要在上传成功后立刻拉取并归档。

九、云开发相关能力

云函数上传:

await ci.cloud.uploadFunction({
  project,
  env: 'prod-1g2h3j4k',
  name: 'orderNotify',
  path: './cloudfunctions/orderNotify',
  remoteNpmInstall: true,
})

remoteNpmInstall: true 时云端装依赖,不上传 node_modules;false 则全量上传。生产建议 true,包更小、构建更稳定。

定时触发器:

await ci.cloud.createTimeTrigger({
  project,
  envId: 'prod-1g2h3j4k',
  functionName: 'orderNotify',
  triggersConfig: [{
    name: 'dailyReport',
    type: 'timer',
    config: '0 0 2 * * * *',
  }],
})

注意:目前只支持一个触发器,数组只能填一项。

静态网站与云存储:

await ci.cloud.uploadStaticStorage({
  project, env: 'prod-1g2h3j4k',
  path: './web/dist', remotePath: '/',
})

await ci.cloud.uploadStorage({
  project, env: 'prod-1g2h3j4k',
  path: './assets', remotePath: '/assets',
})

云托管新建版本参数较多(CPU、内存、副本数、扩缩容阈值、端口等),适合做成发布脚本,不在流水线里逐次手调。

十、命令行模式

不想写 Node 脚本时,CLI 更直接:

# 上传
miniprogram-ci upload \
  --pp ./dist/ \
  --pkp ./private.key \
  --appid wx1234567890abcdef \
  --uv 1.2.0 \
  -r 1 \
  --use-project-config true

# 预览
miniprogram-ci preview \
  --pp ./dist/ \
  --pkp ./private.key \
  --appid wx1234567890abcdef \
  --uv 1.2.0 \
  -r 1 \
  --qrcode-format image \
  --qrcode-output-dest /tmp/qrcode.png

# 质量检查
miniprogram-ci check-code-quality \
  --pp ./dist/ \
  --pkp ./private.key \
  --appid wx1234567890abcdef \
  --save-path ./quality.json

# 构建 npm
miniprogram-ci pack-npm --pp ./dist/ --pkp ./private.key --appid wx1234567890abcdef

常用缩写:

参数 含义
--pp projectPath
--pkp privateKeyPath
--uv upload version
-r robot 编号
--use-project-config 继承项目配置

子命令还有 get-compiled-result、get-dev-source-map、pack-npm-manually、cloud functions upload、cloud staticstorage upload、cloud storage upload、cloud container create。

十一、接入 GitHub Actions

一个可直接用的流水线:打 tag 时自动构建、检查、上传。

name: miniprogram-ci

on:
  push:
    tags: ['v*']

jobs:
  release:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: npm

      - run: npm ci
      - run: npm run build

      - name: 写入上传密钥
        run: echo "${{ secrets.MP_PRIVATE_KEY }}" > private.key

      - name: 构建 npm
        run: npx miniprogram-ci pack-npm --pp ./dist --appid ${{ secrets.MP_APPID }} --pkp ./private.key

      - name: 包体积与质量检查
        run: npx miniprogram-ci check-code-quality --pp ./dist --appid ${{ secrets.MP_APPID }} --pkp ./private.key --save-path quality.json

      - name: 上传体验版
        run: npx miniprogram-ci upload --pp ./dist --appid ${{ secrets.MP_APPID }} --pkp ./private.key --uv ${GITHUB_REF_NAME#v} -r 1 --use-project-config true

      - name: 清理密钥
        if: always()
        run: rm -f private.key

几个约定:

  • 密钥放 Secrets,用 echo 写入临时文件,job 结束前删掉。
  • 出口 IP 必须提前加进白名单;GitHub 托管 runner 的 IP 会变,建议用自建 runner 或固定出口。
  • 版本号直接取 tag,去掉前缀 v。
  • 质量检查放上传之前,不通过就不进入上传步骤。

十二、常见问题排查

1. invalid ip / 权限不足

白名单没配,或出口 IP 变了。先 curl ifconfig.me 确认当前出口,再回后台核对。

2. 编译时报找不到文件

Project 实例化早于构建完成。把 new ci.Project() 移到 npm run build 之后。

3. 包体积突然变大

先 getCompiledResult 解包看清单,再跑 checkCodeQuality 看 PACKAGE_SIZE_LIMIT 和 CONTAINS_UNUSED_*。常见原因:node_modules 没被 ignores 排除、source map 被一起打包、图片没走 CDN。

4. 上传后小程序里没有 miniprogram_npm

packNpm 没执行,或执行顺序在 build 之前。注意 packNpm 写在 build 之后。

5. 命令行里 searchQuery 传不进去

& 被 shell 吃掉了,写成 \& 或整体加引号。

6. 多流水线互相覆盖版本

不同环境用不同 robot;版本号带环境前缀,如 test-1.2.0-45、prod-1.2.0。

7. 代理环境上传超时

miniprogram-ci 会依次读 HTTPS_PROXY、https_proxy、HTTP_PROXY、http_proxy、npm 的 proxy 配置。也可以显式指定 --proxy,或调用 ci.proxy()。务必把 servicewechat.com 加进 no_proxy,否则请求会被代理拦截。

8. 进程跑完不退出

旧版本问题,2.1.5 起已修复;若仍遇到,检查是否有未关闭的句柄,并在脚本末尾显式 process.exit(0)。

十三、能力边界与最佳实践

能做与不能做,前面已经说过。这里给几条落地建议:

  1. 上传不等于发布。CI 的终点是「体验版就绪」,审核与发布永远留人工一步。
  2. 包体积设闸门。checkCodeQuality 接进流水线,比事后优化便宜得多。
  3. 密钥最小暴露。只在需要的步骤落盘,用完即删,禁止打进镜像。
  4. 版本号可追溯。tag + commit sha,后台记录和代码仓库能一一对上。
  5. sourceMap 归档。上传成功立刻拉取,按版本号存到对象存储,线上报错才有得查。
  6. robot 分环境。测试、预发、生产各用各的,日志和版本列表才不会混。

小结

miniprogram-ci 的价值不在于「多了一个上传接口」,而在于把小程序的交付过程变成了可编排的代码。一旦上传、预览、包体积检查都进了流水线,团队就不必再为「谁传的包」「为什么这次大了 300KB」浪费时间。

先把 preview 跑通,再接 checkCodeQuality,最后接 upload——按这个顺序落地,问题最容易定位。


参考:miniprogram-ci 官方 README(v2.1.31)与微信开放文档。