diff --git a/Jenkinsfile_dev b/Jenkinsfile_dev index 033ad58..2489ec8 100644 --- a/Jenkinsfile_dev +++ b/Jenkinsfile_dev @@ -9,4 +9,6 @@ Pipeline_dev( winbakeLabel: "unity_2022_3_dev", // optional node labels. Valid entries: macbakeLabel, linuxbakeLabel, winbakeLabel, syncLabel execUnityMethod:"MyBuildTool.StartBuildAndroid_dev", + uploadEnabled: true, + uploadTokenCredentialsId: "apk-upload-token", ) \ No newline at end of file diff --git a/vars/Pipeline_dev.groovy b/vars/Pipeline_dev.groovy index 3796437..c094f5b 100644 --- a/vars/Pipeline_dev.groovy +++ b/vars/Pipeline_dev.groovy @@ -68,6 +68,7 @@ def call(Map config) booleanParam(name: 'DebugBuild', defaultValue: false, description: 'To Create Debug Build for Profiling') booleanParam(name: 'CleanCache', defaultValue: false, description: 'Force Clean Script and Package Cache') booleanParam(name: 'CleanLibrary', defaultValue: false, description: 'Force Clean the Whole Library Folder') + booleanParam(name: 'BuildNonTuyoo', defaultValue: false, description: 'Build the without_sdk APK package') } options { @@ -139,6 +140,53 @@ def call(Map config) archiveArtifact("Android_dev") } } + stage("Upload APK") + { + when + { + expression { config.uploadEnabled == true } + } + options + { + timeout(10) + } + steps + { + catchError(buildResult: 'UNSTABLE', stageResult: 'UNSTABLE') + { + script + { + env.APK_UPLOAD_STATUS = 'FAILED' + env.APK_UPLOAD_MESSAGE = '' + env.APK_UPLOAD_FILE = '' + env.APK_UPLOAD_VERSION = '' + env.APK_UPLOAD_PACKAGE_TYPE = '' + def packageType = config.uploadPackageType ?: (params.BuildNonTuyoo ? 'without_sdk' : 'with_sdk') + + try { + def uploadResult = uploadApk([ + publishSubFolder: 'Android_dev', + uploadEnv: 'dev', + packageType: packageType, + uploadUrl: config.uploadUrl, + uploadTokenCredentialsId: config.uploadTokenCredentialsId, + uploadVersion: config.uploadVersion + ]) + + env.APK_UPLOAD_STATUS = 'SUCCESS' + env.APK_UPLOAD_MESSAGE = 'APK uploaded successfully.' + env.APK_UPLOAD_FILE = uploadResult.apkName + env.APK_UPLOAD_VERSION = uploadResult.version + env.APK_UPLOAD_PACKAGE_TYPE = uploadResult.packageType + } catch (Exception ex) { + env.APK_UPLOAD_MESSAGE = ex.message ?: 'APK upload failed.' + env.APK_UPLOAD_PACKAGE_TYPE = packageType + throw ex + } + } + } + } + } } post { @@ -154,6 +202,17 @@ def call(Map config) // 主仓库提交记录与 prod / stable 保持一致 def changeString = getMainRepoChangeString(currentBuild.changeSets) + def uploadStatus = config.uploadEnabled == true ? '未执行' : '已跳过' + if (env.APK_UPLOAD_STATUS == 'SUCCESS') { + uploadStatus = env.APK_UPLOAD_VERSION ? + "成功: ${env.APK_UPLOAD_FILE} (${env.APK_UPLOAD_PACKAGE_TYPE}, version ${env.APK_UPLOAD_VERSION})" : + "成功: ${env.APK_UPLOAD_FILE} (${env.APK_UPLOAD_PACKAGE_TYPE})" + } else if (env.APK_UPLOAD_STATUS == 'FAILED') { + uploadStatus = env.APK_UPLOAD_PACKAGE_TYPE ? + "失败: ${env.APK_UPLOAD_PACKAGE_TYPE}, ${env.APK_UPLOAD_MESSAGE}" : + "失败: ${env.APK_UPLOAD_MESSAGE}" + } + // 收集子模块当前提交记录,仅显示本次有变化的子模块 def submoduleChangeString = "" def submoduleStateFile = ".jenkins_submodule_state_dev.json" @@ -238,6 +297,7 @@ def call(Map config) "- 分支: main", "- 触发者: ${triggerUser}", "- 耗时: ${currentBuild.durationString}${failedStage}", + "- APK 上传: ${uploadStatus}", "\n链接:", "- [下载链接](${env.BUILD_URL})", changeString diff --git a/vars/uploadApk.groovy b/vars/uploadApk.groovy new file mode 100644 index 0000000..52a30d3 --- /dev/null +++ b/vars/uploadApk.groovy @@ -0,0 +1,118 @@ +//================================================================= +// Byway Studios Inc. Confidential Information. +// Copyright Byway Studios Inc. All rights reserved +//================================================================= + +def call(Map config) +{ + def publishSubFolder = config.publishSubFolder + def uploadEnv = config.uploadEnv + def uploadUrl = config.uploadUrl ?: 'https://gadmin.bywaystudios.com/api/open/apk/upload/' + def uploadTokenCredentialsId = config.uploadTokenCredentialsId ?: 'apk-upload-token' + def packageType = config.packageType ?: 'with_sdk' + + if (!publishSubFolder) { + error('publishSubFolder is required for APK upload.') + } + + if (!uploadEnv) { + error('uploadEnv is required for APK upload.') + } + + if (!(packageType in ['with_sdk', 'without_sdk'])) { + error("packageType must be with_sdk or without_sdk, got: ${packageType}") + } + + def apkListOutput = bat( + returnStdout: true, + script: "@echo off\r\ndir /b /a-d /o-d \"apk\\${publishSubFolder}\\*.apk\" 2>nul" + ).trim() + + if (!apkListOutput) { + error("No APK found under apk/${publishSubFolder}.") + } + + def apkNames = apkListOutput.readLines().collect { it.trim() }.findAll { it } + def apkName = apkNames[0] + def apkPath = "${env.WORKSPACE}\\apk\\${publishSubFolder}\\${apkName}".replace('\\', '/') + def version = config.uploadVersion ?: extractVersionFromFileName(apkName) + + if (apkNames.size() > 1) { + echo "Multiple APKs found, uploading newest file: ${apkName}" + } + + def curlArgs = [ + "curl.exe -sS -w \"\\nHTTP_STATUS:%%{http_code}\" -X POST \"${uploadUrl}\"", + " -H \"X-Apk-Upload-Token: %APK_UPLOAD_TOKEN%\"", + " -F \"env=${uploadEnv}\"", + " -F \"packageType=${packageType}\"", + " -F \"file=@${apkPath};type=application/vnd.android.package-archive\"" + ] + + if (version) { + curlArgs << " -F \"version=${version}\"" + } + + withCredentials([string(credentialsId: uploadTokenCredentialsId, variable: 'APK_UPLOAD_TOKEN')]) { + def curlOutput = bat( + returnStdout: true, + label: 'Upload APK to server', + script: "@echo off\r\n" + curlArgs.join(" ^\r\n") + ).trim() + + def response = parseCurlResponse(curlOutput) + if (!(response.statusCode ==~ /2\\d\\d/)) { + error("APK upload failed with HTTP ${response.statusCode}: ${truncateResponse(response.body)}") + } + + echo "APK upload succeeded: HTTP ${response.statusCode}" + if (response.body) { + echo "APK upload response: ${truncateResponse(response.body)}" + } + } + + return [ + apkName: apkName, + version: version ?: '', + packageType: packageType, + uploadEnv: uploadEnv, + uploadUrl: uploadUrl + ] +} + +def extractVersionFromFileName(String fileName) +{ + def matcher = fileName =~ /(?i)(?:^|[^0-9])v?(\d+(?:\.\d+){1,3})(?:[^0-9]|$)/ + if (matcher.find()) { + return matcher.group(1) + } + + return '' +} + +def parseCurlResponse(String curlOutput) +{ + def statusMarker = 'HTTP_STATUS:' + def statusIndex = curlOutput.lastIndexOf(statusMarker) + if (statusIndex < 0) { + error('APK upload returned an unexpected response without HTTP status marker.') + } + + return [ + body: curlOutput.substring(0, statusIndex).trim(), + statusCode: curlOutput.substring(statusIndex + statusMarker.length()).trim() + ] +} + +def truncateResponse(String responseBody) +{ + if (!responseBody) { + return '' + } + + if (responseBody.length() <= 400) { + return responseBody + } + + return responseBody.take(400) + '...' +} \ No newline at end of file