miniprogram-ci

Use when the user wants to automate WeChat mini-program upload, preview, or npm packaging via CI/CD, generate deployment scripts, set up miniprogram-ci workflows, or create preview QR codes automatically. Trigger whenever the user mentions "上传小程序", "预览", "CI 部署", "miniprogram-ci", "自动化上传", "发布小程序版本", "生成预览二维码", "打包npm", "pack-npm", "构建npm依赖", "GitHub Actions 小程序", "pnpm 小程序部署", or asks to integrate WeChat mini-program with continuous integration pipelines (GitHub Actions, GitLab CI, etc.).

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "miniprogram-ci" with this command: npx skills add whinc/wechat-miniprogram-skills/whinc-wechat-miniprogram-skills-miniprogram-ci

微信小程序 CI 自动化

概述

本 skill 帮助生成可直接运行的 Node.js 脚本,用于实现小程序代码的自动预览、打包依赖、上传等操作。脚本基于用户项目配置参数化生成,支持 CI/CD 流水线集成(GitHub Actions、GitLab CI 等)。

核心职责:根据用户项目信息生成可重复执行的命令行脚本,用户执行生成的脚本完成实际的部署任务。


Step 1:收集必要信息

执行前先确认以下信息(如用户未提供则逐项询问):

1.1 操作类型

询问用户: "你需要哪种能力?"

操作说明适用场景
打包依赖(pack-npm)构建 npm 依赖至 miniprogram_npm 目录项目使用 npm 模块时需先执行
预览(preview)生成预览二维码,供开发/测试扫码体验开发阶段快速验证
上传(upload)上传代码至微信后台版本管理提测、发布新版本
多个组合同时生成多个脚本完整 CI 流程(先 pack-npm,再 preview/upload)

1.2 编译产物目录

小程序编译后的输出目录路径,miniprogram-ci 需要指向该目录。

常见路径:

  • Taro 项目:dist/
  • 原生项目:项目根目录或 miniprogram/
  • uni-app:dist/build/mp-weixin/

询问用户: "请确认小程序编译产物目录(默认 dist/):"

检查目录是否存在:

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

1.3 包管理器

询问用户(若无法判断): "项目使用哪个包管理器?"

也可通过以下方式自动判断:

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

1.4 现有脚本检查

检查 scripts/ 目录下是否已存在相关脚本:

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

Step 2:前置条件检查

2.1 安装 miniprogram-ci

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

若未安装,根据包管理器安装:

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

2.2 获取上传密钥

告知用户获取路径:

  1. 登录 微信公众平台
  2. 进入:开发管理 → 开发设置 → 小程序代码上传
  3. 点击「生成」下载密钥文件(private.*.key

安全提醒:

  • ❌ 密钥文件绝对不能提交到代码仓库
  • ✅ 在 .gitignore 中添加 *.keyprivate.*.key
  • ✅ 在 CI/CD 中使用 secrets 管理密钥内容

2.3 配置 IP 白名单

告知用户:

  • 微信公众平台 → 开发设置 → 小程序代码上传 → IP 白名单
  • 添加 CI 服务器的出口 IP
  • 本地开发可临时关闭白名单,但生产环境强烈建议开启

Step 3:创建脚本

根据用户选择的操作类型,创建对应脚本。以下模板使用环境变量读取敏感配置,支持 CI/CD 集成。

3.1 打包依赖脚本模板

若项目使用 npm 模块且用户需要打包依赖能力,创建 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 预览脚本模板

若用户需要预览能力,创建 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 上传脚本模板

若用户需要上传能力,创建 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:注册 npm scripts

package.jsonscripts 中添加(根据用户选择的操作):

{
  "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:环境变量配置指引

本地开发

创建 .env 文件(需配合 dotenv 或 shell source):

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

提醒用户:.env*.key 添加到 .gitignore

CI/CD 配置(GitHub Actions 示例)

以下提供两个版本,按包管理器选用:

npm / yarn 项目(简洁版)

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 项目(完整版,含重试与 Release)

此模板经过实际项目(pnpm + Taro)验证,包含所有坑的解决方案。

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 配置:

Secret 名称说明
MP_PRIVATE_KEY密钥文件完整内容cat private.wxXXXX.key 的输出
MP_APPIDwxXXXXXXXXXXXXXXXX小程序 AppID

常见错误处理

错误现象原因解决方案
invalid ipIP 不在白名单微信后台添加 IP 或临时关闭白名单
permission denied密钥无效或无权限重新生成密钥;检查是否有上传权限
project.config.json not found项目路径错误确认 MP_PROJECT_PATH 指向编译产物目录
Error: getaddrinfo ENOTFOUND网络问题检查代理设置或网络连接
上传后版本未出现robot 编号冲突不同任务使用不同 robot 编号
Cannot find module 'picocolors'(或 nanoid/non-securepnpm 严格依赖隔离导致 miniprogram-ci 内部依赖链断裂见下方「pnpm 项目特殊配置」
上传失败: undefined / timeoutGitHub Actions runner 到微信上传服务器跨境网络不稳定(60s 超时)upload.js 中加入重试逻辑,见下方模板
创建 Release 失败: HTTP 403GitHub Actions 默认 GITHUB_TOKENcontents: write 权限在 job 中显式声明 permissions: contents: write

pnpm 项目特殊配置

pnpm 默认启用严格的依赖隔离(symlink node_modules),会导致 miniprogram-ci 内部依赖(如 picocolorsnanoid/non-securecssnano)无法被正确解析,即使它们已被间接安装。

解决方案: 在项目根目录创建(或修改).npmrc,添加:

shamefully-hoist=true

然后必须重新生成 lockfile 才能生效(仅修改 .npmrc 不够):

rm pnpm-lock.yaml
CI=true pnpm install

注意:此配置会提升所有包到根 node_modules,行为类似 npm/yarn。若担心影响其他依赖,可使用更精细的 public-hoist-pattern[] 配置,但实践中对 miniprogram-ci 需要大量条目,不如直接使用 shamefully-hoist=true


脚本参数速查

preview.js

环境变量必填说明
MP_APPID小程序 AppID
MP_PRIVATE_KEY_PATH密钥文件路径
MP_PROJECT_PATH编译产物目录
MP_ROBOT机器人编号(默认 1)
MP_PAGE_PATH预览打开的页面
MP_SEARCH_QUERY页面查询参数

upload.js

参数必填说明
--version <v>版本号
--desc <d>版本描述
--pack-npm上传前执行 npm 构建

安全检查清单

在交付脚本前,提醒用户确认:

  • *.key.env 已添加到 .gitignore
  • 生产环境已开启 IP 白名单(或 CI 使用固定出口 IP 的 self-hosted runner)
  • CI/CD 中密钥通过 secrets 管理,而非明文
  • ci-artifacts/ 目录已添加到 .gitignore(如包含敏感日志)
  • pnpm 项目:.npmrc 已添加 shamefully-hoist=true 并重新生成 lockfile(解决 miniprogram-ci 内部依赖缺失)
  • GitHub Actions workflow 已声明 permissions: contents: write(如需创建 Release)
  • upload.js 包含超时重试逻辑(应对跨境网络不稳定,超时 err.message 可能为 "timeout""undefined" 或空字符串)

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

Coding

miniprogram-automation

No summary provided by upstream source.

Repository SourceNeeds Review
General

taro documentation

No summary provided by upstream source.

Repository SourceNeeds Review
General

ahooks

No summary provided by upstream source.

Repository SourceNeeds Review
General

formily core fundamentals

No summary provided by upstream source.

Repository SourceNeeds Review
miniprogram-ci | V50.AI