|
32 | 32 | required: false |
33 | 33 | default: '' |
34 | 34 | 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 |
35 | 40 |
|
36 | 41 | defaults: |
37 | 42 | run: |
@@ -116,3 +121,229 @@ jobs: |
116 | 121 | path: ./test-logs/failure_report.md |
117 | 122 | if-no-files-found: ignore |
118 | 123 | 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 |
0 commit comments