SuperAPI AI Directory

miniprogram-ci

General & other

View repository

Updated 10-15 23:43

About this skill

WeChat Mini Program CI automation

Overview

This skill helps generate directly runnable Node.js scripts for operations such as automatic preview, dependency packaging, and upload of Mini Program code. The scripts are generated parameterized based on the user's project configuration and support CI/CD pipeline integration (GitHub Actions, GitLab CI, etc.).

Core responsibilities: Generate repeatable command-line scripts based on the user's project information; the user executes the generated scripts to complete the actual deployment tasks.


Step 1: Collect necessary information

Before execution, first confirm the following information (if the user has not provided it, ask item by item):

1.1 Operation type

Ask the user: "Which capability do you need?"

Operation Description Applicable scenario
Package dependencies (pack-npm) Build npm dependencies into the miniprogram_npm directory Must be executed first when the project uses npm modules
Preview (preview) Generate a preview QR code for development/testing scan-code trial Quick verification during development
Upload (upload) Upload code to WeChat backend version management Submit for testing, release a new version
Multiple combinations Generate multiple scripts at the same time Complete CI process (pack-npm first, then preview/upload)

1.2 Compilation artifact directory

The output directory path after Mini Program compilation,miniprogram-ci needs to point to this directory.

Common paths:

  • Taro project:dist/
  • Native project: project root directory or miniprogram/
  • uni-app:dist/build/mp-weixin/

Ask the user: "Please confirm the Mini Program compilation artifact directory (default dist/):"

Check whether the directory exists:

ls <编译产物目录>/project.config.json 2>/dev/null || echo "目录不存在或缺少 project.config.json"

1.3 Package manager

Ask the user (if it cannot be determined): "Which package manager does the project use?"

It can also be automatically determined in the following ways:

[ -f pnpm-lock.yaml ] && echo "pnpm" || \
[ -f yarn.lock ]      && echo "yarn" || \
echo "npm"

1.4 Existing script check

Check whether related scripts already exist under the scripts/ directory:

ls scripts/preview.js scripts/upload.js scripts/ci-*.js 2>/dev/null || echo "需要创建"

Step 2: Prerequisite check

2.1 Install miniprogram-ci

ls node_modules/miniprogram-ci 2>/dev/null && echo "已安装" || echo "未安装"

If not installed, install according to the package manager:

# pnpm
pnpm add miniprogram-ci --save-dev
# npm
npm install miniprogram-ci --save-dev
# yarn
yarn add miniprogram-ci --dev

2.2 Obtain the upload key

Tell the user the path to obtain it:

  1. Log in to WeChat Official Accounts Platform
  2. Go to: Development Management → Development Settings → Mini Program Code Upload
  3. Click "Generate" to download the key file (private.*.key)

Security reminder:

  • ❌ Key fileabsolutely must notbe committed to the code repository
  • ✅ In .gitignore add *.key and private.*.key
  • ✅ Use secrets in CI/CD to manage key contents

2.3 Configure the IP whitelist

Tell the user:

  • WeChat Official Accounts Platform → Development Settings → Mini Program Code Upload → IP Whitelist
  • Add the egress IP of the CI server
  • For local development, the whitelist can be temporarily disabled, but for the production environmentit is strongly recommended to enable it

Step 3: Create the script

Create the corresponding script according to the operation type selected by the user. The following template uses environment variables to read sensitive configurations, supporting CI/CD integration.

3.1 Packaging Dependencies Script Template

If the project uses npm modules and the user needspackaging dependenciescapability, create scripts/pack-npm.js:

#!/usr/bin/env node

/**
 * 微信小程序 NPM 打包脚本
 * 使用 miniprogram-ci 构建 npm 依赖至 miniprogram_npm 目录
 *
 * 环境变量:
 *   MP_APPID        - 小程序 AppID(必填)
 *   MP_PROJECT_PATH - 编译产物目录(必填)
 *
 * 用法:
 *   node scripts/pack-npm.js
 */

const ci = require('miniprogram-ci');
const fs = require('fs');
const path = require('path');

// ─────────────────────────────────────────────────────────────────────────────
// 配置
// ─────────────────────────────────────────────────────────────────────────────

const CONFIG = {
  appid: process.env.MP_APPID,
  projectPath: process.env.MP_PROJECT_PATH,
};

// ─────────────────────────────────────────────────────────────────────────────
// 工具函数
// ─────────────────────────────────────────────────────────────────────────────

function validateConfig() {
  const required = { MP_APPID: CONFIG.appid, MP_PROJECT_PATH: CONFIG.projectPath };
  const missing = Object.entries(required).filter(([, v]) => !v).map(([k]) => k);
  if (missing.length) {
    console.error(`❌ 缺少环境变量: ${missing.join(', ')}`);
    process.exit(1);
  }
  if (!fs.existsSync(path.resolve(CONFIG.projectPath))) {
    console.error(`❌ 项目路径不存在: ${CONFIG.projectPath}`);
    process.exit(1);
  }
}

// ─────────────────────────────────────────────────────────────────────────────
// 主流程
// ─────────────────────────────────────────────────────────────────────────────

async function main() {
  console.log('🔍 校验配置...');
  validateConfig();

  console.log('\n📋 NPM 打包配置:');
  console.log(`   AppID:       ${CONFIG.appid}`);
  console.log(`   项目路径:    ${path.resolve(CONFIG.projectPath)}`);

  const project = new ci.Project({
    appid: CONFIG.appid,
    type: 'miniProgram',
    projectPath: path.resolve(CONFIG.projectPath),
    ignores: ['node_modules/**/*'],
  });

  console.log('\n🚀 打包依赖...');
  try {
    const result = await ci.packNpm(project, {
      reporter: (msg) => console.log(`   ${msg}`),
    });

    console.log('\n✅ NPM 打包完成!');
    console.log(`📦 结果: ${JSON.stringify(result)}`);
  } catch (err) {
    console.error(`\n❌ NPM 打包失败: ${err.message}`);
    process.exit(1);
  }
}

main();

3.2 Preview Script Template

If the user needspreviewcapability, create scripts/preview.js:

#!/usr/bin/env node

/**
 * 微信小程序预览脚本
 * 使用 miniprogram-ci 生成预览二维码
 *
 * 环境变量:
 *   MP_APPID            - 小程序 AppID(必填)
 *   MP_PRIVATE_KEY_PATH - 上传密钥路径(必填)
 *   MP_PROJECT_PATH     - 编译产物目录(必填)
 *   MP_ROBOT            - 机器人编号 1-30(默认 1)
 *
 * 可选环境变量:
 *   MP_PAGE_PATH        - 预览打开的页面路径
 *   MP_SEARCH_QUERY     - 页面查询参数
 *
 * 用法:
 *   node scripts/preview.js
 *   MP_PAGE_PATH=pages/detail/index MP_SEARCH_QUERY="id=123" node scripts/preview.js
 */

const ci = require('miniprogram-ci');
const fs = require('fs');
const path = require('path');

// ─────────────────────────────────────────────────────────────────────────────
// 配置
// ─────────────────────────────────────────────────────────────────────────────

const CONFIG = {
  appid: process.env.MP_APPID,
  privateKeyPath: process.env.MP_PRIVATE_KEY_PATH,
  projectPath: process.env.MP_PROJECT_PATH,
  robot: parseInt(process.env.MP_ROBOT, 10) || 1,
  pagePath: process.env.MP_PAGE_PATH || '',
  searchQuery: process.env.MP_SEARCH_QUERY || '',
  outputDir: path.resolve(process.cwd(), 'ci-artifacts/previews'),
};

// ─────────────────────────────────────────────────────────────────────────────
// 工具函数
// ─────────────────────────────────────────────────────────────────────────────

function validateConfig() {
  const required = { MP_APPID: CONFIG.appid, MP_PRIVATE_KEY_PATH: CONFIG.privateKeyPath, MP_PROJECT_PATH: CONFIG.projectPath };
  const missing = Object.entries(required).filter(([, v]) => !v).map(([k]) => k);
  if (missing.length) {
    console.error(`❌ 缺少环境变量: ${missing.join(', ')}`);
    process.exit(1);
  }
  if (CONFIG.robot < 1 || CONFIG.robot > 30) {
    console.error('❌ MP_ROBOT 必须在 1-30 之间');
    process.exit(1);
  }
  if (!fs.existsSync(path.resolve(CONFIG.privateKeyPath))) {
    console.error(`❌ 密钥文件不存在: ${CONFIG.privateKeyPath}`);
    process.exit(1);
  }
  if (!fs.existsSync(path.resolve(CONFIG.projectPath))) {
    console.error(`❌ 项目路径不存在: ${CONFIG.projectPath}`);
    process.exit(1);
  }
}

function ensureDir(dir) {
  if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
}

function timestamp() {
  return new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
}

// ─────────────────────────────────────────────────────────────────────────────
// 主流程
// ─────────────────────────────────────────────────────────────────────────────

async function main() {
  console.log('🔍 校验配置...');
  validateConfig();

  ensureDir(CONFIG.outputDir);
  const qrcodePath = path.join(CONFIG.outputDir, `preview-${timestamp()}.png`);

  console.log('\n📋 预览配置:');
  console.log(`   AppID:       ${CONFIG.appid}`);
  console.log(`   项目路径:    ${path.resolve(CONFIG.projectPath)}`);
  console.log(`   机器人编号:  ${CONFIG.robot}`);
  if (CONFIG.pagePath) console.log(`   页面路径:    ${CONFIG.pagePath}`);
  if (CONFIG.searchQuery) console.log(`   查询参数:    ${CONFIG.searchQuery}`);
  console.log(`   二维码输出:  ${qrcodePath}`);

  const project = new ci.Project({
    appid: CONFIG.appid,
    type: 'miniProgram',
    projectPath: path.resolve(CONFIG.projectPath),
    privateKeyPath: path.resolve(CONFIG.privateKeyPath),
    ignores: ['node_modules/**/*'],
  });

  console.log('\n🚀 生成预览...');
  try {
    const result = await ci.preview({
      project,
      desc: `Preview by robot ${CONFIG.robot} at ${new Date().toLocaleString()}`,
      setting: { es6: true, es7: true, minify: true, autoPrefixWXSS: true },
      qrcodeFormat: 'image',
      qrcodeOutputDest: qrcodePath,
      robot: CONFIG.robot,
      ...(CONFIG.pagePath && { pagePath: CONFIG.pagePath }),
      ...(CONFIG.searchQuery && { searchQuery: CONFIG.searchQuery }),
    });

    console.log('\n✅ 预览成功!');
    console.log(`📱 二维码: ${qrcodePath}`);
    if (result?.subPackageInfo) {
      console.log('\n📦 包大小:');
      result.subPackageInfo.forEach(p => console.log(`   ${p.name || '主包'}: ${(p.size / 1024 / 1024).toFixed(2)} MB`));
    }
  } catch (err) {
    console.error(`\n❌ 预览失败: ${err.message}`);
    if (err.message.includes('invalid ip')) console.error('💡 请将当前 IP 添加到微信后台白名单');
    process.exit(1);
  }
}

main();

3.3 Upload Script Template

If the user needsuploadcapability, create scripts/upload.js:

#!/usr/bin/env node

/**
 * 微信小程序上传脚本
 * 使用 miniprogram-ci 上传代码至微信后台
 *
 * 环境变量:
 *   MP_APPID            - 小程序 AppID(必填)
 *   MP_PRIVATE_KEY_PATH - 上传密钥路径(必填)
 *   MP_PROJECT_PATH     - 编译产物目录(必填)
 *   MP_ROBOT            - 机器人编号 1-30(默认 1)
 *
 * 命令行参数:
 *   --version <版本号>  必填
 *   --desc <描述>       必填
 *   --pack-npm          可选,上传前执行 npm 构建
 *
 * 用法:
 *   node scripts/upload.js --version 1.0.0 --desc "修复登录问题"
 *   node scripts/upload.js --version 1.0.0 --desc "新功能" --pack-npm
 */

const ci = require('miniprogram-ci');
const fs = require('fs');
const path = require('path');

// ─────────────────────────────────────────────────────────────────────────────
// 配置
// ─────────────────────────────────────────────────────────────────────────────

const CONFIG = {
  appid: process.env.MP_APPID,
  privateKeyPath: process.env.MP_PRIVATE_KEY_PATH,
  projectPath: process.env.MP_PROJECT_PATH,
  robot: parseInt(process.env.MP_ROBOT, 10) || 1,
  outputDir: path.resolve(process.cwd(), 'ci-artifacts/uploads'),
};

// ─────────────────────────────────────────────────────────────────────────────
// 命令行解析
// ─────────────────────────────────────────────────────────────────────────────

function parseArgs() {
  const args = process.argv.slice(2);
  const result = { version: null, desc: null, packNpm: false };
  for (let i = 0; i < args.length; i++) {
    if (args[i] === '--version' && args[i + 1]) result.version = args[++i];
    else if (args[i] === '--desc' && args[i + 1]) result.desc = args[++i];
    else if (args[i] === '--pack-npm') result.packNpm = true;
    else if (args[i] === '--help' || args[i] === '-h') { printHelp(); process.exit(0); }
  }
  return result;
}

function printHelp() {
  console.log(`
用法: node scripts/upload.js [选项]

选项:
  --version <版本号>   必填,如 1.0.0
  --desc <描述>        必填,版本描述
  --pack-npm           上传前执行 npm 构建
  --help               显示帮助
`);
}

// ─────────────────────────────────────────────────────────────────────────────
// 工具函数
// ─────────────────────────────────────────────────────────────────────────────

function validateConfig() {
  const required = { MP_APPID: CONFIG.appid, MP_PRIVATE_KEY_PATH: CONFIG.privateKeyPath, MP_PROJECT_PATH: CONFIG.projectPath };
  const missing = Object.entries(required).filter(([, v]) => !v).map(([k]) => k);
  if (missing.length) {
    console.error(`❌ 缺少环境变量: ${missing.join(', ')}`);
    process.exit(1);
  }
  if (CONFIG.robot < 1 || CONFIG.robot > 30) {
    console.error('❌ MP_ROBOT 必须在 1-30 之间');
    process.exit(1);
  }
  if (!fs.existsSync(path.resolve(CONFIG.privateKeyPath))) {
    console.error(`❌ 密钥文件不存在: ${CONFIG.privateKeyPath}`);
    process.exit(1);
  }
  if (!fs.existsSync(path.resolve(CONFIG.projectPath))) {
    console.error(`❌ 项目路径不存在: ${CONFIG.projectPath}`);
    process.exit(1);
  }
}

function ensureDir(dir) {
  if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
}

function timestamp() {
  return new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
}

// ─────────────────────────────────────────────────────────────────────────────
// 上传(含超时重试)
// ─────────────────────────────────────────────────────────────────────────────

const MAX_RETRIES = 3;
const RETRY_DELAY_MS = 5000;

function sleep(ms) {
  return new Promise((resolve) => setTimeout(resolve, ms));
}

/**
 * 上传并在超时时自动重试
 * 微信上传服务器在 GitHub Actions 等 CI 环境下可能因跨境网络超时,
 * err.message 可为 "timeout"、"undefined" 或空字符串。
 */
async function uploadWithRetry(project, args) {
  for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
    try {
      if (attempt > 1) {
        console.log(`\n🔄 第 ${attempt} 次重试上传...`);
        await sleep(RETRY_DELAY_MS);
      }
      return await ci.upload({
        project,
        version: args.version,
        desc: args.desc,
        robot: CONFIG.robot,
        setting: { es6: true, es7: true, minify: true, autoPrefixWXSS: true },
        onProgressUpdate: (info) => { if (typeof info === 'string') console.log(`   ${info}`); },
      });
    } catch (err) {
      const errMsg = err.message || String(err);
      // 超时的 err.message 可能是 "timeout"、"undefined" 或空字符串
      const isTimeout = errMsg === 'timeout' || errMsg === 'undefined' || !errMsg;
      if (isTimeout && attempt < MAX_RETRIES) {
        console.warn(`\n⚠️  上传超时(第 ${attempt}/${MAX_RETRIES} 次),${RETRY_DELAY_MS / 1000}s 后重试...`);
        continue;
      }
      throw err;
    }
  }
}

function saveResult(result, args) {
  ensureDir(CONFIG.outputDir);
  const filename = `upload-${args.version}-${timestamp()}.json`;
  const filepath = path.join(CONFIG.outputDir, filename);
  const data = {
    timestamp: new Date().toISOString(),
    version: args.version,
    desc: args.desc,
    robot: CONFIG.robot,
    result,
  };
  fs.writeFileSync(filepath, JSON.stringify(data, null, 2));
  console.log(`📄 结果已保存: ${filepath}`);
}

// ─────────────────────────────────────────────────────────────────────────────
// 主流程
// ─────────────────────────────────────────────────────────────────────────────

async function main() {
  const args = parseArgs();

  if (!args.version) { console.error('❌ 必须指定 --version'); process.exit(1); }
  if (!args.desc) { console.error('❌ 必须指定 --desc'); process.exit(1); }

  console.log('🔍 校验配置...');
  validateConfig();

  console.log('\n📋 上传配置:');
  console.log(`   AppID:       ${CONFIG.appid}`);
  console.log(`   项目路径:    ${path.resolve(CONFIG.projectPath)}`);
  console.log(`   机器人编号:  ${CONFIG.robot}`);
  console.log(`   版本号:      ${args.version}`);
  console.log(`   版本描述:    ${args.desc}`);
  console.log(`   packNpm:     ${args.packNpm ? '是' : '否'}`);

  const project = new ci.Project({
    appid: CONFIG.appid,
    type: 'miniProgram',
    projectPath: path.resolve(CONFIG.projectPath),
    privateKeyPath: path.resolve(CONFIG.privateKeyPath),
    ignores: ['node_modules/**/*'],
  });

  if (args.packNpm) {
    console.log('\n📦 执行 npm 构建...');
    try {
      await ci.packNpm(project, { reporter: console.log });
      console.log('✅ npm 构建完成');
    } catch (err) {
      console.error(`❌ npm 构建失败: ${err.message}`);
      process.exit(1);
    }
  }

  console.log('\n🚀 上传代码...');
  try {
    const result = await uploadWithRetry(project, args);

    console.log('\n✅ 上传成功!');
    if (result?.subPackageInfo) {
      console.log('\n📦 包大小:');
      result.subPackageInfo.forEach(p => console.log(`   ${p.name || '主包'}: ${(p.size / 1024 / 1024).toFixed(2)} MB`));
    }
    saveResult({ success: true, ...result }, args);
  } catch (err) {
    console.error(`\n❌ 上传失败: ${err.message}`);
    if (err.message.includes('invalid ip')) console.error('💡 请将当前 IP 添加到微信后台白名单');
    saveResult({ success: false, error: err.message }, args);
    process.exit(1);
  }
}

main();

Step 4: Register npm scripts

In package.json of scripts add (according to the operation selected by the user):

{
  "scripts": {
    "ci:pack-npm": "node scripts/pack-npm.js",
    "ci:preview": "node scripts/preview.js",
    "ci:upload": "node scripts/upload.js",
    "ci:upload:npm": "node scripts/upload.js --pack-npm"
  }
}

Step 5: Environment Variable Configuration Guide

Local Development

Create the .env file (requires dotenv or shell source):

MP_APPID=wx1234567890abcdef
MP_PRIVATE_KEY_PATH=./private.wxXXXX.key
MP_PROJECT_PATH=./dist
MP_ROBOT=1

Remind the user: Add .env and *.key to .gitignore。

CI/CD Configuration (GitHub Actions Example)

Two versions are provided below, choose according to your package manager:

npm / yarn project (concise version)

name: Deploy Mini Program

on:
  push:
    branches:
      - main
  workflow_dispatch:

jobs:
  upload:
    runs-on: ubuntu-latest
    # ⚠️ 创建 GitHub Release 必须声明,否则报 HTTP 403
    permissions:
      contents: write

    steps:
      - uses: actions/checkout@v4

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: '20'

      - name: Install dependencies
        run: npm ci

      - name: Build
        run: npm run build

      - name: Generate version
        id: version
        run: echo "version=$(date +%Y%m%d).$(git rev-parse HEAD | cut -c1-6)" >> "$GITHUB_OUTPUT"

      - name: Write private key
        run: |
          echo "${{ secrets.MP_PRIVATE_KEY }}" > private.key
          chmod 600 private.key

      - name: Upload to WeChat
        env:
          MP_APPID: ${{ secrets.MP_APPID }}
          MP_PRIVATE_KEY_PATH: ./private.key
          MP_PROJECT_PATH: ./dist
          MP_ROBOT: 1
        run: npm run ci:upload -- --version "${{ steps.version.outputs.version }}" --desc "CI 自动上传"

      - name: Cleanup
        if: always()
        run: rm -f private.key

pnpm project (full version, including retry and Release)

This template has been verified in a real project (pnpm + Taro) and includes solutions to all pitfalls.

name: WeChat Mini Program CI

on:
  push:
    branches:
      - main
  # 支持手动触发并自定义描述
  workflow_dispatch:
    inputs:
      desc:
        description: '版本描述(留空则自动生成)'
        required: false
        default: ''

jobs:
  deploy:
    name: 测试 → 构建 → 上传微信后台
    runs-on: ubuntu-latest
    # ⚠️ 必须声明,否则创建 GitHub Release 会报 HTTP 403
    permissions:
      contents: write

    steps:
      - name: Checkout
        uses: actions/checkout@v4

      # ⚠️ pnpm 必须先于 setup-node 安装,否则 cache: 'pnpm' 会报错
      - name: Setup pnpm
        uses: pnpm/action-setup@v4
        with:
          version: 10

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'pnpm'

      - name: Install dependencies
        # ⚠️ 需要先提交 pnpm-lock.yaml,否则 --frozen-lockfile 会失败
        run: pnpm install --frozen-lockfile

      - name: Run tests
        run: pnpm test

      - name: Generate version
        id: version
        run: |
          DATE=$(date +%Y%m%d)
          COMMIT=$(git rev-parse HEAD | cut -c1-6)
          VERSION="${DATE}.${COMMIT}"
          echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
          echo "Generated version: ${VERSION}"

      - name: Build mini program
        run: pnpm build:weapp

      - name: Write private key
        run: |
          echo "${{ secrets.MP_PRIVATE_KEY }}" > private.key
          chmod 600 private.key

      - name: Upload to WeChat
        env:
          MP_APPID: ${{ secrets.MP_APPID }}
          MP_PRIVATE_KEY_PATH: ./private.key
          MP_PROJECT_PATH: ./dist
          MP_ROBOT: 1
        run: |
          VERSION="${{ steps.version.outputs.version }}"
          DESC="${{ github.event.inputs.desc }}"
          if [ -z "$DESC" ]; then DESC="CI 自动发布 ${VERSION}"; fi
          pnpm ci:upload -- --version "$VERSION" --desc "$DESC"

      - name: Cleanup private key
        if: always()
        run: rm -f private.key

      - name: Create GitHub Release
        if: github.event_name == 'push'
        env:
          GH_TOKEN: ${{ github.token }}
        run: |
          VERSION="${{ steps.version.outputs.version }}"
          COMMIT_MSG=$(git log -1 --pretty=format:"%s")
          gh release create "v${VERSION}" \
            --title "v${VERSION}" \
            --notes "## 发布内容

          - 版本号: \`${VERSION}\`
          - 提交信息: ${COMMIT_MSG}
          - 提交哈希: \`${{ github.sha }}\`

          > 已自动上传至微信小程序后台,请前往微信公众平台提交审核。" \
            --latest

GitHub Secrets configuration:

Secret name Value Description
MP_PRIVATE_KEY Full contents of the key file cat private.wxXXXX.key output
MP_APPID wxXXXXXXXXXXXXXXXX Mini Program AppID

Common Error Handling

Error symptom Cause Solution
invalid ip IP is not in the whitelist Add the IP in the WeChat backend or temporarily disable the whitelist
permission denied The key is invalid or has no permission Regenerate the key; check whether it has upload permission
project.config.json not found Incorrect project path Confirm that MP_PROJECT_PATH points to the build output directory
Error: getaddrinfo ENOTFOUND Network issue Check the proxy settings or network connection
The version does not appear after upload Robot number conflict Use different robot numbers for different tasks
Cannot find module 'picocolors'(or nanoid/non-secure) pnpm strict dependency isolation causes the internal dependency chain of miniprogram-ci to break See "Special Configuration for pnpm Projects" below
Upload failed: undefined / timeout Cross-border network between the GitHub Actions runner and the WeChat upload server is unstable (60s timeout) In upload.js add retry logic, see the template below
Failed to create Release: HTTP 403 By default, GitHub Actions GITHUB_TOKEN has no contents: write permission Explicitly declare in the job permissions: contents: write

Special Configuration for pnpm Projects

pnpm enables strict dependency isolation by default (symlink node_modules), which causes miniprogram-ci internal dependencies (such as picocolors、nanoid/non-secure、cssnano) cannot be resolved correctly, even if they have been installed indirectly.

Solution: Create (or modify) in the project root directory.npmrc, add:

shamefully-hoist=true

Thenthe lockfile must be regenerated for it to take effect (modifying only .npmrc is not enough):

rm pnpm-lock.yaml
CI=true pnpm install

Note: This configuration hoists all packages to the root node_modules, behaving like npm/yarn. If you are worried about affecting other dependencies, you can use the more fine-grained public-hoist-pattern[] configuration, but in practice miniprogram-ci requires a large number of entries, so it is better to just use shamefully-hoist=true。


Script parameter quick reference

preview.js

Environment variables Required Description
MP_APPID ✅ Mini Program AppID
MP_PRIVATE_KEY_PATH ✅ Key file path
MP_PROJECT_PATH ✅ Build output directory
MP_ROBOT ❌ Robot number (default 1)
MP_PAGE_PATH ❌ Page opened in preview
MP_SEARCH_QUERY ❌ Page query parameters

upload.js

Parameter Required Description
--version <v> ✅ Version number
--desc <d> ✅ Version description
--pack-npm ❌ Run npm build before upload

Security checklist

Before delivering the script, remind the user to confirm:

  • *.key and .env have been added to .gitignore
  • IP whitelist is enabled in the production environment (or CI uses a self-hosted runner with a fixed egress IP)
  • In CI/CD, secrets are managed via secrets rather than in plaintext
  • ci-artifacts/ directory has been added to .gitignore(if it contains sensitive logs)
  • pnpm project:.npmrc has added shamefully-hoist=true and regenerated the lockfile (to resolve missing internal dependencies of miniprogram-ci)
  • GitHub Actions workflow has declared permissions: contents: write(if a Release needs to be created)
  • upload.js includes timeout retry logic (to handle unstable cross-border networks; on timeout, err.message may be "timeout"、"undefined" or an empty string)