ci: 添加小程序自动部署配置

- 添加 Gitea Actions workflow: miniapp-preview.yml
- 添加 CI 脚本: upload-ci.cjs, preview-ci.cjs
- 添加版本解析: resolve-version.cjs, resolve-upload-desc.cjs
- 添加 mini/package.json 用于 miniprogram-ci 依赖
This commit is contained in:
gouki
2026-08-06 09:20:43 +00:00
parent a3e6aa9c01
commit 9c178e6d49
6 changed files with 534 additions and 0 deletions
+73
View File
@@ -0,0 +1,73 @@
#!/usr/bin/env node
const fs = require("node:fs");
const path = require("node:path");
const process = require("node:process");
const ci = require("miniprogram-ci");
function requireEnvironment(name) {
const value = process.env[name];
if (!value) {
throw new Error(`${name} is required.`);
}
return value;
}
async function main() {
const projectPath = process.cwd();
const privateKeyPath = path.resolve(
requireEnvironment("WECHAT_CI_PRIVATE_KEY_PATH"),
);
const qrcodeOutputDest = path.resolve(
requireEnvironment("MINIAPP_QR_OUTPUT"),
);
const appid = requireEnvironment("MINIAPP_APPID");
const robot = Number.parseInt(process.env.MINIAPP_ROBOT ?? "1", 10);
const versionLabel = requireEnvironment("MINIAPP_VERSION_LABEL");
if (!fs.existsSync(path.join(projectPath, "project.config.json"))) {
throw new Error("The working directory must contain project.config.json.");
}
if (!fs.existsSync(privateKeyPath)) {
throw new Error("The WeChat code-upload key file does not exist.");
}
if (!Number.isInteger(robot) || robot < 1 || robot > 30) {
throw new Error("MINIAPP_ROBOT must be an integer from 1 to 30.");
}
const project = new ci.Project({
appid,
type: "miniProgram",
projectPath,
privateKeyPath,
ignores: ["node_modules/**/*", ".git/**/*"],
});
await ci.preview({
project,
desc: `Automated preview ${versionLabel}`.slice(0, 50),
robot,
setting: {
useProjectConfig: true,
es6: true,
es7: true,
enhance: true,
minify: true,
autoPrefixWXSS: true,
},
qrcodeFormat: "image",
qrcodeOutputDest,
onProgressUpdate: (progress) => {
if (typeof progress === "string") {
console.log(progress);
}
},
});
console.log(`Preview QR code created: ${qrcodeOutputDest}`);
}
main().catch((error) => {
console.error(`Mini Program preview failed: ${error.message}`);
process.exitCode = 1;
});
+151
View File
@@ -0,0 +1,151 @@
/**
* Build the WeChat upload "项目备注" from recent change summaries.
* Prefer recent git commit subjects (Chinese commit messages in this repo);
* fall back to the latest CHANGELOG bullets when git history is unavailable.
*/
const fs = require("node:fs");
const path = require("node:path");
const { execFileSync } = require("node:child_process");
const DEFAULT_MAX_LENGTH = 100;
function cleanCommitSubject(subject) {
return String(subject || "")
.trim()
.replace(
/^(feat|fix|docs|style|refactor|perf|test|chore|ci|build|revert)(\([^)]*\))?:\s*/i,
"",
)
.replace(/\s+/g, " ")
.trim();
}
function truncateDesc(text, maxLength = DEFAULT_MAX_LENGTH) {
const value = String(text || "").trim();
if (!value) {
return "";
}
if ([...value].length <= maxLength) {
return value;
}
const chars = [...value].slice(0, Math.max(0, maxLength - 1));
return `${chars.join("")}`;
}
function collectRecentCommitSubjects(repoRoot, limit = 5) {
try {
const output = execFileSync(
"git",
[
"log",
`-${Math.max(1, limit * 3)}`,
"--pretty=%s",
"--",
"mini/",
"CHANGELOG.md",
"Features.md",
],
{
cwd: repoRoot,
encoding: "utf8",
stdio: ["ignore", "pipe", "ignore"],
},
);
const subjects = [];
const seen = new Set();
for (const line of output.split("\n")) {
const cleaned = cleanCommitSubject(line);
if (!cleaned || seen.has(cleaned)) {
continue;
}
// Skip pure docs/chore noise when better subjects exist.
if (/^(说明|文档|记录)/.test(cleaned) && subjects.length > 0) {
continue;
}
seen.add(cleaned);
subjects.push(cleaned);
if (subjects.length >= limit) {
break;
}
}
return subjects;
} catch {
return [];
}
}
function collectRecentChangelogBullets(repoRoot, limit = 5) {
const changelogPath = path.join(repoRoot, "CHANGELOG.md");
if (!fs.existsSync(changelogPath)) {
return [];
}
const lines = fs.readFileSync(changelogPath, "utf8").split(/\r?\n/);
const bullets = [];
const seen = new Set();
for (const line of lines) {
const match = line.match(/^\s*-\s+(.+)\s*$/);
if (!match) {
continue;
}
const bullet = match[1].replace(/\s+/g, " ").trim();
if (!bullet || seen.has(bullet)) {
continue;
}
seen.add(bullet);
bullets.push(bullet);
if (bullets.length >= limit) {
break;
}
}
return bullets;
}
function resolveUploadDesc({
repoRoot,
versionLabel = "",
maxLength = DEFAULT_MAX_LENGTH,
maxItems = 3,
} = {}) {
const root = repoRoot || process.cwd();
const fromGit = collectRecentCommitSubjects(root, maxItems);
const fromChangelog = collectRecentChangelogBullets(root, maxItems);
const parts = fromGit.length > 0 ? fromGit : fromChangelog;
if (parts.length === 0) {
const shortSha = String(versionLabel || "")
.trim()
.slice(0, 7);
return truncateDesc(
shortSha ? `CI upload ${shortSha}` : "CI upload",
maxLength,
);
}
// Fit as many summaries as possible within the WeChat remark limit.
const selected = [];
for (const part of parts) {
const candidate = [...selected, part].join("");
if ([...candidate].length > maxLength) {
if (selected.length === 0) {
return truncateDesc(part, maxLength);
}
break;
}
selected.push(part);
}
return truncateDesc(selected.join(""), maxLength);
}
module.exports = {
cleanCommitSubject,
collectRecentChangelogBullets,
collectRecentCommitSubjects,
resolveUploadDesc,
truncateDesc,
};
+95
View File
@@ -0,0 +1,95 @@
/**
* Resolve the WeChat Mini Program upload version.
*
* WeChat requires x.y.z with each segment in 0..999.
* Server and miniapp versions are independent on purpose.
*
* Priority:
* 1. MINIAPP_VERSION_OVERRIDE — full manual version (rare / release pinning)
* 2. MINIAPP_BUILD_NUMBER — auto version from CI run number
* 3. package.json version — local / fallback
*/
const fs = require("node:fs");
const path = require("node:path");
function readPackageVersion(projectPath) {
const packageJson = JSON.parse(
fs.readFileSync(path.join(projectPath, "package.json"), "utf8"),
);
if (!packageJson.version) {
throw new Error("package.json version is missing.");
}
return String(packageJson.version);
}
function parseSemverParts(version) {
const parts = String(version)
.trim()
.split(".")
.map((part) => Number.parseInt(part, 10));
if (
parts.length < 1 ||
parts.some((part) => !Number.isInteger(part) || part < 0 || part > 999)
) {
throw new Error(
`Invalid version "${version}". WeChat requires x.y.z with each segment 0-999.`,
);
}
return {
major: parts[0] ?? 0,
minor: parts[1] ?? 0,
patch: parts[2] ?? 0,
};
}
/**
* Encode a monotonic CI build number into WeChat x.y.z.
* Keeps package.json major; puts build into minor.patch with base 100.
* Examples (major=1): build 99 → 1.0.99, build 100 → 1.1.0, build 101 → 1.1.1
*/
function versionFromBuildNumber(major, buildNumber) {
if (!Number.isInteger(buildNumber) || buildNumber < 1) {
throw new Error("MINIAPP_BUILD_NUMBER must be a positive integer.");
}
if (!Number.isInteger(major) || major < 0 || major > 999) {
throw new Error("package.json major version must be an integer 0-999.");
}
const minor = Math.floor(buildNumber / 100) % 1000;
const patch = buildNumber % 100;
return `${major}.${minor}.${patch}`;
}
/** @param {Record<string, string | undefined>} [env] */
function resolveVersion(projectPath, env = process.env) {
const override = (env.MINIAPP_VERSION_OVERRIDE || "").trim();
if (override) {
parseSemverParts(override);
return override;
}
const packageVersion = readPackageVersion(projectPath);
const { major } = parseSemverParts(packageVersion);
const buildRaw = (env.MINIAPP_BUILD_NUMBER || "").trim();
if (buildRaw) {
const buildNumber = Number.parseInt(buildRaw, 10);
return versionFromBuildNumber(major, buildNumber);
}
// Legacy fixed override — prefer not to use; keeps version stuck across uploads.
const legacy = (env.MINIAPP_VERSION || "").trim();
if (legacy) {
parseSemverParts(legacy);
return legacy;
}
return packageVersion;
}
module.exports = {
parseSemverParts,
resolveVersion,
versionFromBuildNumber,
};
+108
View File
@@ -0,0 +1,108 @@
#!/usr/bin/env node
const fs = require("node:fs");
const path = require("node:path");
const process = require("node:process");
const ci = require("miniprogram-ci");
const { resolveVersion } = require("./resolve-version.cjs");
const { resolveUploadDesc } = require("./resolve-upload-desc.cjs");
function requireEnvironment(name) {
const value = process.env[name];
if (!value) {
throw new Error(`${name} is required.`);
}
return value;
}
async function main() {
const projectPath = process.cwd();
const repoRoot = path.resolve(projectPath, "..");
const privateKeyPath = path.resolve(
requireEnvironment("WECHAT_CI_PRIVATE_KEY_PATH"),
);
const appid = requireEnvironment("MINIAPP_APPID");
const robot = Number.parseInt(process.env.MINIAPP_ROBOT ?? "1", 10);
const versionLabel = requireEnvironment("MINIAPP_VERSION_LABEL");
const version = resolveVersion(projectPath);
const resultPath = path.resolve(
process.env.MINIAPP_UPLOAD_RESULT ||
path.join(projectPath, "ci-artifacts", "upload-result.json"),
);
if (!fs.existsSync(path.join(projectPath, "project.config.json"))) {
throw new Error("The working directory must contain project.config.json.");
}
if (!fs.existsSync(privateKeyPath)) {
throw new Error("The WeChat code-upload key file does not exist.");
}
if (!Number.isInteger(robot) || robot < 1 || robot > 30) {
throw new Error("MINIAPP_ROBOT must be an integer from 1 to 30.");
}
const project = new ci.Project({
appid,
type: "miniProgram",
projectPath,
privateKeyPath,
ignores: ["node_modules/**/*", ".git/**/*", "ci-artifacts/**/*"],
});
const desc = resolveUploadDesc({
repoRoot,
versionLabel,
maxLength: Number.parseInt(process.env.MINIAPP_UPLOAD_DESC_MAX || "100", 10),
maxItems: Number.parseInt(process.env.MINIAPP_UPLOAD_DESC_ITEMS || "3", 10),
});
console.log(`Uploading Mini Program version=${version} robot=${robot}`);
console.log(`Upload remark: ${desc}`);
const uploadResult = await ci.upload({
project,
version,
desc,
robot,
setting: {
useProjectConfig: true,
es6: true,
es7: true,
enhance: true,
minify: true,
autoPrefixWXSS: true,
},
onProgressUpdate: (progress) => {
if (typeof progress === "string") {
console.log(progress);
}
},
});
fs.mkdirSync(path.dirname(resultPath), { recursive: true });
fs.writeFileSync(
resultPath,
`${JSON.stringify(
{
appid,
version,
robot,
desc,
versionLabel,
uploadedAt: new Date().toISOString(),
result: uploadResult,
},
null,
2,
)}\n`,
);
console.log(`Upload succeeded: version ${version}`);
console.log(`Upload result saved: ${resultPath}`);
console.log(
"If this robot upload is already selected as 体验版 in WeChat admin, it becomes the new experience version automatically. Otherwise open 版本管理 → 开发版本 → 选为体验版 once.",
);
}
main().catch((error) => {
console.error(`Mini Program upload failed: ${error.message}`);
process.exitCode = 1;
});
+14
View File
@@ -0,0 +1,14 @@
{
"name": "lunar-mini",
"version": "1.0.0",
"description": "祈福小助手 - 万年历微信小程序",
"private": true,
"scripts": {
"test": "echo \"No tests yet\" && exit 0",
"upload": "node ci/upload-ci.cjs",
"preview": "node ci/preview-ci.cjs"
},
"devDependencies": {
"miniprogram-ci": "^2.0.0"
}
}