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