//================================================================= // Byway Studios Inc. Confidential Information. // Copyright Byway Studios Inc. All rights reserved //================================================================= import com.cloudbees.groovy.cps.NonCPS import groovy.json.JsonOutput import groovy.json.JsonSlurperClassic @NonCPS def getTriggerUser(buildCauses) { def triggerUser = "自动触发" if (buildCauses && buildCauses.size() > 0 && buildCauses[0].userName) { triggerUser = buildCauses[0].userName } return triggerUser } @NonCPS def getFailedStage(build) { def failedStage = "" if (build.currentResult == 'FAILURE') { def stages = build.rawBuild.getAction(org.jenkinsci.plugins.workflow.job.views.FlowGraphAction)?.getNodes() if (stages) { for (stage in stages) { if (stage.getError() != null) { failedStage = "\n- 失败阶段: ${stage.getDisplayName()}" break } } } } return failedStage } @NonCPS def getMainRepoChangeString(changeLogSets) { def changeString = "" if (changeLogSets && changeLogSets.size() > 0) { changeString = "\n\n提交记录:\n" for (int i = 0; i < changeLogSets.size(); i++) { def entries = changeLogSets[i].items for (int j = 0; j < entries.length; j++) { def entry = entries[j] changeString += "- ${entry.commitId.take(7)} ${entry.msg} - ${entry.author}\n" } } } else { changeString = "\n\n提交记录: 无变更" } return changeString } def call(Map config) { pipeline { agent { label config.winbakeLabel } parameters { booleanParam(name: 'CleanBuild', defaultValue: false, description: 'Force Clean Build to Avoid Previous Build Cache') 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 { skipDefaultCheckout(true) // ★ 关键:禁止 Jenkins 自动 checkout library } stages { stage("Checkout Unity Project") { options { timeout(30) } steps { script { // 配置 Git 支持长路径(解决 Windows 260 字符限制) bat 'git config --global core.longpaths true' } script { // 最小化处理:checkout 前仅重置子模块,避免残留改动导致冲突 bat ''' if exist .gitmodules ( git submodule foreach --recursive git reset --hard ) ''' } checkout([ $class: 'GitSCM', branches: [[name: '*/main']], // 分支名 userRemoteConfigs: [[ url: 'git@gitea.bywaystudios.com:pet_home/AplusB_Pet_nation.git', credentialsId: '503eaa43-0676-40ac-81c0-d9c5cc8b4ff7' ]], extensions: [ [$class: 'SubmoduleOption', disableSubmodules: false, parentCredentials: true, recursiveSubmodules: true, trackingSubmodules: false, // 关闭,避免冲突 reference: '', timeout: 20, threads: 1], [$class: 'PruneStaleBranch'] ] ]) } } stage("Build Unity Android") { options { timeout(60) } steps { BuildAndroid(config) } } stage("AchiveArtifact") { options { timeout(10) } steps { 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, uploadToken: config.uploadToken, 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 { always { script { // 获取触发者 def triggerUser = getTriggerUser(currentBuild.getBuildCauses()) // 获取失败阶段(仅失败时) def failedStage = getFailedStage(currentBuild) // 主仓库提交记录与 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" def previousSubmoduleStates = [:] def currentSubmoduleStates = [:] if (fileExists(submoduleStateFile)) { try { previousSubmoduleStates = new JsonSlurperClassic().parseText(readFile(submoduleStateFile)) ?: [:] } catch (Exception ignored) { previousSubmoduleStates = [:] } } if (fileExists('.git') && fileExists('.gitmodules')) { try { def submoduleStatusOutput = bat( returnStdout: true, script: '@echo off\r\ngit submodule status --recursive' ).trim() def submoduleLines = submoduleStatusOutput ? submoduleStatusOutput.readLines().findAll { it?.trim() } : [] def submoduleSections = [] submoduleLines.each { rawLine -> def line = rawLine.replace('\r', '').trim() if (!line || line.length() < 42) { return } def remainder = line.substring(1).trim() def firstSpaceIndex = remainder.indexOf(' ') if (firstSpaceIndex <= 0) { return } def pathPart = remainder.substring(firstSpaceIndex + 1).trim() def branchInfoIndex = pathPart.indexOf(' (') def submodulePath = (branchInfoIndex >= 0 ? pathPart.substring(0, branchInfoIndex) : pathPart).trim() if (!submodulePath) { return } def currentSha = bat( returnStdout: true, script: "@echo off\r\ngit -C \"${submodulePath}\" rev-parse HEAD" ).trim() def currentHeadMessage = bat( returnStdout: true, script: "@echo off\r\ngit -C \"${submodulePath}\" log -1 --pretty=format:\"%%h %%s - %%an\"" ).trim() currentSubmoduleStates[submodulePath] = [sha: currentSha] if (currentSha) { def previousSha = previousSubmoduleStates[submodulePath]?.sha if (previousSha && previousSha != currentSha) { submoduleSections << "- 子模块: ${submodulePath}\n- 当前提交: ${currentHeadMessage}" } } } if (submoduleSections.size() > 0) { submoduleChangeString = "\n\n子模块提交记录:\n" + submoduleSections.join("\n") } writeFile( file: submoduleStateFile, text: JsonOutput.prettyPrint(JsonOutput.toJson(currentSubmoduleStates)) ) } catch (Exception ex) { echo "子模块提交记录收集失败: ${ex.message}" submoduleChangeString = "\n\n子模块提交记录: 收集失败" } } def dingtalkText = [ "### ${currentBuild.currentResult}", "\n构建信息:", "- 项目: ${env.JOB_NAME}", "- 构建号: #${env.BUILD_NUMBER}", "- 分支: main", "- 触发者: ${triggerUser}", "- 耗时: ${currentBuild.durationString}${failedStage}", "- APK 上传: ${uploadStatus}", "\n链接:", "- [下载链接](${env.BUILD_URL})", changeString ] if (submoduleChangeString) { dingtalkText << submoduleChangeString } dingtalk( robot:'dingtalk_unity_1', type:'MARKDOWN', title:"${currentBuild.currentResult} - dev #${env.BUILD_NUMBER}", text: dingtalkText ) } } } } }