基础信息

接入须知

接入产品线联系王浩彬,进行产品线管理账户创建,其中创建的项目务必改为私有,避免项目公开

Sonar 访问地址:

http://itsonar.gz.cvte.cn

前端接入

接入逻辑:通过gitlab ci runner来进行代码静态扫描

添加文件总体结构

配置gitlab ci

  • 编写.gitlab-ci.yml
stages:
  - test
  - sonar
  - report
variables:
  SONAR_PROJECT_KEY: tz-plugin #此处换成snonar的项目key

unit-test:
  image: node:16.20.2
  stage: test
  before_script:
    # - npm install pnpm@8.10.3 -g
    # - echo 'set npm registry'
    # - npm config set registry https://registry.npmmirror.com
    # - npm config set @cvte:registry http://artifactory.gz.cvte.cn/artifactory/api/npm/cvte-npm-registry/
    # - npm config set @cvte-eus:registry http://artifactory.gz.cvte.cn/artifactory/api/npm/cvte-npm-registry/
    # - pnpm install
    # - npm install --force
    - npm install --legacy-peer-deps
  script:
    # - npm run test:ci
    - npm run test
  coverage: /All files[^|]*\|[^|]*\s+([\d\.]+)/
  artifacts:
    paths:
      - __test__/
      - test-report.xml
      - coverage
  allow_failure: true

sonarqube-check:
  stage: sonar
  image: 
    name: sonarsource/sonar-scanner-cli:latest
    entrypoint: [""]
  variables:
    SONAR_USER_HOME: "${CI_PROJECT_DIR}/.sonar"  # Defines the location of the analysis task cache
    GIT_DEPTH: "0"  # Tells git to fetch all the branches of the project, required by the analysis task
  cache:
    key: "${CI_JOB_NAME}"
    paths:
      - .sonar/cache
  script: 
    - sonar-scanner
  allow_failure: true

report:
  image: python:latest
  stage: report
  script:
    - pip install requests
    - python report.py
  • 配置gitlab runner
  • 配置gitlab变量,参考sonar的指引

sonar-project.properties,配置sonar的扫码配置

sonar.projectKey=tz-plugin
sonar.qualitygate.wait=true

# Source
sonar.sources=/builds/it-frontend/tz-plugin/packages
# Where to find tests file, also src
sonar.tests=/builds/it-frontend/tz-plugin/packages
# But we get specific here
# We don't need to exclude it in sonar.sources because it is automatically taken care of
sonar.test.inclusions=src/**/*.test.js,src/**/*.test.jsx,src/**/*.test.ts,src/**/*.test.tsx
# Now specify path of lcov and testlog
#sonar.testExecutionReportPaths=/test-reporter.xml
sonar.javascript.lcov.reportPaths=/builds/it-frontend/tz-plugin/__test__/coverage/lcov.info

report.py 配置企微机器人推送,可选

主要调整配置信息即可,其中
SONAR_USERNAME = “squ_2a5598f035ff6e165ae052ece4cffb2b025bfd87”

import requests
import json
import sys
import base64
import os
from datetime import datetime

def get_sonar_auth_header(username, password):
    auth_str = f"{username}:{password}"
    auth_bytes = auth_str.encode('ascii')
    base64_auth = base64.b64encode(auth_bytes).decode('ascii')
    return {"Authorization": f"Basic {base64_auth}"}

def get_latest_analysis(sonar_url, project_key, auth_header):
    """
    获取最新的 Sonar 分析结果
    """
    api_url = f"{sonar_url}/api/project_analyses/search"
    params = {
        "project": project_key,
        "ps": 1  # 只获取最新的一次分析
    }

    try:
        response = requests.get(api_url, params=params, headers=auth_header)
        response.raise_for_status()
        analyses = response.json()['analyses']
        return analyses[0] if analyses else None
    except requests.RequestException as e:
        print(f"获取Sonar分析结果时出错: {e}")
        sys.exit(1)

def get_sonar_issues(sonar_url, project_key, auth_header, created_after):
    """
    获取 Sonar 最新扫描发现的问题
    """
    api_url = f"{sonar_url}/api/issues/search"
    params = {
        "componentKeys": project_key,
        "createdAfter": created_after,
        "ps": 100  # 每页显示的问题数,可以根据需要调整
    }

    all_issues = []
    page = 1

    while True:
        params['p'] = page
        try:
            response = requests.get(api_url, params=params, headers=auth_header)
            response.raise_for_status()
            data = response.json()
            issues = data['issues']
            all_issues.extend(issues)

            if len(issues) < params['ps']:
                break

            page += 1
        except requests.RequestException as e:
            print(f"获取Sonar问题时出错: {e}")
            sys.exit(1)

    return all_issues

def get_sonar_metrics(sonar_url, project_key, auth_header):
    """
    获取 Sonar 项目的度量指标,包括单元测试覆盖率
    """
    api_url = f"{sonar_url}/api/measures/component"
    params = {
        "component": project_key,
        "metricKeys": "coverage,tests,test_errors,test_failures,test_execution_time"
    }

    try:
        response = requests.get(api_url, params=params, headers=auth_header)
        response.raise_for_status()
        return {measure['metric']: measure['value'] for measure in response.json()['component']['measures']}
    except requests.RequestException as e:
        print(f"获取Sonar度量指标时出错: {e}")
        sys.exit(1)

def format_message(latest_analysis, issues, metrics, sonar_url, project_key):
    """
    格式化 Sonar 结果为企业微信消息,包含 GitLab CI 信息和 Sonar 仪表板链接
    """
    # 获取 GitLab CI 环境变量
    ci_project_name = os.environ.get('CI_PROJECT_NAME', 'Unknown Project')
    ci_commit_sha = os.environ.get('CI_COMMIT_SHORT_SHA', 'Unknown Commit')
    ci_commit_branch = os.environ.get('CI_COMMIT_BRANCH', 'Unknown Branch')
    ci_commit_author = os.environ.get('GITLAB_USER_NAME', 'Unknown User')

    sonar_dashboard_url = f"{sonar_url}/dashboard?id={project_key}"

    message = f"Sonar 代码检测结果 - {ci_project_name}\n\n"
    message += f"构建信息:\n"
    message += f"- 分支: {ci_commit_branch}\n"
    message += f"- 提交: {ci_commit_sha}\n"
    message += f"- 作者: {ci_commit_author}\n\n"

    if latest_analysis:
        message += f"最新分析日期: {latest_analysis['date'][:10]}\n\n"

    message += f"本次扫描发现的问题:\n"
    issue_counts = {'BUG': 0, 'VULNERABILITY': 0, 'CODE_SMELL': 0}
    for issue in issues:
        issue_counts[issue['type']] += 1

    message += f"- Bugs: {issue_counts['BUG']}\n"
    message += f"- 漏洞: {issue_counts['VULNERABILITY']}\n"
    message += f"- 代码异味: {issue_counts['CODE_SMELL']}\n\n"

    message += f"单元测试信息:\n"
    message += f"- 覆盖率: {metrics.get('coverage', 'N/A')}%\n"
    message += f"- 测试用例数: {metrics.get('tests', 'N/A')}\n"
    message += f"- 错误数: {metrics.get('test_errors', 'N/A')}\n"
    message += f"- 失败数: {metrics.get('test_failures', 'N/A')}\n"
    message += f"- 执行时间: {float(metrics.get('test_execution_time', 0)) / 1000:.2f} 秒\n\n"

    message += f"详细报告: {sonar_dashboard_url}"

    return message

def send_to_wechat(webhook_url, message):
    """
    发送消息到企业微信机器人
    """
    headers = {'Content-Type': 'application/json'}
    data = {
        "msgtype": "text",
        "text": {
            "content": message
        }
    }

    try:
        response = requests.post(webhook_url, headers=headers, data=json.dumps(data))
        response.raise_for_status()
        print("消息已成功发送到企业微信")
    except requests.RequestException as e:
        print(f"发送消息到企业微信时出错: {e}")
        sys.exit(1)

if __name__ == "__main__":
    # 配置信息
    SONAR_URL = "http://itsonar.gz.cvte.cn"
    PROJECT_KEY = os.environ.get('SONAR_PROJECT_KEY', 'your-project-key')
    SONAR_USERNAME = "squ_2a5598f035ff6e165ae052ece4cffb2b025bfd87"
    SONAR_PASSWORD = ""
    WECHAT_WEBHOOK = "https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=c144bea9-1adf-4da4-99f7-1672bd90628f" ## 换成自己的群机器人

    auth_header = get_sonar_auth_header(SONAR_USERNAME, SONAR_PASSWORD)

    # 获取最新的 Sonar 分析结果
    latest_analysis = get_latest_analysis(SONAR_URL, PROJECT_KEY, auth_header)

    # 获取最新扫描发现的问题
    created_after = latest_analysis['date'] if latest_analysis else None
    issues = get_sonar_issues(SONAR_URL, PROJECT_KEY, auth_header, created_after)

    # 获取项目度量指标,包括单元测试覆盖率
    metrics = get_sonar_metrics(SONAR_URL, PROJECT_KEY, auth_header)

    # 格式化消息
    message = format_message(latest_analysis, issues, metrics, SONAR_URL, PROJECT_KEY)

    # 发送到企业微信
    send_to_wechat(WECHAT_WEBHOOK, message)

后端接入

参考文档:https://kb.cvte.com/pages/viewpage.action?pageId=436872198

作者:王浩彬  创建时间:2024-09-20 11:27
最后编辑:王浩彬  更新时间:2024-12-23 11:22