feat(ci): 接入AgentLoop Trace上报,实现CI全链路监控 (#598) #624
@@ -64,6 +64,18 @@ jobs:
|
||||
echo "🔧 包含全栈变更,运行完整CI"
|
||||
fi
|
||||
|
||||
- name: Report CI trace
|
||||
if: always()
|
||||
shell: sh
|
||||
env:
|
||||
AGENTLOOP_LICENSE_KEY: ${{ secrets.AGENTLOOP_LICENSE_KEY }}
|
||||
run: |
|
||||
STATUS="ok"
|
||||
[ ${{ job.status }} = "success" ] || STATUS="error"
|
||||
START_TIME=""
|
||||
[ -f /tmp/ci_job_start_time ] && START_TIME=$(cat /tmp/ci_job_start_time)
|
||||
python3 scripts/ci/ci_trace_report.py --service xiaoxia-saas-ci --status $STATUS --start-time "$START_TIME" || true
|
||||
|
||||
validate:
|
||||
needs: check-frontend-only
|
||||
if: always() && needs.check-frontend-only.outputs.skip_backend != 'true'
|
||||
@@ -148,6 +160,18 @@ jobs:
|
||||
run: |
|
||||
set +e
|
||||
NOTIFY_MODE=failure JOB_NAME="Validate Code Quality And Tests" python3 scripts/ci_notify.py
|
||||
- name: Report CI trace
|
||||
if: always()
|
||||
shell: sh
|
||||
env:
|
||||
AGENTLOOP_LICENSE_KEY: ${{ secrets.AGENTLOOP_LICENSE_KEY }}
|
||||
run: |
|
||||
STATUS="ok"
|
||||
[ ${{ job.status }} = "success" ] || STATUS="error"
|
||||
START_TIME=""
|
||||
[ -f /tmp/ci_job_start_time ] && START_TIME=$(cat /tmp/ci_job_start_time)
|
||||
python3 scripts/ci/ci_trace_report.py --service xiaoxia-saas-ci --status $STATUS --start-time "$START_TIME" || true
|
||||
|
||||
unit-tests:
|
||||
needs: check-frontend-only
|
||||
if: always() && needs.check-frontend-only.outputs.skip_backend != 'true'
|
||||
@@ -201,6 +225,18 @@ jobs:
|
||||
set +e
|
||||
NOTIFY_MODE=failure JOB_NAME="Unit Tests" python3 scripts/ci_notify.py
|
||||
|
||||
- name: Report CI trace
|
||||
if: always()
|
||||
shell: sh
|
||||
env:
|
||||
AGENTLOOP_LICENSE_KEY: ${{ secrets.AGENTLOOP_LICENSE_KEY }}
|
||||
run: |
|
||||
STATUS="ok"
|
||||
[ ${{ job.status }} = "success" ] || STATUS="error"
|
||||
START_TIME=""
|
||||
[ -f /tmp/ci_job_start_time ] && START_TIME=$(cat /tmp/ci_job_start_time)
|
||||
python3 scripts/ci/ci_trace_report.py --service xiaoxia-saas-ci --status $STATUS --start-time "$START_TIME" || true
|
||||
|
||||
integration-tests:
|
||||
name: Integration Tests
|
||||
runs-on: ci-l2
|
||||
@@ -253,6 +289,18 @@ jobs:
|
||||
set +e
|
||||
NOTIFY_MODE=failure JOB_NAME="Integration Tests" python3 scripts/ci_notify.py
|
||||
|
||||
- name: Report CI trace
|
||||
if: always()
|
||||
shell: sh
|
||||
env:
|
||||
AGENTLOOP_LICENSE_KEY: ${{ secrets.AGENTLOOP_LICENSE_KEY }}
|
||||
run: |
|
||||
STATUS="ok"
|
||||
[ ${{ job.status }} = "success" ] || STATUS="error"
|
||||
START_TIME=""
|
||||
[ -f /tmp/ci_job_start_time ] && START_TIME=$(cat /tmp/ci_job_start_time)
|
||||
python3 scripts/ci/ci_trace_report.py --service xiaoxia-saas-ci --status $STATUS --start-time "$START_TIME" || true
|
||||
|
||||
frontend-lint:
|
||||
name: Frontend Lint
|
||||
runs-on: ci-l2
|
||||
@@ -304,6 +352,18 @@ jobs:
|
||||
set +e
|
||||
NOTIFY_MODE=failure JOB_NAME="Frontend Lint" python3 scripts/ci_notify.py
|
||||
|
||||
- name: Report CI trace
|
||||
if: always()
|
||||
shell: sh
|
||||
env:
|
||||
AGENTLOOP_LICENSE_KEY: ${{ secrets.AGENTLOOP_LICENSE_KEY }}
|
||||
run: |
|
||||
STATUS="ok"
|
||||
[ ${{ job.status }} = "success" ] || STATUS="error"
|
||||
START_TIME=""
|
||||
[ -f /tmp/ci_job_start_time ] && START_TIME=$(cat /tmp/ci_job_start_time)
|
||||
python3 scripts/ci/ci_trace_report.py --service xiaoxia-saas-ci --status $STATUS --start-time "$START_TIME" || true
|
||||
|
||||
frontend-unit-test:
|
||||
name: Frontend Unit Tests
|
||||
runs-on: ci-l2
|
||||
@@ -347,6 +407,18 @@ jobs:
|
||||
set +e
|
||||
NOTIFY_MODE=failure JOB_NAME="Frontend Unit Tests" python3 scripts/ci_notify.py
|
||||
|
||||
- name: Report CI trace
|
||||
if: always()
|
||||
shell: sh
|
||||
env:
|
||||
AGENTLOOP_LICENSE_KEY: ${{ secrets.AGENTLOOP_LICENSE_KEY }}
|
||||
run: |
|
||||
STATUS="ok"
|
||||
[ ${{ job.status }} = "success" ] || STATUS="error"
|
||||
START_TIME=""
|
||||
[ -f /tmp/ci_job_start_time ] && START_TIME=$(cat /tmp/ci_job_start_time)
|
||||
python3 scripts/ci/ci_trace_report.py --service xiaoxia-saas-ci --status $STATUS --start-time "$START_TIME" || true
|
||||
|
||||
build-staging:
|
||||
name: Build Staging ${{ matrix.service_display }} Image
|
||||
runs-on: runtime-builder
|
||||
@@ -468,6 +540,18 @@ jobs:
|
||||
set +e
|
||||
NOTIFY_MODE=failure JOB_NAME="Build Staging ${{ matrix.service_display }} Image" python3 scripts/ci_notify.py
|
||||
|
||||
- name: Report CI trace
|
||||
if: always()
|
||||
shell: sh
|
||||
env:
|
||||
AGENTLOOP_LICENSE_KEY: ${{ secrets.AGENTLOOP_LICENSE_KEY }}
|
||||
run: |
|
||||
STATUS="ok"
|
||||
[ ${{ job.status }} = "success" ] || STATUS="error"
|
||||
START_TIME=""
|
||||
[ -f /tmp/ci_job_start_time ] && START_TIME=$(cat /tmp/ci_job_start_time)
|
||||
python3 scripts/ci/ci_trace_report.py --service xiaoxia-saas-ci --status $STATUS --start-time "$START_TIME" || true
|
||||
|
||||
deploy-staging:
|
||||
name: Deploy Staging (Watchtower auto-deploy)
|
||||
runs-on: runtime-builder
|
||||
@@ -605,6 +689,18 @@ jobs:
|
||||
set +e
|
||||
NOTIFY_MODE=failure JOB_NAME="Deploy Staging" python3 scripts/ci_notify.py
|
||||
|
||||
- name: Report CI trace
|
||||
if: always()
|
||||
shell: sh
|
||||
env:
|
||||
AGENTLOOP_LICENSE_KEY: ${{ secrets.AGENTLOOP_LICENSE_KEY }}
|
||||
run: |
|
||||
STATUS="ok"
|
||||
[ ${{ job.status }} = "success" ] || STATUS="error"
|
||||
START_TIME=""
|
||||
[ -f /tmp/ci_job_start_time ] && START_TIME=$(cat /tmp/ci_job_start_time)
|
||||
python3 scripts/ci/ci_trace_report.py --service xiaoxia-saas-ci --status $STATUS --start-time "$START_TIME" || true
|
||||
|
||||
staging-e2e:
|
||||
name: Staging E2E Tests
|
||||
runs-on: runtime-builder
|
||||
@@ -649,6 +745,18 @@ jobs:
|
||||
set +e
|
||||
NOTIFY_MODE=failure JOB_NAME="Staging E2E Tests" python3 scripts/ci_notify.py
|
||||
|
||||
- name: Report CI trace
|
||||
if: always()
|
||||
shell: sh
|
||||
env:
|
||||
AGENTLOOP_LICENSE_KEY: ${{ secrets.AGENTLOOP_LICENSE_KEY }}
|
||||
run: |
|
||||
STATUS="ok"
|
||||
[ ${{ job.status }} = "success" ] || STATUS="error"
|
||||
START_TIME=""
|
||||
[ -f /tmp/ci_job_start_time ] && START_TIME=$(cat /tmp/ci_job_start_time)
|
||||
python3 scripts/ci/ci_trace_report.py --service xiaoxia-saas-ci --status $STATUS --start-time "$START_TIME" || true
|
||||
|
||||
staging-api-tests:
|
||||
name: Staging API Integration Tests
|
||||
runs-on: runtime-builder
|
||||
@@ -691,6 +799,18 @@ jobs:
|
||||
set +e
|
||||
NOTIFY_MODE=failure JOB_NAME="Staging API Integration Tests" python3 scripts/ci_notify.py
|
||||
|
||||
- name: Report CI trace
|
||||
if: always()
|
||||
shell: sh
|
||||
env:
|
||||
AGENTLOOP_LICENSE_KEY: ${{ secrets.AGENTLOOP_LICENSE_KEY }}
|
||||
run: |
|
||||
STATUS="ok"
|
||||
[ ${{ job.status }} = "success" ] || STATUS="error"
|
||||
START_TIME=""
|
||||
[ -f /tmp/ci_job_start_time ] && START_TIME=$(cat /tmp/ci_job_start_time)
|
||||
python3 scripts/ci/ci_trace_report.py --service xiaoxia-saas-ci --status $STATUS --start-time "$START_TIME" || true
|
||||
|
||||
build-production:
|
||||
name: Build Production ${{ matrix.service_display }} Image
|
||||
runs-on: runtime-builder
|
||||
@@ -808,6 +928,18 @@ jobs:
|
||||
set +e
|
||||
NOTIFY_MODE=failure JOB_NAME="Build Production ${{ matrix.service_display }} Image" python3 scripts/ci_notify.py
|
||||
|
||||
- name: Report CI trace
|
||||
if: always()
|
||||
shell: sh
|
||||
env:
|
||||
AGENTLOOP_LICENSE_KEY: ${{ secrets.AGENTLOOP_LICENSE_KEY }}
|
||||
run: |
|
||||
STATUS="ok"
|
||||
[ ${{ job.status }} = "success" ] || STATUS="error"
|
||||
START_TIME=""
|
||||
[ -f /tmp/ci_job_start_time ] && START_TIME=$(cat /tmp/ci_job_start_time)
|
||||
python3 scripts/ci/ci_trace_report.py --service xiaoxia-saas-ci --status $STATUS --start-time "$START_TIME" || true
|
||||
|
||||
deploy-production:
|
||||
name: Deploy Production
|
||||
runs-on: runtime-builder
|
||||
@@ -931,6 +1063,18 @@ jobs:
|
||||
set +e
|
||||
NOTIFY_MODE=failure JOB_NAME="Deploy Production" python3 scripts/ci_notify.py
|
||||
|
||||
- name: Report CI trace
|
||||
if: always()
|
||||
shell: sh
|
||||
env:
|
||||
AGENTLOOP_LICENSE_KEY: ${{ secrets.AGENTLOOP_LICENSE_KEY }}
|
||||
run: |
|
||||
STATUS="ok"
|
||||
[ ${{ job.status }} = "success" ] || STATUS="error"
|
||||
START_TIME=""
|
||||
[ -f /tmp/ci_job_start_time ] && START_TIME=$(cat /tmp/ci_job_start_time)
|
||||
python3 scripts/ci/ci_trace_report.py --service xiaoxia-saas-ci --status $STATUS --start-time "$START_TIME" || true
|
||||
|
||||
production-e2e:
|
||||
name: Production Browser E2E
|
||||
runs-on: runtime-builder
|
||||
@@ -975,6 +1119,18 @@ jobs:
|
||||
set +e
|
||||
NOTIFY_MODE=failure JOB_NAME="Production Browser E2E" python3 scripts/ci_notify.py
|
||||
|
||||
- name: Report CI trace
|
||||
if: always()
|
||||
shell: sh
|
||||
env:
|
||||
AGENTLOOP_LICENSE_KEY: ${{ secrets.AGENTLOOP_LICENSE_KEY }}
|
||||
run: |
|
||||
STATUS="ok"
|
||||
[ ${{ job.status }} = "success" ] || STATUS="error"
|
||||
START_TIME=""
|
||||
[ -f /tmp/ci_job_start_time ] && START_TIME=$(cat /tmp/ci_job_start_time)
|
||||
python3 scripts/ci/ci_trace_report.py --service xiaoxia-saas-ci --status $STATUS --start-time "$START_TIME" || true
|
||||
|
||||
acr-cleanup:
|
||||
name: ACR Image Cleanup
|
||||
runs-on: runtime-builder
|
||||
@@ -1021,3 +1177,15 @@ jobs:
|
||||
run: |
|
||||
set +e
|
||||
NOTIFY_MODE=failure JOB_NAME="ACR Image Cleanup" python3 scripts/ci_notify.py
|
||||
- name: Report CI trace
|
||||
if: always()
|
||||
shell: sh
|
||||
env:
|
||||
AGENTLOOP_LICENSE_KEY: ${{ secrets.AGENTLOOP_LICENSE_KEY }}
|
||||
run: |
|
||||
STATUS="ok"
|
||||
[ ${{ job.status }} = "success" ] || STATUS="error"
|
||||
START_TIME=""
|
||||
[ -f /tmp/ci_job_start_time ] && START_TIME=$(cat /tmp/ci_job_start_time)
|
||||
python3 scripts/ci/ci_trace_report.py --service xiaoxia-saas-ci --status $STATUS --start-time "$START_TIME" || true
|
||||
|
||||
|
||||
@@ -34,3 +34,15 @@ jobs:
|
||||
python3 scripts/ci_trigger_monitor.py
|
||||
# 监控脚本永远不fail,避免告警风暴
|
||||
exit 0
|
||||
- name: Report CI trace
|
||||
if: always()
|
||||
shell: sh
|
||||
env:
|
||||
AGENTLOOP_LICENSE_KEY: ${{ secrets.AGENTLOOP_LICENSE_KEY }}
|
||||
run: |
|
||||
STATUS="ok"
|
||||
[ ${{ job.status }} = "success" ] || STATUS="error"
|
||||
START_TIME=""
|
||||
[ -f /tmp/ci_job_start_time ] && START_TIME=$(cat /tmp/ci_job_start_time)
|
||||
python3 scripts/ci/ci_trace_report.py --service xiaoxia-saas-ci --status $STATUS --start-time "$START_TIME" || true
|
||||
|
||||
|
||||
@@ -51,3 +51,15 @@ jobs:
|
||||
python3 scripts/ci_code_review.py
|
||||
# 审查脚本异常不影响 CI 通过
|
||||
continue-on-error: true
|
||||
- name: Report CI trace
|
||||
if: always()
|
||||
shell: sh
|
||||
env:
|
||||
AGENTLOOP_LICENSE_KEY: ${{ secrets.AGENTLOOP_LICENSE_KEY }}
|
||||
run: |
|
||||
STATUS="ok"
|
||||
[ ${{ job.status }} = "success" ] || STATUS="error"
|
||||
START_TIME=""
|
||||
[ -f /tmp/ci_job_start_time ] && START_TIME=$(cat /tmp/ci_job_start_time)
|
||||
python3 scripts/ci/ci_trace_report.py --service xiaoxia-saas-ci --status $STATUS --start-time "$START_TIME" || true
|
||||
|
||||
|
||||
@@ -106,6 +106,18 @@ jobs:
|
||||
echo "======================================"
|
||||
exit $SMOKE_EXIT
|
||||
|
||||
- name: Report CI trace
|
||||
if: always()
|
||||
shell: sh
|
||||
env:
|
||||
AGENTLOOP_LICENSE_KEY: ${{ secrets.AGENTLOOP_LICENSE_KEY }}
|
||||
run: |
|
||||
STATUS="ok"
|
||||
[ ${{ job.status }} = "success" ] || STATUS="error"
|
||||
START_TIME=""
|
||||
[ -f /tmp/ci_job_start_time ] && START_TIME=$(cat /tmp/ci_job_start_time)
|
||||
python3 scripts/ci/ci_trace_report.py --service xiaoxia-saas-ci --status $STATUS --start-time "$START_TIME" || true
|
||||
|
||||
# ── 2. Staging API 集成测试 ─────────────────────────────────────────
|
||||
staging-api-tests:
|
||||
name: Staging API Integration Tests
|
||||
@@ -243,6 +255,18 @@ jobs:
|
||||
echo "report=FAIL" >> "${GITHUB_OUTPUT}"
|
||||
fi
|
||||
|
||||
- name: Report CI trace
|
||||
if: always()
|
||||
shell: sh
|
||||
env:
|
||||
AGENTLOOP_LICENSE_KEY: ${{ secrets.AGENTLOOP_LICENSE_KEY }}
|
||||
run: |
|
||||
STATUS="ok"
|
||||
[ ${{ job.status }} = "success" ] || STATUS="error"
|
||||
START_TIME=""
|
||||
[ -f /tmp/ci_job_start_time ] && START_TIME=$(cat /tmp/ci_job_start_time)
|
||||
python3 scripts/ci/ci_trace_report.py --service xiaoxia-saas-ci --status $STATUS --start-time "$START_TIME" || true
|
||||
|
||||
# ── 3. Staging 浏览器 E2E ──────────────────────────────────────────
|
||||
staging-e2e:
|
||||
name: Staging Browser E2E
|
||||
@@ -332,6 +356,18 @@ jobs:
|
||||
echo "=========================================="
|
||||
exit $EXIT_CODE
|
||||
|
||||
- name: Report CI trace
|
||||
if: always()
|
||||
shell: sh
|
||||
env:
|
||||
AGENTLOOP_LICENSE_KEY: ${{ secrets.AGENTLOOP_LICENSE_KEY }}
|
||||
run: |
|
||||
STATUS="ok"
|
||||
[ ${{ job.status }} = "success" ] || STATUS="error"
|
||||
START_TIME=""
|
||||
[ -f /tmp/ci_job_start_time ] && START_TIME=$(cat /tmp/ci_job_start_time)
|
||||
python3 scripts/ci/ci_trace_report.py --service xiaoxia-saas-ci --status $STATUS --start-time "$START_TIME" || true
|
||||
|
||||
# ── 4. 性能基线巡检 ────────────────────────────────────────────────
|
||||
performance-check:
|
||||
name: Performance Baseline Check
|
||||
@@ -580,6 +616,18 @@ jobs:
|
||||
exit 0
|
||||
fi
|
||||
|
||||
- name: Report CI trace
|
||||
if: always()
|
||||
shell: sh
|
||||
env:
|
||||
AGENTLOOP_LICENSE_KEY: ${{ secrets.AGENTLOOP_LICENSE_KEY }}
|
||||
run: |
|
||||
STATUS="ok"
|
||||
[ ${{ job.status }} = "success" ] || STATUS="error"
|
||||
START_TIME=""
|
||||
[ -f /tmp/ci_job_start_time ] && START_TIME=$(cat /tmp/ci_job_start_time)
|
||||
python3 scripts/ci/ci_trace_report.py --service xiaoxia-saas-ci --status $STATUS --start-time "$START_TIME" || true
|
||||
|
||||
# ── 5. 每日巡检汇总报告 ────────────────────────────────────────────
|
||||
daily-report:
|
||||
name: Daily Check Report
|
||||
@@ -656,3 +704,15 @@ jobs:
|
||||
# 不 exit 1,因为我们用了 always(),保持 report job 成功,
|
||||
# 但其他失败的 job 已经让整体流水线标记为失败
|
||||
fi
|
||||
- name: Report CI trace
|
||||
if: always()
|
||||
shell: sh
|
||||
env:
|
||||
AGENTLOOP_LICENSE_KEY: ${{ secrets.AGENTLOOP_LICENSE_KEY }}
|
||||
run: |
|
||||
STATUS="ok"
|
||||
[ ${{ job.status }} = "success" ] || STATUS="error"
|
||||
START_TIME=""
|
||||
[ -f /tmp/ci_job_start_time ] && START_TIME=$(cat /tmp/ci_job_start_time)
|
||||
python3 scripts/ci/ci_trace_report.py --service xiaoxia-saas-ci --status $STATUS --start-time "$START_TIME" || true
|
||||
|
||||
|
||||
@@ -175,6 +175,18 @@ jobs:
|
||||
echo "⏰ 等待超时(20分钟),CI尚未全部完成"
|
||||
exit 0
|
||||
|
||||
- name: Report CI trace
|
||||
if: always()
|
||||
shell: sh
|
||||
env:
|
||||
AGENTLOOP_LICENSE_KEY: ${{ secrets.AGENTLOOP_LICENSE_KEY }}
|
||||
run: |
|
||||
STATUS="ok"
|
||||
[ ${{ job.status }} = "success" ] || STATUS="error"
|
||||
START_TIME=""
|
||||
[ -f /tmp/ci_job_start_time ] && START_TIME=$(cat /tmp/ci_job_start_time)
|
||||
python3 scripts/ci/ci_trace_report.py --service xiaoxia-saas-ci --status $STATUS --start-time "$START_TIME" || true
|
||||
|
||||
auto-merge:
|
||||
name: Auto Merge on CI Green + Approved
|
||||
runs-on: ci-check
|
||||
@@ -343,3 +355,15 @@ jobs:
|
||||
echo
|
||||
echo "等待超时(30分钟)"
|
||||
exit 0
|
||||
- name: Report CI trace
|
||||
if: always()
|
||||
shell: sh
|
||||
env:
|
||||
AGENTLOOP_LICENSE_KEY: ${{ secrets.AGENTLOOP_LICENSE_KEY }}
|
||||
run: |
|
||||
STATUS="ok"
|
||||
[ ${{ job.status }} = "success" ] || STATUS="error"
|
||||
START_TIME=""
|
||||
[ -f /tmp/ci_job_start_time ] && START_TIME=$(cat /tmp/ci_job_start_time)
|
||||
python3 scripts/ci/ci_trace_report.py --service xiaoxia-saas-ci --status $STATUS --start-time "$START_TIME" || true
|
||||
|
||||
|
||||
@@ -193,3 +193,15 @@ jobs:
|
||||
> /dev/null
|
||||
echo "Cleanup comment posted"
|
||||
|
||||
- name: Report CI trace
|
||||
if: always()
|
||||
shell: sh
|
||||
env:
|
||||
AGENTLOOP_LICENSE_KEY: ${{ secrets.AGENTLOOP_LICENSE_KEY }}
|
||||
run: |
|
||||
STATUS="ok"
|
||||
[ ${{ job.status }} = "success" ] || STATUS="error"
|
||||
START_TIME=""
|
||||
[ -f /tmp/ci_job_start_time ] && START_TIME=$(cat /tmp/ci_job_start_time)
|
||||
python3 scripts/ci/ci_trace_report.py --service xiaoxia-saas-ci --status $STATUS --start-time "$START_TIME" || true
|
||||
|
||||
|
||||
@@ -272,3 +272,15 @@ jobs:
|
||||
run: |
|
||||
set +e
|
||||
NOTIFY_MODE=failure JOB_NAME="Deploy Preview Environment" python3 scripts/ci_notify.py
|
||||
- name: Report CI trace
|
||||
if: always()
|
||||
shell: sh
|
||||
env:
|
||||
AGENTLOOP_LICENSE_KEY: ${{ secrets.AGENTLOOP_LICENSE_KEY }}
|
||||
run: |
|
||||
STATUS="ok"
|
||||
[ ${{ job.status }} = "success" ] || STATUS="error"
|
||||
START_TIME=""
|
||||
[ -f /tmp/ci_job_start_time ] && START_TIME=$(cat /tmp/ci_job_start_time)
|
||||
python3 scripts/ci/ci_trace_report.py --service xiaoxia-saas-ci --status $STATUS --start-time "$START_TIME" || true
|
||||
|
||||
|
||||
Executable
+375
@@ -0,0 +1,375 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
CI Trace Report Script - Reports CI Trace data to AgentLoop from Gitea Actions workflows.
|
||||
|
||||
Usage in CI workflow jobs:
|
||||
- At start: python3 scripts/ci/ci_trace_report.py --status running
|
||||
- At end: python3 scripts/ci/ci_trace_report.py --status ok --start-time $CI_TRACE_START_TIME
|
||||
|
||||
Environment variables (built-in Gitea Actions):
|
||||
GITEA_REPOSITORY / GITHUB_REPOSITORY - repository (owner/repo)
|
||||
GITEA_WORKFLOW / GITHUB_WORKFLOW - workflow name
|
||||
GITEA_JOB / GITHUB_JOB - job ID
|
||||
GITEA_SHA / GITHUB_SHA - commit SHA
|
||||
GITEA_REF_NAME / GITHUB_REF_NAME - branch name
|
||||
GITEA_RUN_ID / GITHUB_RUN_ID - run ID
|
||||
GITEA_ACTOR / GITHUB_ACTOR - trigger actor
|
||||
GITEA_EVENT_NAME / GITHUB_EVENT_NAME - event type
|
||||
PR_NUMBER / GITEA_PR_NUMBER - PR number (if PR triggered)
|
||||
|
||||
AgentLoop configuration (injected via Secrets):
|
||||
AGENTLOOP_LICENSE_KEY - LicenseKey (required)
|
||||
AGENTLOOP_ENDPOINT - Trace endpoint (optional, has default)
|
||||
AGENTLOOP_PROJECT - SLS Project name (optional)
|
||||
AGENTLOOP_WORKSPACE - CMS Workspace name (optional)
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
import uuid
|
||||
|
||||
# ========== Default Configuration ==========
|
||||
DEFAULT_ENDPOINT = "https://proj-xtrace-495e81719a1fd9a2c5fd671eefafbe-cn-hangzhou.cn-hangzhou.log.aliyuncs.com/apm/trace/opentelemetry/v1/traces"
|
||||
DEFAULT_PROJECT = "proj-xtrace-495e81719a1fd9a2c5fd671eefafbe-cn-hangzhou"
|
||||
DEFAULT_WORKSPACE = "agentloop-13b8d6efb7fde6e9b193eb982ade68e2"
|
||||
|
||||
|
||||
# ========== OTLP Protobuf Manual Encoding ==========
|
||||
|
||||
|
||||
def _encode_varint(value):
|
||||
result = bytearray()
|
||||
while value > 0x7F:
|
||||
result.append((value & 0x7F) | 0x80)
|
||||
value >>= 7
|
||||
result.append(value & 0x7F)
|
||||
return bytes(result)
|
||||
|
||||
|
||||
def _encode_tag(field_number, wire_type):
|
||||
return _encode_varint((field_number << 3) | wire_type)
|
||||
|
||||
|
||||
def _encode_string_field(field_number, value):
|
||||
value_bytes = value.encode("utf-8")
|
||||
return _encode_tag(field_number, 2) + _encode_varint(len(value_bytes)) + value_bytes
|
||||
|
||||
|
||||
def _encode_bytes_field(field_number, value_bytes):
|
||||
return _encode_tag(field_number, 2) + _encode_varint(len(value_bytes)) + value_bytes
|
||||
|
||||
|
||||
def _encode_int_field(field_number, value):
|
||||
return _encode_tag(field_number, 0) + _encode_varint(value & 0xFFFFFFFFFFFFFFFF)
|
||||
|
||||
|
||||
def _encode_message_field(field_number, message_bytes):
|
||||
return _encode_tag(field_number, 2) + _encode_varint(len(message_bytes)) + message_bytes
|
||||
|
||||
|
||||
def _encode_key_value(key, value_str):
|
||||
any_value = _encode_string_field(1, value_str)
|
||||
return _encode_string_field(1, key) + _encode_message_field(2, any_value)
|
||||
|
||||
|
||||
def _encode_status(status_code, status_msg=""):
|
||||
data = _encode_int_field(1, status_code)
|
||||
if status_msg:
|
||||
data += _encode_string_field(2, status_msg)
|
||||
return data
|
||||
|
||||
|
||||
def _encode_span(
|
||||
trace_id_bytes,
|
||||
span_id_bytes,
|
||||
parent_span_id_bytes,
|
||||
name,
|
||||
start_time_unix_nano,
|
||||
end_time_unix_nano,
|
||||
span_kind,
|
||||
attributes,
|
||||
status_code,
|
||||
status_msg="",
|
||||
):
|
||||
data = b""
|
||||
data += _encode_bytes_field(1, trace_id_bytes)
|
||||
data += _encode_bytes_field(2, span_id_bytes)
|
||||
if parent_span_id_bytes:
|
||||
data += _encode_bytes_field(3, parent_span_id_bytes)
|
||||
data += _encode_string_field(4, name)
|
||||
data += _encode_int_field(5, span_kind)
|
||||
data += _encode_int_field(6, start_time_unix_nano)
|
||||
data += _encode_int_field(7, end_time_unix_nano)
|
||||
for key, value in attributes.items():
|
||||
kv = _encode_key_value(key, str(value))
|
||||
data += _encode_message_field(9, kv)
|
||||
status = _encode_status(status_code, status_msg)
|
||||
data += _encode_message_field(12, status)
|
||||
return data
|
||||
|
||||
|
||||
def _encode_resource_spans(service_name, scope_spans_bytes):
|
||||
svc_kv = _encode_key_value("service.name", service_name)
|
||||
resource = _encode_message_field(1, svc_kv)
|
||||
data = _encode_message_field(1, resource)
|
||||
data += _encode_message_field(2, scope_spans_bytes)
|
||||
return data
|
||||
|
||||
|
||||
def _encode_scope_spans(scope_name, spans_bytes_list):
|
||||
scope = _encode_string_field(1, scope_name)
|
||||
data = _encode_message_field(1, scope)
|
||||
for span_bytes in spans_bytes_list:
|
||||
data += _encode_message_field(2, span_bytes)
|
||||
return data
|
||||
|
||||
|
||||
def _encode_traces_data(resource_spans_bytes_list):
|
||||
data = b""
|
||||
for rs_bytes in resource_spans_bytes_list:
|
||||
data += _encode_message_field(1, rs_bytes)
|
||||
return data
|
||||
|
||||
|
||||
# ========== Helper Functions ==========
|
||||
|
||||
|
||||
def _gen_trace_id():
|
||||
return uuid.uuid4().bytes
|
||||
|
||||
|
||||
def _gen_span_id():
|
||||
return uuid.uuid4().bytes[:8]
|
||||
|
||||
|
||||
def _env(name, default=""):
|
||||
"""Get env var with GITEA_/GITHUB_ prefix fallback."""
|
||||
val = os.getenv(name, "")
|
||||
if val:
|
||||
return val
|
||||
if name.startswith("GITEA_"):
|
||||
alt = "GITHUB_" + name[6:]
|
||||
return os.getenv(alt, default)
|
||||
if name.startswith("GITHUB_"):
|
||||
alt = "GITEA_" + name[7:]
|
||||
return os.getenv(alt, default)
|
||||
return default
|
||||
|
||||
|
||||
def _get_pr_number():
|
||||
"""Get PR number from environment or event file."""
|
||||
pr = os.getenv("PR_NUMBER", "") or os.getenv("GITEA_PR_NUMBER", "")
|
||||
if pr:
|
||||
return pr
|
||||
|
||||
event_path = os.getenv("GITHUB_EVENT_PATH", "") or os.getenv("GITEA_EVENT_PATH", "")
|
||||
if event_path and os.path.isfile(event_path):
|
||||
try:
|
||||
with open(event_path, "r") as f:
|
||||
event = json.load(f)
|
||||
if "pull_request" in event and "number" in event["pull_request"]:
|
||||
return str(event["pull_request"]["number"])
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return ""
|
||||
|
||||
|
||||
def _get_ci_attributes():
|
||||
"""Collect attributes from CI environment variables."""
|
||||
attrs = {
|
||||
"ci.repo": _env("GITEA_REPOSITORY") or _env("GITHUB_REPOSITORY") or "unknown",
|
||||
"ci.workflow": _env("GITEA_WORKFLOW") or _env("GITHUB_WORKFLOW") or "unknown",
|
||||
"ci.job": _env("GITEA_JOB") or _env("GITHUB_JOB") or "unknown",
|
||||
"ci.commit_sha": _env("GITEA_SHA") or _env("GITHUB_SHA") or "unknown",
|
||||
"ci.branch": _env("GITEA_REF_NAME") or _env("GITHUB_REF_NAME") or "unknown",
|
||||
"ci.run_id": _env("GITEA_RUN_ID") or _env("GITHUB_RUN_ID") or "unknown",
|
||||
"ci.actor": _env("GITEA_ACTOR") or _env("GITHUB_ACTOR") or "unknown",
|
||||
"ci.event": _env("GITEA_EVENT_NAME") or _env("GITHUB_EVENT_NAME") or "unknown",
|
||||
}
|
||||
pr = _get_pr_number()
|
||||
if pr:
|
||||
attrs["ci.pr_number"] = pr
|
||||
return attrs
|
||||
|
||||
|
||||
# ========== Trace Building & Reporting ==========
|
||||
|
||||
|
||||
def build_trace(service_name, trace_name, status, duration_ms, attributes=None):
|
||||
"""Build an OTLP trace payload (protobuf bytes). No external dependencies."""
|
||||
trace_id = _gen_trace_id()
|
||||
end_time = int(time.time() * 1e9)
|
||||
start_time = end_time - int(duration_ms * 1e6)
|
||||
status_code = 1 if status in ("ok", "running") else 2
|
||||
status_msg = "" if status in ("ok", "running") else "Job failed"
|
||||
|
||||
main_attrs = {
|
||||
"agent.trace_name": trace_name,
|
||||
"agent.service": service_name,
|
||||
"ci.trace_status": status,
|
||||
}
|
||||
if attributes:
|
||||
main_attrs.update(attributes)
|
||||
|
||||
main_span = _encode_span(
|
||||
trace_id_bytes=trace_id,
|
||||
span_id_bytes=_gen_span_id(),
|
||||
parent_span_id_bytes=b"",
|
||||
name=trace_name,
|
||||
start_time_unix_nano=start_time,
|
||||
end_time_unix_nano=end_time,
|
||||
span_kind=1,
|
||||
attributes=main_attrs,
|
||||
status_code=status_code,
|
||||
status_msg=status_msg,
|
||||
)
|
||||
|
||||
scope_spans = _encode_scope_spans("ci-trace", [main_span])
|
||||
resource_spans = _encode_resource_spans(service_name, scope_spans)
|
||||
return _encode_traces_data([resource_spans])
|
||||
|
||||
|
||||
def report_ci_trace(
|
||||
service_name,
|
||||
trace_name,
|
||||
status="ok",
|
||||
duration_ms=1000,
|
||||
endpoint=None,
|
||||
license_key=None,
|
||||
project=None,
|
||||
workspace=None,
|
||||
extra_attributes=None,
|
||||
):
|
||||
"""
|
||||
Report CI Trace data. Returns (success: bool, message: str).
|
||||
Never raises exceptions; returns False on failure.
|
||||
"""
|
||||
try:
|
||||
endpoint = endpoint or os.getenv("AGENTLOOP_ENDPOINT", DEFAULT_ENDPOINT)
|
||||
license_key = license_key or os.getenv("AGENTLOOP_LICENSE_KEY", "")
|
||||
project = project or os.getenv("AGENTLOOP_PROJECT", DEFAULT_PROJECT)
|
||||
workspace = workspace or os.getenv("AGENTLOOP_WORKSPACE", DEFAULT_WORKSPACE)
|
||||
|
||||
if not license_key:
|
||||
return False, "[Trace] skipped: AGENTLOOP_LICENSE_KEY not configured"
|
||||
|
||||
attrs = _get_ci_attributes()
|
||||
if extra_attributes:
|
||||
attrs.update(extra_attributes)
|
||||
|
||||
payload = build_trace(
|
||||
service_name=service_name,
|
||||
trace_name=trace_name,
|
||||
status=status,
|
||||
duration_ms=duration_ms,
|
||||
attributes=attrs,
|
||||
)
|
||||
|
||||
headers = {
|
||||
"Content-Type": "application/x-protobuf",
|
||||
"x-arms-license-key": license_key,
|
||||
"x-arms-project": project,
|
||||
"x-cms-workspace": workspace,
|
||||
}
|
||||
|
||||
req = urllib.request.Request(endpoint, data=payload, headers=headers, method="POST")
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=10) as resp:
|
||||
status_code = resp.status
|
||||
resp_body = resp.read().decode("utf-8", errors="replace")
|
||||
except urllib.error.HTTPError as e:
|
||||
status_code = e.code
|
||||
resp_body = e.read().decode("utf-8", errors="replace")
|
||||
|
||||
if status_code in (200, 202):
|
||||
return True, (f"[Trace] success: {service_name} / {trace_name} " f"({status}, {duration_ms}ms)")
|
||||
else:
|
||||
return False, (f"[Trace] failed: HTTP {status_code} - {resp_body[:200]}")
|
||||
except Exception as e:
|
||||
return False, f"[Trace] error: {type(e).__name__}: {str(e)}"
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="CI AgentLoop Trace Reporter")
|
||||
parser.add_argument(
|
||||
"--service",
|
||||
dest="service_name",
|
||||
default=os.getenv("TRACE_SERVICE", ""),
|
||||
help="Service name (also via TRACE_SERVICE env)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--name",
|
||||
dest="trace_name",
|
||||
default=os.getenv("TRACE_NAME", ""),
|
||||
help="Trace name (also via TRACE_NAME env)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--status",
|
||||
default=os.getenv("TRACE_STATUS", "ok"),
|
||||
choices=["ok", "error", "running"],
|
||||
help="Status: ok / error / running (default ok)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--start-time",
|
||||
dest="start_time",
|
||||
default=os.getenv("TRACE_START_TIME", ""),
|
||||
help="Start timestamp (seconds) for duration calculation",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--duration-ms",
|
||||
dest="duration_ms",
|
||||
type=int,
|
||||
default=0,
|
||||
help="Direct duration in ms; takes precedence over --start-time",
|
||||
)
|
||||
parser.add_argument("--attrs", default="", help="Extra attributes (JSON string)")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
if not args.service_name:
|
||||
print("[Trace] skipped: no service specified (--service or TRACE_SERVICE)")
|
||||
sys.exit(0)
|
||||
|
||||
duration_ms = args.duration_ms
|
||||
if duration_ms <= 0 and args.start_time:
|
||||
try:
|
||||
start_ts = float(args.start_time)
|
||||
duration_ms = int((time.time() - start_ts) * 1000)
|
||||
except (ValueError, TypeError):
|
||||
duration_ms = 1000
|
||||
if duration_ms <= 0:
|
||||
duration_ms = 1000
|
||||
|
||||
extra_attrs = {}
|
||||
if args.attrs:
|
||||
try:
|
||||
extra_attrs = json.loads(args.attrs)
|
||||
except json.JSONDecodeError:
|
||||
pass
|
||||
|
||||
trace_name = args.trace_name
|
||||
if not trace_name:
|
||||
wf = _env("GITEA_WORKFLOW") or _env("GITHUB_WORKFLOW") or "CI"
|
||||
job = _env("GITEA_JOB") or _env("GITHUB_JOB") or "job"
|
||||
trace_name = f"{wf} / {job}"
|
||||
|
||||
success, msg = report_ci_trace(
|
||||
service_name=args.service_name,
|
||||
trace_name=trace_name,
|
||||
status=args.status,
|
||||
duration_ms=duration_ms,
|
||||
extra_attributes=extra_attrs,
|
||||
)
|
||||
|
||||
print(msg)
|
||||
sys.exit(0)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user