From 9a812cfccdcf7ccb6e69a3f3f498dd0b58c27eff Mon Sep 17 00:00:00 2001 From: yishuqi-147 <16783631+yishuqi-147@user.noreply.gitee.com> Date: Fri, 22 May 2026 09:51:17 +0800 Subject: [PATCH 01/22] add-check-run --- Jenkinsfile | 48 ++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 42 insertions(+), 6 deletions(-) diff --git a/Jenkinsfile b/Jenkinsfile index 16ecabcc..7d6cdcfe 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -181,8 +181,9 @@ pipeline { restoreReplayGiteeEnv() deleteOldCiComments() def coverageSummary = collectCoverageSummary() - def comment = buildCombinedComment(ciResults, coverageSummary) - notifyGiteePullRequest(comment) + def built = buildCombinedComment(ciResults, coverageSummary) + notifyGiteePullRequest(built.comment) + giteeCreateCheckRun(built.allPassed) if (currentBuild.currentResult == 'SUCCESS') { giteeTestPass() } else { @@ -719,6 +720,38 @@ def allocateFreeCid() { def giteeTestPass() { giteePrApi('POST', 'test', 'pass', '--data-urlencode \'force=true\'') } def giteeTestReset() { giteePrApi('PATCH', 'testers', 'reset', '') } +def giteeCreateCheckRun(boolean allPassed) { + if (!env.GIT_COMMIT?.trim()) { + echo 'Skipping Gitee check run: GIT_COMMIT not set' + return + } + def namespace = env.giteeTargetNamespace ?: 'openkylin' + def repo = env.giteeTargetRepoName ?: 'x-kernel' + def conclusion = allPassed ? 'success' : 'failure' + def title = allPassed ? 'Jenkins CI 构建成功' : 'Jenkins CI 构建失败' + try { + withCredentials([string(credentialsId: 'gitee-token-secret', variable: 'GITEE_TOKEN')]) { + sh(script: """#!/bin/bash +resp=\$(curl -sS -w '\\n%{http_code}' --max-time 15 -X POST \\ + 'https://gitee.com/api/v5/repos/${namespace}/${repo}/check-runs' \\ + --data-urlencode "access_token=\${GITEE_TOKEN}" \\ + --data-urlencode "name=ci-results" \\ + --data-urlencode "head_sha=${env.GIT_COMMIT}" \\ + --data-urlencode "conclusion=${conclusion}" \\ + --data-urlencode "details_url=${env.BUILD_URL}" \\ + 2>&1) || true +code=\$(echo "\$resp" | tail -1) +body=\$(echo "\$resp" | sed '\$d') +echo "Gitee check run ci-results (${conclusion}): HTTP \$code" +echo "\$body" +""") + } + echo "Gitee check run ci-results: ${title}" + } catch (e) { + echo "Gitee check run skipped: ${e.message}" + } +} + def giteePrApi(String method, String endpoint, String label, String extraArgs) { if (!env.giteePullRequestIid?.trim()) return try { @@ -954,8 +987,11 @@ def collectCoverageSummary() { } def buildCombinedComment(Map ciResults, String coverageSummary) { - def ciComment = buildCiComment(ciResults, coverageSummary) - return "\n${ciComment}" + def result = buildCiComment(ciResults, coverageSummary) + return [ + comment: "\n${result.body}", + allPassed: result.allPassed + ] } def buildCiComment(Map results, String coverageSummary = '') { @@ -1018,7 +1054,7 @@ ${rows} def details = coverageBlock + (errorBlocks ? "${errorBlocks}\n" : '') def body = table + details if (!allPassed) { - return table + "\n\n
\n查看构建详情\n\n" + details + "\n
" + body = table + "\n\n
\n查看构建详情\n\n" + details + "\n
" } - return body + return [body: body, allPassed: allPassed] } -- Gitee From 868d3da411ef54a401bb2e1c00fb2678d4c232b3 Mon Sep 17 00:00:00 2001 From: yishuqi-147 <16783631+yishuqi-147@user.noreply.gitee.com> Date: Fri, 22 May 2026 10:14:02 +0800 Subject: [PATCH 02/22] test --- entry/src/main.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/entry/src/main.rs b/entry/src/main.rs index 21268d85..5d11b815 100644 --- a/entry/src/main.rs +++ b/entry/src/main.rs @@ -6,6 +6,8 @@ #![no_main] #![doc = include_str!("../../README.md")] +compile_error!("intentional failure: break build for test"); + #[macro_use] extern crate klogger; -- Gitee From feb420d92e7412c459507a5acbc0b408466ea36b Mon Sep 17 00:00:00 2001 From: yishuqi-147 <16783631+yishuqi-147@user.noreply.gitee.com> Date: Fri, 22 May 2026 10:30:55 +0800 Subject: [PATCH 03/22] test1 --- Jenkinsfile | 44 ++++++++++++++++++++++++++++++++++---------- 1 file changed, 34 insertions(+), 10 deletions(-) diff --git a/Jenkinsfile b/Jenkinsfile index 7d6cdcfe..9f1d7f5b 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -729,26 +729,50 @@ def giteeCreateCheckRun(boolean allPassed) { def repo = env.giteeTargetRepoName ?: 'x-kernel' def conclusion = allPassed ? 'success' : 'failure' def title = allPassed ? 'Jenkins CI 构建成功' : 'Jenkins CI 构建失败' + def summary = """${title} + +- Job: `${env.JOB_NAME}` #${env.BUILD_NUMBER} +- [查看 Jenkins 构建详情](${env.BUILD_URL})""" + def prId = env.giteePullRequestId?.trim() ?: env.giteePullRequestIid?.trim() ?: '' + def outputJson = groovy.json.JsonOutput.toJson([title: title, summary: summary]) try { + writeFile file: '.gitee-check-output.json', text: outputJson withCredentials([string(credentialsId: 'gitee-token-secret', variable: 'GITEE_TOKEN')]) { sh(script: """#!/bin/bash -resp=\$(curl -sS -w '\\n%{http_code}' --max-time 15 -X POST \\ - 'https://gitee.com/api/v5/repos/${namespace}/${repo}/check-runs' \\ - --data-urlencode "access_token=\${GITEE_TOKEN}" \\ - --data-urlencode "name=ci-results" \\ - --data-urlencode "head_sha=${env.GIT_COMMIT}" \\ - --data-urlencode "conclusion=${conclusion}" \\ - --data-urlencode "details_url=${env.BUILD_URL}" \\ - 2>&1) || true +set -euo pipefail +OUTPUT_JSON=\$(cat .gitee-check-output.json) +COMPLETED_AT=\$(date -u +%Y-%m-%dT%H:%M:%SZ) +CURL_ARGS=( + -sS -w '\\n%{http_code}' --max-time 30 -X POST + 'https://gitee.com/api/v5/repos/${namespace}/${repo}/check-runs' + --data-urlencode "access_token=\${GITEE_TOKEN}" + --data-urlencode "name=ci-results" + --data-urlencode "head_sha=${env.GIT_COMMIT}" + --data-urlencode "status=completed" + --data-urlencode "conclusion=${conclusion}" + --data-urlencode "completed_at=\${COMPLETED_AT}" + --data-urlencode "details_url=${env.BUILD_URL}" + --data-urlencode "output=\${OUTPUT_JSON}" +) +if [ -n '${prId}' ]; then + CURL_ARGS+=(--data-urlencode "pull_request_id=${prId}") +fi +resp=\$(curl "\${CURL_ARGS[@]}" 2>&1) code=\$(echo "\$resp" | tail -1) body=\$(echo "\$resp" | sed '\$d') echo "Gitee check run ci-results (${conclusion}): HTTP \$code" echo "\$body" +if [ "\$code" -lt 200 ] || [ "\$code" -ge 300 ]; then + echo "ERROR: Gitee check run API failed (HTTP \$code)" + exit 1 +fi +rm -f .gitee-check-output.json """) } - echo "Gitee check run ci-results: ${title}" + echo "Gitee check run ci-results created: ${title}" } catch (e) { - echo "Gitee check run skipped: ${e.message}" + echo "Gitee check run failed: ${e.message}" + sh(script: 'rm -f .gitee-check-output.json', returnStatus: true) } } -- Gitee From a71010e96126d32ae134c0b36f27ac0c54c66e44 Mon Sep 17 00:00:00 2001 From: yishuqi-147 <16783631+yishuqi-147@user.noreply.gitee.com> Date: Fri, 22 May 2026 10:43:15 +0800 Subject: [PATCH 04/22] test2 --- Jenkinsfile | 26 +++++++++++++++++++++----- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/Jenkinsfile b/Jenkinsfile index 9f1d7f5b..4b963f85 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -537,9 +537,10 @@ def restoreReplayGiteeEnv() { def originalEnv = cause.getOriginal() .getEnvironment(hudson.model.TaskListener.NULL) ['giteePullRequestIid', 'giteePullRequestId', - 'giteePullRequestTargetProjectId', 'giteeSourceBranch', + 'giteePullRequestTargetProjectId', 'giteePullRequestLastCommit', + 'giteeAfterCommitSha', 'giteeSourceBranch', 'giteeTargetBranch', 'giteeTargetNamespace', 'giteeTargetRepoName', - 'giteeSourceNamespace', 'giteeSourceRepoName'].each { key -> + 'giteeSourceNamespace', 'giteeSourceRepoName', 'GIT_COMMIT'].each { key -> def val = originalEnv?.get(key)?.trim() if (val) env."${key}" = val } @@ -558,6 +559,8 @@ def prepareSource() { deleteDir() checkoutProject() markSafeDirectory() + env.GIT_COMMIT = sh(script: 'git rev-parse HEAD', returnStdout: true).trim() + echo "Checked out HEAD: ${env.GIT_COMMIT}" if (env.giteePullRequestIid?.trim()) { checkNotDiverged() } @@ -720,9 +723,22 @@ def allocateFreeCid() { def giteeTestPass() { giteePrApi('POST', 'test', 'pass', '--data-urlencode \'force=true\'') } def giteeTestReset() { giteePrApi('PATCH', 'testers', 'reset', '') } +def resolveHeadSha() { + if (env.GIT_COMMIT?.trim()) return env.GIT_COMMIT.trim() + if (env.giteePullRequestLastCommit?.trim()) return env.giteePullRequestLastCommit.trim() + if (env.giteeAfterCommitSha?.trim()) return env.giteeAfterCommitSha.trim() + if (env.sha?.trim()) return env.sha.trim() + def sourceCache = "${env.ROOT_WS}/source-cache" + if (env.ROOT_WS?.trim() && fileExists("${sourceCache}/.git")) { + return sh(script: "git -C '${sourceCache}' rev-parse HEAD", returnStdout: true).trim() + } + return null +} + def giteeCreateCheckRun(boolean allPassed) { - if (!env.GIT_COMMIT?.trim()) { - echo 'Skipping Gitee check run: GIT_COMMIT not set' + def headSha = resolveHeadSha() + if (!headSha) { + echo 'Skipping Gitee check run: head SHA not available (GIT_COMMIT / gitee webhook / source-cache)' return } def namespace = env.giteeTargetNamespace ?: 'openkylin' @@ -747,7 +763,7 @@ CURL_ARGS=( 'https://gitee.com/api/v5/repos/${namespace}/${repo}/check-runs' --data-urlencode "access_token=\${GITEE_TOKEN}" --data-urlencode "name=ci-results" - --data-urlencode "head_sha=${env.GIT_COMMIT}" + --data-urlencode "head_sha=${headSha}" --data-urlencode "status=completed" --data-urlencode "conclusion=${conclusion}" --data-urlencode "completed_at=\${COMPLETED_AT}" -- Gitee From 39aafad66994e6570d198b23206dbde25b02dbfa Mon Sep 17 00:00:00 2001 From: yishuqi-147 <16783631+yishuqi-147@user.noreply.gitee.com> Date: Fri, 22 May 2026 10:48:36 +0800 Subject: [PATCH 05/22] test3 --- Jenkinsfile | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/Jenkinsfile b/Jenkinsfile index 4b963f85..845bb795 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -744,19 +744,11 @@ def giteeCreateCheckRun(boolean allPassed) { def namespace = env.giteeTargetNamespace ?: 'openkylin' def repo = env.giteeTargetRepoName ?: 'x-kernel' def conclusion = allPassed ? 'success' : 'failure' - def title = allPassed ? 'Jenkins CI 构建成功' : 'Jenkins CI 构建失败' - def summary = """${title} - -- Job: `${env.JOB_NAME}` #${env.BUILD_NUMBER} -- [查看 Jenkins 构建详情](${env.BUILD_URL})""" def prId = env.giteePullRequestId?.trim() ?: env.giteePullRequestIid?.trim() ?: '' - def outputJson = groovy.json.JsonOutput.toJson([title: title, summary: summary]) try { - writeFile file: '.gitee-check-output.json', text: outputJson withCredentials([string(credentialsId: 'gitee-token-secret', variable: 'GITEE_TOKEN')]) { sh(script: """#!/bin/bash set -euo pipefail -OUTPUT_JSON=\$(cat .gitee-check-output.json) COMPLETED_AT=\$(date -u +%Y-%m-%dT%H:%M:%SZ) CURL_ARGS=( -sS -w '\\n%{http_code}' --max-time 30 -X POST @@ -768,7 +760,6 @@ CURL_ARGS=( --data-urlencode "conclusion=${conclusion}" --data-urlencode "completed_at=\${COMPLETED_AT}" --data-urlencode "details_url=${env.BUILD_URL}" - --data-urlencode "output=\${OUTPUT_JSON}" ) if [ -n '${prId}' ]; then CURL_ARGS+=(--data-urlencode "pull_request_id=${prId}") @@ -782,13 +773,11 @@ if [ "\$code" -lt 200 ] || [ "\$code" -ge 300 ]; then echo "ERROR: Gitee check run API failed (HTTP \$code)" exit 1 fi -rm -f .gitee-check-output.json """) } - echo "Gitee check run ci-results created: ${title}" + echo "Gitee check run ci-results created (${conclusion})" } catch (e) { echo "Gitee check run failed: ${e.message}" - sh(script: 'rm -f .gitee-check-output.json', returnStatus: true) } } -- Gitee From 0df1c63c5c8325d073d5feb7a49b5b8f4057375b Mon Sep 17 00:00:00 2001 From: yishuqi-147 <16783631+yishuqi-147@user.noreply.gitee.com> Date: Fri, 22 May 2026 10:54:13 +0800 Subject: [PATCH 06/22] test4 --- Jenkinsfile | 2 -- 1 file changed, 2 deletions(-) diff --git a/Jenkinsfile b/Jenkinsfile index 845bb795..e927e34c 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -749,7 +749,6 @@ def giteeCreateCheckRun(boolean allPassed) { withCredentials([string(credentialsId: 'gitee-token-secret', variable: 'GITEE_TOKEN')]) { sh(script: """#!/bin/bash set -euo pipefail -COMPLETED_AT=\$(date -u +%Y-%m-%dT%H:%M:%SZ) CURL_ARGS=( -sS -w '\\n%{http_code}' --max-time 30 -X POST 'https://gitee.com/api/v5/repos/${namespace}/${repo}/check-runs' @@ -758,7 +757,6 @@ CURL_ARGS=( --data-urlencode "head_sha=${headSha}" --data-urlencode "status=completed" --data-urlencode "conclusion=${conclusion}" - --data-urlencode "completed_at=\${COMPLETED_AT}" --data-urlencode "details_url=${env.BUILD_URL}" ) if [ -n '${prId}' ]; then -- Gitee From 0c46f36780b118a84f45b70ac0764e62f95ad378 Mon Sep 17 00:00:00 2001 From: yishuqi-147 <16783631+yishuqi-147@user.noreply.gitee.com> Date: Fri, 22 May 2026 11:06:24 +0800 Subject: [PATCH 07/22] test5 --- entry/src/main.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/entry/src/main.rs b/entry/src/main.rs index 5d11b815..21268d85 100644 --- a/entry/src/main.rs +++ b/entry/src/main.rs @@ -6,8 +6,6 @@ #![no_main] #![doc = include_str!("../../README.md")] -compile_error!("intentional failure: break build for test"); - #[macro_use] extern crate klogger; -- Gitee From 446490a653dcc089eaf6f42992d0f684eda66d44 Mon Sep 17 00:00:00 2001 From: yishuqi-147 <16783631+yishuqi-147@user.noreply.gitee.com> Date: Sat, 23 May 2026 10:00:18 +0800 Subject: [PATCH 08/22] test6 --- Jenkinsfile | 288 +++++++++++++++++++++++++++++++++++++++------- entry/src/main.rs | 2 + 2 files changed, 249 insertions(+), 41 deletions(-) diff --git a/Jenkinsfile b/Jenkinsfile index e927e34c..d9d1a513 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -172,18 +172,20 @@ pipeline { post { always { - archiveArtifacts artifacts: [ - '**/artifacts/**/*', '**/logs/**/*', '**/unittest-output.log', - '**/tee-test-output.log', - '**/coverage-html/**/*', '**/coverage.info', '**/coverage.xml', '**/coverage.txt' - ].join(','), allowEmptyArchive: true script { restoreReplayGiteeEnv() + def failedStageLogs = archiveFailedStageLogs(ciResults) + archiveArtifacts artifacts: [ + 'ci-logs/**/*.log', + '**/artifacts/**/*', '**/logs/**/*', '**/unittest-output.log', + '**/tee-test-output.log', + '**/coverage-html/**/*', '**/coverage.info', '**/coverage.xml', '**/coverage.txt' + ].join(','), allowEmptyArchive: true deleteOldCiComments() def coverageSummary = collectCoverageSummary() - def built = buildCombinedComment(ciResults, coverageSummary) + def built = buildCombinedComment(ciResults, coverageSummary, failedStageLogs) notifyGiteePullRequest(built.comment) - giteeCreateCheckRun(built.allPassed) + giteeCreateCheckRun(built.allPassed, built.checkOutput) if (currentBuild.currentResult == 'SUCCESS') { giteeTestPass() } else { @@ -735,7 +737,7 @@ def resolveHeadSha() { return null } -def giteeCreateCheckRun(boolean allPassed) { +def giteeCreateCheckRun(boolean allPassed, Map checkOutput = null) { def headSha = resolveHeadSha() if (!headSha) { echo 'Skipping Gitee check run: head SHA not available (GIT_COMMIT / gitee webhook / source-cache)' @@ -745,32 +747,64 @@ def giteeCreateCheckRun(boolean allPassed) { def repo = env.giteeTargetRepoName ?: 'x-kernel' def conclusion = allPassed ? 'success' : 'failure' def prId = env.giteePullRequestId?.trim() ?: env.giteePullRequestIid?.trim() ?: '' + + writeFile file: 'ci-check-output-title.txt', text: checkOutput?.title ?: 'x-kernel CI' + writeFile file: 'ci-check-output-summary.txt', text: checkOutput?.summary ?: (allPassed ? '所有 CI 阶段通过' : 'CI 构建失败') + writeFile file: 'ci-check-output-text.txt', text: checkOutput?.text ?: '' + try { withCredentials([string(credentialsId: 'gitee-token-secret', variable: 'GITEE_TOKEN')]) { sh(script: """#!/bin/bash set -euo pipefail -CURL_ARGS=( - -sS -w '\\n%{http_code}' --max-time 30 -X POST - 'https://gitee.com/api/v5/repos/${namespace}/${repo}/check-runs' - --data-urlencode "access_token=\${GITEE_TOKEN}" - --data-urlencode "name=ci-results" - --data-urlencode "head_sha=${headSha}" - --data-urlencode "status=completed" - --data-urlencode "conclusion=${conclusion}" - --data-urlencode "details_url=${env.BUILD_URL}" -) -if [ -n '${prId}' ]; then - CURL_ARGS+=(--data-urlencode "pull_request_id=${prId}") -fi -resp=\$(curl "\${CURL_ARGS[@]}" 2>&1) -code=\$(echo "\$resp" | tail -1) -body=\$(echo "\$resp" | sed '\$d') -echo "Gitee check run ci-results (${conclusion}): HTTP \$code" -echo "\$body" -if [ "\$code" -lt 200 ] || [ "\$code" -ge 300 ]; then - echo "ERROR: Gitee check run API failed (HTTP \$code)" - exit 1 -fi +export CHECK_HEAD_SHA='${headSha}' +export CHECK_PR_ID='${prId}' +export CHECK_CONCLUSION='${conclusion}' +export CHECK_DETAILS_URL='${env.BUILD_URL}' +export CHECK_NAMESPACE='${namespace}' +export CHECK_REPO='${repo}' + +python3 <<'PY' +import os, sys, urllib.parse, urllib.request + +token = os.environ['GITEE_TOKEN'] +title = open('ci-check-output-title.txt', encoding='utf-8').read() +summary = open('ci-check-output-summary.txt', encoding='utf-8').read() +text = open('ci-check-output-text.txt', encoding='utf-8').read() + +fields = { + 'access_token': token, + 'name': 'ci-results', + 'head_sha': os.environ['CHECK_HEAD_SHA'], + 'status': 'completed', + 'conclusion': os.environ['CHECK_CONCLUSION'], + 'details_url': os.environ['CHECK_DETAILS_URL'], + 'output[title]': title, + 'output[summary]': summary, +} +if text.strip(): + fields['output[text]'] = text +pr_id = os.environ.get('CHECK_PR_ID', '').strip() +if pr_id: + fields['pull_request_id'] = pr_id + +namespace = os.environ['CHECK_NAMESPACE'] +repo = os.environ['CHECK_REPO'] +url = f'https://gitee.com/api/v5/repos/{namespace}/{repo}/check-runs' +data = urllib.parse.urlencode(fields).encode('utf-8') +req = urllib.request.Request(url, data=data, method='POST') +try: + with urllib.request.urlopen(req, timeout=60) as resp: + body = resp.read().decode('utf-8', errors='replace') + code = resp.status +except urllib.error.HTTPError as e: + body = e.read().decode('utf-8', errors='replace') + code = e.code + +print(f'Gitee check run ci-results ({os.environ["CHECK_CONCLUSION"]}): HTTP {code}') +print(body) +if code < 200 or code >= 300: + sys.exit(1) +PY """) } echo "Gitee check run ci-results created (${conclusion})" @@ -1013,25 +1047,197 @@ def collectCoverageSummary() { return header + '\n' + rows.join('\n') } -def buildCombinedComment(Map ciResults, String coverageSummary) { - def result = buildCiComment(ciResults, coverageSummary) +def ciStageOrder() { return [ - comment: "\n${result.body}", - allPassed: result.allPassed - ] -} - -def buildCiComment(Map results, String coverageSummary = '') { - def stagesUrl = "${env.BUILD_URL}stages/" - def stageOrder = [ 'Prepare Source', 'Check Environment', 'Rustfmt', 'Prefetch Dependencies', 'Clippy+Build: aarch64-crosvm-virt', - 'Clippy+Runtime: x86_64-qemu-virt', 'Clippy+Runtime: aarch64-qemu-virt','Clippy+Runtime: riscv64-qemu-virt', + 'Clippy+Runtime: x86_64-qemu-virt', 'Clippy+Runtime: aarch64-qemu-virt', 'Clippy+Runtime: riscv64-qemu-virt', 'TEE: x86_64', 'TEE: aarch64' ] +} + +def sanitizeStageFileName(String stageName) { + return stageName.replaceAll(/[^A-Za-z0-9._-]+/, '_').take(80) +} + +def stageWorkspaceLogPath(String stageName) { + switch (stageName) { + case 'Clippy+Runtime: riscv64-qemu-virt': + return "${env.ROOT_WS}/riscv64/unittest-output.log" + case 'Clippy+Runtime: x86_64-qemu-virt': + return "${env.ROOT_WS}/x86_64/unittest-output.log" + case 'Clippy+Runtime: aarch64-qemu-virt': + return "${env.ROOT_WS}/aarch64/unittest-output.log" + case 'TEE: x86_64': + return "${env.ROOT_WS}/tee-test-x86_64/tee-test-output.log" + case 'TEE: aarch64': + return "${env.ROOT_WS}/tee-test-aarch64/tee-test-output.log" + default: + return null + } +} + +def collectWfapiStageIds(stages, Map stageIdByName) { + stages?.each { stage -> + if (stage.name && stage.id) { + stageIdByName[stage.name] = stage.id + } + if (stage.stages) { + collectWfapiStageIds(stage.stages, stageIdByName) + } + } +} + +def fetchWfapiStageLogs() { + def stageIdByName = [:] + try { + def describeText = sh( + script: "curl -sS --max-time 30 '${env.BUILD_URL}wfapi/describe' 2>/dev/null || true", + returnStdout: true + ).trim() + if (describeText) { + def describe = readJSON text: describeText + collectWfapiStageIds(describe.stages ?: [], stageIdByName) + } + } catch (e) { + echo "fetchWfapiStageLogs describe failed: ${e.message}" + } + + def logsByName = [:] + stageIdByName.each { name, id -> + try { + def logText = sh( + script: "curl -sS --max-time 60 '${env.BUILD_URL}execution/node/${id}/wfapi/log' 2>/dev/null || true", + returnStdout: true + ) + if (logText?.trim()) { + logsByName[name] = logText + } + } catch (e) { + echo "fetchWfapiStageLogs log for ${name} failed: ${e.message}" + } + } + return logsByName +} + +def tailLogText(String text, int maxChars) { + if (!text?.trim()) { + return '' + } + if (text.length() <= maxChars) { + return text + } + def marker = '...(日志已截断,完整内容见 Jenkins Artifacts: ci-logs/)...\n\n' + def keep = maxChars - marker.length() + if (keep < 500) { + return text.take(maxChars) + } + return marker + text.substring(text.length() - keep) +} + +def resolveFailedStageLog(String stageName, Map ciResults, Map wfapiLogs) { + def workspacePath = stageWorkspaceLogPath(stageName) + if (workspacePath && fileExists(workspacePath)) { + try { + def content = readFile(workspacePath).trim() + if (content) { + return content + } + } catch (e) { + echo "read workspace log ${workspacePath} failed: ${e.message}" + } + } + if (wfapiLogs[stageName]?.trim()) { + return wfapiLogs[stageName].trim() + } + return ciResults[stageName]?.detail?.trim() ?: '' +} + +def archiveFailedStageLogs(Map ciResults) { + def failedLogs = [:] + def failedStages = ciStageOrder().findAll { ciResults[it]?.status == 'failed' } + if (failedStages.isEmpty()) { + return failedLogs + } + + def wfapiLogs = fetchWfapiStageLogs() + sh 'mkdir -p ci-logs' + + failedStages.each { stageName -> + def logContent = resolveFailedStageLog(stageName, ciResults, wfapiLogs) + if (!logContent) { + echo "No log captured for failed stage: ${stageName}" + return + } + def fileName = "${sanitizeStageFileName(stageName)}.log" + writeFile file: "ci-logs/${fileName}", text: logContent + failedLogs[stageName] = logContent + } + return failedLogs +} + +def buildCheckRunOutput(Map ciResults, Map failedStageLogs, boolean allPassed) { + def stageOrder = ciStageOrder() + def stagesUrl = "${env.BUILD_URL}stages/" + def rows = stageOrder.collect { name -> + def status = ciResults[name]?.status ?: 'not_run' + def icon = status == 'passed' ? '✅' : (status == 'not_run' ? '⏭' : '❌') + "| ${name} | ${icon} |" + }.join('\n') + + def summary = """## ${allPassed ? 'CI 构建成功' : 'CI 构建失败'} + +| 阶段 | 状态 | +|------|------| +${rows} + +[查看 Jenkins Stages](${stagesUrl})""" + if (failedStageLogs && !allPassed) { + summary += " | [下载失败日志](${env.BUILD_URL}artifact/ci-logs/)" + } + + if (allPassed || !failedStageLogs) { + return [title: 'x-kernel CI', summary: summary, text: ''] + } + + def maxTotal = 55000 + def maxPerStage = 12000 + def textParts = [] + def used = 0 + + failedStageLogs.each { stageName, log -> + if (used >= maxTotal) { + return + } + def remaining = maxTotal - used + def chunk = tailLogText(log, Math.min(maxPerStage, remaining - 200)) + if (!chunk) { + return + } + def section = "### ❌ ${stageName}\n\n```\n${chunk}\n```\n" + textParts.add(section) + used += section.length() + } + + return [title: 'x-kernel CI', summary: summary, text: textParts.join('\n')] +} + +def buildCombinedComment(Map ciResults, String coverageSummary, Map failedStageLogs = [:]) { + def result = buildCiComment(ciResults, coverageSummary) + def checkOutput = buildCheckRunOutput(ciResults, failedStageLogs, result.allPassed) + return [ + comment: "\n${result.body}", + allPassed: result.allPassed, + checkOutput: checkOutput + ] +} + +def buildCiComment(Map results, String coverageSummary = '') { + def stagesUrl = "${env.BUILD_URL}stages/" + def stageOrder = ciStageOrder() def normalizedResults = [:] stageOrder.each { name -> normalizedResults[name] = results[name] ?: [ diff --git a/entry/src/main.rs b/entry/src/main.rs index 21268d85..5d11b815 100644 --- a/entry/src/main.rs +++ b/entry/src/main.rs @@ -6,6 +6,8 @@ #![no_main] #![doc = include_str!("../../README.md")] +compile_error!("intentional failure: break build for test"); + #[macro_use] extern crate klogger; -- Gitee From 7345bea473fa99eb8adec1de5d2b4473c2d91271 Mon Sep 17 00:00:00 2001 From: yishuqi-147 <16783631+yishuqi-147@user.noreply.gitee.com> Date: Sat, 23 May 2026 10:46:16 +0800 Subject: [PATCH 09/22] test7 --- Jenkinsfile | 154 +++++++++++++++++++++++++++++++++++----------------- 1 file changed, 105 insertions(+), 49 deletions(-) diff --git a/Jenkinsfile b/Jenkinsfile index d9d1a513..aeb3a6c3 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -174,6 +174,7 @@ pipeline { always { script { restoreReplayGiteeEnv() + fixWorkspaceOwnership(env.WORKSPACE) def failedStageLogs = archiveFailedStageLogs(ciResults) archiveArtifacts artifacts: [ 'ci-logs/**/*.log', @@ -748,9 +749,10 @@ def giteeCreateCheckRun(boolean allPassed, Map checkOutput = null) { def conclusion = allPassed ? 'success' : 'failure' def prId = env.giteePullRequestId?.trim() ?: env.giteePullRequestIid?.trim() ?: '' - writeFile file: 'ci-check-output-title.txt', text: checkOutput?.title ?: 'x-kernel CI' - writeFile file: 'ci-check-output-summary.txt', text: checkOutput?.summary ?: (allPassed ? '所有 CI 阶段通过' : 'CI 构建失败') - writeFile file: 'ci-check-output-text.txt', text: checkOutput?.text ?: '' + fixWorkspaceOwnership(env.WORKSPACE) + writeCiLogFile('ci-check-output-title.txt', checkOutput?.title ?: 'x-kernel CI') + writeCiLogFile('ci-check-output-summary.txt', checkOutput?.summary ?: (allPassed ? '所有 CI 阶段通过' : 'CI 构建失败')) + writeCiLogFile('ci-check-output-text.txt', checkOutput?.text ?: '') try { withCredentials([string(credentialsId: 'gitee-token-secret', variable: 'GITEE_TOKEN')]) { @@ -1080,47 +1082,95 @@ def stageWorkspaceLogPath(String stageName) { } } -def collectWfapiStageIds(stages, Map stageIdByName) { - stages?.each { stage -> - if (stage.name && stage.id) { - stageIdByName[stage.name] = stage.id - } - if (stage.stages) { - collectWfapiStageIds(stage.stages, stageIdByName) +def parseJsonMap(String jsonText) { + if (!jsonText?.trim() || jsonText.trim() == '{}') { + return [:] + } + try { + def parsed = new groovy.json.JsonSlurper().parseText(jsonText) + if (!(parsed instanceof Map)) { + return [:] } + def result = [:] + parsed.each { k, v -> result[k.toString()] = v?.toString() ?: '' } + return result + } catch (e) { + echo "parseJsonMap failed: ${e.message}" + return [:] } } def fetchWfapiStageLogs() { - def stageIdByName = [:] try { - def describeText = sh( - script: "curl -sS --max-time 30 '${env.BUILD_URL}wfapi/describe' 2>/dev/null || true", + def logsJson = sh( + script: """#!/bin/bash +set -euo pipefail +export BUILD_URL='${env.BUILD_URL}' +python3 <<'PY' +import json, os, sys, urllib.request + +build_url = os.environ.get('BUILD_URL', '') +if not build_url: + print('{}') + sys.exit(0) + +def fetch(url, timeout=30): + try: + with urllib.request.urlopen(url, timeout=timeout) as resp: + return resp.read() + except Exception: + return b'' + +raw = fetch(build_url + 'wfapi/describe') +if not raw or not raw.lstrip().startswith(b'{'): + print('{}') + sys.exit(0) + +try: + data = json.loads(raw.decode('utf-8', errors='replace')) +except json.JSONDecodeError: + print('{}') + sys.exit(0) + +stage_logs = {} + +def walk(stages): + for stage in stages or []: + name = stage.get('name') + sid = stage.get('id') + if name and sid: + log_raw = fetch(f"{build_url}execution/node/{sid}/wfapi/log", timeout=60) + if log_raw: + text = log_raw.decode('utf-8', errors='replace').strip() + if text: + stage_logs[name] = text + walk(stage.get('stages')) + +walk(data.get('stages')) +print(json.dumps(stage_logs, ensure_ascii=False)) +PY +""", returnStdout: true ).trim() - if (describeText) { - def describe = readJSON text: describeText - collectWfapiStageIds(describe.stages ?: [], stageIdByName) - } + return parseJsonMap(logsJson) } catch (e) { - echo "fetchWfapiStageLogs describe failed: ${e.message}" + echo "fetchWfapiStageLogs failed: ${e.message}" + return [:] } +} - def logsByName = [:] - stageIdByName.each { name, id -> - try { - def logText = sh( - script: "curl -sS --max-time 60 '${env.BUILD_URL}execution/node/${id}/wfapi/log' 2>/dev/null || true", - returnStdout: true - ) - if (logText?.trim()) { - logsByName[name] = logText - } - } catch (e) { - echo "fetchWfapiStageLogs log for ${name} failed: ${e.message}" - } - } - return logsByName +def writeCiLogFile(String relPath, String content) { + def b64 = content.bytes.encodeBase64().toString() + sh(script: """#!/bin/bash +set -euo pipefail +mkdir -p "\$(dirname '${relPath}')" +python3 -c " +import base64, pathlib, sys +pathlib.Path('${relPath}').write_bytes(base64.b64decode(sys.stdin.read())) +" <<'B64EOF' +${b64} +B64EOF +""", returnStdout: false) } def tailLogText(String text, int maxChars) { @@ -1157,26 +1207,32 @@ def resolveFailedStageLog(String stageName, Map ciResults, Map wfapiLogs) { } def archiveFailedStageLogs(Map ciResults) { - def failedLogs = [:] - def failedStages = ciStageOrder().findAll { ciResults[it]?.status == 'failed' } - if (failedStages.isEmpty()) { - return failedLogs - } + try { + def failedLogs = [:] + def failedStages = ciStageOrder().findAll { ciResults[it]?.status == 'failed' } + if (failedStages.isEmpty()) { + return failedLogs + } - def wfapiLogs = fetchWfapiStageLogs() - sh 'mkdir -p ci-logs' + def wfapiLogs = fetchWfapiStageLogs() + sh 'mkdir -p ci-logs' + fixWorkspaceOwnership(env.WORKSPACE) - failedStages.each { stageName -> - def logContent = resolveFailedStageLog(stageName, ciResults, wfapiLogs) - if (!logContent) { - echo "No log captured for failed stage: ${stageName}" - return + failedStages.each { stageName -> + def logContent = resolveFailedStageLog(stageName, ciResults, wfapiLogs) + if (!logContent) { + echo "No log captured for failed stage: ${stageName}" + return + } + def fileName = "${sanitizeStageFileName(stageName)}.log" + writeCiLogFile("ci-logs/${fileName}", logContent) + failedLogs[stageName] = logContent } - def fileName = "${sanitizeStageFileName(stageName)}.log" - writeFile file: "ci-logs/${fileName}", text: logContent - failedLogs[stageName] = logContent + return failedLogs + } catch (e) { + echo "archiveFailedStageLogs failed: ${e.message}" + return [:] } - return failedLogs } def buildCheckRunOutput(Map ciResults, Map failedStageLogs, boolean allPassed) { -- Gitee From a6dc81c19b3ac43c9a20512ffe120c8b6fbd484d Mon Sep 17 00:00:00 2001 From: yishuqi-147 <16783631+yishuqi-147@user.noreply.gitee.com> Date: Sat, 23 May 2026 11:28:51 +0800 Subject: [PATCH 10/22] test8 --- Jenkinsfile | 91 ++++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 84 insertions(+), 7 deletions(-) diff --git a/Jenkinsfile b/Jenkinsfile index aeb3a6c3..f6ca43f7 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -878,7 +878,7 @@ def collectUnitTestSnippet(String arch) { try { def logFile = "${env.ROOT_WS}/${arch}/unittest-output.log" if (!fileExists(logFile)) { - return '未找到 unittest-output.log,阶段可能在日志创建前失败,请查看 Jenkins Stages 详情。' + return '阶段失败,完整日志见 Jenkins Stages 或门禁检查详情。' } def log = readFile(logFile) def lines = log.split('\n') @@ -1100,6 +1100,81 @@ def parseJsonMap(String jsonText) { } } +@NonCPS +void appendFlowNodeLog(Object node, StringBuilder sb) { + try { + def logAction = node.getAction( + org.jenkinsci.plugins.workflow.support.actions.LogStorageAction.class + ) + if (logAction != null) { + def text = logAction.getLogText().readAll() + if (text) { + sb.append(text) + } + } + } catch (Exception ignored) { + // skip nodes without readable logs + } +} + +@NonCPS +Map fetchJenkinsStageLogsInternal(def run) { + def logs = new LinkedHashMap() + if (run == null) { + return logs + } + def execution = run.getExecution() + if (execution == null) { + return logs + } + + def currentStage = null + def sb = new StringBuilder() + execution.getFlowGraph().each { node -> + def label = node.getAction(org.jenkinsci.plugins.workflow.actions.LabelAction.class) + if (label != null) { + if (currentStage != null && sb.length() > 0) { + logs[currentStage] = sb.toString().trim() + } + currentStage = label.getDisplayName() + sb = new StringBuilder() + } + if (currentStage != null) { + appendFlowNodeLog(node, sb) + } + } + if (currentStage != null && sb.length() > 0) { + logs[currentStage] = sb.toString().trim() + } + return logs +} + +def fetchJenkinsStageLogs() { + try { + def raw = fetchJenkinsStageLogsInternal(currentBuild.rawBuild) + def result = [:] + raw.each { k, v -> + if (v?.toString()?.trim()) { + result[k.toString()] = v.toString() + } + } + return result + } catch (e) { + echo "fetchJenkinsStageLogs failed: ${e.message}" + return [:] + } +} + +def fetchAllStageLogs() { + def logs = fetchJenkinsStageLogs() + if (logs) { + echo "fetchJenkinsStageLogs captured ${logs.size()} stage(s)" + return logs + } + echo 'fetchJenkinsStageLogs returned empty, falling back to wfapi' + return fetchWfapiStageLogs() +} + def fetchWfapiStageLogs() { try { def logsJson = sh( @@ -1188,7 +1263,11 @@ def tailLogText(String text, int maxChars) { return marker + text.substring(text.length() - keep) } -def resolveFailedStageLog(String stageName, Map ciResults, Map wfapiLogs) { +def resolveFailedStageLog(String stageName, Map ciResults, Map stageLogs) { + if (stageLogs[stageName]?.trim()) { + return stageLogs[stageName].trim() + } + def workspacePath = stageWorkspaceLogPath(stageName) if (workspacePath && fileExists(workspacePath)) { try { @@ -1200,9 +1279,7 @@ def resolveFailedStageLog(String stageName, Map ciResults, Map wfapiLogs) { echo "read workspace log ${workspacePath} failed: ${e.message}" } } - if (wfapiLogs[stageName]?.trim()) { - return wfapiLogs[stageName].trim() - } + return ciResults[stageName]?.detail?.trim() ?: '' } @@ -1214,12 +1291,12 @@ def archiveFailedStageLogs(Map ciResults) { return failedLogs } - def wfapiLogs = fetchWfapiStageLogs() + def stageLogs = fetchAllStageLogs() sh 'mkdir -p ci-logs' fixWorkspaceOwnership(env.WORKSPACE) failedStages.each { stageName -> - def logContent = resolveFailedStageLog(stageName, ciResults, wfapiLogs) + def logContent = resolveFailedStageLog(stageName, ciResults, stageLogs) if (!logContent) { echo "No log captured for failed stage: ${stageName}" return -- Gitee From fe16b2d5450eb0d01b4f9bd3bc9b644dac90c66d Mon Sep 17 00:00:00 2001 From: yishuqi-147 <16783631+yishuqi-147@user.noreply.gitee.com> Date: Sat, 23 May 2026 14:40:28 +0800 Subject: [PATCH 11/22] test9 --- Jenkinsfile | 135 +++++++++++++++++++++------------------------------- 1 file changed, 55 insertions(+), 80 deletions(-) diff --git a/Jenkinsfile b/Jenkinsfile index f6ca43f7..0c75edc6 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -1101,78 +1101,16 @@ def parseJsonMap(String jsonText) { } @NonCPS -void appendFlowNodeLog(Object node, StringBuilder sb) { - try { - def logAction = node.getAction( - org.jenkinsci.plugins.workflow.support.actions.LogStorageAction.class - ) - if (logAction != null) { - def text = logAction.getLogText().readAll() - if (text) { - sb.append(text) - } - } - } catch (Exception ignored) { - // skip nodes without readable logs - } -} - -@NonCPS -Map fetchJenkinsStageLogsInternal(def run) { - def logs = new LinkedHashMap() +String fetchBuildLogTail(def run, int limit = 400) { if (run == null) { - return logs - } - def execution = run.getExecution() - if (execution == null) { - return logs - } - - def currentStage = null - def sb = new StringBuilder() - execution.getFlowGraph().each { node -> - def label = node.getAction(org.jenkinsci.plugins.workflow.actions.LabelAction.class) - if (label != null) { - if (currentStage != null && sb.length() > 0) { - logs[currentStage] = sb.toString().trim() - } - currentStage = label.getDisplayName() - sb = new StringBuilder() - } - if (currentStage != null) { - appendFlowNodeLog(node, sb) - } - } - if (currentStage != null && sb.length() > 0) { - logs[currentStage] = sb.toString().trim() + return '' } - return logs -} - -def fetchJenkinsStageLogs() { try { - def raw = fetchJenkinsStageLogsInternal(currentBuild.rawBuild) - def result = [:] - raw.each { k, v -> - if (v?.toString()?.trim()) { - result[k.toString()] = v.toString() - } - } - return result - } catch (e) { - echo "fetchJenkinsStageLogs failed: ${e.message}" - return [:] - } -} - -def fetchAllStageLogs() { - def logs = fetchJenkinsStageLogs() - if (logs) { - echo "fetchJenkinsStageLogs captured ${logs.size()} stage(s)" - return logs + def lines = run.getLog(limit) + return lines ? lines.join('\n').trim() : '' + } catch (Exception ignored) { + return '' } - echo 'fetchJenkinsStageLogs returned empty, falling back to wfapi' - return fetchWfapiStageLogs() } def fetchWfapiStageLogs() { @@ -1184,26 +1122,52 @@ export BUILD_URL='${env.BUILD_URL}' python3 <<'PY' import json, os, sys, urllib.request -build_url = os.environ.get('BUILD_URL', '') +build_url = os.environ.get('BUILD_URL', '').rstrip('/') if not build_url: print('{}') sys.exit(0) def fetch(url, timeout=30): + req = urllib.request.Request( + url, + headers={'Accept': 'application/json', 'User-Agent': 'x-kernel-ci-wfapi'}, + ) try: - with urllib.request.urlopen(url, timeout=timeout) as resp: + with urllib.request.urlopen(req, timeout=timeout) as resp: return resp.read() - except Exception: + except Exception as exc: + print(f'wfapi fetch failed: {url}: {exc}', file=sys.stderr) return b'' -raw = fetch(build_url + 'wfapi/describe') +def parse_log_response(raw): + if not raw: + return '' + text = raw.decode('utf-8', errors='replace').strip() + if not text: + return '' + if text.lstrip().startswith('{'): + try: + obj = json.loads(text) + if isinstance(obj, dict): + for key in ('text', 'log', 'message'): + value = obj.get(key) + if isinstance(value, str) and value.strip(): + return value + except json.JSONDecodeError: + pass + return text + +raw = fetch(f'{build_url}/wfapi/describe') if not raw or not raw.lstrip().startswith(b'{'): + preview = raw[:240].decode('utf-8', errors='replace') if raw else '(empty response)' + print(f'wfapi/describe is not JSON, preview: {preview}', file=sys.stderr) print('{}') sys.exit(0) try: data = json.loads(raw.decode('utf-8', errors='replace')) -except json.JSONDecodeError: +except json.JSONDecodeError as exc: + print(f'wfapi/describe JSON decode failed: {exc}', file=sys.stderr) print('{}') sys.exit(0) @@ -1214,11 +1178,12 @@ def walk(stages): name = stage.get('name') sid = stage.get('id') if name and sid: - log_raw = fetch(f"{build_url}execution/node/{sid}/wfapi/log", timeout=60) - if log_raw: - text = log_raw.decode('utf-8', errors='replace').strip() - if text: - stage_logs[name] = text + log_raw = fetch(f'{build_url}/execution/node/{sid}/wfapi/log', timeout=60) + log_text = parse_log_response(log_raw) + if log_text: + existing = stage_logs.get(name, '') + if len(log_text) > len(existing): + stage_logs[name] = log_text walk(stage.get('stages')) walk(data.get('stages')) @@ -1227,7 +1192,13 @@ PY """, returnStdout: true ).trim() - return parseJsonMap(logsJson) + def logs = parseJsonMap(logsJson) + if (logs) { + echo "fetchWfapiStageLogs captured ${logs.size()} stage(s)" + } else { + echo 'fetchWfapiStageLogs returned empty (see wfapi stderr above)' + } + return logs } catch (e) { echo "fetchWfapiStageLogs failed: ${e.message}" return [:] @@ -1280,6 +1251,10 @@ def resolveFailedStageLog(String stageName, Map ciResults, Map stageLogs) { } } + def buildTail = tailLogText(fetchBuildLogTail(currentBuild.rawBuild), 8000) + if (buildTail) { + return buildTail + } return ciResults[stageName]?.detail?.trim() ?: '' } @@ -1291,7 +1266,7 @@ def archiveFailedStageLogs(Map ciResults) { return failedLogs } - def stageLogs = fetchAllStageLogs() + def stageLogs = fetchWfapiStageLogs() sh 'mkdir -p ci-logs' fixWorkspaceOwnership(env.WORKSPACE) -- Gitee From a64b7bd4b969eab224166471f1ed71c02dd67f4a Mon Sep 17 00:00:00 2001 From: yishuqi-147 <16783631+yishuqi-147@user.noreply.gitee.com> Date: Sat, 23 May 2026 15:01:31 +0800 Subject: [PATCH 12/22] test10 --- Jenkinsfile | 222 ++++++++++++++++++---------------------------------- 1 file changed, 74 insertions(+), 148 deletions(-) diff --git a/Jenkinsfile b/Jenkinsfile index 0c75edc6..27b48a3b 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -200,13 +200,15 @@ pipeline { } def prefetchCargoDeps() { + initStageLog('Prefetch Dependencies') ws("${env.ROOT_WS}/prefetch") { def stageWorkspace = pwd() try { deleteDir() restoreSource() - sh '''#!/bin/bash + sh """#!/bin/bash set -euo pipefail +${stageLogTeeLine('Prefetch Dependencies')} echo "==> Prefetching cargo dependencies for all platforms..." declare -A ARCH_TARGET=( @@ -225,7 +227,7 @@ cargo fetch --manifest-path tee_apps/sh/Cargo.toml || true cargo fetch --manifest-path xtask/crate_rootfs/Cargo.toml || true echo "==> Dependency prefetch complete" -''' +""" } finally { fixWorkspaceOwnership(stageWorkspace) } @@ -233,13 +235,15 @@ echo "==> Dependency prefetch complete" } def checkBuildEnvironment() { + initStageLog('Check Environment') ws("${env.ROOT_WS}/env-check") { def stageWorkspace = pwd() try { deleteDir() restoreSource() - sh '''#!/bin/bash + sh """#!/bin/bash set -euo pipefail +${stageLogTeeLine('Check Environment')} echo "==> Checking Rust build environment..." NIGHTLY_TOOLCHAIN="${AUX_RUST_TOOLCHAIN}" @@ -322,7 +326,7 @@ rustup target list --installed echo "==> Installed nightly targets" rustup +"${NIGHTLY_TOOLCHAIN}" target list --installed -''' +""" } finally { fixWorkspaceOwnership(stageWorkspace) } @@ -330,15 +334,17 @@ rustup +"${NIGHTLY_TOOLCHAIN}" target list --installed } def runRustfmt() { + initStageLog('Rustfmt') ws("${env.ROOT_WS}/rustfmt") { def stageWorkspace = pwd() try { deleteDir() restoreSource() - sh '''#!/bin/bash + sh """#!/bin/bash set -euo pipefail +${stageLogTeeLine('Rustfmt')} cargo +"${AUX_RUST_TOOLCHAIN}" fmt --all --check -''' +""" } finally { fixWorkspaceOwnership(stageWorkspace) } @@ -346,6 +352,8 @@ cargo +"${AUX_RUST_TOOLCHAIN}" fmt --all --check } def runClippyAndBuild(String platform) { + def stageName = "Clippy+Build: ${platform}" + initStageLog(stageName) ws("${env.ROOT_WS}/clippy-build-${platform}") { def stageWorkspace = pwd() def buildTargetDir = "/xkernel-target/build-${platform}" @@ -356,6 +364,7 @@ def runClippyAndBuild(String platform) { withEnv(["TARGET_DIR=${buildTargetDir}"]) { sh """#!/bin/bash set -euo pipefail +${stageLogTeeLine(stageName)} cp platforms/${platform}/defconfig .config make clippy stdbuf -oL -eL make build @@ -369,6 +378,9 @@ stdbuf -oL -eL make build def runClippyAndRuntime(String arch) { def platform = "${arch}-qemu-virt" + def stageName = "Clippy+Runtime: ${platform}" + def stageLog = stageLogFile(stageName) + initStageLog(stageName) def runtimeTargetDir = targetDirForArch(arch) ws("${env.ROOT_WS}/${arch}") { def stageWorkspace = pwd() @@ -376,9 +388,10 @@ def runClippyAndRuntime(String arch) { deleteDir() restoreSource() - withEnv(["TARGET_DIR=${runtimeTargetDir}"]) { + withEnv(["TARGET_DIR=${runtimeTargetDir}", "STAGE_LOG=${stageLog}"]) { sh """#!/bin/bash set -euo pipefail +${stageLogTeeLine(stageName)} cp platforms/${platform}/defconfig .config make clippy """ @@ -388,6 +401,7 @@ make clippy sh """#!/bin/bash set -euo pipefail +${stageLogTeeLine(stageName)} cp platforms/${platform}/defconfig .config stdbuf -oL -eL make build """ @@ -419,10 +433,11 @@ stdbuf -oL -eL make build "STARRY_SKIP_BUILD=1", "ROOTFS_CACHE_DIR=/xkernel-target/rootfs-cache", "GUEST_CASES_TARGET_DIR=${runtimeTargetDir}/guest-cases-${arch}"]) { - sh '''#!/bin/bash + sh """#!/bin/bash set -euo pipefail +${stageLogTeeLine(stageName)} stdbuf -oL -eL make ci-test run -''' +""" } } } @@ -433,8 +448,10 @@ stdbuf -oL -eL make ci-test run } def runUnitTests(String arch) { + def stageTee = env.STAGE_LOG?.trim() ? "exec > >(tee -a '${env.STAGE_LOG}') 2>&1" : '' sh """#!/bin/bash set -euo pipefail +${stageTee} ROOTFS_VERSION=20260302 ROOTFS_CACHE="/xkernel-target/rootfs-cache" @@ -556,6 +573,7 @@ def restoreReplayGiteeEnv() { } def prepareSource() { + initStageLog('Prepare Source') ws("${env.ROOT_WS}/source-cache") { def sourceWorkspace = pwd() try { @@ -565,7 +583,7 @@ def prepareSource() { env.GIT_COMMIT = sh(script: 'git rev-parse HEAD', returnStdout: true).trim() echo "Checked out HEAD: ${env.GIT_COMMIT}" if (env.giteePullRequestIid?.trim()) { - checkNotDiverged() + checkNotDiverged('Prepare Source') } } finally { fixWorkspaceOwnership(sourceWorkspace) @@ -573,10 +591,12 @@ def prepareSource() { } } -def checkNotDiverged() { +def checkNotDiverged(String stageName = '') { def targetBranch = env.giteeTargetBranch ?: env.DEFAULT_BRANCH + def teeLine = stageName ? stageLogTeeLine(stageName) : '' def result = sh(script: """#!/bin/bash set -euo pipefail +${teeLine} git fetch origin ${targetBranch} --quiet BASE=\$(git merge-base HEAD origin/${targetBranch}) TARGET=\$(git rev-parse origin/${targetBranch}) @@ -909,6 +929,8 @@ def collectUnitTestSnippet(String arch) { } def runTeeStorageTest(String arch) { + def stageName = "TEE: ${arch}" + initStageLog(stageName) def result = [arch: arch, passed: 0, failed: 0, status: 'unknown', errorSnippet: ''] def muslTarget = "${arch}-unknown-linux-musl" def muslLinker = "${arch}-linux-musl-gcc" @@ -926,6 +948,7 @@ def runTeeStorageTest(String arch) { withEnv(["TARGET_DIR=${teeTargetDir}"]) { sh """#!/bin/bash set -euo pipefail +${stageLogTeeLine(stageName)} LIBUTEE_DIR="/xkernel-target/libutee-${arch}" mkdir -p "\${LIBUTEE_DIR}" @@ -1065,6 +1088,26 @@ def sanitizeStageFileName(String stageName) { return stageName.replaceAll(/[^A-Za-z0-9._-]+/, '_').take(80) } +def stageLogFile(String stageName) { + return "${env.ROOT_WS}/ci-logs/${sanitizeStageFileName(stageName)}.log" +} + +def initStageLog(String stageName) { + if (!env.ROOT_WS?.trim()) { + return + } + def logFile = stageLogFile(stageName) + sh """#!/bin/bash +set -euo pipefail +mkdir -p '${env.ROOT_WS}/ci-logs' +: > '${logFile}' +""" +} + +def stageLogTeeLine(String stageName) { + return "exec > >(tee -a '${stageLogFile(stageName)}') 2>&1" +} + def stageWorkspaceLogPath(String stageName) { switch (stageName) { case 'Clippy+Runtime: riscv64-qemu-virt': @@ -1082,126 +1125,15 @@ def stageWorkspaceLogPath(String stageName) { } } -def parseJsonMap(String jsonText) { - if (!jsonText?.trim() || jsonText.trim() == '{}') { - return [:] - } - try { - def parsed = new groovy.json.JsonSlurper().parseText(jsonText) - if (!(parsed instanceof Map)) { - return [:] - } - def result = [:] - parsed.each { k, v -> result[k.toString()] = v?.toString() ?: '' } - return result - } catch (e) { - echo "parseJsonMap failed: ${e.message}" - return [:] - } -} - -@NonCPS -String fetchBuildLogTail(def run, int limit = 400) { - if (run == null) { +def readStageLogFile(String path) { + if (!path?.trim() || !fileExists(path)) { return '' } try { - def lines = run.getLog(limit) - return lines ? lines.join('\n').trim() : '' - } catch (Exception ignored) { - return '' - } -} - -def fetchWfapiStageLogs() { - try { - def logsJson = sh( - script: """#!/bin/bash -set -euo pipefail -export BUILD_URL='${env.BUILD_URL}' -python3 <<'PY' -import json, os, sys, urllib.request - -build_url = os.environ.get('BUILD_URL', '').rstrip('/') -if not build_url: - print('{}') - sys.exit(0) - -def fetch(url, timeout=30): - req = urllib.request.Request( - url, - headers={'Accept': 'application/json', 'User-Agent': 'x-kernel-ci-wfapi'}, - ) - try: - with urllib.request.urlopen(req, timeout=timeout) as resp: - return resp.read() - except Exception as exc: - print(f'wfapi fetch failed: {url}: {exc}', file=sys.stderr) - return b'' - -def parse_log_response(raw): - if not raw: - return '' - text = raw.decode('utf-8', errors='replace').strip() - if not text: - return '' - if text.lstrip().startswith('{'): - try: - obj = json.loads(text) - if isinstance(obj, dict): - for key in ('text', 'log', 'message'): - value = obj.get(key) - if isinstance(value, str) and value.strip(): - return value - except json.JSONDecodeError: - pass - return text - -raw = fetch(f'{build_url}/wfapi/describe') -if not raw or not raw.lstrip().startswith(b'{'): - preview = raw[:240].decode('utf-8', errors='replace') if raw else '(empty response)' - print(f'wfapi/describe is not JSON, preview: {preview}', file=sys.stderr) - print('{}') - sys.exit(0) - -try: - data = json.loads(raw.decode('utf-8', errors='replace')) -except json.JSONDecodeError as exc: - print(f'wfapi/describe JSON decode failed: {exc}', file=sys.stderr) - print('{}') - sys.exit(0) - -stage_logs = {} - -def walk(stages): - for stage in stages or []: - name = stage.get('name') - sid = stage.get('id') - if name and sid: - log_raw = fetch(f'{build_url}/execution/node/{sid}/wfapi/log', timeout=60) - log_text = parse_log_response(log_raw) - if log_text: - existing = stage_logs.get(name, '') - if len(log_text) > len(existing): - stage_logs[name] = log_text - walk(stage.get('stages')) - -walk(data.get('stages')) -print(json.dumps(stage_logs, ensure_ascii=False)) -PY -""", - returnStdout: true - ).trim() - def logs = parseJsonMap(logsJson) - if (logs) { - echo "fetchWfapiStageLogs captured ${logs.size()} stage(s)" - } else { - echo 'fetchWfapiStageLogs returned empty (see wfapi stderr above)' - } - return logs + return readFile(path).trim() } catch (e) { - echo "fetchWfapiStageLogs failed: ${e.message}" - return [:] + echo "read stage log ${path} failed: ${e.message}" + return '' } } @@ -1234,27 +1166,20 @@ def tailLogText(String text, int maxChars) { return marker + text.substring(text.length() - keep) } -def resolveFailedStageLog(String stageName, Map ciResults, Map stageLogs) { - if (stageLogs[stageName]?.trim()) { - return stageLogs[stageName].trim() +def resolveFailedStageLog(String stageName, Map ciResults) { + def primary = readStageLogFile(stageLogFile(stageName)) + if (primary) { + return primary } - def workspacePath = stageWorkspaceLogPath(stageName) - if (workspacePath && fileExists(workspacePath)) { - try { - def content = readFile(workspacePath).trim() - if (content) { - return content - } - } catch (e) { - echo "read workspace log ${workspacePath} failed: ${e.message}" + def legacyPath = stageWorkspaceLogPath(stageName) + if (legacyPath) { + def legacy = readStageLogFile(legacyPath) + if (legacy) { + return legacy } } - def buildTail = tailLogText(fetchBuildLogTail(currentBuild.rawBuild), 8000) - if (buildTail) { - return buildTail - } return ciResults[stageName]?.detail?.trim() ?: '' } @@ -1266,18 +1191,19 @@ def archiveFailedStageLogs(Map ciResults) { return failedLogs } - def stageLogs = fetchWfapiStageLogs() - sh 'mkdir -p ci-logs' + sh "mkdir -p '${env.ROOT_WS}/ci-logs' ci-logs || true" fixWorkspaceOwnership(env.WORKSPACE) failedStages.each { stageName -> - def logContent = resolveFailedStageLog(stageName, ciResults, stageLogs) + def logContent = resolveFailedStageLog(stageName, ciResults) if (!logContent) { echo "No log captured for failed stage: ${stageName}" return } - def fileName = "${sanitizeStageFileName(stageName)}.log" - writeCiLogFile("ci-logs/${fileName}", logContent) + def artifactName = "${sanitizeStageFileName(stageName)}.log" + if (!fileExists("ci-logs/${artifactName}")) { + writeCiLogFile("ci-logs/${artifactName}", logContent) + } failedLogs[stageName] = logContent } return failedLogs -- Gitee From 1858802822e0ad56dea0f53ce1601ef1324045d5 Mon Sep 17 00:00:00 2001 From: yishuqi-147 <16783631+yishuqi-147@user.noreply.gitee.com> Date: Sat, 23 May 2026 15:08:13 +0800 Subject: [PATCH 13/22] test11 --- Jenkinsfile | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/Jenkinsfile b/Jenkinsfile index 27b48a3b..a0610bd3 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -206,9 +206,9 @@ def prefetchCargoDeps() { try { deleteDir() restoreSource() - sh """#!/bin/bash + sh '''#!/bin/bash set -euo pipefail -${stageLogTeeLine('Prefetch Dependencies')} +''' + stageLogTeeLine('Prefetch Dependencies') + ''' echo "==> Prefetching cargo dependencies for all platforms..." declare -A ARCH_TARGET=( @@ -227,7 +227,7 @@ cargo fetch --manifest-path tee_apps/sh/Cargo.toml || true cargo fetch --manifest-path xtask/crate_rootfs/Cargo.toml || true echo "==> Dependency prefetch complete" -""" +''' } finally { fixWorkspaceOwnership(stageWorkspace) } @@ -241,10 +241,9 @@ def checkBuildEnvironment() { try { deleteDir() restoreSource() - sh """#!/bin/bash + sh '''#!/bin/bash set -euo pipefail -${stageLogTeeLine('Check Environment')} - +''' + stageLogTeeLine('Check Environment') + ''' echo "==> Checking Rust build environment..." NIGHTLY_TOOLCHAIN="${AUX_RUST_TOOLCHAIN}" @@ -326,7 +325,7 @@ rustup target list --installed echo "==> Installed nightly targets" rustup +"${NIGHTLY_TOOLCHAIN}" target list --installed -""" +''' } finally { fixWorkspaceOwnership(stageWorkspace) } -- Gitee From 43948ab8f490fbde49ead6c40390d829c03cc962 Mon Sep 17 00:00:00 2001 From: yishuqi-147 <16783631+yishuqi-147@user.noreply.gitee.com> Date: Sat, 23 May 2026 15:13:07 +0800 Subject: [PATCH 14/22] test12 --- Jenkinsfile | 46 ++++++++++------------------------------------ 1 file changed, 10 insertions(+), 36 deletions(-) diff --git a/Jenkinsfile b/Jenkinsfile index a0610bd3..e8e57627 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -1150,21 +1150,6 @@ B64EOF """, returnStdout: false) } -def tailLogText(String text, int maxChars) { - if (!text?.trim()) { - return '' - } - if (text.length() <= maxChars) { - return text - } - def marker = '...(日志已截断,完整内容见 Jenkins Artifacts: ci-logs/)...\n\n' - def keep = maxChars - marker.length() - if (keep < 500) { - return text.take(maxChars) - } - return marker + text.substring(text.length() - keep) -} - def resolveFailedStageLog(String stageName, Map ciResults) { def primary = readStageLogFile(stageLogFile(stageName)) if (primary) { @@ -1236,30 +1221,18 @@ ${rows} return [title: 'x-kernel CI', summary: summary, text: ''] } - def maxTotal = 55000 - def maxPerStage = 12000 - def textParts = [] - def used = 0 - - failedStageLogs.each { stageName, log -> - if (used >= maxTotal) { - return - } - def remaining = maxTotal - used - def chunk = tailLogText(log, Math.min(maxPerStage, remaining - 200)) - if (!chunk) { - return + def textParts = failedStageLogs.collect { stageName, log -> + if (!log?.trim()) { + return null } - def section = "### ❌ ${stageName}\n\n```\n${chunk}\n```\n" - textParts.add(section) - used += section.length() - } + "### ❌ ${stageName}\n\n```\n${log.trim()}\n```\n" + }.findAll { it != null } return [title: 'x-kernel CI', summary: summary, text: textParts.join('\n')] } def buildCombinedComment(Map ciResults, String coverageSummary, Map failedStageLogs = [:]) { - def result = buildCiComment(ciResults, coverageSummary) + def result = buildCiComment(ciResults, coverageSummary, failedStageLogs) def checkOutput = buildCheckRunOutput(ciResults, failedStageLogs, result.allPassed) return [ comment: "\n${result.body}", @@ -1268,7 +1241,7 @@ def buildCombinedComment(Map ciResults, String coverageSummary, Map failedStageL ] } -def buildCiComment(Map results, String coverageSummary = '') { +def buildCiComment(Map results, String coverageSummary = '', Map failedStageLogs = [:]) { def stagesUrl = "${env.BUILD_URL}stages/" def stageOrder = ciStageOrder() def normalizedResults = [:] @@ -1310,9 +1283,10 @@ ${rows} } def errorBlocks = stageOrder.findAll { name -> - normalizedResults[name].status != 'passed' && normalizedResults[name].detail?.trim() + normalizedResults[name].status != 'passed' && + (failedStageLogs[name]?.trim() || normalizedResults[name].detail?.trim()) }.collect { name -> - def detail = normalizedResults[name].detail.take(1000) + def detail = failedStageLogs[name]?.trim() ?: normalizedResults[name].detail.trim() "\n### ❌ ${name}\n\n
\n查看错误详情\n\n" + '```' + "\n${detail}\n" + '```' + "\n
" }.join('\n') -- Gitee From 5bf018a3052f66e8987cd61d77a9ace159cbf3b1 Mon Sep 17 00:00:00 2001 From: yishuqi-147 <16783631+yishuqi-147@user.noreply.gitee.com> Date: Sat, 23 May 2026 15:19:09 +0800 Subject: [PATCH 15/22] test13 --- entry/src/main.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/entry/src/main.rs b/entry/src/main.rs index 5d11b815..3c09f56d 100644 --- a/entry/src/main.rs +++ b/entry/src/main.rs @@ -6,7 +6,6 @@ #![no_main] #![doc = include_str!("../../README.md")] -compile_error!("intentional failure: break build for test"); #[macro_use] extern crate klogger; -- Gitee From 78068b233ea9415e9d842de7b49d71a9fe3d46fc Mon Sep 17 00:00:00 2001 From: yishuqi-147 <16783631+yishuqi-147@user.noreply.gitee.com> Date: Sat, 23 May 2026 15:55:40 +0800 Subject: [PATCH 16/22] test14 --- Jenkinsfile | 91 +++++++++++++++-------------------------------------- 1 file changed, 26 insertions(+), 65 deletions(-) diff --git a/Jenkinsfile b/Jenkinsfile index e8e57627..611eed28 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -177,7 +177,7 @@ pipeline { fixWorkspaceOwnership(env.WORKSPACE) def failedStageLogs = archiveFailedStageLogs(ciResults) archiveArtifacts artifacts: [ - 'ci-logs/**/*.log', + 'stage-logs/**/*.log', '**/artifacts/**/*', '**/logs/**/*', '**/unittest-output.log', '**/tee-test-output.log', '**/coverage-html/**/*', '**/coverage.info', '**/coverage.xml', '**/coverage.txt' @@ -769,9 +769,12 @@ def giteeCreateCheckRun(boolean allPassed, Map checkOutput = null) { def prId = env.giteePullRequestId?.trim() ?: env.giteePullRequestIid?.trim() ?: '' fixWorkspaceOwnership(env.WORKSPACE) - writeCiLogFile('ci-check-output-title.txt', checkOutput?.title ?: 'x-kernel CI') - writeCiLogFile('ci-check-output-summary.txt', checkOutput?.summary ?: (allPassed ? '所有 CI 阶段通过' : 'CI 构建失败')) - writeCiLogFile('ci-check-output-text.txt', checkOutput?.text ?: '') + def checkPayload = groovy.json.JsonOutput.toJson([ + title: checkOutput?.title ?: 'x-kernel CI', + summary: checkOutput?.summary ?: (allPassed ? '所有 CI 阶段通过' : 'CI 构建失败'), + text: checkOutput?.text ?: '', + ]) + writeFile file: 'ci-check-output.json', text: checkPayload try { withCredentials([string(credentialsId: 'gitee-token-secret', variable: 'GITEE_TOKEN')]) { @@ -785,12 +788,15 @@ export CHECK_NAMESPACE='${namespace}' export CHECK_REPO='${repo}' python3 <<'PY' -import os, sys, urllib.parse, urllib.request +import json, os, sys, urllib.parse, urllib.request + +with open('ci-check-output.json', encoding='utf-8') as f: + payload = json.load(f) token = os.environ['GITEE_TOKEN'] -title = open('ci-check-output-title.txt', encoding='utf-8').read() -summary = open('ci-check-output-summary.txt', encoding='utf-8').read() -text = open('ci-check-output-text.txt', encoding='utf-8').read() +title = payload.get('title', 'x-kernel CI') +summary = payload.get('summary', '') +text = payload.get('text', '') fields = { 'access_token': token, @@ -897,7 +903,7 @@ def collectUnitTestSnippet(String arch) { try { def logFile = "${env.ROOT_WS}/${arch}/unittest-output.log" if (!fileExists(logFile)) { - return '阶段失败,完整日志见 Jenkins Stages 或门禁检查详情。' + return '未找到 unittest-output.log,阶段可能在日志创建前失败,请查看 Jenkins Stages 详情。' } def log = readFile(logFile) def lines = log.split('\n') @@ -1088,7 +1094,7 @@ def sanitizeStageFileName(String stageName) { } def stageLogFile(String stageName) { - return "${env.ROOT_WS}/ci-logs/${sanitizeStageFileName(stageName)}.log" + return "${env.ROOT_WS}/stage-logs/${sanitizeStageFileName(stageName)}.log" } def initStageLog(String stageName) { @@ -1098,7 +1104,7 @@ def initStageLog(String stageName) { def logFile = stageLogFile(stageName) sh """#!/bin/bash set -euo pipefail -mkdir -p '${env.ROOT_WS}/ci-logs' +mkdir -p '${env.ROOT_WS}/stage-logs' : > '${logFile}' """ } @@ -1107,23 +1113,6 @@ def stageLogTeeLine(String stageName) { return "exec > >(tee -a '${stageLogFile(stageName)}') 2>&1" } -def stageWorkspaceLogPath(String stageName) { - switch (stageName) { - case 'Clippy+Runtime: riscv64-qemu-virt': - return "${env.ROOT_WS}/riscv64/unittest-output.log" - case 'Clippy+Runtime: x86_64-qemu-virt': - return "${env.ROOT_WS}/x86_64/unittest-output.log" - case 'Clippy+Runtime: aarch64-qemu-virt': - return "${env.ROOT_WS}/aarch64/unittest-output.log" - case 'TEE: x86_64': - return "${env.ROOT_WS}/tee-test-x86_64/tee-test-output.log" - case 'TEE: aarch64': - return "${env.ROOT_WS}/tee-test-aarch64/tee-test-output.log" - default: - return null - } -} - def readStageLogFile(String path) { if (!path?.trim() || !fileExists(path)) { return '' @@ -1136,34 +1125,11 @@ def readStageLogFile(String path) { } } -def writeCiLogFile(String relPath, String content) { - def b64 = content.bytes.encodeBase64().toString() - sh(script: """#!/bin/bash -set -euo pipefail -mkdir -p "\$(dirname '${relPath}')" -python3 -c " -import base64, pathlib, sys -pathlib.Path('${relPath}').write_bytes(base64.b64decode(sys.stdin.read())) -" <<'B64EOF' -${b64} -B64EOF -""", returnStdout: false) -} - def resolveFailedStageLog(String stageName, Map ciResults) { - def primary = readStageLogFile(stageLogFile(stageName)) - if (primary) { - return primary + def stageLog = readStageLogFile(stageLogFile(stageName)) + if (stageLog) { + return stageLog } - - def legacyPath = stageWorkspaceLogPath(stageName) - if (legacyPath) { - def legacy = readStageLogFile(legacyPath) - if (legacy) { - return legacy - } - } - return ciResults[stageName]?.detail?.trim() ?: '' } @@ -1175,7 +1141,7 @@ def archiveFailedStageLogs(Map ciResults) { return failedLogs } - sh "mkdir -p '${env.ROOT_WS}/ci-logs' ci-logs || true" + sh "mkdir -p '${env.ROOT_WS}/stage-logs' stage-logs || true" fixWorkspaceOwnership(env.WORKSPACE) failedStages.each { stageName -> @@ -1184,10 +1150,6 @@ def archiveFailedStageLogs(Map ciResults) { echo "No log captured for failed stage: ${stageName}" return } - def artifactName = "${sanitizeStageFileName(stageName)}.log" - if (!fileExists("ci-logs/${artifactName}")) { - writeCiLogFile("ci-logs/${artifactName}", logContent) - } failedLogs[stageName] = logContent } return failedLogs @@ -1214,7 +1176,7 @@ ${rows} [查看 Jenkins Stages](${stagesUrl})""" if (failedStageLogs && !allPassed) { - summary += " | [下载失败日志](${env.BUILD_URL}artifact/ci-logs/)" + summary += " | [下载失败日志](${env.BUILD_URL}artifact/stage-logs/)" } if (allPassed || !failedStageLogs) { @@ -1232,7 +1194,7 @@ ${rows} } def buildCombinedComment(Map ciResults, String coverageSummary, Map failedStageLogs = [:]) { - def result = buildCiComment(ciResults, coverageSummary, failedStageLogs) + def result = buildCiComment(ciResults, coverageSummary) def checkOutput = buildCheckRunOutput(ciResults, failedStageLogs, result.allPassed) return [ comment: "\n${result.body}", @@ -1241,7 +1203,7 @@ def buildCombinedComment(Map ciResults, String coverageSummary, Map failedStageL ] } -def buildCiComment(Map results, String coverageSummary = '', Map failedStageLogs = [:]) { +def buildCiComment(Map results, String coverageSummary = '') { def stagesUrl = "${env.BUILD_URL}stages/" def stageOrder = ciStageOrder() def normalizedResults = [:] @@ -1283,10 +1245,9 @@ ${rows} } def errorBlocks = stageOrder.findAll { name -> - normalizedResults[name].status != 'passed' && - (failedStageLogs[name]?.trim() || normalizedResults[name].detail?.trim()) + normalizedResults[name].status != 'passed' && normalizedResults[name].detail?.trim() }.collect { name -> - def detail = failedStageLogs[name]?.trim() ?: normalizedResults[name].detail.trim() + def detail = normalizedResults[name].detail.take(1000) "\n### ❌ ${name}\n\n
\n查看错误详情\n\n" + '```' + "\n${detail}\n" + '```' + "\n
" }.join('\n') -- Gitee From 42c122dd631c1aabeb6e12e5b1c1bb0fe70f505a Mon Sep 17 00:00:00 2001 From: yishuqi-147 <16783631+yishuqi-147@user.noreply.gitee.com> Date: Sat, 23 May 2026 15:58:30 +0800 Subject: [PATCH 17/22] test15 --- entry/src/main.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/entry/src/main.rs b/entry/src/main.rs index 3c09f56d..21268d85 100644 --- a/entry/src/main.rs +++ b/entry/src/main.rs @@ -6,7 +6,6 @@ #![no_main] #![doc = include_str!("../../README.md")] - #[macro_use] extern crate klogger; -- Gitee From fd634955d5c028dd1ba3756cc456e27d211a58f5 Mon Sep 17 00:00:00 2001 From: yishuqi-147 <16783631+yishuqi-147@user.noreply.gitee.com> Date: Sat, 23 May 2026 16:28:25 +0800 Subject: [PATCH 18/22] test16 --- Jenkinsfile | 61 +++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 59 insertions(+), 2 deletions(-) diff --git a/Jenkinsfile b/Jenkinsfile index 611eed28..4d2b7cf9 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -798,6 +798,32 @@ title = payload.get('title', 'x-kernel CI') summary = payload.get('summary', '') text = payload.get('text', '') +MAX_TEXT_BYTES = 65535 + +def truncate_output_text(content): + if not content: + return '' + encoded = content.encode('utf-8') + if len(encoded) <= MAX_TEXT_BYTES: + return content + artifacts_url = os.environ['CHECK_DETAILS_URL'].rstrip('/') + '/artifact/stage-logs/' + notice = ( + f'...(日志共 {len(encoded)} 字节,超过 Gitee 上限 {MAX_TEXT_BYTES} 字节,以下为末尾片段。' + f'完整日志: {artifacts_url})' + '\\n\\n' + ) + budget = MAX_TEXT_BYTES - len(notice.encode('utf-8')) + if budget <= 0: + return notice[:MAX_TEXT_BYTES] + tail = encoded[-budget:] + while tail: + try: + return notice + tail.decode('utf-8') + except UnicodeDecodeError: + tail = tail[1:] + return notice + +text = truncate_output_text(text) + fields = { 'access_token': token, 'name': 'ci-results', @@ -830,7 +856,7 @@ except urllib.error.HTTPError as e: print(f'Gitee check run ci-results ({os.environ["CHECK_CONCLUSION"]}): HTTP {code}') print(body) if code < 200 or code >= 300: - sys.exit(1) + print('WARNING: Gitee check run update failed; CI result still available in Jenkins and PR comment.', file=sys.stderr) PY """) } @@ -1159,6 +1185,32 @@ def archiveFailedStageLogs(Map ciResults) { } } +def truncateGiteeCheckText(String text, int maxBytes = 65000) { + if (!text?.trim()) { + return '' + } + def bytes = text.getBytes('UTF-8') + if (bytes.length <= maxBytes) { + return text + } + def artifactsUrl = "${env.BUILD_URL}artifact/stage-logs/" + def notice = "\n\n...(日志共 ${bytes.length} 字节,超过 Gitee output.text 上限 ${maxBytes} 字节,以下为末尾片段。完整日志: ${artifactsUrl})\n\n" + def noticeBytes = notice.getBytes('UTF-8').length + def budget = maxBytes - noticeBytes + if (budget <= 0) { + return notice.take(Math.min(notice.length(), maxBytes)) + } + def tailBytes = bytes[-budget..-1] as byte[] + while (tailBytes.length > 0) { + def tail = new String(tailBytes, 'UTF-8') + if (!tail.startsWith('\uFFFD') || tailBytes.length == 1) { + return notice + tail + } + tailBytes = tailBytes[1..-1] as byte[] + } + return notice +} + def buildCheckRunOutput(Map ciResults, Map failedStageLogs, boolean allPassed) { def stageOrder = ciStageOrder() def stagesUrl = "${env.BUILD_URL}stages/" @@ -1177,6 +1229,11 @@ ${rows} [查看 Jenkins Stages](${stagesUrl})""" if (failedStageLogs && !allPassed) { summary += " | [下载失败日志](${env.BUILD_URL}artifact/stage-logs/)" + def logLinks = failedStageLogs.keySet().collect { stageName -> + def fileName = "${sanitizeStageFileName(stageName)}.log" + "[${stageName}](${env.BUILD_URL}artifact/stage-logs/${fileName})" + }.join(' | ') + summary += "\n\n失败阶段日志: ${logLinks}" } if (allPassed || !failedStageLogs) { @@ -1190,7 +1247,7 @@ ${rows} "### ❌ ${stageName}\n\n```\n${log.trim()}\n```\n" }.findAll { it != null } - return [title: 'x-kernel CI', summary: summary, text: textParts.join('\n')] + return [title: 'x-kernel CI', summary: summary, text: truncateGiteeCheckText(textParts.join('\n'))] } def buildCombinedComment(Map ciResults, String coverageSummary, Map failedStageLogs = [:]) { -- Gitee From 200fd6d9556619769b9eb63b20ae3a30a791d2c1 Mon Sep 17 00:00:00 2001 From: yishuqi-147 <16783631+yishuqi-147@user.noreply.gitee.com> Date: Sat, 23 May 2026 16:59:52 +0800 Subject: [PATCH 19/22] test17 --- Jenkinsfile | 28 +--------------------------- 1 file changed, 1 insertion(+), 27 deletions(-) diff --git a/Jenkinsfile b/Jenkinsfile index 4d2b7cf9..1a573685 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -1185,32 +1185,6 @@ def archiveFailedStageLogs(Map ciResults) { } } -def truncateGiteeCheckText(String text, int maxBytes = 65000) { - if (!text?.trim()) { - return '' - } - def bytes = text.getBytes('UTF-8') - if (bytes.length <= maxBytes) { - return text - } - def artifactsUrl = "${env.BUILD_URL}artifact/stage-logs/" - def notice = "\n\n...(日志共 ${bytes.length} 字节,超过 Gitee output.text 上限 ${maxBytes} 字节,以下为末尾片段。完整日志: ${artifactsUrl})\n\n" - def noticeBytes = notice.getBytes('UTF-8').length - def budget = maxBytes - noticeBytes - if (budget <= 0) { - return notice.take(Math.min(notice.length(), maxBytes)) - } - def tailBytes = bytes[-budget..-1] as byte[] - while (tailBytes.length > 0) { - def tail = new String(tailBytes, 'UTF-8') - if (!tail.startsWith('\uFFFD') || tailBytes.length == 1) { - return notice + tail - } - tailBytes = tailBytes[1..-1] as byte[] - } - return notice -} - def buildCheckRunOutput(Map ciResults, Map failedStageLogs, boolean allPassed) { def stageOrder = ciStageOrder() def stagesUrl = "${env.BUILD_URL}stages/" @@ -1247,7 +1221,7 @@ ${rows} "### ❌ ${stageName}\n\n```\n${log.trim()}\n```\n" }.findAll { it != null } - return [title: 'x-kernel CI', summary: summary, text: truncateGiteeCheckText(textParts.join('\n'))] + return [title: 'x-kernel CI', summary: summary, text: textParts.join('\n')] } def buildCombinedComment(Map ciResults, String coverageSummary, Map failedStageLogs = [:]) { -- Gitee From 7e607f25dcad3659d0a8a0e7547613f5e0efb9b1 Mon Sep 17 00:00:00 2001 From: yishuqi-147 <16783631+yishuqi-147@user.noreply.gitee.com> Date: Sat, 23 May 2026 17:24:31 +0800 Subject: [PATCH 20/22] test18 --- entry/src/unittest_simple.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/entry/src/unittest_simple.rs b/entry/src/unittest_simple.rs index 72972589..14cd4666 100644 --- a/entry/src/unittest_simple.rs +++ b/entry/src/unittest_simple.rs @@ -17,7 +17,7 @@ use unittest::{TestResult, assert, assert_eq, assert_ne, def_test}; #[def_test] fn test_basic_addition() { let a = 2 + 2; - assert_eq!(a, 4); + assert_eq!(a, 5); // intentional failure for testing } /// String comparison test -- Gitee From 3ca3e548b74ea42068d0d866d878ed5cda68faf7 Mon Sep 17 00:00:00 2001 From: yishuqi-147 <16783631+yishuqi-147@user.noreply.gitee.com> Date: Mon, 25 May 2026 09:05:14 +0800 Subject: [PATCH 21/22] test19 --- entry/src/unittest_simple.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/entry/src/unittest_simple.rs b/entry/src/unittest_simple.rs index 14cd4666..72972589 100644 --- a/entry/src/unittest_simple.rs +++ b/entry/src/unittest_simple.rs @@ -17,7 +17,7 @@ use unittest::{TestResult, assert, assert_eq, assert_ne, def_test}; #[def_test] fn test_basic_addition() { let a = 2 + 2; - assert_eq!(a, 5); // intentional failure for testing + assert_eq!(a, 4); } /// String comparison test -- Gitee From 3f5dad20e606828567b827d703b9790021234678 Mon Sep 17 00:00:00 2001 From: yishuqi-147 <16783631+yishuqi-147@user.noreply.gitee.com> Date: Mon, 25 May 2026 14:55:04 +0800 Subject: [PATCH 22/22] simplify --- Jenkinsfile | 153 ++++++++++++++++++++++++++++++++++------------------ 1 file changed, 101 insertions(+), 52 deletions(-) diff --git a/Jenkinsfile b/Jenkinsfile index 1a573685..c0479ed6 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -186,7 +186,7 @@ pipeline { def coverageSummary = collectCoverageSummary() def built = buildCombinedComment(ciResults, coverageSummary, failedStageLogs) notifyGiteePullRequest(built.comment) - giteeCreateCheckRun(built.allPassed, built.checkOutput) + giteeCreateAllCheckRuns(ciResults, failedStageLogs) if (currentBuild.currentResult == 'SUCCESS') { giteeTestPass() } else { @@ -409,6 +409,15 @@ stdbuf -oL -eL make build git branch: "${env.TEST_HARNESS_BRANCH}", url: "${env.TEST_HARNESS_REPO}" markSafeDirectory() + sh '''#!/bin/bash +set -euo pipefail +# harness: cargo build 仅输出错误,减少 stage 日志体积 +find . \\( -name Makefile -o -name '*.mk' -o -name '*.sh' \\) -type f 2>/dev/null | while IFS= read -r f; do + grep -qE 'cargo[[:space:]]+build' "$f" 2>/dev/null || continue + grep -qE 'cargo[[:space:]]+build[[:space:]]+--quiet' "$f" 2>/dev/null && continue + sed -i -E 's/cargo[[:space:]]+build/cargo build --quiet/g' "$f" +done +''' def hostfwdPort def vsockCid @@ -430,6 +439,7 @@ stdbuf -oL -eL make build } withEnv(["XKERNEL_REMOTE=${pwd()}/..", "ARCH=${arch}", "STARRY_SKIP_BUILD=1", + "CARGO_TERM_QUIET=true", "ROOTFS_CACHE_DIR=/xkernel-target/rootfs-cache", "GUEST_CASES_TARGET_DIR=${runtimeTargetDir}/guest-cases-${arch}"]) { sh """#!/bin/bash @@ -447,7 +457,10 @@ stdbuf -oL -eL make ci-test run } def runUnitTests(String arch) { - def stageTee = env.STAGE_LOG?.trim() ? "exec > >(tee -a '${env.STAGE_LOG}') 2>&1" : '' + def ansiFilter = stageLogAnsiFilterPipe() + def stageTee = env.STAGE_LOG?.trim() + ? "exec > >(${ansiFilter} | tee -a '${env.STAGE_LOG}') 2>&1" + : '' sh """#!/bin/bash set -euo pipefail ${stageTee} @@ -470,7 +483,7 @@ if [ "${arch}" = "aarch64" ]; then fi set +e -timeout \${TIMEOUT} stdbuf -oL -eL make UNITTEST=y VSOCK=n NET=n run | tee unittest-output.log +timeout \${TIMEOUT} stdbuf -oL -eL make UNITTEST=y VSOCK=n NET=n run | ${ansiFilter} | tee unittest-output.log status=\${PIPESTATUS[0]} set -e @@ -757,21 +770,38 @@ def resolveHeadSha() { return null } -def giteeCreateCheckRun(boolean allPassed, Map checkOutput = null) { +def stageCheckConclusion(String status) { + switch (status) { + case 'passed': return 'success' + case 'failed': return 'failure' + default: return 'skipped' + } +} + +def giteeCreateAllCheckRuns(Map ciResults, Map failedStageLogs = [:]) { + ciStageOrder().each { stageName -> + def status = ciResults[stageName]?.status ?: 'not_run' + def conclusion = stageCheckConclusion(status) + def output = buildStageCheckRunOutput(stageName, ciResults, failedStageLogs) + giteeCreateCheckRun(stageName, conclusion, output) + } +} + +def giteeCreateCheckRun(String checkName, String conclusion, Map checkOutput = null) { def headSha = resolveHeadSha() if (!headSha) { - echo 'Skipping Gitee check run: head SHA not available (GIT_COMMIT / gitee webhook / source-cache)' + echo "Skipping Gitee check run ${checkName}: head SHA not available (GIT_COMMIT / gitee webhook / source-cache)" return } def namespace = env.giteeTargetNamespace ?: 'openkylin' def repo = env.giteeTargetRepoName ?: 'x-kernel' - def conclusion = allPassed ? 'success' : 'failure' def prId = env.giteePullRequestId?.trim() ?: env.giteePullRequestIid?.trim() ?: '' fixWorkspaceOwnership(env.WORKSPACE) def checkPayload = groovy.json.JsonOutput.toJson([ - title: checkOutput?.title ?: 'x-kernel CI', - summary: checkOutput?.summary ?: (allPassed ? '所有 CI 阶段通过' : 'CI 构建失败'), + name: checkName, + title: checkOutput?.title ?: checkName, + summary: checkOutput?.summary ?: '', text: checkOutput?.text ?: '', ]) writeFile file: 'ci-check-output.json', text: checkPayload @@ -788,19 +818,34 @@ export CHECK_NAMESPACE='${namespace}' export CHECK_REPO='${repo}' python3 <<'PY' -import json, os, sys, urllib.parse, urllib.request +import json, os, re, sys, urllib.parse, urllib.request with open('ci-check-output.json', encoding='utf-8') as f: payload = json.load(f) token = os.environ['GITEE_TOKEN'] -title = payload.get('title', 'x-kernel CI') +check_name = payload.get('name', 'ci-results') +title = payload.get('title', check_name) summary = payload.get('summary', '') text = payload.get('text', '') MAX_TEXT_BYTES = 65535 +ANSI_CSI_RE = re.compile(r'\\x1b\\[[0-9;?]*[ -/]*[@-~]', re.IGNORECASE) +ANSI_CSI_ALT_RE = re.compile(r'\\x9b[0-9;?]*[ -/]*[@-~]', re.IGNORECASE) +ANSI_SGR_RE = re.compile(r'\\[(?:\\d{1,3};?)*[mK]') + +def strip_ansi_escapes(content): + if not content: + return '' + text = ANSI_CSI_RE.sub('', content) + text = ANSI_CSI_ALT_RE.sub('', text) + text = ANSI_SGR_RE.sub('', text) + text = text.replace('\\r', '') + text = re.sub(r'\\n{4,}', '\\n\\n\\n', text) + return text.strip() def truncate_output_text(content): + content = strip_ansi_escapes(content) if not content: return '' encoded = content.encode('utf-8') @@ -826,7 +871,7 @@ text = truncate_output_text(text) fields = { 'access_token': token, - 'name': 'ci-results', + 'name': check_name, 'head_sha': os.environ['CHECK_HEAD_SHA'], 'status': 'completed', 'conclusion': os.environ['CHECK_CONCLUSION'], @@ -853,16 +898,16 @@ except urllib.error.HTTPError as e: body = e.read().decode('utf-8', errors='replace') code = e.code -print(f'Gitee check run ci-results ({os.environ["CHECK_CONCLUSION"]}): HTTP {code}') +print(f'Gitee check run {check_name} ({os.environ["CHECK_CONCLUSION"]}): HTTP {code}') print(body) if code < 200 or code >= 300: - print('WARNING: Gitee check run update failed; CI result still available in Jenkins and PR comment.', file=sys.stderr) + print(f'WARNING: Gitee check run {check_name} update failed; CI result still available in Jenkins and PR comment.', file=sys.stderr) PY """) } - echo "Gitee check run ci-results created (${conclusion})" + echo "Gitee check run ${checkName} created (${conclusion})" } catch (e) { - echo "Gitee check run failed: ${e.message}" + echo "Gitee check run ${checkName} failed: ${e.message}" } } @@ -1119,6 +1164,23 @@ def sanitizeStageFileName(String stageName) { return stageName.replaceAll(/[^A-Za-z0-9._-]+/, '_').take(80) } +def stageLogAnsiFilterPipe() { + return "sed -u -e 's/\\x1[Bb]\\[[0-9;]*[a-zA-Z]//g' -e 's/\\x9[Bb]\\[[0-9;]*[a-zA-Z]//g' -e 's/\\[[0-9;]*[mK]//g'" +} + +def sanitizeStageLogForDisplay(String text) { + if (!text?.trim()) { + return '' + } + def cleaned = text + .replaceAll(/\u001B\[[0-9;?]*[ -\/]*[@-~]/, '') + .replaceAll(/\u009B[0-9;?]*[ -\/]*[@-~]/, '') + .replaceAll(/\[(?:\d{1,3};?)*[mK]/, '') + .replaceAll(/\r/, '') + .replaceAll(/\n{4,}/, '\n\n\n') + return cleaned.trim() +} + def stageLogFile(String stageName) { return "${env.ROOT_WS}/stage-logs/${sanitizeStageFileName(stageName)}.log" } @@ -1136,7 +1198,9 @@ mkdir -p '${env.ROOT_WS}/stage-logs' } def stageLogTeeLine(String stageName) { - return "exec > >(tee -a '${stageLogFile(stageName)}') 2>&1" + def logFile = stageLogFile(stageName) + def filter = stageLogAnsiFilterPipe() + return "exec > >(${filter} | tee -a '${logFile}') 2>&1" } def readStageLogFile(String path) { @@ -1154,9 +1218,9 @@ def readStageLogFile(String path) { def resolveFailedStageLog(String stageName, Map ciResults) { def stageLog = readStageLogFile(stageLogFile(stageName)) if (stageLog) { - return stageLog + return sanitizeStageLogForDisplay(stageLog) } - return ciResults[stageName]?.detail?.trim() ?: '' + return sanitizeStageLogForDisplay(ciResults[stageName]?.detail?.trim() ?: '') } def archiveFailedStageLogs(Map ciResults) { @@ -1185,52 +1249,37 @@ def archiveFailedStageLogs(Map ciResults) { } } -def buildCheckRunOutput(Map ciResults, Map failedStageLogs, boolean allPassed) { - def stageOrder = ciStageOrder() +def buildStageCheckRunOutput(String stageName, Map ciResults, Map failedStageLogs = [:]) { + def status = ciResults[stageName]?.status ?: 'not_run' def stagesUrl = "${env.BUILD_URL}stages/" - def rows = stageOrder.collect { name -> - def status = ciResults[name]?.status ?: 'not_run' - def icon = status == 'passed' ? '✅' : (status == 'not_run' ? '⏭' : '❌') - "| ${name} | ${icon} |" - }.join('\n') - - def summary = """## ${allPassed ? 'CI 构建成功' : 'CI 构建失败'} - -| 阶段 | 状态 | -|------|------| -${rows} - -[查看 Jenkins Stages](${stagesUrl})""" - if (failedStageLogs && !allPassed) { - summary += " | [下载失败日志](${env.BUILD_URL}artifact/stage-logs/)" - def logLinks = failedStageLogs.keySet().collect { stageName -> - def fileName = "${sanitizeStageFileName(stageName)}.log" - "[${stageName}](${env.BUILD_URL}artifact/stage-logs/${fileName})" - }.join(' | ') - summary += "\n\n失败阶段日志: ${logLinks}" - } - - if (allPassed || !failedStageLogs) { - return [title: 'x-kernel CI', summary: summary, text: ''] + def logUrl = "${env.BUILD_URL}artifact/stage-logs/${sanitizeStageFileName(stageName)}.log" + def detail = ciResults[stageName]?.detail?.trim() ?: '该阶段未执行,通常是前序阶段失败导致。请查看 Jenkins Stages 详情。' + + def summary + if (status == 'passed') { + summary = "## ✅ ${stageName} 通过\n\n[查看 Jenkins Stages](${stagesUrl})" + } else if (status == 'failed') { + summary = "## ❌ ${stageName} 失败\n\n${detail}\n\n[阶段日志](${logUrl}) | [Jenkins Stages](${stagesUrl})" + } else { + summary = "## ⏭ ${stageName} 未执行\n\n${detail}\n\n[查看 Jenkins Stages](${stagesUrl})" } - def textParts = failedStageLogs.collect { stageName, log -> - if (!log?.trim()) { - return null + def text = '' + if (status == 'failed') { + def log = failedStageLogs[stageName]?.trim() ?: detail + if (log) { + text = "```\n${log}\n```" } - "### ❌ ${stageName}\n\n```\n${log.trim()}\n```\n" - }.findAll { it != null } + } - return [title: 'x-kernel CI', summary: summary, text: textParts.join('\n')] + return [title: stageName, summary: summary, text: text] } def buildCombinedComment(Map ciResults, String coverageSummary, Map failedStageLogs = [:]) { def result = buildCiComment(ciResults, coverageSummary) - def checkOutput = buildCheckRunOutput(ciResults, failedStageLogs, result.allPassed) return [ comment: "\n${result.body}", allPassed: result.allPassed, - checkOutput: checkOutput ] } -- Gitee