Created
December 30, 2025 11:43
-
-
Save jeje-andal/cee5985dc17510d663fc0207c4ea7772 to your computer and use it in GitHub Desktop.
Universal SonarQube Scanner for AK Projects (AK.Server, AK.Web, AK.Mobile) Requires SONAR_TOKEN environment variable to be set. Usage: python universal_scan.py server python universal_scan.py web origin/develop python universal_scan.py mobile --skip-tests Setup: 1. Generate token at http://localhost:9000 (User Account > Security > Tokens) 2. Set…
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
| #!/usr/bin/env python3 | |
| """ | |
| universal_scan.py - One script to scan AK.Server, AK.Web, and AK.Mobile | |
| Usage: | |
| python universal_scan.py <project_type> [target_branch] [--skip-tests] | |
| Examples: | |
| python universal_scan.py server | |
| python universal_scan.py web origin/develop | |
| python universal_scan.py mobile --skip-tests | |
| Environment Variables Required: | |
| SONAR_TOKEN - Your SonarQube user token (generate from User Account > Security > Tokens) | |
| Setup: | |
| 1. Generate token at http://localhost:9000 (User Account > Security > Tokens) | |
| 2. Set environment variable: | |
| Windows PowerShell: [System.Environment]::SetEnvironmentVariable('SONAR_TOKEN', 'sqa_your_token_here', 'User') | |
| Linux/Mac: export SONAR_TOKEN="sqa_your_token_here" (add to ~/.bashrc or ~/.zshrc) | |
| """ | |
| import subprocess | |
| import sys | |
| import argparse | |
| import os | |
| import shutil | |
| from pathlib import Path | |
| # ============================================================================ | |
| # CONFIGURATION | |
| # ============================================================================ | |
| # SonarQube Server Settings | |
| SONAR_URL = os.environ.get("SONARQUBE_URL", "http://localhost:9000") | |
| DEFAULT_BRANCH = os.environ.get("SONAR_DEFAULT_BRANCH", "origin/develop") | |
| # Single token for all projects (simplifies setup and improves security) | |
| SONAR_TOKEN = os.environ.get("SONAR_TOKEN") | |
| if not SONAR_TOKEN: | |
| print("[!] ERROR: SONAR_TOKEN environment variable not set!") | |
| print("") | |
| print(" Please set the SONAR_TOKEN environment variable before running this script.") | |
| print("") | |
| print(" Generate token at: http://localhost:9000") | |
| print(" (Go to: User Account > My Account > Security > Tokens)") | |
| print("") | |
| print(" Then set the environment variable:") | |
| print("") | |
| print(" Windows PowerShell (permanent):") | |
| print(" [System.Environment]::SetEnvironmentVariable('SONAR_TOKEN', 'sqa_your_token_here', 'User')") | |
| print("") | |
| print(" Windows CMD (temporary session):") | |
| print(" set SONAR_TOKEN=sqa_your_token_here") | |
| print("") | |
| print(" Linux/Mac (add to ~/.bashrc or ~/.zshrc):") | |
| print(" export SONAR_TOKEN='sqa_your_token_here'") | |
| print("") | |
| print(" After setting, restart your terminal and run this script again.") | |
| sys.exit(1) | |
| # Project Specific Configurations | |
| PROJECTS = { | |
| "server": { | |
| "path": "AK.Server", | |
| "key": "AK.Server.PR", | |
| "token": SONAR_TOKEN, # Uses single token from environment variable | |
| "type": "dotnet", | |
| "extensions": ["cs"], | |
| # Matches: "Test/" folders, ".UnitTest" projects, or files ending in Test.cs | |
| "test_pattern": r"(?i)(\/tests?\/|\.unittests?\/|\.tests?\/|test\.cs$|tests\.cs$)", | |
| "sln_path": "src/AK.Server.sln" | |
| }, | |
| "web": { | |
| "path": "AK.Web", | |
| "key": "AK.Web.PR", | |
| "token": SONAR_TOKEN, # Uses single token from environment variable | |
| "type": "node", | |
| "extensions": ["ts", "html", "scss", "css"], | |
| # Matches: files ending in .spec.ts | |
| "test_pattern": r"\.spec\.ts$", | |
| "coverage_path": "coverage/lcov.info" | |
| }, | |
| "mobile": { | |
| "path": "AK.Mobile", | |
| "key": "AK.Mobile.PR", | |
| "token": SONAR_TOKEN, # Uses single token from environment variable | |
| "type": "flutter", | |
| "extensions": ["dart"], | |
| # Matches: test folder or _test.dart | |
| "test_pattern": r"(^test\/|.*_test\.dart$)", | |
| "coverage_path": "coverage/lcov.info" | |
| } | |
| } | |
| # ============================================================================ | |
| # CORE LOGIC | |
| # ============================================================================ | |
| def run_cmd(command, cwd=None, shell=True): | |
| """Helper to run shell commands.""" | |
| print(f" > Executing: {command}") | |
| try: | |
| subprocess.run(command, cwd=cwd, check=True, shell=shell) | |
| except subprocess.CalledProcessError as e: | |
| print(f"\n[!] Error executing command in {cwd or '.'}") | |
| sys.exit(1) | |
| def get_changed_files(target_branch, extensions, cwd): | |
| """Generic Git Diff logic.""" | |
| print(f"\n[1] Detecting changed files (ext: {extensions})...") | |
| # Git diff relative to the specific project folder | |
| cmd = ['git', 'diff', '--name-only', f'{target_branch}...HEAD'] | |
| result = subprocess.run(cmd, cwd=cwd, capture_output=True, text=True, check=True) | |
| files = [] | |
| for f in result.stdout.strip().split('\n'): | |
| if not f: continue | |
| # Check extension | |
| if any(f.endswith(f".{ext}") for ext in extensions): | |
| # Check if file still exists (wasn't deleted) | |
| if (Path(cwd) / f).exists(): | |
| files.append(f) | |
| return files | |
| def separate_files(files, pattern_regex): | |
| """Generic Regex Separation.""" | |
| import re | |
| source = [] | |
| tests = [] | |
| for f in files: | |
| if re.search(pattern_regex, f): | |
| tests.append(f) | |
| else: | |
| source.append(f) | |
| return source, tests | |
| # ============================================================================ | |
| # SCANNER IMPLEMENTATIONS | |
| # ============================================================================ | |
| def scan_dotnet(config, source, tests, cwd, skip_tests=False): | |
| """Handler for C# .NET Scanner (Stateful: Begin -> Build -> End)""" | |
| # 1. Reset | |
| sonar_folder = Path(cwd) / ".sonarqube" | |
| if sonar_folder.exists(): | |
| shutil.rmtree(sonar_folder) | |
| # 2. Inclusions | |
| src_inc = ",".join(source) if source else "None" | |
| test_inc = ",".join(tests) if tests else "None" | |
| # 3. Begin | |
| print("\n[2] Starting .NET Scanner (Begin)...") | |
| run_cmd(f'dotnet sonarscanner begin /k:"{config["key"]}" ' | |
| f'/d:sonar.host.url="{SONAR_URL}" ' | |
| f'/d:sonar.token="{config["token"]}" ' | |
| f'/d:sonar.cs.opencover.reportsPaths="**/coverage.opencover.xml" ' | |
| f'/d:sonar.inclusions="{src_inc}" ' | |
| f'/d:sonar.test.inclusions="{test_inc}"', cwd=cwd) | |
| # 4. Build | |
| print("\n[3] Building Solution...") | |
| run_cmd(f'dotnet build {config["sln_path"]}', cwd=cwd) | |
| # 5. Test (Full Suite for accuracy, or add --filter if too slow) | |
| if not skip_tests: | |
| print("\n[4] Running Tests...") | |
| run_cmd(f'dotnet test {config["sln_path"]} --collect:"XPlat Code Coverage;Format=opencover"', cwd=cwd) | |
| else: | |
| print("\n[4] Skipping test execution (--skip-tests flag)") | |
| # 6. End | |
| print("\n[5] Uploading Results (End)...") | |
| run_cmd(f'dotnet sonarscanner end /d:sonar.token="{config["token"]}"', cwd=cwd) | |
| def scan_cli_based(config, source, tests, cwd, scanner_cmd, skip_tests=False): | |
| """Handler for Node/Flutter (Stateless: Single Command)""" | |
| # 1. Run Tests First (to generate coverage) - unless skipped | |
| if not skip_tests: | |
| print("\n[2] Running Tests (Coverage)...") | |
| if config["type"] == "node": | |
| # Angular | |
| run_cmd("npm run test -- --no-watch --code-coverage", cwd=cwd) | |
| elif config["type"] == "flutter": | |
| # Flutter | |
| run_cmd("flutter test --coverage", cwd=cwd) | |
| else: | |
| print("\n[2] Skipping test execution (--skip-tests flag)") | |
| # 2. Prepare Scanner Command | |
| src_inc = ",".join(source) if source else "None" | |
| test_inc = ",".join(tests) if tests else "None" | |
| print("\n[3] Running SonarScanner...") | |
| # We construct the arguments manually to pass to the scanner binary | |
| args = [ | |
| f"-Dsonar.projectKey={config['key']}", | |
| f"-Dsonar.host.url={SONAR_URL}", | |
| f"-Dsonar.token={config['token']}", | |
| f"-Dsonar.sources={ 'src' if config['type'] == 'node' else 'lib' }", | |
| f"-Dsonar.tests={ 'src' if config['type'] == 'node' else 'test' }", | |
| f"-Dsonar.inclusions={src_inc}", | |
| f"-Dsonar.test.inclusions={test_inc}" | |
| ] | |
| # Only add coverage property if tests were run | |
| if not skip_tests: | |
| cov_prop = "sonar.typescript.lcov.reportPaths" if config["type"] == "node" else "sonar.flutter.coverage.reportPath" | |
| args.append(f"-D{cov_prop}={config['coverage_path']}") | |
| full_cmd = f"{scanner_cmd} {' '.join(args)}" | |
| run_cmd(full_cmd, cwd=cwd) | |
| # ============================================================================ | |
| # MAIN ENTRY | |
| # ============================================================================ | |
| if __name__ == "__main__": | |
| parser = argparse.ArgumentParser( | |
| description="Unified SonarQube PR Scanner for AK Projects", | |
| epilog="Requires SONAR_TOKEN environment variable to be set." | |
| ) | |
| parser.add_argument("project", choices=["server", "web", "mobile"], | |
| help="Project to scan (server, web, or mobile)") | |
| parser.add_argument("target", nargs="?", default=DEFAULT_BRANCH, | |
| help="Target branch for comparison (default: %(default)s)") | |
| parser.add_argument("--skip-tests", action="store_true", | |
| help="Skip test execution and scan source code only") | |
| args = parser.parse_args() | |
| cfg = PROJECTS[args.project] | |
| project_cwd = str(Path(os.getcwd()) / cfg["path"]) | |
| print(f"=== Starting Analysis for {args.project.upper()} ===") | |
| print(f"Target Branch: {args.target}") | |
| print(f"Project Path: {project_cwd}") | |
| print(f"SonarQube URL: {SONAR_URL}") | |
| if args.skip_tests: | |
| print("WARNING: Test execution SKIPPED - analyzing source code only") | |
| # 1. Get Changes | |
| files = get_changed_files(args.target, cfg["extensions"], project_cwd) | |
| if not files: | |
| print("\nNo relevant changes detected. Exiting.") | |
| sys.exit(0) | |
| # 2. Separate Source vs Tests | |
| src_files, test_files = separate_files(files, cfg["test_pattern"]) | |
| print(f"\nScope: {len(src_files)} Source files, {len(test_files)} Test files") | |
| # 3. Execute Specific Scanner Logic | |
| if cfg["type"] == "dotnet": | |
| scan_dotnet(cfg, src_files, test_files, project_cwd, args.skip_tests) | |
| elif cfg["type"] == "node": | |
| scan_cli_based(cfg, src_files, test_files, project_cwd, "npx sonar-scanner", args.skip_tests) | |
| elif cfg["type"] == "flutter": | |
| scan_cli_based(cfg, src_files, test_files, project_cwd, "sonar-scanner", args.skip_tests) | |
| print(f"\n=== {args.project.upper()} Analysis Complete ===") |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment