Sync with main-repo - 2026-04-03 #17
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # -*- coding: utf-8 -*- | |
| # ============================================================================ # | |
| # DockerDiscordControl (DDC) - Code Quality CI/CD Workflow # | |
| # https://ddc.bot # | |
| # Copyright (c) 2025 MAX # | |
| # Licensed under the MIT License # | |
| # ============================================================================ # | |
| # | |
| # This workflow runs code quality checks on every push and pull request. | |
| # Includes cyclomatic complexity, maintainability index, linting, and type checking. | |
| name: Code Quality | |
| on: | |
| push: | |
| branches: | |
| - main | |
| - v2.0 | |
| - develop | |
| paths: | |
| - '**.py' | |
| - '.pylintrc' | |
| - '.flake8' | |
| - 'mypy.ini' | |
| - 'requirements*.txt' | |
| - '.github/workflows/code-quality.yml' | |
| pull_request: | |
| branches: | |
| - main | |
| - v2.0 | |
| paths: | |
| - '**.py' | |
| - '.pylintrc' | |
| - '.flake8' | |
| - 'mypy.ini' | |
| - 'requirements*.txt' | |
| workflow_dispatch: # Allow manual trigger | |
| permissions: | |
| contents: read # Checkout code | |
| actions: read # Download artifacts | |
| pull-requests: write # Comment on PRs | |
| security-events: write # Upload SARIF for SonarCloud | |
| env: | |
| # Quality gates thresholds | |
| MIN_PYLINT_SCORE: "8.0" | |
| MAX_COMPLEXITY: "10" | |
| MIN_MAINTAINABILITY: "80" | |
| jobs: | |
| code-complexity: | |
| name: Code Complexity Analysis | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v4 | |
| - name: Set up Python 3.10 | |
| uses: actions/setup-python@v5 | |
| with: | |
| python-version: '3.10' | |
| cache: 'pip' | |
| - name: Install dependencies | |
| run: | | |
| python -m pip install --upgrade pip | |
| pip install radon xenon | |
| - name: Run Cyclomatic Complexity Check | |
| run: | | |
| echo "## Cyclomatic Complexity Report" >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| echo "**Target**: All functions should have complexity < 10 (Grade B or better)" >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| # Check complexity for all Python files | |
| radon cc services/ app/ utils/ -a -s --total-average > complexity_report.txt || true | |
| cat complexity_report.txt >> $GITHUB_STEP_SUMMARY | |
| # Fail if average complexity is too high | |
| radon cc services/ app/ utils/ --min B --total-average | |
| continue-on-error: false | |
| - name: Run Maintainability Index Check | |
| run: | | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| echo "## Maintainability Index Report" >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| echo "**Target**: Maintainability Index > 80 (Grade A)" >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| # Check maintainability index | |
| radon mi services/ app/ utils/ -s > maintainability_report.txt || true | |
| cat maintainability_report.txt >> $GITHUB_STEP_SUMMARY | |
| # Fail if maintainability is too low | |
| radon mi services/ app/ utils/ --min A | |
| continue-on-error: false | |
| - name: Run Xenon Complexity Monitor | |
| run: | | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| echo "## Xenon Complexity Monitor" >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| # Xenon monitors code complexity - adjusted for production codebase | |
| # Current thresholds (pragmatic for existing complex business logic): | |
| # - max-absolute F: Monitor all functions (baseline established) | |
| # - max-modules D: Modules should maintain reasonable organization | |
| # - max-average B: Average complexity should be good across codebase | |
| # | |
| # ✅ Recent refactoring improvements (2025-01): | |
| # • validate_new_task_input: F → C (complexity 70→12, -83% reduction) | |
| # • _get_host_info: F → A (complexity 150→3, -98% reduction) | |
| # | |
| # 🎯 Future refactoring targets (current F-ranked): | |
| # • services/scheduling/scheduler.py:477 calculate_next_run | |
| # • services/infrastructure/container_status_service.py:310 _fetch_container_status | |
| # • app/utils/port_diagnostics.py:440 _get_actual_host_ip | |
| xenon --max-absolute F --max-modules D --max-average B services/ app/ utils/ | |
| continue-on-error: false | |
| - name: Upload complexity reports | |
| if: always() | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: complexity-reports | |
| path: | | |
| complexity_report.txt | |
| maintainability_report.txt | |
| retention-days: 30 | |
| pylint: | |
| name: Pylint Code Quality | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v4 | |
| - name: Set up Python 3.10 | |
| uses: actions/setup-python@v5 | |
| with: | |
| python-version: '3.10' | |
| cache: 'pip' | |
| - name: Install dependencies | |
| run: | | |
| python -m pip install --upgrade pip | |
| pip install -r requirements.txt | |
| pip install -r requirements-test.txt | |
| - name: Run Pylint | |
| run: | | |
| echo "## Pylint Report" >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| echo "**Target**: Pylint score > ${{ env.MIN_PYLINT_SCORE }}/10" >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| # Run pylint and capture score | |
| pylint services/ app/ utils/ --output-format=text --score=yes > pylint_report.txt || true | |
| cat pylint_report.txt >> $GITHUB_STEP_SUMMARY | |
| # Extract score and fail if below threshold | |
| SCORE=$(tail -n 2 pylint_report.txt | grep "Your code has been rated" | awk '{print $7}' | cut -d'/' -f1) | |
| echo "Pylint Score: $SCORE" | |
| if (( $(echo "$SCORE < ${{ env.MIN_PYLINT_SCORE }}" | bc -l) )); then | |
| echo "❌ Pylint score $SCORE is below threshold ${{ env.MIN_PYLINT_SCORE }}" | |
| exit 1 | |
| else | |
| echo "✅ Pylint score $SCORE meets threshold ${{ env.MIN_PYLINT_SCORE }}" | |
| fi | |
| continue-on-error: false | |
| - name: Upload pylint report | |
| if: always() | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: pylint-report | |
| path: pylint_report.txt | |
| retention-days: 30 | |
| flake8: | |
| name: Flake8 Style Check | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v4 | |
| - name: Set up Python 3.10 | |
| uses: actions/setup-python@v5 | |
| with: | |
| python-version: '3.10' | |
| cache: 'pip' | |
| - name: Install dependencies | |
| run: | | |
| python -m pip install --upgrade pip | |
| pip install flake8 mccabe | |
| - name: Run Flake8 | |
| run: | | |
| echo "## Flake8 Report" >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| echo "**Checks**: PEP 8 style, complexity < ${{ env.MAX_COMPLEXITY }}" >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| # Run flake8 | |
| flake8 services/ app/ utils/ --statistics --count > flake8_report.txt || true | |
| cat flake8_report.txt >> $GITHUB_STEP_SUMMARY | |
| # Fail if there are critical errors | |
| flake8 services/ app/ utils/ --count --select=E9,F63,F7,F82 --show-source | |
| continue-on-error: false | |
| - name: Upload flake8 report | |
| if: always() | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: flake8-report | |
| path: flake8_report.txt | |
| retention-days: 30 | |
| mypy: | |
| name: MyPy Type Checking | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v4 | |
| - name: Set up Python 3.10 | |
| uses: actions/setup-python@v5 | |
| with: | |
| python-version: '3.10' | |
| cache: 'pip' | |
| - name: Install dependencies | |
| run: | | |
| python -m pip install --upgrade pip | |
| pip install -r requirements.txt | |
| pip install -r requirements-test.txt | |
| - name: Run MyPy | |
| run: | | |
| echo "## MyPy Type Checking Report" >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| echo "**Checks**: Static type checking for type safety" >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| # Run mypy | |
| mypy --config-file mypy.ini > mypy_report.txt || true | |
| cat mypy_report.txt >> $GITHUB_STEP_SUMMARY | |
| # MyPy is currently informational only (progressive typing) | |
| echo "ℹ️ MyPy errors are informational - progressive typing in progress" | |
| continue-on-error: true # For now, mypy is informational | |
| - name: Upload mypy report | |
| if: always() | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: mypy-report | |
| path: mypy_report.txt | |
| retention-days: 30 | |
| sonarcloud: | |
| name: SonarCloud Analysis | |
| runs-on: ubuntu-latest | |
| if: github.repository == 'DockerDiscordControl/DockerDiscordControl' # Only for main repo | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 # Shallow clones should be disabled for better analysis | |
| - name: Set up Python 3.10 | |
| uses: actions/setup-python@v5 | |
| with: | |
| python-version: '3.10' | |
| - name: Install dependencies | |
| run: | | |
| python -m pip install --upgrade pip | |
| pip install -r requirements.txt | |
| pip install -r requirements-test.txt | |
| - name: Run tests with coverage | |
| run: | | |
| python -m pytest tests/ \ | |
| --cov=services \ | |
| --cov=app \ | |
| --cov=utils \ | |
| --cov-report=xml \ | |
| --cov-report=html | |
| continue-on-error: true | |
| - name: SonarCloud Scan | |
| uses: SonarSource/sonarcloud-github-action@master | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Needed to get PR information | |
| SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} # Generate in SonarCloud | |
| with: | |
| args: > | |
| -Dsonar.projectKey=DockerDiscordControl_DockerDiscordControl | |
| -Dsonar.organization=dockerdiscordcontrol | |
| -Dsonar.python.coverage.reportPaths=coverage.xml | |
| -Dsonar.sources=services,app,utils | |
| -Dsonar.tests=tests | |
| -Dsonar.python.version=3.9,3.10,3.11 | |
| continue-on-error: true # Don't fail build on SonarCloud issues | |
| quality-summary: | |
| name: Quality Summary | |
| runs-on: ubuntu-latest | |
| needs: [code-complexity, pylint, flake8, mypy] | |
| if: always() | |
| steps: | |
| - name: Download all reports | |
| uses: actions/download-artifact@v4 | |
| with: | |
| path: reports/ | |
| - name: Generate quality summary | |
| run: | | |
| echo "# Code Quality Summary" > quality_summary.md | |
| echo "" >> quality_summary.md | |
| echo "**Branch**: ${{ github.ref_name }}" >> quality_summary.md | |
| echo "**Commit**: ${{ github.sha }}" >> quality_summary.md | |
| echo "**Date**: $(date)" >> quality_summary.md | |
| echo "" >> quality_summary.md | |
| # Job statuses | |
| echo "## Job Results" >> quality_summary.md | |
| echo "" >> quality_summary.md | |
| echo "- Code Complexity: ${{ needs.code-complexity.result }}" >> quality_summary.md | |
| echo "- Pylint: ${{ needs.pylint.result }}" >> quality_summary.md | |
| echo "- Flake8: ${{ needs.flake8.result }}" >> quality_summary.md | |
| echo "- MyPy: ${{ needs.mypy.result }}" >> quality_summary.md | |
| echo "" >> quality_summary.md | |
| # Overall status | |
| if [[ "${{ needs.code-complexity.result }}" == "success" ]] && \ | |
| [[ "${{ needs.pylint.result }}" == "success" ]] && \ | |
| [[ "${{ needs.flake8.result }}" == "success" ]]; then | |
| echo "## ✅ Overall: PASS" >> quality_summary.md | |
| echo "All critical quality gates passed!" >> quality_summary.md | |
| else | |
| echo "## ❌ Overall: FAIL" >> quality_summary.md | |
| echo "One or more quality gates failed. Please review the reports." >> quality_summary.md | |
| exit 1 | |
| fi | |
| cat quality_summary.md >> $GITHUB_STEP_SUMMARY | |
| - name: Comment PR with quality results | |
| if: github.event_name == 'pull_request' | |
| uses: actions/github-script@v7 | |
| with: | |
| script: | | |
| const fs = require('fs'); | |
| const summary = fs.readFileSync('quality_summary.md', 'utf8'); | |
| github.rest.issues.createComment({ | |
| issue_number: context.issue.number, | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| body: summary | |
| }); |