Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
172 changes: 172 additions & 0 deletions .github/workflows/sonarqube.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
name: SonarQube

# Project-agnostic SonarQube workflow — copy to .github/workflows/ in any repo.
# The project key is taken from the SONAR_PROJECT_KEY repo variable (falling back
# to the repo name), so nothing here is hardcoded to a single project.
#
# Per-repo setup (see sonarqube-selfhosted README § "Onboard a new repo"):
# gh secret set SONAR_TOKEN
# gh secret set SONAR_HOST_URL
# gh variable set SONAR_PROJECT_KEY --body "<project-key>"
#
# Adjust the trigger branches and the coverage block to the project.

on:
push:
branches: [main]
pull_request:
branches: [main]
workflow_dispatch:

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

permissions:
contents: read
pull-requests: write

Comment on lines +25 to +28
env:
# Falls back to the repository name when the variable is unset.
SONAR_PROJECT_KEY: ${{ vars.SONAR_PROJECT_KEY || github.event.repository.name }}

jobs:
scan:
name: 📡 SonarQube
runs-on: ubuntu-latest
timeout-minutes: 30

steps:
- name: 📥 Checkout
uses: actions/checkout@v4
with:
persist-credentials: false
# Full history for accurate blame/new-code detection
fetch-depth: 0

# Skip gracefully (with a warning) until the self-hosted server is up
- name: 🔑 Check SonarQube secrets
id: creds
env:
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
SONAR_HOST_URL: ${{ secrets.SONAR_HOST_URL }}
run: |
if [ -n "$SONAR_TOKEN" ] && [ -n "$SONAR_HOST_URL" ]; then
echo "present=true" >> $GITHUB_OUTPUT
else
echo "present=false" >> $GITHUB_OUTPUT
echo "⚠️ **SonarQube skipped — SONAR_TOKEN / SONAR_HOST_URL secrets are not set**" >> $GITHUB_STEP_SUMMARY
Comment on lines +50 to +58

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Не вимагайте ручного секрету SONAR_HOST_URL.

За цілями PR автоматично налаштовуються SONAR_HOST_URL і SONAR_PROJECT_KEY, а вручну додається лише SONAR_TOKEN. Наразі репозиторій із одним токеном завжди отримує present=false і пропускає сканування. Передавайте URL через централізовану organization/repository variable або зафіксуйте адресу сервера у workflow.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/sonarqube.yml around lines 50 - 58, Update the SonarQube
prerequisite check to require only SONAR_TOKEN, while sourcing SONAR_HOST_URL
from the centralized organization/repository variable or a fixed workflow value.
Ensure repositories configured with just the token set present=true and continue
passing the automatically configured SONAR_PROJECT_KEY and host URL to the scan.

fi

# --- OPTIONAL: coverage for the scanner -----------------------------
# Customise or delete per project. If you generate an lcov report here,
# point sonar.javascript.lcov.reportPaths at it in sonar-project.properties.
# Note: vitest writes SF: paths relative to its cwd, while SonarQube resolves
# them relative to the repo root — rewrite the prefix if they differ, e.g.
# sed -i 's|^SF:|SF:apps/<app>/|' <path>/coverage/lcov.info
#
# - name: 🟢 Setup Node and pnpm
# if: steps.creds.outputs.present == 'true'
# uses: ./.github/actions/setup-node-pnpm
# with:
# node-version: '22'
# - name: 🧪 Generate coverage
# if: steps.creds.outputs.present == 'true'
# run: pnpm test:coverage || true

Comment on lines +61 to +76
# PR analysis relies on the community-branch-plugin on the server
- name: 📡 Scan (PR analysis)
if: steps.creds.outputs.present == 'true' && github.event_name == 'pull_request'
uses: sonarsource/sonarqube-scan-action@v8.2.0 # nosemgrep: generic.secrets.security.detected-sonarqube-docs-api-key.detected-sonarqube-docs-api-key
env:
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
SONAR_HOST_URL: ${{ secrets.SONAR_HOST_URL }}
with:
args: >
-Dsonar.projectKey=${{ env.SONAR_PROJECT_KEY }}
-Dsonar.pullrequest.key=${{ github.event.pull_request.number }}
-Dsonar.pullrequest.branch=${{ github.head_ref }}
-Dsonar.pullrequest.base=${{ github.base_ref }}
-Dsonar.qualitygate.wait=false

- name: 📡 Scan (branch analysis)
if: steps.creds.outputs.present == 'true' && github.event_name != 'pull_request'
uses: sonarsource/sonarqube-scan-action@v8.2.0 # nosemgrep: generic.secrets.security.detected-sonarqube-docs-api-key.detected-sonarqube-docs-api-key
env:
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
SONAR_HOST_URL: ${{ secrets.SONAR_HOST_URL }}
with:
args: >
-Dsonar.projectKey=${{ env.SONAR_PROJECT_KEY }}
-Dsonar.branch.name=${{ github.ref_name }}
-Dsonar.qualitygate.wait=false

# Sticky PR comment (updated in place, not re-posted) with the findings
- name: 💬 Comment findings on PR
if: always() && github.event_name == 'pull_request' && steps.creds.outputs.present == 'true'

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Публікація застарілого результату

always() запускає цей крок після помилки або скасування поточного scan. Оскільки наступні API-запити прив’язані лише до project key і номера PR, вони можуть отримати попередній завершений аналіз та опублікувати його як результат поточного commit.

Suggested change
if: always() && github.event_name == 'pull_request' && steps.creds.outputs.present == 'true'
if: success() && github.event_name == 'pull_request' && steps.creds.outputs.present == 'true'

Fix in Cursor

env:
GH_TOKEN: ${{ github.token }}
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
SONAR_HOST_URL: ${{ secrets.SONAR_HOST_URL }}
PR_NUMBER: ${{ github.event.pull_request.number }}
RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
run: |
# The scan step no longer waits on sonar.qualitygate.wait, so the gate
# may still be computing server-side when this step starts — poll
# briefly instead of a single immediate check.
GATE="UNAVAILABLE"
for _ in $(seq 1 12); do
if RESP=$(curl -sf -u "$SONAR_TOKEN:" \
"$SONAR_HOST_URL/api/qualitygates/project_status?projectKey=$SONAR_PROJECT_KEY&pullRequest=$PR_NUMBER"); then
GATE=$(echo "$RESP" | jq -r '.projectStatus.status // "NONE"')
[ "$GATE" != "NONE" ] && break

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: PR reruns can update the sticky comment with the previous analysis’s gate and issues. Poll the submitted Compute Engine task (or otherwise wait for this scan’s analysis) before fetching project_status and issues.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At .github/workflows/sonarqube.yml, line 122:

<comment>PR reruns can update the sticky comment with the previous analysis’s gate and issues. Poll the submitted Compute Engine task (or otherwise wait for this scan’s analysis) before fetching `project_status` and issues.</comment>

<file context>
@@ -0,0 +1,172 @@
+            if RESP=$(curl -sf -u "$SONAR_TOKEN:" \
+              "$SONAR_HOST_URL/api/qualitygates/project_status?projectKey=$SONAR_PROJECT_KEY&pullRequest=$PR_NUMBER"); then
+              GATE=$(echo "$RESP" | jq -r '.projectStatus.status // "NONE"')
+              [ "$GATE" != "NONE" ] && break
+            fi
+            sleep 5
</file context>

fi
sleep 5
done
Comment on lines +106 to +125

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
rg -n 'report-task|ceTaskId|analysisId|qualitygates/project_status|always\(\)' \
  .github/workflows/sonarqube.yml

Repository: vaulttec-dev/chrome-extension

Length of output: 400


🏁 Script executed:

#!/bin/bash
set -euo pipefail
file=".github/workflows/sonarqube.yml"
wc -l "$file"
sed -n '90,170p' "$file" | cat -n

Repository: vaulttec-dev/chrome-extension

Length of output: 4966


🏁 Script executed:

#!/bin/bash
set -euo pipefail
file=".github/workflows/sonarqube.yml"
sed -n '1,120p' "$file" | cat -n

Repository: vaulttec-dev/chrome-extension

Length of output: 6013


🌐 Web query:

SonarQube API /api/qualitygates/project_status pullRequest analysisId semantics latest analysis report-task.txt ceTaskId

💡 Result:

To programmatically check the Quality Gate status of a SonarQube analysis, you must interact with the asynchronous analysis process [1][2]. The report-task.txt file, generated in the scanner's working directory upon successful submission, provides the necessary entry point [3][4][5]. Process Overview: 1. Identify the ceTaskId: After the SonarScanner finishes submitting the report, a report-task.txt file is created containing the ceTaskId (Compute Engine Task ID) [3][1]. 2. Monitor Task Completion: Use the /api/ce/task endpoint with the ceTaskId to poll the status of the background analysis [1][2]. GET /api/ce/task?id= Continue polling until the task.status field returns "SUCCESS" [1][2]. 3. Retrieve analysisId: Once the status is "SUCCESS", the same /api/ce/task response will contain the analysisId (located in task.analysisId) [1][2]. 4. Get Quality Gate Status: Use the analysisId with the /api/qualitygates/project_status endpoint to get the final result [6][3][2]. GET /api/qualitygates/project_status?analysisId= Regarding /api/qualitygates/project_status and Pull Requests: While you can query this endpoint using projectKey and branch/pullRequest parameters (e.g., /api/qualitygates/project_status?projectKey=&pullRequest=), these requests may return inconsistent or summary-only data depending on your environment and authentication method [7][6]. For precise, reliable results—especially when automating CI/CD pipelines—it is considered best practice to use the analysisId obtained from the specific background task, as it ensures you are checking the status of the exact analysis just completed [6][1]. Top Results: [6][3][1][2]

Citations:


🌐 Web query:

SonarQube qualitygates/project_status pullRequest returns latest analysis or previous analysis while background task still running

💡 Result:

When calling the SonarQube API endpoint api/qualitygates/project_status while a background task is still in progress, the API will return the status of the most recently completed analysis [1][2][3]. It does not automatically wait for or return the results of the pending analysis currently being processed by the compute engine [4][5]. Because the analysis is not fully incorporated into the project state until its corresponding background task completes (reaching a status of SUCCESS), any API call made before that completion will reflect the state from the previous, finished analysis [4][6][5]. To ensure you are retrieving the results of the specific, ongoing analysis, you should use one of the following methods: 1. Use sonar.qualitygate.wait=true: Adding this parameter to your scanner execution forces the analysis process to wait until the background task is fully processed by the SonarQube server before exiting [6][5]. This is generally the most robust approach for CI/CD pipelines [6]. 2. Poll the Background Task API: After the scanner finishes, you can use the ceTaskId (available in the report-task.txt file generated by the scanner) to poll the api/ce/task endpoint [7][2][8]. Once the task status is SUCCESS, you can then safely call api/qualitygates/project_status using the analysisId returned by the task status response [7][8]. 3. Monitor via Webhooks: Configure SonarQube webhooks to notify your system once a background task completes [9][10]. This avoids the need for polling altogether by pushing the result to you when it is ready [9]. Attempting to check the quality gate status immediately after the scanner finishes without waiting for the background task will frequently result in receiving "stale" or inaccurate data [1][3].

Citations:


Прив’яжіть PR-коментар до поточного Sonar task
Запит лише за projectKey і pullRequest може показати попередній завершений аналіз, якщо поточний background task ще обробляється. Зчитайте ceTaskId з report-task.txt, дочекайтеся SUCCESS для цього task і беріть project_status за analysisId; без метаданих цього запуску коментар краще пропускати.

🧰 Tools
🪛 Betterleaks (1.6.1)

[high] 119-119: Discovered a potential basic authorization token provided in a curl command, which could compromise the curl accessed resource.

(curl-auth-user)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/sonarqube.yml around lines 106 - 125, Update the
quality-gate polling step to read ceTaskId from report-task.txt, poll the Sonar
Compute Engine task until it reaches SUCCESS, and then query project_status
using that task’s analysisId instead of only projectKey and PR_NUMBER. If the
report metadata is missing or the task does not complete successfully, skip
posting the PR comment; preserve the existing comment flow for a successfully
resolved current analysis.


ISSUES_FILE=$(mktemp)
if ! curl -sf -u "$SONAR_TOKEN:" \
"$SONAR_HOST_URL/api/issues/search?components=$SONAR_PROJECT_KEY&pullRequest=$PR_NUMBER&resolved=false&ps=100" \

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Несумісний параметр фільтра API

Запит використовує застарілий параметр components, тоді як актуальний контракт /api/issues/search очікує componentKeys. На сервері без сумісного alias запит завершиться помилкою, після чого гілка exit 0 пропустить PR-коментар і залишить workflow успішним без результатів аналізу.

Suggested change
"$SONAR_HOST_URL/api/issues/search?components=$SONAR_PROJECT_KEY&pullRequest=$PR_NUMBER&resolved=false&ps=100" \
"$SONAR_HOST_URL/api/issues/search?componentKeys=$SONAR_PROJECT_KEY&pullRequest=$PR_NUMBER&resolved=false&ps=100" \

Fix in Cursor

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: PR comments undercount findings once a PR has over 100 unresolved issues, while presenting the truncated count as total. Use the response total and paginate, or label the result as the first 100 findings.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At .github/workflows/sonarqube.yml, line 129:

<comment>PR comments undercount findings once a PR has over 100 unresolved issues, while presenting the truncated count as total. Use the response `total` and paginate, or label the result as the first 100 findings.</comment>

<file context>
@@ -0,0 +1,172 @@
+
+          ISSUES_FILE=$(mktemp)
+          if ! curl -sf -u "$SONAR_TOKEN:" \
+            "$SONAR_HOST_URL/api/issues/search?components=$SONAR_PROJECT_KEY&pullRequest=$PR_NUMBER&resolved=false&ps=100" \
+            > "$ISSUES_FILE"; then
+            echo "::warning::SonarQube API unavailable — skipping PR comment"
</file context>

> "$ISSUES_FILE"; then
Comment on lines +128 to +130

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Звіт обрізається на 100 issues

ps=100 завантажує лише першу сторінку, тоді як код використовує довжину отриманого масиву як загальну кількість. Для PR із понад 100 issues коментар покаже неправильний підсумок і повністю приховає решту результатів; потрібно обходити сторінки або брати загальну кількість із paging.total.

Fix in Cursor

echo "::warning::SonarQube API unavailable — skipping PR comment"
exit 0
fi

case "$GATE" in
OK) GATE_LINE="✅ Quality gate: **пройдено**" ;;
ERROR) GATE_LINE="❌ Quality gate: **не пройдено**" ;;
*) GATE_LINE="ℹ️ Quality gate: $GATE" ;;
esac

BODY_FILE=$(mktemp)
{
echo "<!-- sonarqube-report -->"
echo "## 📡 SonarQube — якість коду"
echo ""
echo "$GATE_LINE"
echo ""
jq -r --arg key "$SONAR_PROJECT_KEY" '
(.issues // []) as $i
| if ($i | length) == 0 then
"✅ Зауважень у змінених файлах немає."
else
( $i | sort_by({BLOCKER:0, CRITICAL:1, MAJOR:2, MINOR:3, INFO:4}[.severity] // 5) ) as $s
| "Знайдено **\($s | length)** зауважень:\n\n" +
"| Серйозність | Файл | Зауваження |\n|---|---|---|\n" +
( [ $s[:20][] | "| \(.severity) | `\(.component | sub("^" + $key + ":"; ""))\(if .line then ":\(.line)" else "" end)` | \(.message | gsub("\\|"; "\\\\|")) |" ] | join("\n") ) +
(if ($s|length) > 20 then "\n\n…і ще \(($s|length)-20) зауважень — повний список у SonarQube." else "" end)
end
' "$ISSUES_FILE"
echo ""
echo "_Повний звіт: [SonarQube]($SONAR_HOST_URL/dashboard?id=$SONAR_PROJECT_KEY&pullRequest=$PR_NUMBER) · [лог запуску]($RUN_URL)_"
} > "$BODY_FILE"

COMMENT_ID=$(gh api "repos/${{ github.repository }}/issues/$PR_NUMBER/comments" --paginate \
--jq '[.[] | select(.body | startswith("<!-- sonarqube-report -->"))][0].id // empty')
if [ -n "$COMMENT_ID" ]; then
gh api -X PATCH "repos/${{ github.repository }}/issues/comments/$COMMENT_ID" -F body=@"$BODY_FILE" > /dev/null
echo "Updated existing SonarQube comment (id=$COMMENT_ID)"
else
gh api "repos/${{ github.repository }}/issues/$PR_NUMBER/comments" -F body=@"$BODY_FILE" > /dev/null
echo "Posted new SonarQube comment"
fi
15 changes: 15 additions & 0 deletions sonar-project.properties
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# SonarQube scanner config. Server/onboarding: eloicompany/sonarqube-selfhosted repo.
sonar.projectKey=chrome-extension

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: sonar.projectKey is hardcoded here as chrome-extension, but the workflow also passes -Dsonar.projectKey=${{ env.SONAR_PROJECT_KEY }} (which defaults to the repo name or a variable). Having two sources of truth means local scans and CI could silently target different projects if either value drifts. Consider removing the hardcoded key from this file and relying solely on the CI-provided value (or vice versa).

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At sonar-project.properties, line 2:

<comment>`sonar.projectKey` is hardcoded here as `chrome-extension`, but the workflow also passes `-Dsonar.projectKey=${{ env.SONAR_PROJECT_KEY }}` (which defaults to the repo name or a variable). Having two sources of truth means local scans and CI could silently target different projects if either value drifts. Consider removing the hardcoded key from this file and relying solely on the CI-provided value (or vice versa).</comment>

<file context>
@@ -0,0 +1,15 @@
+# SonarQube scanner config. Server/onboarding: eloicompany/sonarqube-selfhosted repo.
+sonar.projectKey=chrome-extension
+sonar.sources=.
+sonar.exclusions=\
</file context>
Suggested change
sonar.projectKey=chrome-extension
# sonar.projectKey is provided by CI via -Dsonar.projectKey; set here only for local scans.
# sonar.projectKey=chrome-extension

sonar.sources=.
Comment on lines +1 to +3
sonar.exclusions=\
**/node_modules/**,\
**/.next/**,\
**/dist/**,\
**/build/**,\
**/vendor/**,\
**/*.min.js,\
**/*.lock
sonar.test.inclusions=\
**/*.test.ts,\
**/*.test.tsx,\
**/tests/**
Loading