Skip to content

Commit 3f9cfbe

Browse files
authored
[CI]Result upload obs (vllm-project#12786)
### What this PR does / why we need it? After E2E tests complete, the workflow: Collects selected-test logs as GitHub Artifacts. Generates failure_report.md by comparing failed tests with recommended tests. Uploads the report as a GitHub Artifact. Archives each PR report into a date-based .tar.gz object in Huawei OBS. Preserves the actual uploaded archive as a GitHub Artifact for troubleshooting. Adds a local OBS upload/download smoke test with SHA256 and content verification. Keeps the analysis and OBS archival path non-blocking for the main E2E CI gate. ### Does this PR introduce _any_ user-facing change? N/A ### How was this patch tested? - vLLM version: v0.25.1 - vLLM main: vllm-project/vllm@fe784ff --------- Signed-off-by: Xuyzhen <958522639@qq.com>
1 parent 019d3e9 commit 3f9cfbe

2 files changed

Lines changed: 234 additions & 0 deletions

File tree

.github/workflows/_analyze_failure.yaml

Lines changed: 231 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,11 @@ on:
3232
required: false
3333
default: ''
3434
description: 'Content of recommendations from upstream job output (priority 1)'
35+
secrets:
36+
OBS_ACCESS_KEY_FAILURE_ANALYSIS:
37+
required: false
38+
OBS_SECRET_ACCESS_KEY_FAILURE_ANALYSIS:
39+
required: false
3540

3641
defaults:
3742
run:
@@ -116,3 +121,229 @@ jobs:
116121
path: ./test-logs/failure_report.md
117122
if-no-files-found: ignore
118123
retention-days: 14
124+
125+
archive-failure-report-to-obs:
126+
name: Archive failure report to OBS
127+
needs: analyze
128+
if: ${{ always() }}
129+
continue-on-error: true
130+
runs-on: ubuntu-latest
131+
concurrency:
132+
group: failure-analysis-obs-daily-archive
133+
cancel-in-progress: false
134+
env:
135+
OBS_ACCESS_KEY: ${{ secrets.OBS_ACCESS_KEY_FAILURE_ANALYSIS }}
136+
OBS_SECRET_KEY: ${{ secrets.OBS_SECRET_ACCESS_KEY_FAILURE_ANALYSIS }}
137+
PR_NUMBER: ${{ github.event.pull_request.number }}
138+
PR_TITLE: ${{ github.event.pull_request.title }}
139+
RUN_ID: ${{ github.run_id }}
140+
RUN_ATTEMPT: ${{ github.run_attempt }}
141+
steps:
142+
- name: Download failure analysis report
143+
continue-on-error: true
144+
uses: actions/download-artifact@v7
145+
with:
146+
name: failure-analysis-report
147+
path: ./failure-analysis-report
148+
149+
- name: Install OBS SDK
150+
if: always()
151+
run: |
152+
if ! pip install esdk-obs-python --quiet --default-timeout=30 --retries 1; then
153+
echo "::warning::Failed to install OBS SDK; skipping OBS archival."
154+
fi
155+
156+
- name: Add report to daily archive and upload to OBS
157+
if: always()
158+
run: |
159+
set +e
160+
python3 - <<'PY'
161+
import os
162+
import shutil
163+
import tarfile
164+
import unicodedata
165+
from datetime import datetime
166+
from pathlib import Path
167+
from zoneinfo import ZoneInfo
168+
169+
OBS_BUCKET = "vllm-ascend"
170+
OBS_PREFIX = "ci/precision-test"
171+
OBS_SERVER = "https://obs.cn-north-4.myhuaweicloud.com"
172+
OBS_REQUEST_TIMEOUT_SECONDS = 30
173+
OBS_MAX_RETRY_COUNT = 0
174+
REPORT_PATH = Path("failure-analysis-report/failure_report.md")
175+
WORK_DIR = Path("failure-analysis-archive")
176+
CONTENT_DIR = WORK_DIR / "contents"
177+
ARTIFACT_DIR = WORK_DIR / "uploaded"
178+
179+
180+
def warn(message):
181+
print(f"::warning::{message}")
182+
183+
184+
def sanitize_filename(value):
185+
normalized = unicodedata.normalize("NFKC", value)
186+
sanitized = "".join(
187+
char if char.isalnum() or char in ".-_" else "_"
188+
for char in normalized
189+
)
190+
while "__" in sanitized:
191+
sanitized = sanitized.replace("__", "_")
192+
return sanitized.strip("._-")[:80] or "untitled"
193+
194+
195+
def extract_archive(archive_path, destination):
196+
try:
197+
destination_root = destination.resolve()
198+
with tarfile.open(archive_path, "r:gz") as archive:
199+
for member in archive.getmembers():
200+
target = (destination / member.name).resolve()
201+
if target != destination_root and destination_root not in target.parents:
202+
warn(f"Unsafe archive path: {member.name}; skipping OBS archival.")
203+
return False
204+
if not (member.isfile() or member.isdir()):
205+
warn(f"Unsupported archive entry: {member.name}; skipping OBS archival.")
206+
return False
207+
archive.extractall(destination)
208+
return True
209+
except Exception as exc:
210+
warn(f"Failed to extract existing OBS archive: {exc}")
211+
return False
212+
213+
214+
def main():
215+
if not REPORT_PATH.is_file() or REPORT_PATH.stat().st_size == 0:
216+
print("::notice::No failure analysis report was produced; skipping OBS upload.")
217+
return
218+
219+
access_key = os.environ.get("OBS_ACCESS_KEY", "")
220+
secret_key = os.environ.get("OBS_SECRET_KEY", "")
221+
if not access_key or not secret_key:
222+
warn("OBS failure-analysis credentials are not configured; skipping upload.")
223+
return
224+
225+
try:
226+
from obs import ObsClient
227+
except Exception as exc:
228+
warn(f"OBS SDK is unavailable; skipping OBS archival: {exc}")
229+
return
230+
231+
now = datetime.now(ZoneInfo("Asia/Shanghai"))
232+
archive_date = now.strftime("%Y-%m-%d")
233+
timestamp = now.strftime("%Y%m%dT%H%M%S%z")
234+
pr_number = os.environ.get("PR_NUMBER") or "unknown"
235+
pr_title = sanitize_filename(os.environ.get("PR_TITLE", "untitled"))
236+
run_id = os.environ.get("RUN_ID") or "unknown"
237+
run_attempt = os.environ.get("RUN_ATTEMPT") or "1"
238+
report_name = (
239+
f"PR-{pr_number}_{pr_title}_{timestamp}_"
240+
f"run-{run_id}-{run_attempt}.md"
241+
)
242+
archive_name = f"{archive_date}.tar.gz"
243+
obs_key = f"{OBS_PREFIX}/{archive_name}"
244+
archive_path = WORK_DIR / archive_name
245+
new_archive_path = WORK_DIR / f"new-{archive_name}"
246+
247+
CONTENT_DIR.mkdir(parents=True, exist_ok=True)
248+
client = ObsClient(
249+
access_key_id=access_key,
250+
secret_access_key=secret_key,
251+
server=OBS_SERVER,
252+
timeout=OBS_REQUEST_TIMEOUT_SECONDS,
253+
max_retry_count=OBS_MAX_RETRY_COUNT,
254+
)
255+
try:
256+
try:
257+
response = client.getObject(
258+
OBS_BUCKET,
259+
obs_key,
260+
downloadPath=str(archive_path),
261+
)
262+
except Exception as exc:
263+
warn(
264+
f"OBS download failed or timed out after "
265+
f"{OBS_REQUEST_TIMEOUT_SECONDS}s: {exc}"
266+
)
267+
return
268+
269+
if response.status < 300:
270+
print(f"Downloaded existing archive: {obs_key}")
271+
if not extract_archive(archive_path, CONTENT_DIR):
272+
return
273+
elif response.status == 404:
274+
print(f"No archive exists for {archive_date}; creating a new one")
275+
else:
276+
warn(
277+
f"Failed to download {obs_key}: status={response.status}, "
278+
f"code={response.errorCode}, message={response.errorMessage}"
279+
)
280+
return
281+
282+
shutil.copy2(REPORT_PATH, CONTENT_DIR / report_name)
283+
with tarfile.open(new_archive_path, "w:gz") as archive:
284+
for report in sorted(CONTENT_DIR.rglob("*")):
285+
if report.is_file():
286+
archive.add(report, arcname=report.relative_to(CONTENT_DIR))
287+
288+
with tarfile.open(new_archive_path, "r:gz") as archive:
289+
if report_name not in archive.getnames():
290+
warn(f"New report is missing from {archive_name}; skipping upload.")
291+
return
292+
293+
try:
294+
response = client.putFile(
295+
OBS_BUCKET,
296+
obs_key,
297+
str(new_archive_path),
298+
)
299+
except Exception as exc:
300+
warn(
301+
f"OBS upload failed or timed out after "
302+
f"{OBS_REQUEST_TIMEOUT_SECONDS}s: {exc}"
303+
)
304+
return
305+
306+
if response.status >= 300:
307+
warn(
308+
f"Failed to upload {obs_key}: status={response.status}, "
309+
f"code={response.errorCode}, message={response.errorMessage}"
310+
)
311+
return
312+
print(f"Uploaded: {report_name} -> {obs_key}")
313+
ARTIFACT_DIR.mkdir(parents=True, exist_ok=True)
314+
shutil.copy2(new_archive_path, ARTIFACT_DIR / archive_name)
315+
finally:
316+
try:
317+
client.close()
318+
except Exception as exc:
319+
warn(f"Failed to close OBS client: {exc}")
320+
321+
summary_path = os.environ.get("GITHUB_STEP_SUMMARY")
322+
if summary_path:
323+
with open(summary_path, "a", encoding="utf-8") as summary:
324+
summary.write("## Failure analysis OBS archive\n\n")
325+
summary.write(f"- Report: {report_name}\n")
326+
summary.write(f"- Object: {obs_key}\n")
327+
328+
329+
try:
330+
main()
331+
except Exception as exc:
332+
warn(f"Unexpected failure while archiving the report: {exc}")
333+
PY
334+
status=$?
335+
if [ "$status" -ne 0 ]; then
336+
echo "::warning::Failure analysis archival exited with status $status; ignoring."
337+
fi
338+
exit 0
339+
340+
- name: Upload OBS archive copy
341+
if: always()
342+
continue-on-error: true
343+
uses: actions/upload-artifact@v7
344+
with:
345+
name: failure-analysis-obs-archive-${{ github.run_id }}-${{ github.run_attempt }}
346+
path: ./failure-analysis-archive/uploaded/*.tar.gz
347+
if-no-files-found: ignore
348+
retention-days: 14
349+
compression-level: 0

.github/workflows/pr_test.yaml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -497,3 +497,6 @@ jobs:
497497
log_artifact_pattern: selected-test-logs-*
498498
recommendations_file: .github/workflows/scripts/recommended_pytest_paths.txt
499499
recommendations_content: ${{ needs.recommend-tests-from-coverage.outputs.coverage_paths || '' }}
500+
secrets:
501+
OBS_ACCESS_KEY_FAILURE_ANALYSIS: ${{ secrets.OBS_ACCESS_KEY_FAILURE_ANALYSIS }}
502+
OBS_SECRET_ACCESS_KEY_FAILURE_ANALYSIS: ${{ secrets.OBS_SECRET_ACCESS_KEY_FAILURE_ANALYSIS }}

0 commit comments

Comments
 (0)