Skip to content
149 changes: 149 additions & 0 deletions Workflow file for this run .github/workflows/sonar.yml at
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
name: CI / SonarCloud Scan
on:
push:
branches:
- dev
- master
pull_request:
branches:
- master # Only trigger PR checks into master (adjust if needed)

jobs:
sonarcloud:
runs-on: ubuntu-latest

steps:
# 1️ Checkout repository
- name: Checkout code
uses: actions/checkout@v4

# 2️ Setup Node.js
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: '18'

# 3️ Cache Node.js dependencies
- name: Cache Node modules
uses: actions/cache@v4
with:
path: ~/.npm
key: ${{ runner.os }}-node-${{ hashFiles('package-lock.json') }}
restore-keys: |
${{ runner.os }}-node-

# 4️ Install dependencies
- name: Install dependencies
run: npm ci

# 5️ Run tests with coverage (pipeline continues even if no tests)
- name: Run tests with coverage
run: npm test -- --coverage --watchAll=false --passWithNoTests || true

# 6️ Build project (React + Node 18 fix)
- name: Build project
run: |
CI=false npm run build --if-present
env:
NODE_OPTIONS: --openssl-legacy-provider

# 7️ SonarCloud Scan for push events
- name: SonarCloud Scan (push)
if: github.event_name == 'push'
uses: SonarSource/sonarcloud-github-action@v2
env:
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
with:
args: >
-Dsonar.projectKey=RADhaigude_invoice-generator-react
-Dsonar.organization=radhaigude
-Dsonar.sources=.

# 8️ SonarCloud Scan for pull requests
- name: SonarCloud Scan (PR)
if: github.event_name == 'pull_request'
uses: SonarSource/sonarcloud-github-action@v2
env:
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
with:
args: >
-Dsonar.projectKey=RADhaigude_invoice-generator-react
-Dsonar.organization=radhaigude
-Dsonar.sources=.
-Dsonar.pullrequest.key=${{ github.event.pull_request.number }}
-Dsonar.pullrequest.branch=${{ github.head_ref }}
-Dsonar.pullrequest.base=${{ github.base_ref }}

# 9️ Fetch Quality Gate JSON from SonarCloud
- name: Fetch Quality Gate status
run: |
if [ "${{ github.event_name }}" = "pull_request" ]; then
curl -s -u ${{ secrets.SONAR_TOKEN }}: \
"https://sonarcloud.io/api/qualitygates/project_status?projectKey=RADhaigude_invoice-generator-react&pullRequest=${{ github.event.pull_request.number }}" \
-o sonar_quality_gate.json
else
curl -s -u ${{ secrets.SONAR_TOKEN }}: \
"https://sonarcloud.io/api/qualitygates/project_status?projectKey=RADhaigude_invoice-generator-react" \
-o sonar_quality_gate.json
fi
cat sonar_quality_gate.json

# Convert Quality Gate JSON to styled HTML report
- name: Convert Quality Gate JSON to Styled HTML
run: |
echo "<html><head><title>SonarCloud Quality Gate</title>" > sonar_quality_gate.html
echo "<style>
body { font-family: Arial, sans-serif; margin: 20px; }
h1 { color: #333; }
.section { margin-bottom: 20px; border: 1px solid #ccc; border-radius: 6px; }
.header { padding: 8px; font-weight: bold; color: #fff; }
.failed { background-color: #e74c3c; }
.passed { background-color: #2ecc71; }
.skipped { background-color: #f1c40f; }
table { width: 100%; border-collapse: collapse; margin-top: 10px; }
th, td { border: 1px solid #ccc; padding: 6px; text-align: left; }
</style></head><body>" >> sonar_quality_gate.html

STATUS=$(jq -r '.projectStatus.status' sonar_quality_gate.json)
echo "<h1>SonarCloud Quality Gate: $STATUS</h1>" >> sonar_quality_gate.html

# Failed Conditions
echo "<div class='section'><div class='header failed'> Failed Conditions</div><table><tr><th>Metric</th><th>Actual</th><th>Threshold</th></tr>" >> sonar_quality_gate.html
jq -c '.projectStatus.conditions[] | select(.status=="ERROR")' sonar_quality_gate.json | while read row; do
METRIC=$(echo $row | jq -r '.metricKey')
ACTUAL=$(echo $row | jq -r '.actualValue')
THRESHOLD=$(echo $row | jq -r '.errorThreshold')
echo "<tr><td>$METRIC</td><td>$ACTUAL</td><td>$THRESHOLD</td></tr>" >> sonar_quality_gate.html
done
echo "</table></div>" >> sonar_quality_gate.html

# Skipped / Warn
echo "<div class='section'><div class='header skipped'> Warnings / Skipped</div><table><tr><th>Metric</th><th>Actual</th><th>Threshold</th></tr>" >> sonar_quality_gate.html
jq -c '.projectStatus.conditions[] | select(.status=="WARN")' sonar_quality_gate.json | while read row; do
METRIC=$(echo $row | jq -r '.metricKey')
ACTUAL=$(echo $row | jq -r '.actualValue')
THRESHOLD=$(echo $row | jq -r '.errorThreshold')
echo "<tr><td>$METRIC</td><td>$ACTUAL</td><td>$THRESHOLD</td></tr>" >> sonar_quality_gate.html
done
echo "</table></div>" >> sonar_quality_gate.html

# Passed
echo "<div class='section'><div class='header passed'>Passed Conditions</div><table><tr><th>Metric</th><th>Actual</th><th>Threshold</th></tr>" >> sonar_quality_gate.html
jq -c '.projectStatus.conditions[] | select(.status=="OK")' sonar_quality_gate.json | while read row; do
METRIC=$(echo $row | jq -r '.metricKey')
ACTUAL=$(echo $row | jq -r '.actualValue')
THRESHOLD=$(echo $row | jq -r '.errorThreshold')
echo "<tr><td>$METRIC</td><td>$ACTUAL</td><td>$THRESHOLD</td></tr>" >> sonar_quality_gate.html
done
echo "</table></div>" >> sonar_quality_gate.html

echo "</body></html>" >> sonar_quality_gate.html

# 1️ Upload artifacts (JSON + HTML)
- name: Upload Quality Gate artifacts
uses: actions/upload-artifact@v4
with:
name: sonar-quality-gate
path: |
sonar_quality_gate.json
sonar_quality_gate.html
249 changes: 249 additions & 0 deletions sonar.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,249 @@
name: CI / SonarCloud Scan

on:
push:
branches: [ dev, master ]

permissions:
contents: read
pull-requests: write

concurrency:
group: ci-${{ github.ref }}
cancel-in-progress: true

jobs:
build-test-scan:
runs-on: ubuntu-latest
timeout-minutes: 25

steps:
# 1️ Checkout repository
- name: Checkout code
uses: actions/checkout@v4

# 2️ Tooling (Node + jq + pandoc)
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: '18'

- name: Install OS tooling
run: |
sudo apt-get update
sudo apt-get install -y jq pandoc
pandoc --version

# 3️ Cache Node modules
- name: Cache Node modules
uses: actions/cache@v4
with:
path: ~/.npm
key: ${{ runner.os }}-node-${{ hashFiles('package-lock.json') }}
restore-keys: |
${{ runner.os }}-node-

# 4️ Install dependencies
- name: Install dependencies
run: npm ci

# 5️ Run tests with coverage
- name: Run tests with coverage
run: |
npm install --save-dev jest-canvas-mock
echo "import 'jest-canvas-mock';" > src/setupTests.js
npx jest --coverage --watchAll=false --passWithNoTests --bail=0 \
--json --outputFile=jest-results.json \
--coverageReporters=text --coverageReporters=lcov --coverageReporters=html || echo "⚠️ Some tests failed, but continuing..."

# 6️ Generate comprehensive test coverage report
- name: Generate test coverage report
run: |
mkdir -p reports/sonar/coverage
cp -r coverage/lcov-report/* reports/sonar/coverage/ || true
echo "# Test Coverage Summary" > reports/sonar/coverage/SUMMARY.md
echo "Generated on: $(date)" >> reports/sonar/coverage/SUMMARY.md
echo "## Coverage Metrics" >> reports/sonar/coverage/SUMMARY.md
if [ -f coverage/coverage-summary.json ]; then
echo "| Category | Percentage |" >> reports/sonar/coverage/SUMMARY.md
echo "|----------|-----------:|" >> reports/sonar/coverage/SUMMARY.md
echo "| Lines | $(jq -r '.total.lines.pct' coverage/coverage-summary.json)% |" >> reports/sonar/coverage/SUMMARY.md
echo "| Statements | $(jq -r '.total.statements.pct' coverage/coverage-summary.json)% |" >> reports/sonar/coverage/SUMMARY.md
echo "| Functions | $(jq -r '.total.functions.pct' coverage/coverage-summary.json)% |" >> reports/sonar/coverage/SUMMARY.md
echo "| Branches | $(jq -r '.total.branches.pct' coverage/coverage-summary.json)% |" >> reports/sonar/coverage/SUMMARY.md
fi

# 7️ Build project
- name: Build project
run: CI=false npm run build --if-present
env:
NODE_OPTIONS: --openssl-legacy-provider

# 8️ SonarCloud Scan for pushes
- name: SonarCloud Scan
if: github.event_name == 'push'
uses: SonarSource/sonarcloud-github-action@v2
env:
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
with:
args: >
-Dsonar.projectKey=RADhaigude_invoice-generator-react
-Dsonar.organization=radhaigude
-Dsonar.sources=.
-Dsonar.exclusions=node_modules/**,coverage/**,build/**,dist/**,**/*.spec.ts,**/*.spec.tsx,**/*.test.ts,**/*.test.tsx,**/*.test.js
-Dsonar.javascript.lcov.reportPaths=coverage/lcov.info

# 10 Fetch Quality Gate JSON
- name: Fetch Quality Gate status
env:
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
run: |
curl -s -u "$SONAR_TOKEN:" \
"https://sonarcloud.io/api/qualitygates/project_status?projectKey=RADhaigude_invoice-generator-react" \
-o sonar_quality_gate.json
cat sonar_quality_gate.json

# 1️1 Build HTML view of Quality Gate
- name: Convert Quality Gate JSON to HTML
run: |
echo "<html><head><title>SonarCloud Quality Gate</title></head><body>" > sonar_quality_gate.html
echo "<h1>Quality Gate Status: $(jq -r '.projectStatus.status' sonar_quality_gate.json)</h1>" >> sonar_quality_gate.html
echo "<table border='1' cellpadding='5'><tr><th>Metric</th><th>Status</th><th>Actual Value</th><th>Error Threshold</th></tr>" >> sonar_quality_gate.html
jq -c '.projectStatus.conditions[]' sonar_quality_gate.json | while read row; do
METRIC=$(echo "$row" | jq -r '.metricKey')
STATUS=$(echo "$row" | jq -r '.status')
ACTUAL=$(echo "$row" | jq -r '.actualValue')
THRESHOLD=$(echo "$row" | jq -r '.errorThreshold')
echo "<tr><td>$METRIC</td><td>$STATUS</td><td>$ACTUAL</td><td>$THRESHOLD</td></tr>" >> sonar_quality_gate.html
done
echo "</table></body></html>" >> sonar_quality_gate.html

# 1️2 Generate multi-format Sonar reports
- name: Generate SonarCloud Reports
env:
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
run: |
python3 -m pip install --upgrade pip
python3 -m pip install requests openpyxl python-docx pypandoc lxml
cat > sonar_report.py <<'EOF'
import requests, csv, os, base64, sys
from openpyxl import Workbook
from docx import Document
import pypandoc
# Ensure Pandoc is available
try:
pypandoc.get_pandoc_version()
except OSError:
pypandoc.download_pandoc()
token = os.getenv("SONAR_TOKEN")
project = "RADhaigude_invoice-generator-react"
metrics = ",".join([
"bugs","vulnerabilities","code_smells",
"coverage","duplicated_lines_density",
"reliability_rating","security_rating","sqale_rating",
"ncloc","cognitive_complexity"
])
url = f"https://sonarcloud.io/api/measures/component?component={project}&metricKeys={metrics}"
headers = {"Authorization": "Basic " + base64.b64encode(f"{token}:".encode()).decode()}
r = requests.get(url, headers=headers)
if r.status_code != 200:
print(" API call failed:", r.status_code, r.text)
sys.exit(1)
data = r.json()["component"]["measures"]
# CSV
with open("sonar_report.csv", "w", newline="") as f:
writer = csv.writer(f)
writer.writerow(["Metric", "Value"])
for m in data:
writer.writerow([m["metric"], m.get("value", "")])
# XLSX
wb = Workbook()
ws = wb.active
ws.title = "Sonar Measures"
ws.append(["Metric", "Value"])
for m in data:
ws.append([m["metric"], m.get("value", "")])
wb.save("sonar_report.xlsx")
# DOCX
doc = Document()
doc.add_heading("SonarCloud Report", 0)
table = doc.add_table(rows=1, cols=2)
hdr = table.rows[0].cells
hdr[0].text, hdr[1].text = "Metric", "Value"
for m in data:
row = table.add_row().cells
row[0].text, row[1].text = m["metric"], m.get("value", "")
doc.save("sonar_report.docx")
# Markdown
md_content = "# SonarCloud Report\n\n| Metric | Value |\n|--------|-------|\n"
for m in data:
md_content += f"| {m['metric']} | {m.get('value','')} |\n"
with open("sonar_report.md", "w") as f:
f.write(md_content)
# RTF
pypandoc.convert_text(md_content, 'rtf', format='md', outputfile='sonar_report_quality.rtf', extra_args=['--standalone'])
print(" Reports generated: CSV, XLSX, DOCX, MD, RTF")
EOF
python3 sonar_report.py

# 1️3 Create Job Summary
- name: Create Job Summary
if: always()
run: |
echo "## CI Summary" >> $GITHUB_STEP_SUMMARY
if [ -f jest-results.json ]; then
TOTAL=$(jq -r '.numTotalTests' jest-results.json 2>/dev/null || echo "NA")
PASSED=$(jq -r '.numPassedTests' jest-results.json 2>/dev/null || echo "NA")
FAILED=$(jq -r '.numFailedTests' jest-results.json 2>/dev/null || echo "NA")
echo "### Tests" >> $GITHUB_STEP_SUMMARY
echo "| Total | Passed | Failed |" >> $GITHUB_STEP_SUMMARY
echo "|------:|------:|-------:|" >> $GITHUB_STEP_SUMMARY
echo "| $TOTAL | $PASSED | $FAILED |" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
fi
if [ -f coverage/coverage-summary.json ]; then
LINES=$(jq -r '.total.lines.pct' coverage/coverage-summary.json)
STMT=$(jq -r '.total.statements.pct' coverage/coverage-summary.json)
FUNC=$(jq -r '.total.functions.pct' coverage/coverage-summary.json)
BRAN=$(jq -r '.total.branches.pct' coverage/coverage-summary.json)
echo "### Coverage" >> $GITHUB_STEP_SUMMARY
echo "| Lines % | Statements % | Functions % | Branches % |" >> $GITHUB_STEP_SUMMARY
echo "|--------:|------------:|------------:|-----------:|" >> $GITHUB_STEP_SUMMARY
echo "| $LINES | $STMT | $FUNC | $BRAN |" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
fi
if [ -f sonar_quality_gate.json ]; then
QG=$(jq -r '.projectStatus.status' sonar_quality_gate.json)
echo "### SonarCloud Quality Gate" >> $GITHUB_STEP_SUMMARY
echo "- **Status:** \`$QG\`" >> $GITHUB_STEP_SUMMARY
fi

# 1️4 Enforce Quality Gate
- name: Fail if Quality Gate fails (protect master)
if: github.event_name == 'push' && github.ref == 'refs/heads/master'
run: |
STATUS=$(jq -r '.projectStatus.status' sonar_quality_gate.json)
if [ "$STATUS" != "OK" ]; then
echo "Quality Gate is $STATUS. Failing the job to protect master."
exit 1

# 1️5 Upload comprehensive reports
- name: Upload comprehensive reports
if: always()
uses: actions/upload-artifact@v4
with:
name: comprehensive-sonar-report
path: |
reports/sonar/
jest-results.json
coverage/lcov.info
coverage/coverage-summary.json
sonar_quality_gate.json
sonar_quality_gate.html
sonar_report.csv
sonar_report.xlsx
sonar_report.docx
sonar_report.md
sonar_report_quality.rtf
build/**
retention-days: 7
Loading