diff --git a/.gitea/workflows/ci-pipeline.yml b/.gitea/workflows/ci-pipeline.yml index 041f2652e..c834c285a 100755 --- a/.gitea/workflows/ci-pipeline.yml +++ b/.gitea/workflows/ci-pipeline.yml @@ -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 + diff --git a/.gitea/workflows/ci-trigger-monitor.yml b/.gitea/workflows/ci-trigger-monitor.yml index 69234b47a..eac6df81d 100644 --- a/.gitea/workflows/ci-trigger-monitor.yml +++ b/.gitea/workflows/ci-trigger-monitor.yml @@ -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 + diff --git a/.gitea/workflows/code-review.yml b/.gitea/workflows/code-review.yml index 23f3c1eb0..fcc59cdad 100644 --- a/.gitea/workflows/code-review.yml +++ b/.gitea/workflows/code-review.yml @@ -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 + diff --git a/.gitea/workflows/daily-check.yml b/.gitea/workflows/daily-check.yml index 209ae1283..b352213db 100644 --- a/.gitea/workflows/daily-check.yml +++ b/.gitea/workflows/daily-check.yml @@ -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 + diff --git a/.gitea/workflows/pr-automation.yml b/.gitea/workflows/pr-automation.yml index 9f263b8e7..64ecbfa7b 100755 --- a/.gitea/workflows/pr-automation.yml +++ b/.gitea/workflows/pr-automation.yml @@ -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 + diff --git a/.gitea/workflows/preview-cleanup.yml b/.gitea/workflows/preview-cleanup.yml index c40c1df15..a1977ab08 100755 --- a/.gitea/workflows/preview-cleanup.yml +++ b/.gitea/workflows/preview-cleanup.yml @@ -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 + diff --git a/.gitea/workflows/preview-deploy.yml b/.gitea/workflows/preview-deploy.yml index ca5230268..e5ad2f0c0 100755 --- a/.gitea/workflows/preview-deploy.yml +++ b/.gitea/workflows/preview-deploy.yml @@ -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 + diff --git a/scripts/ci/ci_trace_report.py b/scripts/ci/ci_trace_report.py new file mode 100755 index 000000000..eb53f1422 --- /dev/null +++ b/scripts/ci/ci_trace_report.py @@ -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()