如何用 GitHub Actions 持续部署微信和支付宝小程序

使用微信官方 miniprogram-ci 和支付宝官方 minidev,把小程序构建、检查与版本上传接入 GitHub Actions。方案不依赖具体框架,原生项目和跨端框架都能使用。

微信官方的 miniprogram-ci 和支付宝官方的 minidev 都可以在命令行里上传小程序代码。把它们接入 GitHub Actions 后,主分支每次更新都能自动构建,并分别上传微信开发版本和支付宝体验版。

下面这套配置不依赖具体框架。原生小程序可以用,Taro、uni-app、MPX 等跨端项目也可以用;只要最后能得到平台认识的构建目录即可。

部署流程

这套流程只关心构建产物,不关心源码使用什么框架:

git push
  -> 安装依赖
  -> 测试和构建
  -> 微信:miniprogram-ci 上传开发版本
  -> 支付宝:minidev 上传体验版

原生项目可以直接上传源码目录。跨端项目需要先执行各平台的构建命令,再上传对应的编译目录。测试和构建可以共用一个 job,微信、支付宝上传则适合拆成两个并行 job,互不影响。

常见差异只有两项:

项目类型 构建命令 上传目录
原生小程序 可以省略 平台工程配置文件所在目录
Taro 项目中的平台构建命令 通常在 dist
uni-app 项目中的平台构建命令 以项目配置的输出目录为准
MPX 项目中的跨端构建命令 例如 dist/wxdist/ali

部署脚本不需要理解这些框架。微信上传目录里要有 project.config.json,支付宝上传目录里要有 mini.project.json

安装官方上传工具

在项目中安装 miniprogram-ciminidev

npm install --save-dev --save-exact miniprogram-ci minidev

建议锁定明确版本,并提交 lockfile。持续集成环境应该安装已经审核过的依赖解析结果,不要在部署时临时升级依赖。

项目还需要相应的构建命令。例如:

{
  "scripts": {
    "test": "your_test_command_here",
    "build:miniprograms": "your_cross_platform_build_command_here"
  }
}

只部署一个平台时,构建命令当然也可以只构建一个平台。原生小程序不需要编译时,可以删除构建脚本和 workflow 里的对应步骤。

生成微信代码上传密钥

登录微信公众平台,进入 开发管理 -> 开发设置 -> 小程序代码上传,生成代码上传密钥。

下载得到的文件是 PEM 私钥:

-----BEGIN PRIVATE KEY-----
private_key_content_here
-----END PRIVATE KEY-----

密钥拥有预览和上传代码的权限。不要把它放进仓库,也不要输出到 Actions 日志。

同一页面还可以配置 IP 白名单。普通 GitHub-hosted runner 没有固定出口 IP,本教程使用 ubuntu-latest,因此需要关闭微信代码上传 IP 白名单。需要保留白名单时,请改用具有固定出口 IP 的 runner。

这是安全性与维护成本之间的选择。关闭白名单后,私钥成为主要凭证,更应该限制它的使用范围。

生成支付宝开发工具密钥

支付宝这里最容易卡住的不是密钥本身,而是入口藏得有点深。

登录支付宝开放平台并进入控制台后,不要在某个小程序的开发设置里反复翻。正确路径在页面右上角:

  1. 点击右上角头像
  2. 进入 账户中心
  3. 进入 密钥管理
  4. 选择 开发工具密钥
  5. 生成身份密钥并下载 config.json

尤其是 账户中心 这一步,不点头像很难找到。知道路径以后不过几次点击,不知道时却足够在控制台里绕上几圈。

下载得到的是完整的工具身份文件,大致如下:

{
  "alipay": {
    "authentication": {
      "toolId": "...",
      "privateKey": "-----BEGIN PRIVATE KEY-----..."
    }
  }
}

后面配置 ALIPAY_IDENTITY_KEY 时,要保存这个 config.json完整内容,不能只复制 privateKey。它也不是支付接口使用的应用私钥,两种密钥不要混在一起。

同一个支付宝账号同时只有一份开发工具密钥有效。重新生成密钥,或者在其他电脑执行 minidev login,都会让旧密钥失效。CI 使用固定密钥文件,正是为了避免每台 runner 都扫码登录。

顺手说一句,支付宝小程序目前本身不收费,对个人项目很友好。流量确实不算多,后台也谈不上好用,还有一套“小程序分”常常打得让人摸不着头脑。不过免费平台肯给上传接口,也就没什么可抱怨的了。

配置 GitHub Environment

打开仓库的 Settings -> Environments,分别创建 wechat-uploadalipay-upload 两个 Environment。只部署一个平台时,创建对应的一个即可。

wechat-upload 中添加:

  • Variable WECHAT_APPID: 微信小程序 AppID
  • Secret WECHAT_PRIVATE_KEY: 完整的 PEM 私钥内容

alipay-upload 中添加:

  • Variable ALIPAY_APP_ID: 支付宝小程序 AppID
  • Secret ALIPAY_IDENTITY_KEY: 下载得到的 config.json 完整内容

Environment 不是服务器。它用于管理部署变量、Secrets 和审批规则。上传任务声明对应的 environment 后,才能读取其中的值。

如果仓库不需要环境隔离,也可以使用 Repository Variables 和 Repository Secrets。两种方式都能运行,Environment 的权限边界更清楚。

封装仓库内的上传 Action

把上传逻辑放进仓库内的 Composite Action,可以让 workflow 保持简短,也便于补充参数校验和测试。

微信 Action

创建目录:

.github/actions/wechat-upload/
  action.yml
  upload.cjs

action.yml 声明 AppID、私钥和构建目录:

name: WeChat Mini Program Upload
description: Upload a built WeChat Mini Program with miniprogram-ci.

inputs:
  appid:
    description: WeChat Mini Program AppID.
    required: true
  private-key:
    description: Content of the code upload private key.
    required: true
  project-path:
    description: Directory containing project.config.json.
    required: true

runs:
  using: composite
  steps:
    - shell: bash
      env:
        INPUT_APPID: ${{ inputs.appid }}
        INPUT_PRIVATE_KEY: ${{ inputs.private-key }}
        INPUT_PROJECT_PATH: ${{ inputs.project-path }}
      run: node "$GITHUB_ACTION_PATH/upload.cjs"

Action 通过当前 step 的环境变量读取 Secret,不把私钥放进命令参数。

upload.cjs 调用微信官方接口:

const fs = require('node:fs')
const os = require('node:os')
const path = require('node:path')
const ci = require('miniprogram-ci')

const root = process.cwd()
const projectPath = path.resolve(process.env.INPUT_PROJECT_PATH)
const { version } = require(path.join(root, 'package.json'))
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'wechat-ci-'))
const keyPath = path.join(tempDir, 'private.key')
const privateKey = process.env.INPUT_PRIVATE_KEY.replace(/\\n/g, '\n')

fs.writeFileSync(keyPath, privateKey, { mode: 0o600 })

const project = new ci.Project({
  appid: process.env.INPUT_APPID,
  type: 'miniProgram',
  projectPath,
  privateKeyPath: keyPath
})

创建项目对象后执行上传,并在结束时删除临时密钥:

;(async () => {
  try {
    await ci.upload({
      project,
      version,
      desc: `Actions #${process.env.GITHUB_RUN_NUMBER} ` +
        `(${process.env.GITHUB_SHA.slice(0, 7)})`,
      robot: 1,
      setting: {},
      onProgressUpdate: console.log
    })
  } finally {
    fs.rmSync(tempDir, { recursive: true, force: true })
  }
})().catch((error) => {
  console.error(error)
  process.exitCode = 1
})

版本号直接读取 package.jsonversion。GitHub run number 和 commit SHA 放进版本描述,用来区分同一产品版本的不同构建。

实际项目还可以增加这些校验:

  • 检查 project.config.json 是否存在
  • 检查配置文件中的 AppID 是否与 Action 输入一致
  • 限制 CI 机器人编号为 130
  • 把版本号和描述压成单行

上传失败应该直接结束任务。不要捕获错误后继续返回成功状态。

支付宝 Action

支付宝 Action 的结构相同,只是输入换成 AppID、身份密钥和支付宝构建目录:

.github/actions/alipay-upload/
  action.yml
  upload.cjs

action.yml 把输入转成当前 step 的环境变量:

name: Alipay Mini Program Upload
description: Upload a built Alipay Mini Program with minidev.

inputs:
  appid:
    required: true
  identity-key:
    required: true
  project-path:
    required: true

runs:
  using: composite
  steps:
    - shell: bash
      env:
        INPUT_APPID: ${{ inputs.appid }}
        INPUT_IDENTITY_KEY: ${{ inputs.identity-key }}
        INPUT_PROJECT_PATH: ${{ inputs.project-path }}
      run: node "$GITHUB_ACTION_PATH/upload.cjs"

upload.cjsALIPAY_IDENTITY_KEY 的完整内容写进临时文件,再交给官方 minidev

const fs = require('node:fs')
const os = require('node:os')
const path = require('node:path')
const { minidev } = require('minidev')

const root = process.cwd()
const projectPath = path.resolve(process.env.INPUT_PROJECT_PATH)
const { version } = require(path.join(root, 'package.json'))
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'alipay-ci-'))
const identityKeyPath = path.join(tempDir, 'config.json')

fs.writeFileSync(identityKeyPath, process.env.INPUT_IDENTITY_KEY, { mode: 0o600 })

;(async () => {
  try {
    await minidev.upload({
      appId: process.env.INPUT_APPID,
      clientType: 'alipay',
      project: projectPath,
      identityKeyPath,
      version,
      versionDescription: `Actions #${process.env.GITHUB_RUN_NUMBER}`,
      experience: true
    })
  } finally {
    fs.rmSync(tempDir, { recursive: true, force: true })
  }
})().catch((error) => {
  console.error(error)
  process.exitCode = 1
})

experience: true 会在上传成功后把该版本设为支付宝体验版。这个账号需要有对应权限,否则可以先去掉该参数,只上传版本。

编写部署 workflow

创建 .github/workflows/deploy-miniprograms.yml。工作流只响应主分支推送和手动触发,不响应 Pull Request(PR)。先做一次测试和双端构建,再把产物交给两个上传 job:

name: Deploy Mini Programs

on:
  push:
    branches:
      - main
  workflow_dispatch:

permissions:
  contents: read

jobs:
  validate:
    runs-on: ubuntu-latest
    timeout-minutes: 20
    steps:
      - uses: actions/checkout@v7

      - uses: actions/setup-node@v6
        with:
          node-version: lts/*
          cache: npm

      - run: npm ci
      - run: npm test --if-present
      - run: npm run build:miniprograms

      - uses: actions/upload-artifact@v7
        with:
          name: wechat-miniprogram
          path: dist/wx

      - uses: actions/upload-artifact@v7
        with:
          name: alipay-miniprogram
          path: dist/ali

  upload-wechat:
    needs: validate
    runs-on: ubuntu-latest
    environment: wechat-upload
    concurrency:
      group: wechat-upload
      cancel-in-progress: false
    steps:
      - uses: actions/checkout@v7
      - uses: actions/setup-node@v6
        with:
          node-version: lts/*
          cache: npm
      - run: npm ci

      - uses: actions/download-artifact@v8
        with:
          name: wechat-miniprogram
          path: dist/wx

      - name: Upload WeChat development version
        uses: ./.github/actions/wechat-upload
        with:
          appid: ${{ vars.WECHAT_APPID }}
          private-key: ${{ secrets.WECHAT_PRIVATE_KEY }}
          project-path: dist/wx

  upload-alipay:
    needs: validate
    runs-on: ubuntu-latest
    environment: alipay-upload
    concurrency:
      group: alipay-upload
      cancel-in-progress: false
    steps:
      - uses: actions/checkout@v7
      - uses: actions/setup-node@v6
        with:
          node-version: lts/*
          cache: npm
      - run: npm ci

      - uses: actions/download-artifact@v8
        with:
          name: alipay-miniprogram
          path: dist/ali

      - name: Upload Alipay experience version
        uses: ./.github/actions/alipay-upload
        with:
          appid: ${{ vars.ALIPAY_APP_ID }}
          identity-key: ${{ secrets.ALIPAY_IDENTITY_KEY }}
          project-path: dist/ali

把构建命令以及 dist/wxdist/ali 换成项目自己的配置。使用 pnpm 或 Yarn 时,也只需要替换安装、缓存和脚本命令。

两个上传 job 各自使用独立的 Environment 和并发组。连续推送时,同一平台的上传会排队,微信与支付宝之间则可以并行。

验证部署

提交 workflow、Action 和上传脚本后,推送到 main

git add .github package.json package-lock.json
git commit -m "feat: deploy Mini Programs with GitHub Actions"
git push origin main

打开仓库的 Actions 页面,依次确认:

  1. 依赖安装成功
  2. 测试和双端构建成功
  3. 微信上传步骤显示 miniprogram-ci 编译与上传进度
  4. 支付宝上传步骤显示 minidev 构建与上传结果
  5. 微信后台出现新的开发版本,支付宝后台出现新的体验版

微信上传提示 IP 不在白名单时,检查微信公众平台的 IP 白名单设置。支付宝提示身份校验失败时,先确认 ALIPAY_IDENTITY_KEY 保存的是完整 config.json,再检查开发工具密钥是否被重新生成过。

部署边界

miniprogram-ci.upload() 对应微信开发者工具里的“上传”,只生成微信后台开发版本。minidev.upload() 会上传支付宝版本;示例中的 experience: true 还会把它设为体验版。两者都没有自动提审或正式发布。

开发版本上传适合自动执行。提审和发布会直接影响用户,还涉及版本说明、隐私能力和审核材料,保留人工确认更稳妥。

一条可靠的流水线不必包办所有事情。它应该把重复劳动拿走,把真正需要判断的地方留给人。

参考资料