Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0eb1262db6 | |||
| 63f138e2d6 | |||
| d898856f50 | |||
| acffa364b3 | |||
| 4805e961f0 | |||
| 1aeeb23621 | |||
| 4a21b89a0b | |||
| 2982ef5259 | |||
| 5fff7e518d | |||
| a93149b8e5 |
+32
-12
@@ -151,6 +151,7 @@ jobs:
|
||||
shell: sh
|
||||
run: "set +eu\nif [ -n \"$JOB_START_TIME\" ]; then\n END_TIME=$(date +%s)\n DURATION=$((END_TIME - JOB_START_TIME))\n MINS=$((DURATION / 60))\n SECS=$((DURATION % 60))\n echo \"JOB_DURATION_SECONDS=$DURATION\" >> $GITHUB_ENV\n echo \"=== Job Duration: ${MINS}m${SECS}s ===\"\nelse\n echo \"JOB_DURATION_SECONDS=0\" >> $GITHUB_ENV\n echo \"=== Job Duration: unknown ===\"\nfi\n"
|
||||
- name: Notify on failure
|
||||
continue-on-error: true
|
||||
if: failure()
|
||||
shell: sh
|
||||
env:
|
||||
@@ -221,6 +222,7 @@ jobs:
|
||||
shell: sh
|
||||
run: "set +eu\nif [ -n \"$JOB_START_TIME\" ]; then\n END_TIME=$(date +%s)\n DURATION=$((END_TIME - JOB_START_TIME))\n MINS=$((DURATION / 60))\n SECS=$((DURATION % 60))\n echo \"JOB_DURATION_SECONDS=$DURATION\" >> $GITHUB_ENV\n echo \"=== Job Duration: ${MINS}m${SECS}s ===\"\nelse\n echo \"JOB_DURATION_SECONDS=0\" >> $GITHUB_ENV\n echo \"=== Job Duration: unknown ===\"\nfi\n"
|
||||
- name: Notify on failure
|
||||
continue-on-error: true
|
||||
if: failure()
|
||||
shell: sh
|
||||
env:
|
||||
@@ -233,7 +235,7 @@ jobs:
|
||||
integration-tests:
|
||||
name: Integration Tests
|
||||
runs-on: host
|
||||
timeout-minutes: 20
|
||||
timeout-minutes: 30
|
||||
if: always()
|
||||
needs: validate
|
||||
env:
|
||||
@@ -337,6 +339,7 @@ jobs:
|
||||
shell: sh
|
||||
run: "set +eu\nif [ -n \"$JOB_START_TIME\" ]; then\n END_TIME=$(date +%s)\n DURATION=$((END_TIME - JOB_START_TIME))\n MINS=$((DURATION / 60))\n SECS=$((DURATION % 60))\n echo \"JOB_DURATION_SECONDS=$DURATION\" >> $GITHUB_ENV\n echo \"=== Job Duration: ${MINS}m${SECS}s ===\"\nelse\n echo \"JOB_DURATION_SECONDS=0\" >> $GITHUB_ENV\n echo \"=== Job Duration: unknown ===\"\nfi\n"
|
||||
- name: Notify on failure
|
||||
continue-on-error: true
|
||||
if: failure()
|
||||
shell: sh
|
||||
env:
|
||||
@@ -368,7 +371,7 @@ jobs:
|
||||
'
|
||||
- name: Install dependencies
|
||||
shell: sh
|
||||
run: "set -eu\nNPM_CACHE_VOLUME=\"xiaoxia-npm-cache\"\nif ! docker volume inspect \"$NPM_CACHE_VOLUME\" >/dev/null 2>&1; then\n docker volume create \"$NPM_CACHE_VOLUME\" >/dev/null\n echo \"Created npm cache volume: $NPM_CACHE_VOLUME\"\nfi\n\ndocker run --rm \\\n -v \"$PWD:/workspace\" \\\n -v \"$NPM_CACHE_VOLUME:/workspace/apps/web/node_modules\" -w /workspace/apps/web \\\n docker.m.daocloud.io/library/node:20 \\\n sh -lc 'npm ci'\n"
|
||||
run: "set -eu\nNPM_CACHE_VOLUME=\"xiaoxia-npm-cache\"\nif ! docker volume inspect \"$NPM_CACHE_VOLUME\" >/dev/null 2>&1; then\n docker volume create \"$NPM_CACHE_VOLUME\" >/dev/null\n echo \"Created npm cache volume: $NPM_CACHE_VOLUME\"\nfi\n\ndocker run --rm \\\n -v \"$PWD:/workspace\" \\\n -v \"$NPM_CACHE_VOLUME:/workspace/apps/web/node_modules\" -w /workspace/apps/web \\\n docker.m.daocloud.io/library/node:20 \\\n sh -lc 'rm -rf node_modules/* 2>/dev/null; npm ci --include=dev'\n"
|
||||
- name: Run ESLint
|
||||
shell: sh
|
||||
run: "set -eu\nNPM_CACHE_VOLUME=\"xiaoxia-npm-cache\"\nif ! docker volume inspect \"$NPM_CACHE_VOLUME\" >/dev/null 2>&1; then\n docker volume create \"$NPM_CACHE_VOLUME\" >/dev/null\n echo \"Created npm cache volume: $NPM_CACHE_VOLUME\"\nfi\n\ndocker run --rm \\\n -v \"$PWD:/workspace\" \\\n -v \"$NPM_CACHE_VOLUME:/workspace/apps/web/node_modules\" -w /workspace/apps/web \\\n docker.m.daocloud.io/library/node:20 \\\n sh -lc 'npx eslint src --ext .ts,.tsx --max-warnings 50'\n"
|
||||
@@ -380,12 +383,13 @@ jobs:
|
||||
run: "set -eu\nNPM_CACHE_VOLUME=\"xiaoxia-npm-cache\"\nif ! docker volume inspect \"$NPM_CACHE_VOLUME\" >/dev/null 2>&1; then\n docker volume create \"$NPM_CACHE_VOLUME\" >/dev/null\n echo \"Created npm cache volume: $NPM_CACHE_VOLUME\"\nfi\n\ndocker run --rm \\\n -v \"$PWD:/workspace\" \\\n -v \"$NPM_CACHE_VOLUME:/workspace/apps/web/node_modules\" -w /workspace/apps/web \\\n docker.m.daocloud.io/library/node:20 \\\n sh -lc 'npx prettier --check \"src/**/*.{ts,tsx,md}\"'\n"
|
||||
- name: Run Vitest tests
|
||||
shell: sh
|
||||
run: "set -eu\nNPM_CACHE_VOLUME=\"xiaoxia-npm-cache\"\nif ! docker volume inspect \"$NPM_CACHE_VOLUME\" >/dev/null 2>&1; then\n docker volume create \"$NPM_CACHE_VOLUME\" >/dev/null\n echo \"Created npm cache volume: $NPM_CACHE_VOLUME\"\nfi\n\ndocker run --rm \\\n -v \"$PWD:/workspace\" \\\n -v \"$NPM_CACHE_VOLUME:/workspace/apps/web/node_modules\" -w /workspace/apps/web \\\n docker.m.daocloud.io/library/node:20 \\\n sh -lc 'npx vitest run src/test'\n"
|
||||
run: "set -eu\nNPM_CACHE_VOLUME=\"xiaoxia-npm-cache\"\nif ! docker volume inspect \"$NPM_CACHE_VOLUME\" >/dev/null 2>&1; then\n docker volume create \"$NPM_CACHE_VOLUME\" >/dev/null\n echo \"Created npm cache volume: $NPM_CACHE_VOLUME\"\nfi\n\ndocker run --rm \\\n -v \"$PWD:/workspace\" \\\n -v \"$NPM_CACHE_VOLUME:/workspace/apps/web/node_modules\" -w /workspace/apps/web \\\n docker.m.daocloud.io/library/node:20 \\\n sh -lc 'ls node_modules/.bin/vitest 2>/dev/null && node_modules/.bin/vitest run src/test || npm exec vitest run src/test'\n"
|
||||
- name: Job duration summary
|
||||
if: always()
|
||||
shell: sh
|
||||
run: "set +eu\nif [ -n \"$JOB_START_TIME\" ]; then\n END_TIME=$(date +%s)\n DURATION=$((END_TIME - JOB_START_TIME))\n MINS=$((DURATION / 60))\n SECS=$((DURATION % 60))\n echo \"JOB_DURATION_SECONDS=$DURATION\" >> $GITHUB_ENV\n echo \"=== Job Duration: ${MINS}m${SECS}s ===\"\nelse\n echo \"JOB_DURATION_SECONDS=0\" >> $GITHUB_ENV\n echo \"=== Job Duration: unknown ===\"\nfi\n"
|
||||
- name: Notify on failure
|
||||
continue-on-error: true
|
||||
if: failure()
|
||||
shell: sh
|
||||
env:
|
||||
@@ -400,7 +404,7 @@ jobs:
|
||||
runs-on:
|
||||
- saas
|
||||
- build-farm
|
||||
timeout-minutes: 20
|
||||
timeout-minutes: 30
|
||||
needs:
|
||||
- validate
|
||||
- frontend-lint
|
||||
@@ -447,6 +451,7 @@ jobs:
|
||||
shell: sh
|
||||
run: "set +eu\nif [ -n \"$JOB_START_TIME\" ]; then\n END_TIME=$(date +%s)\n DURATION=$((END_TIME - JOB_START_TIME))\n MINS=$((DURATION / 60))\n SECS=$((DURATION % 60))\n echo \"JOB_DURATION_SECONDS=$DURATION\" >> $GITHUB_ENV\n echo \"=== Job Duration: ${MINS}m${SECS}s ===\"\nelse\n echo \"JOB_DURATION_SECONDS=0\" >> $GITHUB_ENV\n echo \"=== Job Duration: unknown ===\"\nfi\n"
|
||||
- name: Notify on failure
|
||||
continue-on-error: true
|
||||
if: failure()
|
||||
shell: sh
|
||||
env:
|
||||
@@ -461,7 +466,7 @@ jobs:
|
||||
runs-on:
|
||||
- saas
|
||||
- build-farm
|
||||
timeout-minutes: 20
|
||||
timeout-minutes: 40
|
||||
needs:
|
||||
- validate
|
||||
- frontend-lint
|
||||
@@ -501,13 +506,14 @@ jobs:
|
||||
run: "set -eu\n# 确保使用 docker-container driver 以支持 cache export 功能\nif ! docker buildx inspect ci-builder > /dev/null 2>&1; then\n docker buildx create --use --name ci-builder --driver docker-container\n echo \"Created ci-builder (docker-container driver)\"\nelse\n docker buildx use ci-builder\n echo \"Using existing ci-builder\"\nfi\ndocker buildx inspect --bootstrap\n"
|
||||
- name: Build and push Worker image (buildx cache)
|
||||
shell: sh
|
||||
run: "set -eu\nREGISTRY=\"git.xiaoxiajianji.com/xiaoxia/xiaoxia-saas\"\nIMAGE_NAME=\"xiaoxia-saas-worker\"\nCACHE_REF=\"${REGISTRY}/worker-cache:develop\"\n\nCACHE_FROM=\"type=registry,ref=${CACHE_REF},ignore-error=true\"\n\nif [ \"${CACHE_MODE}\" = \"read-write\" ]; then\n CACHE_TO=\"type=registry,ref=${CACHE_REF},mode=max\"\n echo \"Building Worker image with read-write cache...\"\n docker buildx build --build-arg APP_VERSION=\"${GITHUB_SHA}\" --cache-from \"${CACHE_FROM}\" --cache-to \"${CACHE_TO}\" -f infra/docker/worker.Dockerfile -t \"${REGISTRY}/${IMAGE_NAME}:${GITHUB_SHA}\" --push .\nelse\n echo \"Building Worker image with read-only cache...\"\n docker buildx build --build-arg APP_VERSION=\"${GITHUB_SHA}\" --cache-from \"${CACHE_FROM}\" -f infra/docker/worker.Dockerfile -t \"${REGISTRY}/${IMAGE_NAME}:${GITHUB_SHA}\" --push \
|
||||
run: "set -eu\nREGISTRY=\"git.xiaoxiajianji.com/xiaoxia/xiaoxia-saas\"\nIMAGE_NAME=\"xiaoxia-saas-worker\"\nCACHE_REF=\"${REGISTRY}/worker-cache:develop\"\n\nCACHE_FROM=\"type=registry,ref=${CACHE_REF},ignore-error=true\"\n\nif [ \"${CACHE_MODE}\" = \"read-write\" ]; then\n CACHE_TO=\"type=registry,ref=${CACHE_REF},mode=min\"\n echo \"Building Worker image with read-write cache...\"\n docker buildx build --build-arg APP_VERSION=\"${GITHUB_SHA}\" --cache-from \"${CACHE_FROM}\" --cache-to \"${CACHE_TO}\" -f infra/docker/worker.Dockerfile -t \"${REGISTRY}/${IMAGE_NAME}:${GITHUB_SHA}\" --push .\nelse\n echo \"Building Worker image with read-only cache...\"\n docker buildx build --build-arg APP_VERSION=\"${GITHUB_SHA}\" --cache-from \"${CACHE_FROM}\" -f infra/docker/worker.Dockerfile -t \"${REGISTRY}/${IMAGE_NAME}:${GITHUB_SHA}\" --push \
|
||||
\ .\nfi\necho \"Worker image pushed: ${REGISTRY}/${IMAGE_NAME}:${GITHUB_SHA}\"\n"
|
||||
- name: Job duration summary
|
||||
if: always()
|
||||
shell: sh
|
||||
run: "set +eu\nif [ -n \"$JOB_START_TIME\" ]; then\n END_TIME=$(date +%s)\n DURATION=$((END_TIME - JOB_START_TIME))\n MINS=$((DURATION / 60))\n SECS=$((DURATION % 60))\n echo \"JOB_DURATION_SECONDS=$DURATION\" >> $GITHUB_ENV\n echo \"=== Job Duration: ${MINS}m${SECS}s ===\"\nelse\n echo \"JOB_DURATION_SECONDS=0\" >> $GITHUB_ENV\n echo \"=== Job Duration: unknown ===\"\nfi\n"
|
||||
- name: Notify on failure
|
||||
continue-on-error: true
|
||||
if: failure()
|
||||
shell: sh
|
||||
env:
|
||||
@@ -522,7 +528,7 @@ jobs:
|
||||
runs-on:
|
||||
- saas
|
||||
- build-farm
|
||||
timeout-minutes: 20
|
||||
timeout-minutes: 30
|
||||
needs:
|
||||
- validate
|
||||
- frontend-lint
|
||||
@@ -572,6 +578,7 @@ jobs:
|
||||
shell: sh
|
||||
run: "set +eu\nif [ -n \"$JOB_START_TIME\" ]; then\n END_TIME=$(date +%s)\n DURATION=$((END_TIME - JOB_START_TIME))\n MINS=$((DURATION / 60))\n SECS=$((DURATION % 60))\n echo \"JOB_DURATION_SECONDS=$DURATION\" >> $GITHUB_ENV\n echo \"=== Job Duration: ${MINS}m${SECS}s ===\"\nelse\n echo \"JOB_DURATION_SECONDS=0\" >> $GITHUB_ENV\n echo \"=== Job Duration: unknown ===\"\nfi\n"
|
||||
- name: Notify on failure
|
||||
continue-on-error: true
|
||||
if: failure()
|
||||
shell: sh
|
||||
env:
|
||||
@@ -607,6 +614,7 @@ jobs:
|
||||
|
||||
'
|
||||
- name: Notify job start
|
||||
continue-on-error: true
|
||||
shell: sh
|
||||
env:
|
||||
CI_NOTIFY_WEBHOOK: ${{ secrets.CI_NOTIFY_WEBHOOK }}
|
||||
@@ -638,6 +646,7 @@ jobs:
|
||||
shell: sh
|
||||
run: "set +eu\nif [ -n \"$JOB_START_TIME\" ]; then\n END_TIME=$(date +%s)\n DURATION=$((END_TIME - JOB_START_TIME))\n MINS=$((DURATION / 60))\n SECS=$((DURATION % 60))\n echo \"JOB_DURATION_SECONDS=$DURATION\" >> $GITHUB_ENV\n echo \"=== Job Duration: ${MINS}m${SECS}s ===\"\nelse\n echo \"JOB_DURATION_SECONDS=0\" >> $GITHUB_ENV\n echo \"=== Job Duration: unknown ===\"\nfi\n"
|
||||
- name: Notify on success
|
||||
continue-on-error: true
|
||||
if: success()
|
||||
shell: sh
|
||||
env:
|
||||
@@ -648,6 +657,7 @@ jobs:
|
||||
|
||||
'
|
||||
- name: Notify on failure
|
||||
continue-on-error: true
|
||||
if: failure()
|
||||
shell: sh
|
||||
env:
|
||||
@@ -687,6 +697,7 @@ jobs:
|
||||
shell: sh
|
||||
run: "set +eu\nif [ -n \"$JOB_START_TIME\" ]; then\n END_TIME=$(date +%s)\n DURATION=$((END_TIME - JOB_START_TIME))\n MINS=$((DURATION / 60))\n SECS=$((DURATION % 60))\n echo \"JOB_DURATION_SECONDS=$DURATION\" >> $GITHUB_ENV\n echo \"=== Job Duration: ${MINS}m${SECS}s ===\"\nelse\n echo \"JOB_DURATION_SECONDS=0\" >> $GITHUB_ENV\n echo \"=== Job Duration: unknown ===\"\nfi\n"
|
||||
- name: Notify on failure
|
||||
continue-on-error: true
|
||||
if: failure()
|
||||
shell: sh
|
||||
env:
|
||||
@@ -726,6 +737,7 @@ jobs:
|
||||
shell: sh
|
||||
run: "set +eu\nif [ -n \"$JOB_START_TIME\" ]; then\n END_TIME=$(date +%s)\n DURATION=$((END_TIME - JOB_START_TIME))\n MINS=$((DURATION / 60))\n SECS=$((DURATION % 60))\n echo \"JOB_DURATION_SECONDS=$DURATION\" >> $GITHUB_ENV\n echo \"=== Job Duration: ${MINS}m${SECS}s ===\"\nelse\n echo \"JOB_DURATION_SECONDS=0\" >> $GITHUB_ENV\n echo \"=== Job Duration: unknown ===\"\nfi\n"
|
||||
- name: Notify on failure
|
||||
continue-on-error: true
|
||||
if: failure()
|
||||
shell: sh
|
||||
env:
|
||||
@@ -740,7 +752,7 @@ jobs:
|
||||
runs-on:
|
||||
- saas
|
||||
- build-farm
|
||||
timeout-minutes: 20
|
||||
timeout-minutes: 30
|
||||
needs:
|
||||
- validate
|
||||
- frontend-lint
|
||||
@@ -800,6 +812,7 @@ jobs:
|
||||
shell: sh
|
||||
run: "set +eu\nif [ -n \"$JOB_START_TIME\" ]; then\n END_TIME=$(date +%s)\n DURATION=$((END_TIME - JOB_START_TIME))\n MINS=$((DURATION / 60))\n SECS=$((DURATION % 60))\n echo \"JOB_DURATION_SECONDS=$DURATION\" >> $GITHUB_ENV\n echo \"=== Job Duration: ${MINS}m${SECS}s ===\"\nelse\n echo \"JOB_DURATION_SECONDS=0\" >> $GITHUB_ENV\n echo \"=== Job Duration: unknown ===\"\nfi\n"
|
||||
- name: Notify on failure
|
||||
continue-on-error: true
|
||||
if: failure()
|
||||
shell: sh
|
||||
env:
|
||||
@@ -814,7 +827,7 @@ jobs:
|
||||
runs-on:
|
||||
- saas
|
||||
- build-farm
|
||||
timeout-minutes: 20
|
||||
timeout-minutes: 40
|
||||
needs:
|
||||
- validate
|
||||
- frontend-lint
|
||||
@@ -864,7 +877,7 @@ jobs:
|
||||
|
||||
echo "Building Production Worker image: ${VERSION}"
|
||||
|
||||
docker buildx build --build-arg APP_VERSION="${VERSION}" --cache-from "type=registry,ref=${CACHE_REF},ignore-error=true" --cache-to "type=registry,ref=${CACHE_REF},mode=max" -f infra/docker/worker.Dockerfile -t "${REGISTRY}/${IMAGE_NAME}:${VERSION}" --push .
|
||||
docker buildx build --build-arg APP_VERSION="${VERSION}" --cache-from "type=registry,ref=${CACHE_REF},ignore-error=true" --cache-to "type=registry,ref=${CACHE_REF},mode=min" -f infra/docker/worker.Dockerfile -t "${REGISTRY}/${IMAGE_NAME}:${VERSION}" --push .
|
||||
|
||||
echo "Production Worker image pushed: ${REGISTRY}/${IMAGE_NAME}:${VERSION}"
|
||||
|
||||
@@ -874,6 +887,7 @@ jobs:
|
||||
shell: sh
|
||||
run: "set +eu\nif [ -n \"$JOB_START_TIME\" ]; then\n END_TIME=$(date +%s)\n DURATION=$((END_TIME - JOB_START_TIME))\n MINS=$((DURATION / 60))\n SECS=$((DURATION % 60))\n echo \"JOB_DURATION_SECONDS=$DURATION\" >> $GITHUB_ENV\n echo \"=== Job Duration: ${MINS}m${SECS}s ===\"\nelse\n echo \"JOB_DURATION_SECONDS=0\" >> $GITHUB_ENV\n echo \"=== Job Duration: unknown ===\"\nfi\n"
|
||||
- name: Notify on failure
|
||||
continue-on-error: true
|
||||
if: failure()
|
||||
shell: sh
|
||||
env:
|
||||
@@ -888,7 +902,7 @@ jobs:
|
||||
runs-on:
|
||||
- saas
|
||||
- build-farm
|
||||
timeout-minutes: 20
|
||||
timeout-minutes: 30
|
||||
needs:
|
||||
- validate
|
||||
- frontend-lint
|
||||
@@ -957,6 +971,7 @@ jobs:
|
||||
shell: sh
|
||||
run: "set +eu\nif [ -n \"$JOB_START_TIME\" ]; then\n END_TIME=$(date +%s)\n DURATION=$((END_TIME - JOB_START_TIME))\n MINS=$((DURATION / 60))\n SECS=$((DURATION % 60))\n echo \"JOB_DURATION_SECONDS=$DURATION\" >> $GITHUB_ENV\n echo \"=== Job Duration: ${MINS}m${SECS}s ===\"\nelse\n echo \"JOB_DURATION_SECONDS=0\" >> $GITHUB_ENV\n echo \"=== Job Duration: unknown ===\"\nfi\n"
|
||||
- name: Notify on failure
|
||||
continue-on-error: true
|
||||
if: failure()
|
||||
shell: sh
|
||||
env:
|
||||
@@ -969,7 +984,7 @@ jobs:
|
||||
deploy-production:
|
||||
name: Deploy Production
|
||||
runs-on: saas
|
||||
timeout-minutes: 20
|
||||
timeout-minutes: 30
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
needs:
|
||||
- build-production-api
|
||||
@@ -992,6 +1007,7 @@ jobs:
|
||||
|
||||
'
|
||||
- name: Notify job start
|
||||
continue-on-error: true
|
||||
shell: sh
|
||||
env:
|
||||
CI_NOTIFY_WEBHOOK: ${{ secrets.CI_NOTIFY_WEBHOOK }}
|
||||
@@ -1029,6 +1045,7 @@ jobs:
|
||||
shell: sh
|
||||
run: "set +eu\nif [ -n \"$JOB_START_TIME\" ]; then\n END_TIME=$(date +%s)\n DURATION=$((END_TIME - JOB_START_TIME))\n MINS=$((DURATION / 60))\n SECS=$((DURATION % 60))\n echo \"JOB_DURATION_SECONDS=$DURATION\" >> $GITHUB_ENV\n echo \"=== Job Duration: ${MINS}m${SECS}s ===\"\nelse\n echo \"JOB_DURATION_SECONDS=0\" >> $GITHUB_ENV\n echo \"=== Job Duration: unknown ===\"\nfi\n"
|
||||
- name: Notify on success
|
||||
continue-on-error: true
|
||||
if: success()
|
||||
shell: sh
|
||||
env:
|
||||
@@ -1039,6 +1056,7 @@ jobs:
|
||||
|
||||
'
|
||||
- name: Notify on failure
|
||||
continue-on-error: true
|
||||
if: failure()
|
||||
shell: sh
|
||||
env:
|
||||
@@ -1078,6 +1096,7 @@ jobs:
|
||||
shell: sh
|
||||
run: "set +eu\nif [ -n \"$JOB_START_TIME\" ]; then\n END_TIME=$(date +%s)\n DURATION=$((END_TIME - JOB_START_TIME))\n MINS=$((DURATION / 60))\n SECS=$((DURATION % 60))\n echo \"JOB_DURATION_SECONDS=$DURATION\" >> $GITHUB_ENV\n echo \"=== Job Duration: ${MINS}m${SECS}s ===\"\nelse\n echo \"JOB_DURATION_SECONDS=0\" >> $GITHUB_ENV\n echo \"=== Job Duration: unknown ===\"\nfi\n"
|
||||
- name: Notify on failure
|
||||
continue-on-error: true
|
||||
if: failure()
|
||||
shell: sh
|
||||
env:
|
||||
@@ -1087,3 +1106,4 @@ jobs:
|
||||
NOTIFY_MODE=failure JOB_NAME="Production Browser E2E" python3 scripts/ci_notify.py
|
||||
|
||||
'
|
||||
|
||||
|
||||
+56
-27
@@ -2,7 +2,7 @@
|
||||
"""
|
||||
统一CI通知脚本 - 发送飞书卡片通知
|
||||
支持三种模式: start / success / failure
|
||||
包含: PR链接、耗时、失败阶段、分支、提交者、Run链接
|
||||
包含: PR链接、耗时、失败阶段、分支、提交者、Run链接、Runner信息
|
||||
|
||||
用法:
|
||||
NOTIFY_MODE=start JOB_NAME="xxx" python3 scripts/ci_notify.py
|
||||
@@ -23,6 +23,12 @@
|
||||
GITHUB_EVENT_NAME - 事件类型 (pull_request / push / ...)
|
||||
GITHUB_PR_NUMBER - PR编号 (PR事件时)
|
||||
GITHUB_PR_TITLE - PR标题 (PR事件时)
|
||||
RUNNER_NAME - Runner名称 (可选,自动获取)
|
||||
|
||||
设计原则:
|
||||
1. 通知失败永远不阻断CI主流程(返回exit code 0)
|
||||
2. 标题包含"CI通知"/"CI告警"关键词,适配飞书webhook关键词校验
|
||||
3. 卡片信息尽量丰富,方便快速定位问题
|
||||
"""
|
||||
|
||||
import json
|
||||
@@ -32,6 +38,7 @@ import urllib.request
|
||||
|
||||
|
||||
def get_env(name, default=""):
|
||||
"""读取环境变量"""
|
||||
return os.environ.get(name, default)
|
||||
|
||||
|
||||
@@ -48,6 +55,20 @@ def format_duration(seconds_str):
|
||||
return seconds_str or "未知"
|
||||
|
||||
|
||||
def classify_job(job_name):
|
||||
"""根据Job名称判断所属阶段"""
|
||||
name = job_name.lower()
|
||||
if any(k in name for k in ["validate", "lint", "unit test", "integration test"]):
|
||||
return "门禁检查"
|
||||
if any(k in name for k in ["build", "image"]):
|
||||
return "镜像构建"
|
||||
if any(k in name for k in ["deploy", "staging", "production"]):
|
||||
return "部署发布"
|
||||
if any(k in name for k in ["e2e", "test", "smoke"]):
|
||||
return "测试验证"
|
||||
return "其他"
|
||||
|
||||
|
||||
def main() -> int:
|
||||
webhook = get_env("CI_NOTIFY_WEBHOOK")
|
||||
if not webhook:
|
||||
@@ -71,49 +92,60 @@ def main() -> int:
|
||||
event_name = get_env("GITHUB_EVENT_NAME", "")
|
||||
pr_number = get_env("GITHUB_PR_NUMBER", "")
|
||||
pr_title = get_env("GITHUB_PR_TITLE", "")
|
||||
runner_name = get_env("RUNNER_NAME", "")
|
||||
|
||||
run_url = f"https://git.xiaoxiajianji.com/{repo}/actions/runs/{run_id}"
|
||||
job_stage = classify_job(job_name)
|
||||
|
||||
# 根据模式设置标题、状态、颜色
|
||||
# 注意:标题中必须包含飞书webhook配置的关键词,否则会报"Key Words Not Found"
|
||||
# 这里加入"CI通知"/"CI告警"关键词提高命中率
|
||||
if mode == "start":
|
||||
title = "🔄 CI 任务开始"
|
||||
title = f"🔄 CI通知:{job_name} 开始构建"
|
||||
status = "blue"
|
||||
button_text = "查看进度"
|
||||
button_type = "primary"
|
||||
elif mode == "success":
|
||||
title = f"✅ {job_name} 成功"
|
||||
title = f"✅ CI通知:{job_name} 构建成功"
|
||||
status = "green"
|
||||
button_text = "查看详情"
|
||||
button_type = "primary"
|
||||
else: # failure
|
||||
title = f"❌ {job_name} 失败"
|
||||
title = f"❌ CI告警:{job_name} 构建失败"
|
||||
status = "red"
|
||||
button_text = "查看失败日志"
|
||||
button_type = "danger"
|
||||
|
||||
# 构建卡片内容
|
||||
content_lines = []
|
||||
content_lines.append(f"**任务**: {job_name}")
|
||||
# 构建卡片内容 - 左侧标签+右侧值的结构化展示
|
||||
fields = []
|
||||
|
||||
fields.append({"is_short": True, "text": {"tag": "lark_md", "content": f"**阶段**\n{job_stage}"}})
|
||||
fields.append({"is_short": True, "text": {"tag": "lark_md", "content": f"**任务**\n{job_name}"}})
|
||||
|
||||
if mode != "start":
|
||||
content_lines.append(f"**耗时**: {duration}")
|
||||
fields.append({"is_short": True, "text": {"tag": "lark_md", "content": f"**耗时**\n{duration}"}})
|
||||
else:
|
||||
fields.append({"is_short": True, "text": {"tag": "lark_md", "content": f"**状态**\n进行中"}})
|
||||
|
||||
if runner_name:
|
||||
fields.append({"is_short": True, "text": {"tag": "lark_md", "content": f"**Runner**\n{runner_name}"}})
|
||||
|
||||
if mode == "failure" and failed_step:
|
||||
content_lines.append(f"**失败阶段**: {failed_step}")
|
||||
fields.append({"is_short": False, "text": {"tag": "lark_md", "content": f"**失败步骤**\n{failed_step}"}})
|
||||
|
||||
# PR信息
|
||||
# PR/分支信息
|
||||
if event_name == "pull_request" and pr_number:
|
||||
pr_url = f"https://git.xiaoxiajianji.com/{repo}/pulls/{pr_number}"
|
||||
pr_display = f"#{pr_number}"
|
||||
if pr_title:
|
||||
pr_display += f" {pr_title}"
|
||||
content_lines.append(f"**PR**: [{pr_display}]({pr_url})")
|
||||
pr_display += f" {pr_title[:30]}"
|
||||
fields.append({"is_short": False, "text": {"tag": "lark_md", "content": f"**PR**\n[{pr_display}]({pr_url})"}})
|
||||
elif event_name == "push":
|
||||
content_lines.append(f"**分支**: {branch}")
|
||||
fields.append({"is_short": True, "text": {"tag": "lark_md", "content": f"**分支**\n{branch}"}})
|
||||
|
||||
content_lines.append(f"**提交**: `{commit}`")
|
||||
content_lines.append(f"**提交者**: {actor}")
|
||||
content_lines.append(f"**Run ID**: {run_id}")
|
||||
fields.append({"is_short": True, "text": {"tag": "lark_md", "content": f"**提交**\n`{commit}`"}})
|
||||
fields.append({"is_short": True, "text": {"tag": "lark_md", "content": f"**提交者**\n{actor}"}})
|
||||
fields.append({"is_short": True, "text": {"tag": "lark_md", "content": f"**Run ID**\n{run_id}"}})
|
||||
|
||||
payload = {
|
||||
"msg_type": "interactive",
|
||||
@@ -128,10 +160,7 @@ def main() -> int:
|
||||
"elements": [
|
||||
{
|
||||
"tag": "div",
|
||||
"text": {
|
||||
"tag": "lark_md",
|
||||
"content": "\n".join(content_lines),
|
||||
},
|
||||
"fields": fields,
|
||||
},
|
||||
{
|
||||
"tag": "action",
|
||||
@@ -158,20 +187,20 @@ def main() -> int:
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=10) as resp:
|
||||
resp_body = resp.read().decode("utf-8")
|
||||
print(f"通知已发送 ({mode})")
|
||||
# 飞书返回code=0表示成功
|
||||
try:
|
||||
result = json.loads(resp_body)
|
||||
if result.get("code", 0) != 0:
|
||||
print(f"飞书返回错误: {result.get('msg', resp_body)}", file=sys.stderr)
|
||||
return 1
|
||||
print(f"通知发送告警: 飞书返回错误 - {result.get('msg', resp_body)}", file=sys.stderr)
|
||||
print(f"通知已发送 ({mode}) - 飞书返回非0,但不阻断CI流程")
|
||||
else:
|
||||
print(f"通知已发送 ({mode})")
|
||||
except json.JSONDecodeError:
|
||||
pass
|
||||
print(f"通知已发送 ({mode})")
|
||||
except Exception as e:
|
||||
print(f"通知发送失败: {e}", file=sys.stderr)
|
||||
# 通知失败不阻断CI主流程,仅打印告警
|
||||
return 0
|
||||
print(f"通知发送告警: {e}", file=sys.stderr)
|
||||
|
||||
# 通知无论成功失败都不阻断CI主流程,统一返回0
|
||||
return 0
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user