Compare commits

..

2 Commits

Author SHA1 Message Date
xiaoxia 1692ae135a fix(ci): 飞书通知消息添加CI构建关键词以通过关键词校验
CI/CD Pipeline / Frontend Lint (pull_request) Failing after 1m49s
CI/CD Pipeline / Production Browser E2E (pull_request) Failing after 1532h24m10s
CI/CD Pipeline / Staging E2E Tests (pull_request) Failing after 1532h24m10s
CI/CD Pipeline / Deploy Production (pull_request) Failing after 1532h24m11s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Failing after 1532h24m11s
CI/CD Pipeline / Build Production API Image (pull_request) Failing after 1532h24m13s
CI/CD Pipeline / Build Production Web Image (pull_request) Failing after 1532h24m13s
CI/CD Pipeline / Build Staging Web Image (pull_request) Failing after 1532h24m13s
CI/CD Pipeline / Build Staging API Image (pull_request) Failing after 1532h24m13s
CI/CD Pipeline / Validate Code Quality And Tests (pull_request) Has been skipped
CI/CD Pipeline / Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (pull_request) Failing after 1532h55m49s
CI/CD Pipeline / Build Production Worker Image (pull_request) Failing after 1532h55m52s
CI/CD Pipeline / Build Staging Worker Image (pull_request) Failing after 1532h55m52s
2026-07-14 19:03:32 +08:00
xiaoxia 3dc7a0a0bb fix(ci): 飞书通知失败不阻塞部署流程 - 添加continue-on-error 2026-07-14 19:03:21 +08:00
2 changed files with 38 additions and 68 deletions
+12 -12
View File
@@ -235,7 +235,7 @@ jobs:
integration-tests:
name: Integration Tests
runs-on: host
timeout-minutes: 30
timeout-minutes: 20
if: always()
needs: validate
env:
@@ -371,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 'rm -rf node_modules/* 2>/dev/null; npm ci --include=dev'\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 'npm ci'\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"
@@ -383,7 +383,7 @@ 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 'ls node_modules/.bin/vitest 2>/dev/null && node_modules/.bin/vitest run src/test || npm exec 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 'npx vitest run src/test'\n"
- name: Job duration summary
if: always()
shell: sh
@@ -404,7 +404,7 @@ jobs:
runs-on:
- saas
- build-farm
timeout-minutes: 30
timeout-minutes: 20
needs:
- validate
- frontend-lint
@@ -466,7 +466,7 @@ jobs:
runs-on:
- saas
- build-farm
timeout-minutes: 40
timeout-minutes: 20
needs:
- validate
- frontend-lint
@@ -506,7 +506,7 @@ 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=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 \
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 \
\ .\nfi\necho \"Worker image pushed: ${REGISTRY}/${IMAGE_NAME}:${GITHUB_SHA}\"\n"
- name: Job duration summary
if: always()
@@ -528,7 +528,7 @@ jobs:
runs-on:
- saas
- build-farm
timeout-minutes: 30
timeout-minutes: 20
needs:
- validate
- frontend-lint
@@ -752,7 +752,7 @@ jobs:
runs-on:
- saas
- build-farm
timeout-minutes: 30
timeout-minutes: 20
needs:
- validate
- frontend-lint
@@ -827,7 +827,7 @@ jobs:
runs-on:
- saas
- build-farm
timeout-minutes: 40
timeout-minutes: 20
needs:
- validate
- frontend-lint
@@ -877,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=min" -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=max" -f infra/docker/worker.Dockerfile -t "${REGISTRY}/${IMAGE_NAME}:${VERSION}" --push .
echo "Production Worker image pushed: ${REGISTRY}/${IMAGE_NAME}:${VERSION}"
@@ -902,7 +902,7 @@ jobs:
runs-on:
- saas
- build-farm
timeout-minutes: 30
timeout-minutes: 20
needs:
- validate
- frontend-lint
@@ -984,7 +984,7 @@ jobs:
deploy-production:
name: Deploy Production
runs-on: saas
timeout-minutes: 30
timeout-minutes: 20
if: startsWith(github.ref, 'refs/tags/v')
needs:
- build-production-api
+26 -56
View File
@@ -2,7 +2,7 @@
"""
统一CI通知脚本 - 发送飞书卡片通知
支持三种模式: start / success / failure
包含: PR链接、耗时、失败阶段、分支、提交者、Run链接、Runner信息
包含: PR链接、耗时、失败阶段、分支、提交者、Run链接
用法:
NOTIFY_MODE=start JOB_NAME="xxx" python3 scripts/ci_notify.py
@@ -23,12 +23,6 @@
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
@@ -38,7 +32,6 @@ import urllib.request
def get_env(name, default=""):
"""读取环境变量"""
return os.environ.get(name, default)
@@ -55,20 +48,6 @@ 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:
@@ -92,60 +71,49 @@ 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 = f"🔄 CI通知:{job_name} 开始构建"
title = "🔄 CI构建任务开始"
status = "blue"
button_text = "查看进度"
button_type = "primary"
elif mode == "success":
title = f"✅ CI通知:{job_name} 构建成功"
title = f"✅ CI构建成功 - {job_name}"
status = "green"
button_text = "查看详情"
button_type = "primary"
else: # failure
title = f"❌ CI告警:{job_name} 构建失败"
title = f"❌ CI构建失败 - {job_name}"
status = "red"
button_text = "查看失败日志"
button_type = "danger"
# 构建卡片内容 - 左侧标签+右侧值的结构化展示
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}"}})
# 构建卡片内容(含关键词"CI构建"以通过飞书机器人关键词校验)
content_lines = []
content_lines.append(f"**CI构建任务**: {job_name}")
if mode != "start":
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}"}})
content_lines.append(f"**耗时**: {duration}")
if mode == "failure" and failed_step:
fields.append({"is_short": False, "text": {"tag": "lark_md", "content": f"**失败步骤**\n{failed_step}"}})
content_lines.append(f"**失败阶段**: {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[:30]}"
fields.append({"is_short": False, "text": {"tag": "lark_md", "content": f"**PR**\n[{pr_display}]({pr_url})"}})
pr_display += f" {pr_title}"
content_lines.append(f"**PR**: [{pr_display}]({pr_url})")
elif event_name == "push":
fields.append({"is_short": True, "text": {"tag": "lark_md", "content": f"**分支**\n{branch}"}})
content_lines.append(f"**分支**: {branch}")
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}"}})
content_lines.append(f"**提交**: `{commit}`")
content_lines.append(f"**提交者**: {actor}")
content_lines.append(f"**Run ID**: {run_id}")
payload = {
"msg_type": "interactive",
@@ -160,7 +128,10 @@ def main() -> int:
"elements": [
{
"tag": "div",
"fields": fields,
"text": {
"tag": "lark_md",
"content": "\n".join(content_lines),
},
},
{
"tag": "action",
@@ -187,20 +158,19 @@ 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)
print(f"通知已发送 ({mode}) - 飞书返回非0,但不阻断CI流程")
else:
print(f"通知已发送 ({mode})")
print(f"飞书返回错误: {result.get('msg', resp_body)}", file=sys.stderr)
return 1
except json.JSONDecodeError:
print(f"通知已发送 ({mode})")
pass
except Exception as e:
print(f"通知发送告警: {e}", file=sys.stderr)
print(f"通知发送失败: {e}", file=sys.stderr)
return 1
# 通知无论成功失败都不阻断CI主流程,统一返回0
return 0