refactor: 整理脚本文件到 scripts 目录(任务 2.1/20)

This commit is contained in:
张翔
2026-04-12 15:16:41 +08:00
parent 337284166f
commit f6b9031cd7
28 changed files with 2486 additions and 239 deletions
+158
View File
@@ -0,0 +1,158 @@
#!/usr/bin/env python3
"""
Woodpecker CI 最佳实践对比分析
对比当前配置与 Woodpecker CI 官方最佳实践
"""
import yaml
from pathlib import Path
class BestPracticeAnalyzer:
"""最佳实践分析器"""
def __init__(self, config_path: str):
self.config_path = Path(config_path)
with open(self.config_path, 'r', encoding='utf-8') as f:
self.config = yaml.safe_load(f)
self.best_practices = {
"分支策略": {
"✅ 使用通配符": "支持 feature/**, release/** 等通配符",
"✅ 分层触发": "不同分支触发不同深度的测试",
"✅ 保护主分支": "main 分支只接收自动归档",
"⚠️ 缺少分支保护": "建议在 Git 仓库设置分支保护规则"
},
"测试策略": {
"✅ 分层测试": "feature(dev/smoke) < dev(standard) < release(full)",
"✅ 快速反馈": "feature 分支使用 smoke test 快速验证",
"✅ 质量门禁": "测试失败阻止合并/部署",
"✅ 覆盖率检查": "单元测试包含覆盖率检查"
},
"部署安全": {
"✅ 健康检查": "部署后执行健康检查",
"✅ 自动回滚": "健康检查失败自动回滚",
"✅ 备份机制": "部署前备份当前版本",
"✅ Secret 管理": "使用 Secret 管理敏感信息",
"✅ SSH 密钥": "使用 SSH 密钥进行 Git 操作"
},
"Docker 构建": {
"✅ 镜像标签": "使用 commit SHA 和 latest 标签",
"✅ Docker socket": "挂载 Docker socket",
"✅ 镜像推送": "推送到私有仓库",
"⚠️ 缺少镜像扫描": "建议添加容器安全扫描"
},
"归档策略": {
"✅ 自动归档": "部署成功后自动归档到 main",
"✅ 版本标签": "创建带时间戳的版本标签",
"✅ 动态分支": "支持任意 release/** 分支归档",
"✅ 重试机制": "推送失败自动重试 3 次"
},
"性能优化": {
"⚠️ 缺少缓存": "建议添加 npm 依赖缓存",
"⚠️ 缺少并行": "部分步骤可以并行执行",
"✅ 浅克隆": "使用 depth: 1 减少克隆时间"
},
"通知与监控": {
"⚠️ 缺少通知": "建议添加企业微信/钉钉通知",
"⚠️ 缺少监控": "建议集成 APM 监控",
"✅ 日志输出": "每个步骤都有清晰的日志"
},
"配置管理": {
"✅ YAML 锚点": "使用锚点复用配置",
"✅ 环境变量": "使用环境变量传递配置",
"✅ 注释清晰": "配置文件有详细的注释",
"✅ 结构清晰": "按阶段组织步骤"
}
}
def analyze(self):
"""执行分析"""
print("\n" + "="*70)
print("Woodpecker CI 最佳实践对比分析")
print("="*70)
for category, practices in self.best_practices.items():
print(f"\n📋 {category}")
print("-" * 70)
for practice, description in practices.items():
status = practice.split()[0]
desc = description
if status == "":
print(f" {practice}")
print(f" └─ {desc}")
elif status == "⚠️":
print(f" {practice}")
print(f" └─ {desc}")
elif status == "":
print(f" {practice}")
print(f" └─ {desc}")
print("\n" + "="*70)
print("改进建议优先级")
print("="*70)
recommendations = [
("高优先级", [
"添加 npm 依赖缓存,减少构建时间",
"配置 Git 分支保护规则",
"添加部署通知机制"
]),
("中优先级", [
"添加容器镜像安全扫描",
"集成 APM 性能监控",
"优化并行执行策略"
]),
("低优先级", [
"添加代码质量门禁(如 SonarQube)",
"实现蓝绿部署",
"添加多环境支持(staging"
])
]
for priority, items in recommendations:
print(f"\n🎯 {priority}")
for i, item in enumerate(items, 1):
print(f" {i}. {item}")
print("\n" + "="*70)
print("总体评分")
print("="*70)
total_practices = sum(len(practices) for practices in self.best_practices.values())
passed_practices = sum(
1 for practices in self.best_practices.values()
for practice in practices.keys()
if practice.startswith("")
)
warning_practices = sum(
1 for practices in self.best_practices.values()
for practice in practices.keys()
if practice.startswith("⚠️")
)
score = (passed_practices / total_practices) * 100
print(f"\n✅ 符合最佳实践: {passed_practices}/{total_practices}")
print(f"⚠️ 需要改进: {warning_practices}/{total_practices}")
print(f"📊 总体评分: {score:.1f}/100")
if score >= 80:
print("✅ 配置质量优秀")
elif score >= 60:
print("⚠️ 配置质量良好,但有改进空间")
else:
print("❌ 配置需要重大改进")
print("\n" + "="*70)
def main():
analyzer = BestPracticeAnalyzer(".woodpecker.yml")
analyzer.analyze()
if __name__ == "__main__":
main()
+55
View File
@@ -0,0 +1,55 @@
#!/bin/bash
echo "=== 捕获 Gitea Webhook Header ==="
echo ""
echo "1. 创建临时 webhook 接收器..."
cat > /tmp/capture-webhook.py << 'PYEOF'
#!/usr/bin/env python3
from http.server import HTTPServer, BaseHTTPRequestHandler
import json
import sys
class WebhookHandler(BaseHTTPRequestHandler):
def do_POST(self):
print("\n=== 收到 Webhook 请求 ===")
print(f"Path: {self.path}")
print("\nHeaders:")
for key, value in self.headers.items():
print(f" {key}: {value}")
content_length = int(self.headers.get('Content-Length', 0))
body = self.rfile.read(content_length)
print(f"\nBody (前 500 字符):")
print(body.decode('utf-8')[:500])
self.send_response(200)
self.send_header('Content-type', 'application/json')
self.end_headers()
self.wfile.write(b'{"status":"ok"}')
def log_message(self, format, *args):
pass
if __name__ == '__main__':
port = int(sys.argv[1]) if len(sys.argv) > 1 else 9999
server = HTTPServer(('0.0.0.0', port), WebhookHandler)
print(f"监听端口 {port}...")
server.handle_request()
PYEOF
chmod +x /tmp/capture-webhook.py
echo "2. 在服务器上启动 webhook 接收器..."
echo " 请在另一个终端运行:"
echo " ssh root@139.155.109.62 'python3 /tmp/capture-webhook.py 9999'"
echo ""
echo "3. 或者,让我们检查 Gitea 的 webhook 配置..."
echo ""
echo "检查 Gitea 容器中的 webhook 设置..."
docker exec forgejo ls -la /data/gitea/data/hooks/ 2>/dev/null || echo "没有找到 hooks 目录"
echo ""
echo "检查最近的 webhook 日志..."
docker logs forgejo --since 5m 2>&1 | grep -i webhook | tail -10
+33
View File
@@ -0,0 +1,33 @@
import jenkins.model.*
import org.jenkinsci.plugins.workflow.job.*
def jenkins = Jenkins.getInstance()
def job = jenkins.getItem('novalon-website')
if (job != null) {
println "Job found: ${job.fullName}"
println "Job class: ${job.class}"
def triggers = job.getTriggers()
println "Triggers: ${triggers}"
triggers.each { key, value ->
println "Trigger: ${key} -> ${value}"
}
def properties = job.getProperties()
println "Properties: ${properties}"
properties.each { prop ->
println "Property: ${prop.class}"
if (prop instanceof org.jenkinsci.plugins.workflow.job.properties.PipelineTriggersJobProperty) {
def pipelineTriggers = prop.getTriggers()
println "Pipeline Triggers: ${pipelineTriggers}"
pipelineTriggers.each { trigger ->
println "Pipeline Trigger: ${trigger.class} -> ${trigger}"
}
}
}
} else {
println "Job not found"
}
+37
View File
@@ -0,0 +1,37 @@
#!/bin/bash
# Woodpecker CI 日志诊断脚本
# 需要在 Woodpecker CI 服务器上执行
echo "=========================================="
echo "Woodpecker CI 日志诊断"
echo "=========================================="
echo ""
# 检查 Woodpecker 容器是否运行
echo "1. 检查 Woodpecker 容器状态..."
docker ps | grep woodpecker || echo "❌ Woodpecker 容器未运行"
echo ""
# 查看最近的日志
echo "2. 查看最近的 Woodpecker 日志 (最后 100 行)..."
docker logs woodpecker-server --tail 100 2>&1 | grep -E "(webhook|hook|pipeline|error|fail)" || echo "未找到相关日志"
echo ""
# 查看 Webhook 相关日志
echo "3. 查看 Webhook 处理日志..."
docker logs woodpecker-server --tail 200 2>&1 | grep -i "webhook" || echo "未找到 webhook 日志"
echo ""
# 查看仓库相关日志
echo "4. 查看仓库 novalon/novalon-website 相关日志..."
docker logs woodpecker-server --tail 200 2>&1 | grep -i "novalon-website" || echo "未找到仓库相关日志"
echo ""
# 查看错误日志
echo "5. 查看错误日志..."
docker logs woodpecker-server --tail 200 2>&1 | grep -iE "(error|fail|warn)" || echo "未找到错误日志"
echo ""
echo "=========================================="
echo "诊断完成"
echo "=========================================="
+73
View File
@@ -0,0 +1,73 @@
#!/bin/bash
# 修复Jenkins Nginx配置
cat > /tmp/jenkins-nginx-fix.conf << 'EOF'
# Jenkins CI/CD Server
server {
listen 80;
server_name ci.f.novalon.cn;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl http2;
server_name ci.f.novalon.cn;
ssl_certificate /etc/nginx/ssl/ci.f.novalon.cn/fullchain.pem;
ssl_certificate_key /etc/nginx/ssl/ci.f.novalon.cn/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-CHACHA20-POLY1305;
ssl_prefer_server_ciphers off;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 1d;
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
# Jenkins webhook端点 - 不需要/jenkins前缀
location /generic-webhook-trigger/ {
proxy_pass http://172.17.0.1:8080/generic-webhook-trigger/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Port $server_port;
client_max_body_size 100m;
proxy_connect_timeout 60s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
# Jenkins主应用
location /jenkins/ {
proxy_pass http://172.17.0.1:8080/jenkins/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Port $server_port;
client_max_body_size 100m;
proxy_connect_timeout 60s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
# 默认location - 重定向到/jenkins/
location / {
return 301 https://$host/jenkins/;
}
access_log /var/log/nginx/jenkins-access.log;
error_log /var/log/nginx/jenkins-error.log;
}
EOF
echo "Jenkins Nginx配置已生成"
+46
View File
@@ -0,0 +1,46 @@
#!/usr/bin/env python3
"""
Woodpecker CI 分支匹配测试
检查分支名称是否匹配配置中的规则
"""
import fnmatch
def match_branch(branch_pattern, actual_branch):
"""测试分支匹配"""
if branch_pattern == actual_branch:
return True
if branch_pattern.endswith("/**"):
prefix = branch_pattern[:-3]
return actual_branch.startswith(prefix + "/")
if "*" in branch_pattern:
return fnmatch.fnmatch(actual_branch, branch_pattern)
return False
# 测试当前分支
actual_branch = "release/v1.0.0"
patterns = ["release", "release/**", "release/*"]
print(f"实际分支: {actual_branch}")
print("\n匹配测试:")
for pattern in patterns:
result = match_branch(pattern, actual_branch)
print(f" {pattern:20} -> {'✅ 匹配' if result else '❌ 不匹配'}")
# 测试其他分支
test_branches = [
("feature/new-feature", ["feature/**", "feature/*"]),
("dev", ["dev"]),
("release", ["release", "release/**"]),
("release/v2.0.0", ["release", "release/**"]),
]
print("\n\n其他分支测试:")
for branch, patterns in test_branches:
print(f"\n分支: {branch}")
for pattern in patterns:
result = match_branch(pattern, branch)
print(f" {pattern:20} -> {'✅ 匹配' if result else '❌ 不匹配'}")
+230
View File
@@ -0,0 +1,230 @@
#!/usr/bin/env python3
"""
Woodpecker CI 场景测试
模拟不同分支场景下的 CI/CD 流程执行
"""
import yaml
from pathlib import Path
from typing import Dict, List, Set
class ScenarioTester:
"""场景测试器"""
def __init__(self, config_path: str):
self.config_path = Path(config_path)
with open(self.config_path, 'r', encoding='utf-8') as f:
self.config = yaml.safe_load(f)
self.scenarios = {
"场景1: feature分支开发": {
"branch": "feature/new-feature",
"event": "push",
"expected_steps": [
"lint", "type-check", "security-scan",
"unit-tests", "e2e-smoke"
],
"unexpected_steps": [
"e2e-standard", "e2e-deep", "build-image",
"deploy-production", "archive-to-main"
]
},
"场景2: feature分支PR": {
"branch": "feature/another-feature",
"event": "pull_request",
"expected_steps": [
"lint", "type-check", "security-scan",
"unit-tests", "e2e-smoke"
],
"unexpected_steps": [
"e2e-standard", "e2e-deep", "build-image",
"deploy-production", "archive-to-main"
]
},
"场景3: dev分支集成": {
"branch": "dev",
"event": "push",
"expected_steps": [
"lint", "type-check", "security-scan",
"unit-tests", "e2e-standard"
],
"unexpected_steps": [
"e2e-smoke", "e2e-deep", "build-image",
"deploy-production", "archive-to-main"
]
},
"场景4: release分支部署": {
"branch": "release/v1.0.0",
"event": "push",
"expected_steps": [
"lint", "type-check", "security-scan",
"unit-tests", "e2e-standard", "e2e-deep",
"e2e-performance", "e2e-accessibility", "e2e-visual",
"build-image", "deploy-production", "archive-to-main"
],
"unexpected_steps": ["e2e-smoke"]
},
"场景5: release主分支部署": {
"branch": "release",
"event": "push",
"expected_steps": [
"lint", "type-check", "security-scan",
"unit-tests", "e2e-standard", "e2e-deep",
"e2e-performance", "e2e-accessibility", "e2e-visual",
"build-image", "deploy-production", "archive-to-main"
],
"unexpected_steps": ["e2e-smoke"]
},
"场景6: main分支只读": {
"branch": "main",
"event": "push",
"expected_steps": [],
"unexpected_steps": [
"lint", "type-check", "security-scan",
"unit-tests", "build-image", "deploy-production"
]
}
}
def match_branch(self, pattern: str, branch: str) -> bool:
"""匹配分支模式"""
if pattern == branch:
return True
if pattern.endswith("/**"):
prefix = pattern[:-3]
return branch.startswith(prefix + "/")
return False
def get_triggered_steps(self, branch: str, event: str) -> Set[str]:
"""获取触发的步骤"""
triggered = set()
for step_name, step_config in self.config.get('steps', {}).items():
if not isinstance(step_config, dict):
continue
when_config = step_config.get('when', {})
if not when_config:
triggered.add(step_name)
continue
event_match = False
branch_match = False
if isinstance(when_config, list):
for condition in when_config:
if isinstance(condition, dict):
if 'event' in condition:
if event in condition['event']:
event_match = True
if 'branch' in condition:
for pattern in condition['branch']:
if self.match_branch(pattern, branch):
branch_match = True
break
elif isinstance(when_config, dict):
if 'event' in when_config:
if event in when_config['event']:
event_match = True
if 'branch' in when_config:
for pattern in when_config['branch']:
if self.match_branch(pattern, branch):
branch_match = True
break
if event_match and branch_match:
triggered.add(step_name)
return triggered
def run_scenario(self, scenario_name: str, scenario: Dict) -> Dict:
"""运行单个场景"""
branch = scenario['branch']
event = scenario['event']
expected = set(scenario['expected_steps'])
unexpected = set(scenario['unexpected_steps'])
triggered = self.get_triggered_steps(branch, event)
missing = expected - triggered
extra = triggered & unexpected
passed = len(missing) == 0 and len(extra) == 0
return {
"scenario": scenario_name,
"branch": branch,
"event": event,
"passed": passed,
"triggered": triggered,
"expected": expected,
"missing": missing,
"extra": extra
}
def run_all_scenarios(self):
"""运行所有场景"""
print("\n" + "="*70)
print("Woodpecker CI 场景测试")
print("="*70)
results = []
for scenario_name, scenario in self.scenarios.items():
result = self.run_scenario(scenario_name, scenario)
results.append(result)
print(f"\n📋 {scenario_name}")
print(f" 分支: {result['branch']}")
print(f" 事件: {result['event']}")
if result['passed']:
print(f" ✅ 测试通过")
else:
print(f" ❌ 测试失败")
print(f" 触发步骤 ({len(result['triggered'])}): {', '.join(sorted(result['triggered']))}")
if result['missing']:
print(f" ⚠️ 缺少步骤: {', '.join(sorted(result['missing']))}")
if result['extra']:
print(f" ⚠️ 多余步骤: {', '.join(sorted(result['extra']))}")
print("\n" + "="*70)
print("测试总结")
print("="*70)
passed_count = sum(1 for r in results if r['passed'])
total_count = len(results)
print(f"\n✅ 通过: {passed_count}/{total_count}")
print(f"❌ 失败: {total_count - passed_count}/{total_count}")
if passed_count == total_count:
print("\n✅ 所有场景测试通过!")
else:
print("\n❌ 部分场景测试失败,请检查配置")
print("\n" + "="*70)
return results
def main():
tester = ScenarioTester(".woodpecker.yml")
results = tester.run_all_scenarios()
failed = [r for r in results if not r['passed']]
if failed:
print("\n失败的场景:")
for result in failed:
print(f" - {result['scenario']}")
exit(1)
else:
exit(0)
if __name__ == "__main__":
main()
+33
View File
@@ -0,0 +1,33 @@
#!/bin/bash
echo "=== 测试 Webhook Header 传递 ==="
echo ""
echo "1. 检查 Forgejo 发送的 Webhook Header..."
echo ""
echo "2. 创建测试 Webhook 接收器..."
cat > /tmp/test-webhook.sh << 'EOF'
#!/bin/bash
echo "Received webhook at $(date)"
echo "Headers:"
for header in "$@"; do
echo " $header"
done
echo "Body:"
cat
echo ""
EOF
chmod +x /tmp/test-webhook.sh
echo "3. 测试 Nginx 配置..."
docker exec novalon-nginx nginx -T 2>&1 | grep -A 20 "location /api/" | head -25
echo ""
echo "4. 检查最近的 webhook 请求..."
docker logs woodpecker-server 2>&1 | grep "POST /api/hook" | tail -3
echo ""
echo "5. 检查 webhook 解析日志..."
docker logs woodpecker-server 2>&1 | grep "unsupported hook type" | tail -3
+50
View File
@@ -0,0 +1,50 @@
#!/bin/bash
# 测试企业微信通知脚本(修复版)
WEBHOOK_URL="https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=bb7efcdc-c32f-47b7-a437-d76cab9fba74"
# 模拟 CI 环境变量
STATUS="failure"
BRANCH="release/v1.0.0"
COMMIT="test456"
MESSAGE="fix: 修复husky和企业微信通知问题
- 在 commands 中添加 export HUSKY=0 确保 husky 被禁用
- 修复 MESSAGE_ESCAPED 处理逻辑,避免 shell 解析错误
- 将换行符替换为空格,而不是 \\n 字符串"
AUTHOR="zhangxiang"
PIPELINE_NUMBER="12"
PIPELINE_URL="https://ci.f.novalon.cn/repos/1/pipeline/12"
if [ "$STATUS" = "success" ]; then
STATUS_TEXT="成功"
STATUS_COLOR="info"
else
STATUS_TEXT="失败"
STATUS_COLOR="warning"
fi
TIMESTAMP=$(date "+%Y-%m-%d %H:%M:%S")
# 转义特殊字符(简化处理)
MESSAGE_ESCAPED=$(echo "$MESSAGE" | sed 's/\\/\\\\/g' | sed 's/"/\\"/g' | tr '\n' ' ')
echo "发送企业微信通知..."
echo "状态: $STATUS_TEXT"
echo "分支: $BRANCH"
echo "提交: $COMMIT"
echo "作者: $AUTHOR"
echo "MESSAGE_ESCAPED: $MESSAGE_ESCAPED"
curl -X POST "$WEBHOOK_URL" \
-H 'Content-Type: application/json' \
-d "{
\"msgtype\": \"markdown\",
\"markdown\": {
\"content\": \"## 🚀 Novalon Website 部署通知\\n\\n> **构建状态**: <font color=\\\"${STATUS_COLOR}\\\">${STATUS_TEXT}</font>\\n\\n**项目信息**\\n> 分支: \`${BRANCH}\`\\n> 提交: \`${COMMIT}\`\\n> 作者: ${AUTHOR}\\n\\n**提交信息**\\n> ${MESSAGE_ESCAPED}\\n\\n**操作**\\n> [查看构建详情](${PIPELINE_URL})\\n\\n---\\n> 时间: ${TIMESTAMP}\\n> Pipeline #${PIPELINE_NUMBER}\"
}
}"
echo ""
echo "通知发送完成!"
+63
View File
@@ -0,0 +1,63 @@
#!/bin/bash
# 测试企业微信通知脚本(使用 heredoc)
WEBHOOK_URL="https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=bb7efcdc-c32f-47b7-a437-d76cab9fba74"
# 模拟 CI 环境变量
STATUS="failure"
BRANCH="release/v1.0.0"
COMMIT="test456"
MESSAGE="fix: 使用--ignore-scripts跳过husky并修复企业微信通知
- 使用 npm ci --omit=dev --ignore-scripts 跳过所有脚本
- 本地验证 husky 问题已解决
- 本地验证企业微信通知成功
- 将换行符替换为空格,避免 shell 解析错误"
AUTHOR="zhangxiang"
PIPELINE_NUMBER="13"
REPO_ID="1"
PIPELINE_URL="https://ci.f.novalon.cn/repos/${REPO_ID}/pipeline/${PIPELINE_NUMBER}"
if [ "$STATUS" = "success" ]; then
STATUS_TEXT="成功"
STATUS_COLOR="info"
else
STATUS_TEXT="失败"
STATUS_COLOR="warning"
fi
TIMESTAMP=$(date "+%Y-%m-%d %H:%M:%S")
# 简化处理:移除换行符和引号
MESSAGE_CLEAN=$(echo "$MESSAGE" | tr '\n' ' ' | tr '"' "'")
echo "发送企业微信通知..."
echo "状态: $STATUS_TEXT"
echo "分支: $BRANCH"
echo "提交: $COMMIT"
echo "作者: $AUTHOR"
echo "MESSAGE_CLEAN: $MESSAGE_CLEAN"
echo ""
# 使用 heredoc 构建 JSON,避免复杂的转义
JSON_DATA=$(cat <<EOF
{
"msgtype": "markdown",
"markdown": {
"content": "## 🚀 Novalon Website 部署通知\n\n> **构建状态**: <font color=\"${STATUS_COLOR}\">${STATUS_TEXT}</font>\n\n**项目信息**\n> 分支: \`${BRANCH}\`\n> 提交: \`${COMMIT}\`\n> 作者: ${AUTHOR}\n\n**提交信息**\n> ${MESSAGE_CLEAN}\n\n**操作**\n> [查看构建详情](${PIPELINE_URL})\n\n---\n> 时间: ${TIMESTAMP}\n> Pipeline #${PIPELINE_NUMBER}"
}
}
EOF
)
echo "JSON_DATA:"
echo "$JSON_DATA"
echo ""
curl -X POST "$WEBHOOK_URL" \
-H 'Content-Type: application/json' \
-d "$JSON_DATA"
echo ""
echo "通知发送完成!"
+48
View File
@@ -0,0 +1,48 @@
#!/bin/bash
# 测试企业微信通知脚本(使用 jq
WEBHOOK_URL="https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=bb7efcdc-c32f-47b7-a437-d76cab9fba74"
# 模拟 CI 环境变量
STATUS="failure"
BRANCH="release/v1.0.0"
COMMIT="testjq"
MESSAGE="fix: 使用jq构建JSON避免YAML多行字符串问题
- 使用 jq 来构建 JSON
- 避免 YAML 多行字符串处理问题
- 确保变量正确展开"
AUTHOR="zhangxiang"
PIPELINE_NUMBER="18"
REPO_ID="1"
PIPELINE_URL="https://ci.f.novalon.cn/repos/${REPO_ID}/pipeline/${PIPELINE_NUMBER}"
TIMESTAMP=$(date "+%Y-%m-%d %H:%M:%S")
# 简化处理:移除换行符和引号
MESSAGE_CLEAN=$(echo "$MESSAGE" | tr '\n' ' ' | tr '"' "'")
echo "发送企业微信通知..."
echo "状态: $STATUS"
echo "分支: $BRANCH"
echo "提交: $COMMIT"
echo "作者: $AUTHOR"
echo "MESSAGE_CLEAN: $MESSAGE_CLEAN"
echo ""
# 使用 jq 构建 JSON
CONTENT="## 🚀 Novalon Website 部署通知\n\n> **构建状态**: <font color=\"warning\">失败</font>\n\n**项目信息**\n> 分支: \`${BRANCH}\`\n> 提交: \`${COMMIT}\`\n> 作者: ${AUTHOR}\n\n**提交信息**\n> ${MESSAGE_CLEAN}\n\n**操作**\n> [查看构建详情](${PIPELINE_URL})\n\n---\n> 时间: ${TIMESTAMP}\n> Pipeline #${PIPELINE_NUMBER}"
JSON_DATA=$(echo "{}" | jq --arg content "$CONTENT" '.msgtype = "markdown" | .markdown.content = $content')
echo "JSON_DATA:"
echo "$JSON_DATA"
echo ""
curl -X POST "$WEBHOOK_URL" \
-H 'Content-Type: application/json' \
-d "$JSON_DATA"
echo ""
echo "通知发送完成!"
+51
View File
@@ -0,0 +1,51 @@
#!/bin/bash
# 测试企业微信通知脚本(使用 printf)
WEBHOOK_URL="https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=bb7efcdc-c32f-47b7-a437-d76cab9fba74"
# 模拟 CI 环境变量
STATUS="failure"
BRANCH="release/v1.0.0"
COMMIT="testprintf"
MESSAGE="fix: 使用printf构建JSON避免变量展开问题
- 使用 printf 来构建 JSON
- 避免单引号和双引号组合的问题
- 确保变量正确展开"
AUTHOR="zhangxiang"
PIPELINE_NUMBER="17"
REPO_ID="1"
PIPELINE_URL="https://ci.f.novalon.cn/repos/${REPO_ID}/pipeline/${PIPELINE_NUMBER}"
TIMESTAMP=$(date "+%Y-%m-%d %H:%M:%S")
# 简化处理:移除换行符和引号
MESSAGE_CLEAN=$(echo "$MESSAGE" | tr '\n' ' ' | tr '"' "'")
echo "发送企业微信通知..."
echo "状态: $STATUS"
echo "分支: $BRANCH"
echo "提交: $COMMIT"
echo "作者: $AUTHOR"
echo "MESSAGE_CLEAN: $MESSAGE_CLEAN"
echo ""
# 使用 printf 构建 JSON
JSON_DATA=$(printf '{
"msgtype": "markdown",
"markdown": {
"content": "## 🚀 Novalon Website 部署通知\\n\\n> **构建状态**: <font color=\\"warning\\">失败</font>\\n\\n**项目信息**\\n> 分支: `%s`\\n> 提交: `%s`\\n> 作者: %s\\n\\n**提交信息**\\n> %s\\n\\n**操作**\\n> [查看构建详情](%s)\\n\\n---\\n> 时间: %s\\n> Pipeline #%s"
}
}' "$BRANCH" "$COMMIT" "$AUTHOR" "$MESSAGE_CLEAN" "$PIPELINE_URL" "$TIMESTAMP" "$PIPELINE_NUMBER")
echo "JSON_DATA:"
echo "$JSON_DATA"
echo ""
curl -X POST "$WEBHOOK_URL" \
-H 'Content-Type: application/json' \
-d "$JSON_DATA"
echo ""
echo "通知发送完成!"
+45
View File
@@ -0,0 +1,45 @@
#!/bin/bash
# 测试企业微信通知脚本(使用单引号和双引号组合)
WEBHOOK_URL="https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=bb7efcdc-c32f-47b7-a437-d76cab9fba74"
# 模拟 CI 环境变量
STATUS="failure"
BRANCH="release/v1.0.0"
COMMIT="test789"
MESSAGE="fix: 使用单引号和双引号组合避免heredoc问题
- 移除 heredoc 语法
- 使用单引号和双引号组合来构建 JSON
- 确保变量正确展开"
AUTHOR="zhangxiang"
PIPELINE_NUMBER="16"
REPO_ID="1"
PIPELINE_URL="https://ci.f.novalon.cn/repos/${REPO_ID}/pipeline/${PIPELINE_NUMBER}"
TIMESTAMP=$(date "+%Y-%m-%d %H:%M:%S")
# 简化处理:移除换行符和引号
MESSAGE_CLEAN=$(echo "$MESSAGE" | tr '\n' ' ' | tr '"' "'")
echo "发送企业微信通知..."
echo "状态: $STATUS"
echo "分支: $BRANCH"
echo "提交: $COMMIT"
echo "作者: $AUTHOR"
echo "MESSAGE_CLEAN: $MESSAGE_CLEAN"
echo ""
# 使用单引号和双引号组合,确保变量正确展开
curl -X POST "$WEBHOOK_URL" \
-H 'Content-Type: application/json' \
-d '{
"msgtype": "markdown",
"markdown": {
"content": "## 🚀 Novalon Website 部署通知\n\n> **构建状态**: <font color=\"warning\">失败</font>\n\n**项目信息**\n> 分支: `'"${BRANCH}"'`\n> 提交: `'"${COMMIT}"'`\n> 作者: '"${AUTHOR}"'\n\n**提交信息**\n> '"${MESSAGE_CLEAN}"'\n\n**操作**\n> [查看构建详情]('"${PIPELINE_URL}"')\n\n---\n> 时间: '"${TIMESTAMP}"'\n> Pipeline #'"${PIPELINE_NUMBER}"'"
}
}'
echo ""
echo "通知发送完成!"
+274
View File
@@ -0,0 +1,274 @@
#!/usr/bin/env python3
"""
Woodpecker CI/CD 配置验证脚本
系统性地测试和验收 .woodpecker.yml 配置
"""
import yaml
import sys
from pathlib import Path
from typing import Dict, List, Any
class WoodpeckerValidator:
"""Woodpecker CI 配置验证器"""
def __init__(self, config_path: str):
self.config_path = Path(config_path)
self.config = None
self.errors = []
self.warnings = []
self.info = []
def load_config(self) -> bool:
"""加载配置文件"""
try:
with open(self.config_path, 'r', encoding='utf-8') as f:
self.config = yaml.safe_load(f)
self.info.append("✅ 配置文件加载成功")
return True
except Exception as e:
self.errors.append(f"❌ 配置文件加载失败: {e}")
return False
def validate_structure(self) -> bool:
"""验证配置结构"""
required_keys = ['steps', 'services', 'workspace', 'clone']
for key in required_keys:
if key not in self.config:
self.errors.append(f"❌ 缺少必需的配置项: {key}")
else:
self.info.append(f"✅ 找到配置项: {key}")
return len(self.errors) == 0
def validate_branch_triggers(self) -> Dict[str, List[str]]:
"""验证分支触发条件"""
branch_mapping = {
'feature': ['feature/**'],
'dev': ['dev'],
'release': ['release', 'release/**'],
'main': [] # main 不应该触发任何步骤
}
step_branches = {}
for step_name, step_config in self.config.get('steps', {}).items():
if not isinstance(step_config, dict):
continue
when_config = step_config.get('when', {})
if not when_config:
self.warnings.append(f"⚠️ 步骤 '{step_name}' 没有触发条件")
continue
if isinstance(when_config, list):
branches = []
for condition in when_config:
if isinstance(condition, dict) and 'branch' in condition:
branches.extend(condition['branch'])
elif isinstance(when_config, dict):
branches = when_config.get('branch', [])
else:
branches = []
if branches:
step_branches[step_name] = branches
self.info.append(f"✅ 步骤 '{step_name}' 触发分支: {branches}")
return step_branches
def validate_test_strategy(self) -> Dict[str, List[str]]:
"""验证测试策略分层"""
expected_tests = {
'feature/**': ['lint', 'type-check', 'security-scan', 'unit-tests', 'e2e-smoke'],
'dev': ['lint', 'type-check', 'security-scan', 'unit-tests', 'e2e-standard'],
'release': ['lint', 'type-check', 'security-scan', 'unit-tests', 'e2e-standard',
'e2e-deep', 'e2e-performance', 'e2e-accessibility', 'e2e-visual',
'build-image', 'deploy-production', 'archive-to-main']
}
test_coverage = {}
for branch, expected_steps in expected_tests.items():
test_coverage[branch] = []
for step_name in expected_steps:
if step_name in self.config.get('steps', {}):
test_coverage[branch].append(step_name)
self.info.append(f"✅ 分支 '{branch}' 包含步骤: {step_name}")
else:
self.errors.append(f"❌ 分支 '{branch}' 缺少步骤: {step_name}")
return test_coverage
def validate_archive_logic(self) -> bool:
"""验证归档逻辑"""
archive_step = self.config.get('steps', {}).get('archive-to-main', {})
if not archive_step:
self.errors.append("❌ 缺少 archive-to-main 步骤")
return False
commands = archive_step.get('commands', [])
has_dynamic_branch = False
for cmd in commands:
if isinstance(cmd, str) and 'CURRENT_BRANCH="${CI_COMMIT_BRANCH}"' in cmd:
has_dynamic_branch = True
self.info.append("✅ 归档步骤使用动态分支变量")
break
if not has_dynamic_branch:
self.warnings.append("⚠️ 归档步骤可能未使用动态分支变量")
when_config = archive_step.get('when', {})
branches = when_config.get('branch', [])
if 'release' in branches and 'release/**' in branches:
self.info.append("✅ 归档步骤支持 release 和 release/** 分支")
else:
self.errors.append("❌ 归档步骤分支配置不完整")
return len(self.errors) == 0
def validate_deployment_safety(self) -> bool:
"""验证部署安全性"""
deploy_step = self.config.get('steps', {}).get('deploy-production', {})
if not deploy_step:
self.errors.append("❌ 缺少 deploy-production 步骤")
return False
commands = deploy_step.get('commands', [])
has_rollback = False
has_health_check = False
for cmd in commands:
if isinstance(cmd, str):
if 'rolling back' in cmd.lower() or 'rollback' in cmd.lower():
has_rollback = True
self.info.append("✅ 部署步骤包含回滚机制")
if 'health check' in cmd.lower():
has_health_check = True
self.info.append("✅ 部署步骤包含健康检查")
if not has_rollback:
self.warnings.append("⚠️ 部署步骤可能缺少回滚机制")
if not has_health_check:
self.warnings.append("⚠️ 部署步骤可能缺少健康检查")
secrets = deploy_step.get('environment', {})
if 'SSH_PRIVATE_KEY' in secrets and 'REGISTRY_PASSWORD' in secrets:
self.info.append("✅ 部署步骤使用 Secret 管理敏感信息")
else:
self.errors.append("❌ 部署步骤未正确配置 Secrets")
return len(self.errors) == 0
def validate_docker_build(self) -> bool:
"""验证 Docker 构建配置"""
build_step = self.config.get('steps', {}).get('build-image', {})
if not build_step:
self.errors.append("❌ 缺少 build-image 步骤")
return False
commands = build_step.get('commands', [])
has_tagging = False
for cmd in commands:
if isinstance(cmd, str) and 'docker tag' in cmd:
has_tagging = True
self.info.append("✅ Docker 构建步骤包含镜像标签")
break
if not has_tagging:
self.warnings.append("⚠️ Docker 构建步骤可能缺少镜像标签")
volumes = build_step.get('volumes', [])
if any('/var/run/docker.sock' in str(v) for v in volumes):
self.info.append("✅ Docker 构建步骤挂载了 Docker socket")
else:
self.warnings.append("⚠️ Docker 构建步骤可能未挂载 Docker socket")
return len(self.errors) == 0
def validate_services(self) -> bool:
"""验证服务配置"""
services = self.config.get('services', {})
if 'docker' not in services:
self.warnings.append("⚠️ 缺少 Docker 服务配置")
else:
self.info.append("✅ Docker 服务配置正确")
return True
def generate_report(self) -> str:
"""生成测试报告"""
report = []
report.append("\n" + "="*60)
report.append("Woodpecker CI/CD 配置验证报告")
report.append("="*60)
report.append(f"\n📋 配置文件: {self.config_path}")
report.append(f"📊 总步骤数: {len(self.config.get('steps', {}))}")
report.append("\n✅ 通过的检查:")
for msg in self.info:
report.append(f" {msg}")
report.append("\n⚠️ 警告:")
for msg in self.warnings:
report.append(f" {msg}")
report.append("\n❌ 错误:")
for msg in self.errors:
report.append(f" {msg}")
report.append("\n" + "="*60)
if self.errors:
report.append("❌ 验证失败 - 发现 {} 个错误".format(len(self.errors)))
return "\n".join(report)
else:
report.append("✅ 验证通过 - 配置文件符合要求")
return "\n".join(report)
def main():
"""主函数"""
config_path = ".woodpecker.yml"
if not Path(config_path).exists():
print(f"❌ 配置文件不存在: {config_path}")
sys.exit(1)
validator = WoodpeckerValidator(config_path)
print("🔍 开始验证 Woodpecker CI/CD 配置...")
if not validator.load_config():
print(validator.generate_report())
sys.exit(1)
validator.validate_structure()
validator.validate_branch_triggers()
validator.validate_test_strategy()
validator.validate_archive_logic()
validator.validate_deployment_safety()
validator.validate_docker_build()
validator.validate_services()
print(validator.generate_report())
if validator.errors:
sys.exit(1)
else:
sys.exit(0)
if __name__ == "__main__":
main()
+86
View File
@@ -0,0 +1,86 @@
#!/bin/bash
# 修复Jenkins Nginx配置 - 更新webhook路径
# 在服务器上执行此脚本
# 1. 备份当前配置
docker cp novalon-nginx-secure:/etc/nginx/nginx.conf /tmp/nginx.conf.bak
# 2. 创建新的Jenkins配置
cat > /tmp/jenkins-server.conf << 'EOF'
# Jenkins CI/CD Server
server {
listen 80;
server_name ci.f.novalon.cn;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl http2;
server_name ci.f.novalon.cn;
ssl_certificate /etc/nginx/ssl/ci.f.novalon.cn/fullchain.pem;
ssl_certificate_key /etc/nginx/ssl/ci.f.novalon.cn/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-CHACHA20-POLY1305;
ssl_prefer_server_ciphers off;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 1d;
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
# Jenkins webhook端点 - 直接代理到Jenkins根路径
location /generic-webhook-trigger/ {
proxy_pass http://172.17.0.1:8080/generic-webhook-trigger/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
client_max_body_size 100m;
proxy_connect_timeout 60s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
# Jenkins主应用
location /jenkins/ {
proxy_pass http://172.17.0.1:8080/jenkins/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
client_max_body_size 100m;
proxy_connect_timeout 60s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
# 默认location - 重定向到/jenkins/
location / {
return 301 https://$host/jenkins/;
}
access_log /var/log/nginx/jenkins-access.log;
error_log /var/log/nginx/jenkins-error.log;
}
EOF
# 3. 替换Jenkins配置部分
sed -i '/# Jenkins CI\/CD Server/,/^ }$/d' /tmp/nginx.conf.bak
sed -i "/^}/i $(cat /tmp/jenkins-server.conf)" /tmp/nginx.conf.bak
# 4. 复制回容器并重载
docker cp /tmp/nginx.conf.bak novalon-nginx-secure:/etc/nginx/nginx.conf
docker exec novalon-nginx-secure nginx -t && docker exec novalon-nginx-secure nginx -s reload
echo "Jenkins Nginx配置已更新"