Skip to content

Instantly share code, notes, and snippets.

@jeje-andal
Created December 30, 2025 11:43
Show Gist options
  • Select an option

  • Save jeje-andal/cee5985dc17510d663fc0207c4ea7772 to your computer and use it in GitHub Desktop.

Select an option

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…
#!/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