删除 bin/release-android.mjs
This commit is contained in:
@@ -1,640 +0,0 @@
|
|||||||
#!/usr/bin/env node
|
|
||||||
/**
|
|
||||||
* Android Release Script - Universal Version
|
|
||||||
*
|
|
||||||
* Automatically builds Android APK and uploads to Gitea Release.
|
|
||||||
*
|
|
||||||
* Features:
|
|
||||||
* - Auto-detect Android project root (standalone or monorepo)
|
|
||||||
* - Auto-detect Gitea config from git remote URL (HTTPS/SSH)
|
|
||||||
* - Auto-detect Java Home from Android Studio
|
|
||||||
* - Support tag-based release workflow
|
|
||||||
*
|
|
||||||
* Environment Variables:
|
|
||||||
* - GITEA_TOKEN (required): Gitea API token
|
|
||||||
* - GITEA_API_URL (optional): Override Gitea API URL
|
|
||||||
* - GITEA_OWNER (optional): Override repository owner
|
|
||||||
* - GITEA_REPO (optional): Override repository name
|
|
||||||
*
|
|
||||||
* Usage:
|
|
||||||
* node release-android.mjs [--project <name>] [android-dir]
|
|
||||||
*
|
|
||||||
* --project <name> Specify project name (e.g., admin-tool, android)
|
|
||||||
* android-dir Android project directory (optional, auto-detect if not specified)
|
|
||||||
*
|
|
||||||
* Examples:
|
|
||||||
* node release-android.mjs --project admin-tool
|
|
||||||
* node release-android.mjs --project android ./android
|
|
||||||
*/
|
|
||||||
|
|
||||||
import fs from 'node:fs';
|
|
||||||
import path from 'node:path';
|
|
||||||
import { execSync } from 'node:child_process';
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Project Detection
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Find Android project root directory
|
|
||||||
* Looks for app/build.gradle.kts or build.gradle.kts with android plugin
|
|
||||||
*/
|
|
||||||
function findAndroidRoot(startDir) {
|
|
||||||
const candidates = [
|
|
||||||
startDir, // Current dir is Android root
|
|
||||||
path.join(startDir, 'android'), // Monorepo: ./android/
|
|
||||||
path.join(startDir, '..'), // Parent dir
|
|
||||||
];
|
|
||||||
|
|
||||||
for (const dir of candidates) {
|
|
||||||
const buildGradle = path.join(dir, 'app/build.gradle.kts');
|
|
||||||
const buildGradleGroovy = path.join(dir, 'app/build.gradle');
|
|
||||||
|
|
||||||
if (fs.existsSync(buildGradle) || fs.existsSync(buildGradleGroovy)) {
|
|
||||||
return path.resolve(dir);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Find git repository root
|
|
||||||
*/
|
|
||||||
function findGitRoot(startDir) {
|
|
||||||
try {
|
|
||||||
const result = execSync('git rev-parse --show-toplevel', {
|
|
||||||
cwd: startDir,
|
|
||||||
encoding: 'utf-8',
|
|
||||||
shell: true,
|
|
||||||
stdio: ['pipe', 'pipe', 'pipe']
|
|
||||||
}).trim();
|
|
||||||
return result;
|
|
||||||
} catch {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Detect if project is a monorepo (Android is a subdirectory)
|
|
||||||
*/
|
|
||||||
function isMonorepo(androidDir, gitRoot) {
|
|
||||||
if (!gitRoot) return false;
|
|
||||||
return path.resolve(androidDir) !== path.resolve(gitRoot);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Git/Gitea Configuration
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parse git remote URL to extract Gitea configuration
|
|
||||||
* Supports both HTTPS and SSH formats
|
|
||||||
*/
|
|
||||||
function detectGiteaConfig(gitRoot) {
|
|
||||||
try {
|
|
||||||
const remoteUrl = execSync('git remote get-url origin', {
|
|
||||||
cwd: gitRoot,
|
|
||||||
encoding: 'utf-8',
|
|
||||||
shell: true,
|
|
||||||
stdio: ['pipe', 'pipe', 'pipe']
|
|
||||||
}).trim();
|
|
||||||
|
|
||||||
// HTTPS format: https://git.example.com/owner/repo.git
|
|
||||||
const httpsMatch = remoteUrl.match(/https?:\/\/([^/]+)\/([^/]+)\/([^/]+?)(\.git)?$/);
|
|
||||||
if (httpsMatch) {
|
|
||||||
return {
|
|
||||||
apiUrl: `https://${httpsMatch[1]}`,
|
|
||||||
owner: httpsMatch[2],
|
|
||||||
repo: httpsMatch[3]
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// SSH format: git@git.example.com:owner/repo.git
|
|
||||||
const sshMatch = remoteUrl.match(/git@([^:]+):([^/]+)\/([^/]+?)(\.git)?$/);
|
|
||||||
if (sshMatch) {
|
|
||||||
return {
|
|
||||||
apiUrl: `https://${sshMatch[1]}`,
|
|
||||||
owner: sshMatch[2],
|
|
||||||
repo: sshMatch[3]
|
|
||||||
};
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
// Ignore errors
|
|
||||||
}
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Java Detection
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Detect Java Home, prioritizing Android Studio's bundled JDK
|
|
||||||
*/
|
|
||||||
function detectJavaHome() {
|
|
||||||
const possiblePaths = [
|
|
||||||
// macOS - Android Studio bundled JDK
|
|
||||||
'/Applications/Android Studio.app/Contents/jbr/Contents/Home',
|
|
||||||
'/Applications/Android Studio.app/Contents/jre/Contents/Home',
|
|
||||||
// Linux - Android Studio bundled JDK
|
|
||||||
`${process.env.HOME}/android-studio/jbr`,
|
|
||||||
`${process.env.HOME}/android-studio/jre`,
|
|
||||||
// Environment variable
|
|
||||||
process.env.JAVA_HOME,
|
|
||||||
// Common system paths
|
|
||||||
'/usr/lib/jvm/java-11-openjdk',
|
|
||||||
'/usr/lib/jvm/java-17-openjdk',
|
|
||||||
];
|
|
||||||
|
|
||||||
for (const javaHome of possiblePaths) {
|
|
||||||
if (javaHome && fs.existsSync(javaHome)) {
|
|
||||||
const javaBin = path.join(javaHome, 'bin', 'java');
|
|
||||||
if (fs.existsSync(javaBin)) {
|
|
||||||
return javaHome;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Version & Tag Management
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Read version from build.gradle.kts
|
|
||||||
*/
|
|
||||||
function readVersion(androidDir) {
|
|
||||||
const buildGradlePath = path.join(androidDir, 'app/build.gradle.kts');
|
|
||||||
const buildGradleGroovyPath = path.join(androidDir, 'app/build.gradle');
|
|
||||||
|
|
||||||
let buildGradle;
|
|
||||||
if (fs.existsSync(buildGradlePath)) {
|
|
||||||
buildGradle = fs.readFileSync(buildGradlePath, 'utf-8');
|
|
||||||
} else if (fs.existsSync(buildGradleGroovyPath)) {
|
|
||||||
buildGradle = fs.readFileSync(buildGradleGroovyPath, 'utf-8');
|
|
||||||
} else {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const versionMatch = buildGradle.match(/versionName\s*[=:]\s*["']([^"']+)["']/);
|
|
||||||
return versionMatch ? versionMatch[1] : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Infer project name from Android directory name and git tags
|
|
||||||
* Returns the prefix used in tags (e.g., "myapp", "android", or "")
|
|
||||||
*/
|
|
||||||
function inferProjectName(gitRoot, androidDir) {
|
|
||||||
try {
|
|
||||||
// First, try to infer from Android directory name
|
|
||||||
const dirName = path.basename(androidDir);
|
|
||||||
|
|
||||||
// Get all tags with version pattern
|
|
||||||
const tags = execSync('git tag -l --sort=-version:refname', {
|
|
||||||
cwd: gitRoot,
|
|
||||||
encoding: 'utf-8',
|
|
||||||
shell: true,
|
|
||||||
stdio: ['pipe', 'pipe', 'pipe']
|
|
||||||
}).trim().split('\n').filter(Boolean);
|
|
||||||
|
|
||||||
if (tags.length === 0) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If directory name matches any tag prefix, prefer that
|
|
||||||
// For example: if dir is "admin-tool", look for "admin-tool-*" tags
|
|
||||||
const dirPrefixTags = tags.filter(tag => tag.startsWith(`${dirName}-`));
|
|
||||||
if (dirPrefixTags.length > 0) {
|
|
||||||
return dirName;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If directory is "android" or "app", try "android" prefix
|
|
||||||
if (dirName === 'android' || dirName === 'app') {
|
|
||||||
const androidTags = tags.filter(tag => tag.startsWith('android-'));
|
|
||||||
if (androidTags.length > 0) {
|
|
||||||
return 'android';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fallback: try to extract prefix from recent tags
|
|
||||||
// Patterns: "myapp-1.0.0", "android-1.0.0", "v1.0.0"
|
|
||||||
for (const tag of tags.slice(0, 10)) { // Check recent 10 tags
|
|
||||||
const match = tag.match(/^(.+?)-\d+\.\d+/);
|
|
||||||
if (match) {
|
|
||||||
return match[1]; // Return prefix like "myapp", "android"
|
|
||||||
}
|
|
||||||
if (tag.match(/^v\d+\.\d+/)) {
|
|
||||||
return ''; // v-prefix style (no project name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
} catch {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get latest tag that matches the pattern
|
|
||||||
*/
|
|
||||||
function getLatestTag(gitRoot, projectPrefix) {
|
|
||||||
try {
|
|
||||||
const pattern = projectPrefix ? `${projectPrefix}-*` : 'v*';
|
|
||||||
const tag = execSync(`git tag -l '${pattern}' --sort=-version:refname | head -1`, {
|
|
||||||
cwd: gitRoot,
|
|
||||||
encoding: 'utf-8',
|
|
||||||
shell: true,
|
|
||||||
stdio: ['pipe', 'pipe', 'pipe']
|
|
||||||
}).trim();
|
|
||||||
|
|
||||||
return tag || null;
|
|
||||||
} catch {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get tag information (check existence and get annotation)
|
|
||||||
*/
|
|
||||||
function getTagInfo(tagName, gitRoot) {
|
|
||||||
try {
|
|
||||||
const tagExists = execSync(`git tag -l ${tagName}`, {
|
|
||||||
cwd: gitRoot,
|
|
||||||
encoding: 'utf-8',
|
|
||||||
shell: true,
|
|
||||||
stdio: ['pipe', 'pipe', 'pipe']
|
|
||||||
}).trim();
|
|
||||||
|
|
||||||
if (!tagExists) {
|
|
||||||
return { exists: false };
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get tag annotation
|
|
||||||
const annotation = execSync(`git tag -l --format='%(contents)' ${tagName}`, {
|
|
||||||
cwd: gitRoot,
|
|
||||||
encoding: 'utf-8',
|
|
||||||
shell: true,
|
|
||||||
stdio: ['pipe', 'pipe', 'pipe']
|
|
||||||
}).trim();
|
|
||||||
|
|
||||||
if (annotation) {
|
|
||||||
return { exists: true, message: annotation };
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fall back to commit message
|
|
||||||
const commitMsg = execSync(`git log -1 --format=%B ${tagName}`, {
|
|
||||||
cwd: gitRoot,
|
|
||||||
encoding: 'utf-8',
|
|
||||||
shell: true,
|
|
||||||
stdio: ['pipe', 'pipe', 'pipe']
|
|
||||||
}).trim();
|
|
||||||
|
|
||||||
return { exists: true, message: commitMsg || `Release ${tagName}` };
|
|
||||||
} catch {
|
|
||||||
return { exists: false };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Build & Upload
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Build Android APK
|
|
||||||
*/
|
|
||||||
function buildApk(androidDir, javaHome) {
|
|
||||||
console.log('正在构建 APK...');
|
|
||||||
execSync('./gradlew assembleRelease --quiet', {
|
|
||||||
cwd: androidDir,
|
|
||||||
stdio: 'inherit',
|
|
||||||
shell: true,
|
|
||||||
env: { ...process.env, JAVA_HOME: javaHome }
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Find built APK file
|
|
||||||
*/
|
|
||||||
function findApk(androidDir) {
|
|
||||||
const signedApkPath = path.join(androidDir, 'app/build/outputs/apk/release/app-release.apk');
|
|
||||||
const unsignedApkPath = path.join(androidDir, 'app/build/outputs/apk/release/app-release-unsigned.apk');
|
|
||||||
|
|
||||||
if (fs.existsSync(signedApkPath)) {
|
|
||||||
return { path: signedApkPath, signed: true };
|
|
||||||
} else if (fs.existsSync(unsignedApkPath)) {
|
|
||||||
return { path: unsignedApkPath, signed: false };
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Execute curl and return JSON response
|
|
||||||
*/
|
|
||||||
function curlJson(cmd) {
|
|
||||||
try {
|
|
||||||
const result = execSync(cmd, { encoding: 'utf-8', shell: true, stdio: ['pipe', 'pipe', 'pipe'] });
|
|
||||||
return JSON.parse(result);
|
|
||||||
} catch {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Upload APK to Gitea Release
|
|
||||||
*/
|
|
||||||
function uploadToGitea(config) {
|
|
||||||
const { apiUrl, owner, repo, token, tagName, fileName, apkPath, releaseMessage } = config;
|
|
||||||
|
|
||||||
const repoApiBase = `${apiUrl}/api/v1/repos/${owner}/${repo}`;
|
|
||||||
|
|
||||||
// Get or create release
|
|
||||||
let releaseId;
|
|
||||||
const releases = curlJson(
|
|
||||||
`curl -s -H "Authorization: token ${token}" "${repoApiBase}/releases"`
|
|
||||||
);
|
|
||||||
|
|
||||||
if (Array.isArray(releases)) {
|
|
||||||
const existingRelease = releases.find((r) => r.tag_name === tagName);
|
|
||||||
if (existingRelease) {
|
|
||||||
releaseId = existingRelease.id;
|
|
||||||
console.log(`找到已存在的 Release (ID: ${releaseId})`);
|
|
||||||
|
|
||||||
// Delete existing asset with same name
|
|
||||||
const existingAsset = (existingRelease.assets || []).find((a) => a.name === fileName);
|
|
||||||
if (existingAsset) {
|
|
||||||
console.log(`删除已存在的文件: ${fileName}`);
|
|
||||||
execSync(
|
|
||||||
`curl -s -X DELETE -H "Authorization: token ${token}" "${repoApiBase}/releases/${releaseId}/assets/${existingAsset.id}"`,
|
|
||||||
{ shell: true, stdio: ['pipe', 'pipe', 'pipe'] }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!releaseId) {
|
|
||||||
console.log('创建新 Release...');
|
|
||||||
const releaseData = {
|
|
||||||
tag_name: tagName,
|
|
||||||
name: `Android APK ${tagName}`,
|
|
||||||
body: releaseMessage,
|
|
||||||
draft: false,
|
|
||||||
prerelease: false
|
|
||||||
};
|
|
||||||
|
|
||||||
const tempJsonPath = path.join(process.cwd(), '.release-data.json');
|
|
||||||
fs.writeFileSync(tempJsonPath, JSON.stringify(releaseData));
|
|
||||||
|
|
||||||
const releaseInfo = curlJson(
|
|
||||||
`curl -s -X POST "${repoApiBase}/releases" \
|
|
||||||
-H "Authorization: token ${token}" \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
-d @${tempJsonPath}`
|
|
||||||
);
|
|
||||||
|
|
||||||
fs.unlinkSync(tempJsonPath);
|
|
||||||
|
|
||||||
if (!releaseInfo || !releaseInfo.id) {
|
|
||||||
throw new Error(`Failed to create release: ${JSON.stringify(releaseInfo)}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
releaseId = releaseInfo.id;
|
|
||||||
console.log(`Release 已创建 (ID: ${releaseId})`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Upload APK
|
|
||||||
console.log('上传 APK...');
|
|
||||||
const uploadUrl = `${repoApiBase}/releases/${releaseId}/assets?name=${encodeURIComponent(fileName)}`;
|
|
||||||
const uploadResult = curlJson(
|
|
||||||
`curl -s -X POST "${uploadUrl}" \
|
|
||||||
-H "Authorization: token ${token}" \
|
|
||||||
-F "attachment=@${apkPath}"`
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!uploadResult || !uploadResult.id) {
|
|
||||||
throw new Error(`Failed to upload APK: ${JSON.stringify(uploadResult)}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
releaseUrl: `${apiUrl}/${owner}/${repo}/releases/tag/${tagName}`,
|
|
||||||
fileSize: uploadResult.size
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Main
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parse command line arguments
|
|
||||||
*/
|
|
||||||
function parseArgs() {
|
|
||||||
const args = process.argv.slice(2);
|
|
||||||
let projectName = null;
|
|
||||||
let androidDir = null;
|
|
||||||
|
|
||||||
for (let i = 0; i < args.length; i++) {
|
|
||||||
const arg = args[i];
|
|
||||||
if (arg === '--project' && i + 1 < args.length) {
|
|
||||||
projectName = args[i + 1];
|
|
||||||
i++; // skip next arg
|
|
||||||
} else if (!arg.startsWith('--')) {
|
|
||||||
androidDir = arg;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return { projectName, androidDir };
|
|
||||||
}
|
|
||||||
|
|
||||||
function main() {
|
|
||||||
const cwd = process.cwd();
|
|
||||||
const { projectName, androidDir: argDir } = parseArgs();
|
|
||||||
|
|
||||||
console.log('='.repeat(60));
|
|
||||||
console.log('Android 发布脚本');
|
|
||||||
console.log('='.repeat(60));
|
|
||||||
|
|
||||||
// 1. Find Android project root
|
|
||||||
const searchDir = argDir ? path.resolve(cwd, argDir) : cwd;
|
|
||||||
const androidDir = findAndroidRoot(searchDir);
|
|
||||||
|
|
||||||
if (!androidDir) {
|
|
||||||
console.error('错误:找不到 Android 项目');
|
|
||||||
console.error('请确保当前目录是 Android 项目或指定正确的路径');
|
|
||||||
console.error('用法: release-android [android-dir]');
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
console.log(`Android 项目: ${androidDir}`);
|
|
||||||
|
|
||||||
// 2. Find git root
|
|
||||||
const gitRoot = findGitRoot(androidDir);
|
|
||||||
if (!gitRoot) {
|
|
||||||
console.error('错误:不是 git 仓库');
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
console.log(`Git 根目录: ${gitRoot}`);
|
|
||||||
|
|
||||||
const monorepo = isMonorepo(androidDir, gitRoot);
|
|
||||||
console.log(`项目类型: ${monorepo ? 'Monorepo' : '独立项目'}`);
|
|
||||||
|
|
||||||
// 3. Detect Gitea configuration
|
|
||||||
const detected = detectGiteaConfig(gitRoot);
|
|
||||||
const GITEA_TOKEN = process.env.GITEA_TOKEN || '';
|
|
||||||
const GITEA_API_URL = process.env.GITEA_API_URL || detected.apiUrl || '';
|
|
||||||
const GITEA_OWNER = process.env.GITEA_OWNER || detected.owner || '';
|
|
||||||
const GITEA_REPO = process.env.GITEA_REPO || detected.repo || '';
|
|
||||||
|
|
||||||
if (!GITEA_TOKEN) {
|
|
||||||
console.error('错误:未设置 GITEA_TOKEN 环境变量');
|
|
||||||
console.error('请设置: export GITEA_TOKEN="your_token"');
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!GITEA_API_URL || !GITEA_OWNER || !GITEA_REPO) {
|
|
||||||
console.error('错误:无法检测 Gitea 仓库配置');
|
|
||||||
console.error('请设置环境变量: GITEA_API_URL, GITEA_OWNER, GITEA_REPO');
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`Gitea: ${GITEA_API_URL}/${GITEA_OWNER}/${GITEA_REPO}`);
|
|
||||||
|
|
||||||
// 4. Read version
|
|
||||||
const version = readVersion(androidDir);
|
|
||||||
if (!version) {
|
|
||||||
console.error('错误:无法从 build.gradle 读取 versionName');
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
console.log(`版本: ${version}`);
|
|
||||||
|
|
||||||
// 5. Determine project prefix
|
|
||||||
let projectPrefix;
|
|
||||||
|
|
||||||
if (projectName) {
|
|
||||||
// Use provided project name
|
|
||||||
console.log(`项目前缀: ${projectName} (指定)`);
|
|
||||||
projectPrefix = projectName;
|
|
||||||
|
|
||||||
// Verify that tags with this prefix exist
|
|
||||||
const testTag = getLatestTag(gitRoot, projectPrefix);
|
|
||||||
if (!testTag) {
|
|
||||||
console.error(`错误:找不到匹配 "${projectPrefix}-*" 模式的 git tag`);
|
|
||||||
console.error('');
|
|
||||||
console.error('可用的 tags:');
|
|
||||||
try {
|
|
||||||
const allTags = execSync('git tag -l --sort=-version:refname | head -10', {
|
|
||||||
cwd: gitRoot,
|
|
||||||
encoding: 'utf-8',
|
|
||||||
shell: true,
|
|
||||||
stdio: ['pipe', 'pipe', 'pipe']
|
|
||||||
}).trim();
|
|
||||||
console.error(allTags || ' (未找到 tags)');
|
|
||||||
} catch {
|
|
||||||
console.error(' (无法列出 tags)');
|
|
||||||
}
|
|
||||||
console.error('');
|
|
||||||
console.error(`请创建正确前缀的 tag:`);
|
|
||||||
console.error(` git tag -a ${projectPrefix}-${version} -m "发布说明"`);
|
|
||||||
console.error(` git push origin ${projectPrefix}-${version}`);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Infer project name from directory and tags
|
|
||||||
projectPrefix = inferProjectName(gitRoot, androidDir);
|
|
||||||
if (projectPrefix === null) {
|
|
||||||
console.error('错误:无法从 git tags 推断项目名称');
|
|
||||||
console.error('请确保至少存在一个以下格式的 tag:');
|
|
||||||
console.error(' myapp-1.0.0 或 v1.0.0');
|
|
||||||
console.error('');
|
|
||||||
console.error('或使用 --project 明确指定项目名称:');
|
|
||||||
console.error(' node release-android.mjs --project admin-tool');
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
console.log(`项目前缀: ${projectPrefix || '(v-style)'} (自动检测)`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const tagName = getLatestTag(gitRoot, projectPrefix);
|
|
||||||
if (!tagName) {
|
|
||||||
const expectedTag = projectPrefix ? `${projectPrefix}-${version}` : `v${version}`;
|
|
||||||
console.error(`错误:找不到匹配的 git tag`);
|
|
||||||
console.error(`期望的 tag 格式: ${expectedTag}`);
|
|
||||||
console.error('');
|
|
||||||
console.error('请先创建 tag:');
|
|
||||||
console.error(` git tag -a ${expectedTag} -m "发布说明"`);
|
|
||||||
console.error(` git push origin ${expectedTag}`);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
const tagInfo = getTagInfo(tagName, gitRoot);
|
|
||||||
if (!tagInfo.exists) {
|
|
||||||
console.error(`错误:Git tag "${tagName}" 不存在`);
|
|
||||||
console.error('请推送 tag:');
|
|
||||||
console.error(` git push origin ${tagName}`);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
console.log(`Tag: ${tagName}`);
|
|
||||||
|
|
||||||
// 6. Detect Java
|
|
||||||
const javaHome = detectJavaHome();
|
|
||||||
if (!javaHome) {
|
|
||||||
console.error('错误:找不到 Java 运行环境');
|
|
||||||
console.error('请安装 JDK 或确保 Android Studio 已正确安装');
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
console.log(`Java: ${javaHome}`);
|
|
||||||
|
|
||||||
console.log('');
|
|
||||||
console.log('开始构建...');
|
|
||||||
|
|
||||||
// 7. Build APK
|
|
||||||
try {
|
|
||||||
buildApk(androidDir, javaHome);
|
|
||||||
} catch (err) {
|
|
||||||
console.error('构建失败:', err.message);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 8. Find APK
|
|
||||||
const apk = findApk(androidDir);
|
|
||||||
if (!apk) {
|
|
||||||
console.error('错误:找不到 APK 文件');
|
|
||||||
console.error('检查构建输出: app/build/outputs/apk/release/');
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
console.log(`APK: ${apk.path} (${apk.signed ? '已签名' : '未签名'})`);
|
|
||||||
|
|
||||||
// 9. Upload to Gitea
|
|
||||||
const fileName = projectPrefix
|
|
||||||
? `${projectPrefix}-${version}.apk`
|
|
||||||
: `${GITEA_REPO}-${version}.apk`;
|
|
||||||
|
|
||||||
console.log('');
|
|
||||||
console.log('上传到 Gitea...');
|
|
||||||
|
|
||||||
try {
|
|
||||||
const result = uploadToGitea({
|
|
||||||
apiUrl: GITEA_API_URL,
|
|
||||||
owner: GITEA_OWNER,
|
|
||||||
repo: GITEA_REPO,
|
|
||||||
token: GITEA_TOKEN,
|
|
||||||
tagName,
|
|
||||||
fileName,
|
|
||||||
apkPath: apk.path,
|
|
||||||
releaseMessage: tagInfo.message
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log('');
|
|
||||||
console.log('='.repeat(60));
|
|
||||||
console.log('发布成功!');
|
|
||||||
console.log(`文件: ${fileName}`);
|
|
||||||
console.log(`大小: ${(result.fileSize / 1024 / 1024).toFixed(2)} MB`);
|
|
||||||
console.log(`URL: ${result.releaseUrl}`);
|
|
||||||
console.log('='.repeat(60));
|
|
||||||
} catch (err) {
|
|
||||||
console.error('上传失败:', err.message);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
main();
|
|
||||||
Reference in New Issue
Block a user