mcp-dbutils
by donghao1393
- .github
- workflows
name: Quality Assurance
# 添加明确的权限声明
permissions:
contents: read
pull-requests: write # 允许创建PR评论
issues: write # 允许创建issue评论
checks: write # 允许更新检查状态
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
jobs:
pytest:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:15-alpine
env:
POSTGRES_PASSWORD: postgres
ports:
- 5432:5432
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
steps:
- uses: actions/checkout@v4
- name: Install uv
uses: astral-sh/setup-uv@v4
- name: Set up Python
run: uv python install
- name: Create and activate venv
run: |
uv venv
. .venv/bin/activate
- name: Install dependencies
run: uv pip install -e ".[test]"
- name: Run tests with coverage
id: tests
run: |
uv run pytest \
-v \
--cov=src/mcp_dbutils \
--cov-report=html \
--cov-report=term-missing \
--cov-report=json:coverage.json \
--cov-report=xml:coverage.xml \
tests/
- name: Upload coverage report
uses: actions/upload-artifact@v4
with:
name: coverage-report
path: coverage.xml
- name: Calculate coverage percentage
id: calc_coverage
run: |
COVERAGE=$(jq -r '.totals.percent_covered' coverage.json)
echo "Coverage: $COVERAGE"
echo "percentage=${COVERAGE%.*}" >> $GITHUB_OUTPUT
if (( $(echo "$COVERAGE >= 90" | bc -l) )); then
echo "color=green" >> $GITHUB_OUTPUT
elif (( $(echo "$COVERAGE >= 80" | bc -l) )); then
echo "color=yellow" >> $GITHUB_OUTPUT
else
echo "color=red" >> $GITHUB_OUTPUT
fi
- name: Create Coverage Badge
uses: schneegans/dynamic-badges-action@v1.7.0
with:
auth: ${{ secrets.GIST_SECRET }}
gistID: bdd0a63ec2a816539ff8c136ceb41e48
filename: coverage.json
label: "coverage"
message: "${{ steps.calc_coverage.outputs.percentage }}%"
color: "${{ steps.calc_coverage.outputs.color }}"
namedLogo: python
sonarcloud:
name: SonarCloud
needs: pytest
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Download coverage report
uses: actions/download-artifact@v4
with:
name: coverage-report
- name: SonarCloud Scan
id: sonar_scan
uses: SonarSource/sonarqube-scan-action@v5.0.0
continue-on-error: true
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
- name: SonarQube Quality Gate Check
id: sonar_qg
uses: sonarsource/sonarqube-quality-gate-action@master
continue-on-error: true
timeout-minutes: 5
env:
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
with:
scanMetadataReportFile: .scannerwork/report-task.txt
- name: SonarCloud PR Comment
if: github.event_name == 'pull_request'
uses: actions/github-script@v6
env:
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
SONAR_PROJECT_KEY: 'donghao1393_mcp-dbutils'
with:
script: |
const fs = require('fs');
// 读取质量门禁结果
let qualityGateStatus = 'Unknown';
try {
const reportTaskContent = fs.readFileSync('.scannerwork/report-task.txt', 'utf8');
const ceTaskIdMatch = reportTaskContent.match(/ceTaskId=(.+)/);
if (ceTaskIdMatch && ceTaskIdMatch[1]) {
const ceTaskId = ceTaskIdMatch[1];
// 等待分析完成
let analysisComplete = false;
let retries = 0;
while (!analysisComplete && retries < 10) {
const ceTaskResponse = await fetch(
`https://sonarcloud.io/api/ce/task?id=${ceTaskId}`,
{ headers: { Authorization: `Bearer ${process.env.SONAR_TOKEN}` } }
).then(res => res.json());
if (ceTaskResponse.task && ceTaskResponse.task.status === 'SUCCESS') {
analysisComplete = true;
} else {
retries++;
await new Promise(resolve => setTimeout(resolve, 5000)); // 等待5秒
}
}
}
} catch (error) {
console.error('Error reading report task file:', error);
}
// 获取SonarCloud分析结果
const projectKey = process.env.SONAR_PROJECT_KEY;
const sonarResponse = await fetch(
`https://sonarcloud.io/api/measures/component?component=${projectKey}&metricKeys=bugs,vulnerabilities,code_smells,coverage,duplicated_lines_density`,
{ headers: { Authorization: `Bearer ${process.env.SONAR_TOKEN}` } }
).then(res => res.json());
// 解析结果
let coverage = 'N/A';
let bugs = 'N/A';
let vulnerabilities = 'N/A';
let codeSmells = 'N/A';
let duplication = 'N/A';
if (sonarResponse && sonarResponse.component && sonarResponse.component.measures) {
const measures = sonarResponse.component.measures;
for (const measure of measures) {
switch (measure.metric) {
case 'coverage':
coverage = `${parseFloat(measure.value).toFixed(2)}%`;
break;
case 'bugs':
bugs = measure.value;
break;
case 'vulnerabilities':
vulnerabilities = measure.value;
break;
case 'code_smells':
codeSmells = measure.value;
break;
case 'duplicated_lines_density':
duplication = `${parseFloat(measure.value).toFixed(2)}%`;
break;
}
}
}
// 获取质量门禁状态
const qualityGateResponse = await fetch(
`https://sonarcloud.io/api/qualitygates/project_status?projectKey=${projectKey}`,
{ headers: { Authorization: `Bearer ${process.env.SONAR_TOKEN}` } }
).then(res => res.json());
if (qualityGateResponse && qualityGateResponse.projectStatus) {
qualityGateStatus = qualityGateResponse.projectStatus.status === 'OK' ? '✅ Passed' : '❌ Failed';
}
// 计算总问题数
const totalIssues = parseInt(bugs || 0) + parseInt(vulnerabilities || 0) + parseInt(codeSmells || 0);
// 生成评论内容
let recommendations = [];
if (parseInt(bugs) > 0) recommendations.push('- Fix identified bugs to improve code reliability');
if (parseInt(vulnerabilities) > 0) recommendations.push('- Address security vulnerabilities to enhance application security');
if (parseInt(codeSmells) > 5) recommendations.push('- Refactor code to reduce code smells and improve maintainability');
if (parseFloat(coverage) < 80) recommendations.push('- Increase test coverage to meet the 80% minimum requirement');
if (parseFloat(duplication) > 3) recommendations.push('- Reduce code duplication to improve maintainability');
const comment = [
'## SonarCloud Analysis Results',
'',
'### Summary',
`- **Quality Gate**: ${qualityGateStatus}`,
`- **Coverage**: ${coverage}`,
`- **Total Issues**: ${totalIssues}`,
`- **Code Duplication**: ${duplication}`,
'',
'### Details',
`- **Code Smells**: ${codeSmells}`,
`- **Bugs**: ${bugs}`,
`- **Security Vulnerabilities**: ${vulnerabilities}`,
'',
'### Recommendations',
...recommendations,
'',
`[View full report on SonarCloud](https://sonarcloud.io/dashboard?id=${projectKey})`
].join('\n');
// 发布或更新PR评论
const { data: comments } = await github.rest.issues.listComments({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
});
const botComment = comments.find(comment =>
comment.user.login === 'github-actions[bot]' &&
comment.body.includes('SonarCloud Analysis Results')
);
if (botComment) {
await github.rest.issues.updateComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: botComment.id,
body: comment,
});
console.log('Updated existing SonarCloud comment');
} else {
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
body: comment,
});
console.log('Created new SonarCloud comment');
}
- name: Extract SonarCloud Issues
if: always()
uses: actions/github-script@v6
env:
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
SONAR_PROJECT_KEY: 'donghao1393_mcp-dbutils'
with:
script: |
const fs = require('fs');
// 获取SonarCloud问题
const projectKey = process.env.SONAR_PROJECT_KEY;
console.log(`获取项目 ${projectKey} 的SonarCloud问题...`);
// 获取PR号
let prNumberParam = '';
if (context.issue.number) {
prNumberParam = `&pullRequest=${context.issue.number}`;
console.log(`处理PR #${context.issue.number}的SonarCloud问题`);
} else {
console.log('未检测到PR号,将获取所有未解决的问题');
}
// 获取未解决的问题
const issuesResponse = await fetch(
`https://sonarcloud.io/api/issues/search?componentKeys=${projectKey}${prNumberParam}&resolved=false&ps=500`,
{ headers: { Authorization: `Bearer ${process.env.SONAR_TOKEN}` } }
).then(res => res.json());
console.log(`找到 ${issuesResponse.issues ? issuesResponse.issues.length : 0} 个未解决的问题`);
// 保存原始JSON
fs.writeFileSync('sonar_issues.json', JSON.stringify(issuesResponse, null, 2));
// 格式化为AI友好的Markdown
let markdownContent = '# SonarCloud分析问题报告\n\n';
markdownContent += `[在SonarCloud中查看完整报告](https://sonarcloud.io/project/issues?id=${projectKey})\n\n`;
// 添加问题摘要
if (issuesResponse.issues && issuesResponse.issues.length > 0) {
// 按严重性统计
const severityCounts = {};
// 按类型统计
const typeCounts = {};
issuesResponse.issues.forEach(issue => {
severityCounts[issue.severity] = (severityCounts[issue.severity] || 0) + 1;
typeCounts[issue.type] = (typeCounts[issue.type] || 0) + 1;
});
markdownContent += '## 问题摘要\n\n';
markdownContent += '### 按严重性\n\n';
for (const [severity, count] of Object.entries(severityCounts)) {
let severityText = '';
switch (severity) {
case 'BLOCKER': severityText = '阻断级'; break;
case 'CRITICAL': severityText = '严重级'; break;
case 'MAJOR': severityText = '主要级'; break;
case 'MINOR': severityText = '次要级'; break;
case 'INFO': severityText = '提示级'; break;
default: severityText = severity;
}
markdownContent += `- ${severityText}: ${count}个问题\n`;
}
markdownContent += '\n### 按类型\n\n';
for (const [type, count] of Object.entries(typeCounts)) {
let typeText = '';
switch (type) {
case 'BUG': typeText = 'Bug'; break;
case 'VULNERABILITY': typeText = '安全漏洞'; break;
case 'CODE_SMELL': typeText = '代码异味'; break;
default: typeText = type;
}
markdownContent += `- ${typeText}: ${count}个问题\n`;
}
// 按文件分组问题
const issuesByFile = {};
issuesResponse.issues.forEach(issue => {
const component = issue.component.split(':').pop();
if (!issuesByFile[component]) {
issuesByFile[component] = [];
}
issuesByFile[component].push(issue);
});
markdownContent += '\n## 详细问题列表\n\n';
for (const [file, issues] of Object.entries(issuesByFile)) {
markdownContent += `### 文件: ${file}\n\n`;
issues.forEach(issue => {
let severityText = '';
switch (issue.severity) {
case 'BLOCKER': severityText = '阻断级'; break;
case 'CRITICAL': severityText = '严重级'; break;
case 'MAJOR': severityText = '主要级'; break;
case 'MINOR': severityText = '次要级'; break;
case 'INFO': severityText = '提示级'; break;
default: severityText = issue.severity;
}
let typeText = '';
switch (issue.type) {
case 'BUG': typeText = 'Bug'; break;
case 'VULNERABILITY': typeText = '安全漏洞'; break;
case 'CODE_SMELL': typeText = '代码异味'; break;
default: typeText = issue.type;
}
markdownContent += `- 文件: ${file}\n`;
markdownContent += ` 行号: ${issue.line || 'N/A'}\n`;
markdownContent += ` 问题: ${issue.message}\n`;
markdownContent += ` 严重性: ${severityText}\n`;
markdownContent += ` 类型: ${typeText}\n`;
markdownContent += ` [在SonarCloud中查看](https://sonarcloud.io/project/issues?id=${projectKey}&open=${issue.key})\n\n`;
});
}
// 添加常见问题修复建议
markdownContent += '## 常见问题修复建议\n\n';
// 检查是否存在特定规则的问题
const hasRule = rule => issuesResponse.issues.some(issue => issue.rule === rule);
if (hasRule('python:S1481')) {
markdownContent += '- **未使用的变量**: 删除未使用的变量声明,或者在变量名前添加下划线(例如 `_unused`)\n';
}
if (hasRule('python:S5754')) {
markdownContent += '- **类型检查**: 使用 `isinstance()` 替代 `type()` 进行类型检查\n';
}
if (hasRule('python:S1066')) {
markdownContent += '- **合并嵌套if语句**: 将嵌套的if语句合并为一个使用 `and` 运算符的条件\n';
}
if (hasRule('python:S1172')) {
markdownContent += '- **未使用的参数**: 删除未使用的函数参数,或者在参数名前添加下划线\n';
}
if (hasRule('python:S5806')) {
markdownContent += '- **不必要的lambda**: 使用函数引用替代只调用另一个函数的lambda\n';
}
// 添加通用建议
markdownContent += '- **代码重复**: 提取重复代码为共享函数或类\n';
markdownContent += '- **复杂函数**: 将大型函数拆分为更小的函数,每个函数只做一件事\n';
markdownContent += '- **注释**: 为复杂逻辑添加清晰的注释,解释"为什么"而不仅仅是"做什么"\n';
} else {
markdownContent += '## 没有发现问题\n\n恭喜!SonarCloud没有检测到任何问题。';
}
// 保存Markdown文件
fs.writeFileSync('sonar_report.md', markdownContent);
console.log('已生成SonarCloud问题报告');
- name: Upload SonarCloud Issues Report
if: always()
uses: actions/upload-artifact@v4
with:
name: sonarcloud-issues
path: |
sonar_issues.json
sonar_report.md