docs(sdk): 添加 React Native SDK 文档和 Android/HarmonyOS 发版脚本
- 新增 XuqmGroup React Native SDK 使用文档,包含安装、初始化、HTTP客户端、IM模块、推送模块、版本管理等功能说明 - 添加 Android Gradle 发版任务脚本,支持构建发布 APK 并上传到更新服务 - 添加 HarmonyOS hvigorw 发版任务脚本,支持 HAP 包构建和上传功能 - 实现多平台版本检查、自动重连、灰度发布等发版流程自动化 - 集成商店提交、定时发布、Webhook 回调等发布后处理功能
这个提交包含在:
父节点
b3d510ddae
当前提交
215859f03f
@ -109,6 +109,15 @@ fun httpMultipartPost(url: String, token: String, parts: Map<String, Any>): Stri
|
||||
fun parseJson(json: String, key: String) =
|
||||
Regex(""""$key"\s*:\s*"([^"]+)"""").find(json)?.groupValues?.get(1)
|
||||
|
||||
fun parseJsonBool(json: String, key: String, default: Boolean = false): Boolean =
|
||||
Regex(""""$key"\s*:\s*(true|false)""").find(json)?.groupValues?.get(1)?.toBooleanStrictOrNull() ?: default
|
||||
|
||||
fun parseJsonInt(json: String, key: String, default: Int = 0): Int =
|
||||
Regex(""""$key"\s*:\s*(\d+)""").find(json)?.groupValues?.get(1)?.toIntOrNull() ?: default
|
||||
|
||||
fun parseCsv(value: String?): List<String> =
|
||||
value?.split(",")?.map { it.trim() }?.filter { it.isNotBlank() } ?: emptyList()
|
||||
|
||||
fun promptLine(message: String): String? {
|
||||
val console = System.console() ?: return null
|
||||
print(message)
|
||||
@ -125,6 +134,47 @@ fun promptYesNo(message: String, default: Boolean = false): Boolean {
|
||||
}
|
||||
}
|
||||
|
||||
data class ReleaseDefaults(
|
||||
val enabled: Boolean = true,
|
||||
val defaultStoreTargets: List<String> = emptyList(),
|
||||
val defaultPublishMode: String = "MANUAL",
|
||||
val defaultPublishImmediately: Boolean = false,
|
||||
val defaultScheduledPublishAt: String = "",
|
||||
val defaultAutoPublishAfterReview: Boolean = false,
|
||||
val defaultWebhookUrl: String = "",
|
||||
val defaultForceUpdate: Boolean = false,
|
||||
val defaultGrayEnabled: Boolean = false,
|
||||
val defaultGrayPercent: Int = 0,
|
||||
val defaultPackageName: String = "",
|
||||
val defaultAppStoreUrl: String = "",
|
||||
val defaultMarketUrl: String = "",
|
||||
)
|
||||
|
||||
fun fetchReleaseDefaults(tenantUrl: String, appKey: String): ReleaseDefaults? {
|
||||
if (tenantUrl.isBlank()) return null
|
||||
val body = runCatching {
|
||||
val uri = URI("$tenantUrl/api/sdk/config?appId=$appKey&platform=HARMONY")
|
||||
val req = HttpRequest.newBuilder(uri).GET().build()
|
||||
HttpClient.newHttpClient().send(req, HttpResponse.BodyHandlers.ofString()).body()
|
||||
}.getOrNull() ?: return null
|
||||
if (!parseJsonBool(body, "updateEnabled", false)) return ReleaseDefaults(enabled = false)
|
||||
return ReleaseDefaults(
|
||||
enabled = true,
|
||||
defaultStoreTargets = parseCsv(parseJson(body, "updateDefaultStoreTargets")),
|
||||
defaultPublishMode = parseJson(body, "updateDefaultPublishMode") ?: "MANUAL",
|
||||
defaultPublishImmediately = parseJsonBool(body, "updateDefaultPublishImmediately"),
|
||||
defaultScheduledPublishAt = parseJson(body, "updateDefaultScheduledPublishAt").orEmpty(),
|
||||
defaultAutoPublishAfterReview = parseJsonBool(body, "updateDefaultAutoPublishAfterReview"),
|
||||
defaultWebhookUrl = parseJson(body, "updateDefaultWebhookUrl").orEmpty(),
|
||||
defaultForceUpdate = parseJsonBool(body, "updateDefaultForceUpdate"),
|
||||
defaultGrayEnabled = parseJsonBool(body, "updateDefaultGrayEnabled"),
|
||||
defaultGrayPercent = parseJsonInt(body, "updateDefaultGrayPercent"),
|
||||
defaultPackageName = parseJson(body, "updateDefaultPackageName").orEmpty(),
|
||||
defaultAppStoreUrl = parseJson(body, "updateDefaultAppStoreUrl").orEmpty(),
|
||||
defaultMarketUrl = parseJson(body, "updateDefaultMarketUrl").orEmpty(),
|
||||
)
|
||||
}
|
||||
|
||||
// ── Task ──────────────────────────────────────────────────────────────────
|
||||
|
||||
tasks.register("xuqmRelease") {
|
||||
@ -139,21 +189,39 @@ tasks.register("xuqmRelease") {
|
||||
val serverUrl = cfg.getProperty("xuqm.serverUrl") ?: throw GradleException("xuqm.serverUrl missing")
|
||||
val appKey = cfg.getProperty("xuqm.appKey") ?: throw GradleException("xuqm.appKey missing")
|
||||
val apiToken = cfg.getProperty("xuqm.apiToken") ?: throw GradleException("xuqm.apiToken missing")
|
||||
val autoPublish = cfg.getProperty("xuqm.autoPublishAfterReview", "false").toBoolean()
|
||||
val publishImmediately = cfg.getProperty("xuqm.publishImmediately", "false").toBoolean()
|
||||
var scheduledAt = cfg.getProperty("xuqm.scheduledPublishAt", "")
|
||||
val webhookUrl = cfg.getProperty("xuqm.webhookUrl", "")
|
||||
val marketUrl = cfg.getProperty("xuqm.marketUrl", "")
|
||||
var publishMode = cfg.getProperty("xuqm.publishMode", "").trim().uppercase()
|
||||
val tenantUrl = cfg.getProperty("xuqm.tenantUrl", "").trim()
|
||||
val tenantDefaults = fetchReleaseDefaults(tenantUrl, appKey)
|
||||
val autoPublish = cfg.getProperty("xuqm.autoPublishAfterReview", tenantDefaults?.defaultAutoPublishAfterReview?.toString() ?: "false").toBoolean()
|
||||
var publishImmediately = cfg.getProperty("xuqm.publishImmediately", tenantDefaults?.defaultPublishImmediately?.toString() ?: "false").toBoolean()
|
||||
var scheduledAt = cfg.getProperty("xuqm.scheduledPublishAt", tenantDefaults?.defaultScheduledPublishAt ?: "")
|
||||
var webhookUrl = cfg.getProperty("xuqm.webhookUrl", tenantDefaults?.defaultWebhookUrl ?: "")
|
||||
var marketUrl = cfg.getProperty("xuqm.marketUrl", tenantDefaults?.defaultMarketUrl ?: "")
|
||||
var publishMode = cfg.getProperty("xuqm.publishMode", tenantDefaults?.defaultPublishMode ?: "").trim().uppercase()
|
||||
var dryRun = cfg.getProperty("xuqm.dryRun", "false").toBoolean() || publishMode == "DRY_RUN" || tenantDefaults?.enabled == false
|
||||
val allowVersionMismatch = cfg.getProperty("xuqm.allowVersionMismatch", "false").toBoolean()
|
||||
if (publishMode.isBlank() && !publishImmediately && scheduledAt.isBlank() && !autoPublish && System.console() != null) {
|
||||
publishMode = promptReleaseMode()
|
||||
if (publishMode == "SCHEDULED" && scheduledAt.isBlank()) {
|
||||
scheduledAt = promptLine("Scheduled publish time (ISO datetime): ")?.trim().orEmpty()
|
||||
}
|
||||
}
|
||||
if (publishMode == "DRY_RUN") {
|
||||
dryRun = true
|
||||
println("[xuqm] Dry-run mode enabled.")
|
||||
}
|
||||
if (!scheduledAt.isBlank()) {
|
||||
publishImmediately = false
|
||||
}
|
||||
if (publishMode == "NOW") {
|
||||
publishImmediately = true
|
||||
autoPublish = false
|
||||
} else if (publishMode == "AUTO_REVIEW") {
|
||||
autoPublish = true
|
||||
publishImmediately = false
|
||||
}
|
||||
|
||||
// ── 1. Read local version ──────────────────────────────────────────
|
||||
val (versionName, versionCode) = readHarmonyVersion(projectDir)
|
||||
var (versionName, versionCode) = readHarmonyVersion(projectDir)
|
||||
val bundleName = readHarmonyBundleName(projectDir)
|
||||
println("[xuqm] Local version: $versionName ($versionCode), appKey: $appKey")
|
||||
|
||||
@ -163,18 +231,40 @@ tasks.register("xuqmRelease") {
|
||||
.mapNotNull { it.groupValues[1].toIntOrNull() }.maxOrNull() ?: 0
|
||||
println("[xuqm] Server latest versionCode: $serverCode")
|
||||
|
||||
if (versionCode <= serverCode) {
|
||||
if (!allowVersionMismatch && System.console() != null) {
|
||||
println("[xuqm] Local version is not greater than server latest. Please enter corrected release version info.")
|
||||
val inputVersionName = promptLine("Release versionName [$versionName]: ")?.trim().orEmpty()
|
||||
val inputVersionCode = promptLine("Release versionCode [$versionCode]: ")?.trim().orEmpty()
|
||||
if (inputVersionName.isNotBlank()) versionName = inputVersionName
|
||||
if (inputVersionCode.isNotBlank()) versionCode = inputVersionCode.toInt()
|
||||
}
|
||||
if (versionCode <= serverCode) {
|
||||
throw GradleException(
|
||||
"[xuqm] Local versionCode ($versionCode) ≤ server ($serverCode). " +
|
||||
"Bump versionCode in AppScope/app.json5 first."
|
||||
"[xuqm] Release versionCode ($versionCode) must be greater than server ($serverCode). " +
|
||||
"Bump versionCode in AppScope/app.json5 or pass xuqm.allowVersionMismatch=true if you deliberately override."
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// ── 3. Locate HAP ──────────────────────────────────────────────────
|
||||
// ── 4. Upload release metadata to update service ──────────────────
|
||||
if (marketUrl.isBlank()) {
|
||||
throw GradleException("xuqm.marketUrl missing for Harmony release")
|
||||
}
|
||||
if (dryRun) {
|
||||
println("[xuqm] Dry-run summary:")
|
||||
println(" updateEnabled=${tenantDefaults?.enabled ?: true}")
|
||||
println(" releaseVersion=$versionName ($versionCode)")
|
||||
println(" publishMode=${publishMode.ifBlank { "MANUAL" }}")
|
||||
println(" publishImmediately=$publishImmediately")
|
||||
println(" autoPublishAfterReview=$autoPublish")
|
||||
println(" scheduledAt=${scheduledAt.ifBlank { "-" }}")
|
||||
println(" webhookUrl=${webhookUrl.ifBlank { "-" }}")
|
||||
println(" marketUrl=${marketUrl.ifBlank { "-" }}")
|
||||
println("[xuqm] Dry-run completed. No upload performed.")
|
||||
return@doLast
|
||||
}
|
||||
val parts = mutableMapOf<String, Any>(
|
||||
"appId" to appKey, "platform" to "HARMONY",
|
||||
"versionName" to versionName, "versionCode" to versionCode,
|
||||
@ -192,6 +282,7 @@ tasks.register("xuqmRelease") {
|
||||
?: throw GradleException("[xuqm] Upload failed:\n$uploadResp")
|
||||
println("[xuqm] Uploaded, version ID: $versionId")
|
||||
|
||||
if (publishMode == "NOW") publishImmediately = true
|
||||
if (publishImmediately || publishMode == "NOW") {
|
||||
println("[xuqm] Published immediately in update service.")
|
||||
} else if (publishMode == "SCHEDULED" || scheduledAt.isNotBlank()) {
|
||||
|
||||
正在加载...
在新工单中引用
屏蔽一个用户