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:
@@ -0,0 +1,93 @@
|
||||
name: Publish Mini Program Dev Version
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- "mini/**"
|
||||
- ".gitea/workflows/miniapp-preview.yml"
|
||||
|
||||
jobs:
|
||||
publish:
|
||||
runs-on: new-pet-node
|
||||
steps:
|
||||
- uses: https://gitea.neatcn.com/gouki/action-checkout@v4
|
||||
with:
|
||||
fetch-depth: 20
|
||||
- name: Verify Node runtime
|
||||
run: node --version
|
||||
- name: Install dependencies
|
||||
working-directory: mini
|
||||
run: npm ci
|
||||
- name: Run tests
|
||||
working-directory: mini
|
||||
run: npm test -- --runInBand
|
||||
- name: Configure production API and app version
|
||||
working-directory: mini
|
||||
env:
|
||||
MINIAPP_BUILD_NUMBER: ${{ gitea.run_number }}
|
||||
MINIAPP_VERSION_OVERRIDE: ${{ vars.MINIAPP_VERSION_OVERRIDE }}
|
||||
run: |
|
||||
test -n "$MINIAPP_BUILD_NUMBER"
|
||||
VERSION="$(node -e 'const {resolveVersion}=require("./ci/resolve-version.cjs"); process.stdout.write(resolveVersion(process.cwd()))')"
|
||||
test -n "$VERSION"
|
||||
mkdir -p ci-artifacts
|
||||
printf '%s\n' "$VERSION" > ci-artifacts/resolved-version.txt
|
||||
echo "Resolved Mini Program version for build+upload: $VERSION"
|
||||
- name: Materialize the upload key
|
||||
env:
|
||||
WECHAT_CI_PRIVATE_KEY: ${{ secrets.WECHAT_CI_PRIVATE_KEY }}
|
||||
run: |
|
||||
test -n "$WECHAT_CI_PRIVATE_KEY"
|
||||
umask 077
|
||||
printf '%s' "$WECHAT_CI_PRIVATE_KEY" > "$RUNNER_TEMP/wechat-ci.key"
|
||||
- name: Upload WeChat development version
|
||||
working-directory: mini
|
||||
env:
|
||||
MINIAPP_APPID: ${{ vars.MINIAPP_APPID }}
|
||||
MINIAPP_ROBOT: ${{ vars.MINIAPP_ROBOT }}
|
||||
MINIAPP_BUILD_NUMBER: ${{ gitea.run_number }}
|
||||
MINIAPP_VERSION_OVERRIDE: ${{ vars.MINIAPP_VERSION_OVERRIDE }}
|
||||
WECHAT_CI_PRIVATE_KEY_PATH: ${{ runner.temp }}/wechat-ci.key
|
||||
MINIAPP_UPLOAD_RESULT: ${{ gitea.workspace }}/mini/ci-artifacts/upload-result.json
|
||||
MINIAPP_VERSION_LABEL: ${{ gitea.sha }}
|
||||
MINIAPP_UPLOAD_DESC_ITEMS: "3"
|
||||
MINIAPP_UPLOAD_DESC_MAX: "100"
|
||||
run: |
|
||||
test -n "$MINIAPP_APPID"
|
||||
test -n "$MINIAPP_BUILD_NUMBER"
|
||||
test -s ci-artifacts/resolved-version.txt
|
||||
RESOLVED_VERSION="$(tr -d '\n' < ci-artifacts/resolved-version.txt)"
|
||||
export MINIAPP_VERSION_OVERRIDE="$RESOLVED_VERSION"
|
||||
mkdir -p ci-artifacts
|
||||
node -e 'const c=require("./project.config.json"); if(c.appid!==process.env.MINIAPP_APPID){console.error("MINIAPP_APPID mismatch with project.config.json", process.env.MINIAPP_APPID, c.appid); process.exit(1)}'
|
||||
node -e 'const {resolveVersion}=require("./ci/resolve-version.cjs"); console.log("Resolved Mini Program version:", resolveVersion(process.cwd()))'
|
||||
node -e 'const {resolveUploadDesc}=require("./ci/resolve-upload-desc.cjs"); console.log("Resolved upload remark:", resolveUploadDesc({repoRoot:require("path").resolve(".."), versionLabel:process.env.MINIAPP_VERSION_LABEL}))'
|
||||
python3 - <<'PY'
|
||||
from pathlib import Path
|
||||
import os
|
||||
key_path = Path(os.environ["WECHAT_CI_PRIVATE_KEY_PATH"])
|
||||
text = key_path.read_text()
|
||||
if "\\n" in text and "BEGIN" in text:
|
||||
text = text.replace("\\n", "\n")
|
||||
text = text.replace("\r\n", "\n").strip() + "\n"
|
||||
key_path.write_text(text)
|
||||
content = key_path.read_text()
|
||||
assert "BEGIN" in content and "PRIVATE KEY" in content, "WECHAT_CI_PRIVATE_KEY is not a PEM private key"
|
||||
print("private_key_lines=", len(content.splitlines()))
|
||||
PY
|
||||
node ci/upload-ci.cjs
|
||||
test -s ci-artifacts/upload-result.json
|
||||
cat ci-artifacts/upload-result.json
|
||||
- name: Persist upload result on runner host
|
||||
run: |
|
||||
test -s mini/ci-artifacts/upload-result.json
|
||||
test -d /ci-artifacts
|
||||
cp mini/ci-artifacts/upload-result.json "/ci-artifacts/miniapp-upload-${{ gitea.sha }}.json"
|
||||
cp mini/ci-artifacts/upload-result.json /ci-artifacts/miniapp-upload-latest.json
|
||||
ls -la /ci-artifacts/miniapp-upload-latest.json
|
||||
echo "Upload result saved on runner host: /ci-artifacts/miniapp-upload-latest.json"
|
||||
- name: Remove temporary credentials
|
||||
if: always()
|
||||
run: rm -f "$RUNNER_TEMP/wechat-ci.key"
|
||||
@@ -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;
|
||||
});
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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;
|
||||
});
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user