From c8c0fd11dea2a7d69eb2f45a45cd93e923746210 Mon Sep 17 00:00:00 2001 From: Hrithik-Gavankar Date: Mon, 10 Aug 2026 15:02:33 +0530 Subject: [PATCH 1/4] ci: add SonarCloud static analysis with coverage reporting --- .github/workflows/ci.yml | 22 +++++++ .github/workflows/finalize.yml | 106 +++++++++++++++++++++++++++++++++ sonar-project.properties | 18 ++++++ 3 files changed, 146 insertions(+) create mode 100644 .github/workflows/finalize.yml create mode 100644 sonar-project.properties diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2f385d7..9e41dd7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -62,6 +62,28 @@ jobs: flags: daemon token: ${{ secrets.CODECOV_TOKEN }} + - name: Test daemon with coverage + run: xvfb-run -a npm run test:coverage -w packages/daemon + + - name: Upload coverage for SonarCloud + if: hashFiles('packages/daemon/coverage/lcov.info') != '' + uses: actions/upload-artifact@v7 + with: + name: sonar-coverage + path: packages/daemon/coverage/lcov.info + retention-days: 7 + + - name: Save PR number + if: github.event_name == 'pull_request' + run: echo "PR Number ${{ github.event.pull_request.number }}" > pr_number.txt + + - name: Upload PR number artifact + if: github.event_name == 'pull_request' + uses: actions/upload-artifact@v7 + with: + name: pr_number + path: pr_number.txt + build: needs: lint-and-test strategy: diff --git a/.github/workflows/finalize.yml b/.github/workflows/finalize.yml new file mode 100644 index 0000000..36b8685 --- /dev/null +++ b/.github/workflows/finalize.yml @@ -0,0 +1,106 @@ +--- +name: finalize +on: + workflow_run: + workflows: + - CI + types: + - completed + +permissions: read-all + +jobs: + finalize: + name: finalize + if: | + github.event.workflow_run.conclusion == 'success' && + (github.event.workflow_run.event == 'pull_request' || + (github.event.workflow_run.event == 'push' && github.event.workflow_run.head_branch == 'main')) + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 + with: + fetch-depth: 0 + show-progress: false + + - name: Download coverage artifact + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 + with: + name: sonar-coverage + path: packages/daemon/coverage + github-token: ${{ secrets.GITHUB_TOKEN }} + run-id: ${{ github.event.workflow_run.id }} + + - name: Fetch PR Number artifact + if: github.event.workflow_run.event == 'pull_request' + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 + with: + name: pr_number + path: . + github-token: ${{ secrets.GITHUB_TOKEN }} + run-id: ${{ github.event.workflow_run.id }} + + - name: Extract PR Number + if: github.event.workflow_run.event == 'pull_request' + run: | + cat pr_number.txt + PR_NUM=$(head -n1 pr_number.txt | awk '{print $3}') + echo "Found PR number: $PR_NUM" + echo "PR_NUMBER=$PR_NUM" >> $GITHUB_ENV + + - name: Get Additional PR Information + if: github.event.workflow_run.event == 'pull_request' + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + PR_DATA=$(gh api "repos/${{ github.repository }}/pulls/${{ env.PR_NUMBER }}") + echo "PR_BASE=$(echo "$PR_DATA" | jq -r '.base.ref')" >> $GITHUB_ENV + echo "PR_HEAD=$(echo "$PR_DATA" | jq -r '.head.ref')" >> $GITHUB_ENV + + - name: Checkout PR branch + if: github.event.workflow_run.event == 'pull_request' + run: | + gh pr checkout ${{ env.PR_NUMBER }} || echo "::warning::Failed to checkout PR branch ${{ env.PR_NUMBER }}, this can happen if it was already merged and deleted." + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Prepare SonarCloud args + if: hashFiles('packages/daemon/coverage/lcov.info') != '' + shell: bash + run: | + REPO_NAME="${{ github.repository }}" + COMMIT_SHA="${{ github.event.workflow_run.head_sha }}" + + IFS="/" read -r REPO_OWNER REPO_NAME_ONLY <<< "$REPO_NAME" + + SONAR_ARGS="-Dsonar.projectKey=${REPO_OWNER}_${REPO_NAME_ONLY} -Dsonar.organization=${REPO_OWNER}" + SONAR_ARGS="${SONAR_ARGS} -Dsonar.scm.revision=$COMMIT_SHA" + + WORKFLOW_EVENT="${{ github.event.workflow_run.event }}" + if [[ "$WORKFLOW_EVENT" == "pull_request" && -n "$PR_NUMBER" ]]; then + SONAR_ARGS="${SONAR_ARGS} -Dsonar.pullrequest.key=$PR_NUMBER" + SONAR_ARGS="${SONAR_ARGS} -Dsonar.pullrequest.branch=$PR_HEAD" + SONAR_ARGS="${SONAR_ARGS} -Dsonar.pullrequest.base=$PR_BASE" + fi + + echo "SONAR_ARGS=$SONAR_ARGS" >> $GITHUB_ENV + + - name: Check for coverage files + run: | + if [ -f packages/daemon/coverage/lcov.info ]; then + echo "Coverage Data: Available" + ls -la packages/daemon/coverage/lcov.info + else + echo "Coverage Data: Not available - exiting" + exit 1 + fi + + echo "Running SonarCloud analysis..." + + - name: SonarCloud Scan + uses: SonarSource/sonarqube-scan-action@22918119ff8e1ca75a623e15c8296b6ea4fbe28f # v8 + env: + SONAR_TOKEN: ${{ secrets.CICD_ORG_SONAR_TOKEN_CICD_BOT }} + with: + args: ${{ env.SONAR_ARGS }} diff --git a/sonar-project.properties b/sonar-project.properties new file mode 100644 index 0000000..c80f187 --- /dev/null +++ b/sonar-project.properties @@ -0,0 +1,18 @@ +# cspell: ignore multicriteria +# sonar does not support cobertura coverage xml format, only lcov.info +# +# Branch analysis: SonarCloud treats the default branch automatically. +# Do not hardcode sonar.branch.name or sonar.branch.target here — a static +# target would break other branch scans. For additional long-lived branches, +# set the pattern (Project > Branches) to: ^(next|(branch|release)-.*)$ +sonar.debug=false +sonar.log.level.app=INFO +sonar.javascript.lcov.reportPaths=packages/daemon/coverage/lcov.info +sonar.typescript.lcov.reportPaths=packages/daemon/coverage/lcov.info +sonar.organization=redhat-developer +sonar.projectKey=redhat-developer_abbenay +sonar.sources=packages/daemon/src/,packages/vscode/src/ +sonar.tests=packages/daemon/src/,packages/daemon/tests/,packages/vscode/src/test/ +sonar.exclusions=**/node_modules/**,**/dist/**,**/coverage/**,packages/proto-ts/**,packages/python/** +sonar.test.inclusions=**/*.test.ts +sonar.verbose=false From 0758e2ad615bc1a034bb96171e2c7fc76769419d Mon Sep 17 00:00:00 2001 From: Hrithik-Gavankar Date: Tue, 11 Aug 2026 12:58:55 +0530 Subject: [PATCH 2/4] ci(sonar): fix source/test overlap, harden PR metadata resolution, and dedupe coverage job --- .github/workflows/ci.yml | 16 +----- .github/workflows/finalize.yml | 89 ++++++++++++++++++++-------------- sonar-project.properties | 4 +- 3 files changed, 56 insertions(+), 53 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9e41dd7..930b93c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -49,7 +49,7 @@ jobs: echo '{ "disable-hardware-acceleration": true }' > ~/.vscode/argv.json - name: Test (daemon – with coverage) - run: npm run test:coverage --workspace=packages/daemon + run: xvfb-run -a npm run test:coverage --workspace=packages/daemon - name: Test (vscode) run: xvfb-run -a npm test --workspace=packages/vscode @@ -62,9 +62,6 @@ jobs: flags: daemon token: ${{ secrets.CODECOV_TOKEN }} - - name: Test daemon with coverage - run: xvfb-run -a npm run test:coverage -w packages/daemon - - name: Upload coverage for SonarCloud if: hashFiles('packages/daemon/coverage/lcov.info') != '' uses: actions/upload-artifact@v7 @@ -73,17 +70,6 @@ jobs: path: packages/daemon/coverage/lcov.info retention-days: 7 - - name: Save PR number - if: github.event_name == 'pull_request' - run: echo "PR Number ${{ github.event.pull_request.number }}" > pr_number.txt - - - name: Upload PR number artifact - if: github.event_name == 'pull_request' - uses: actions/upload-artifact@v7 - with: - name: pr_number - path: pr_number.txt - build: needs: lint-and-test strategy: diff --git a/.github/workflows/finalize.yml b/.github/workflows/finalize.yml index 36b8685..366f6c3 100644 --- a/.github/workflows/finalize.yml +++ b/.github/workflows/finalize.yml @@ -21,8 +21,10 @@ jobs: - name: Checkout repository uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 with: + ref: ${{ github.event.workflow_run.head_sha }} fetch-depth: 0 show-progress: false + persist-credentials: false - name: Download coverage artifact uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 @@ -32,59 +34,72 @@ jobs: github-token: ${{ secrets.GITHUB_TOKEN }} run-id: ${{ github.event.workflow_run.id }} - - name: Fetch PR Number artifact - if: github.event.workflow_run.event == 'pull_request' - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 - with: - name: pr_number - path: . - github-token: ${{ secrets.GITHUB_TOKEN }} - run-id: ${{ github.event.workflow_run.id }} - - - name: Extract PR Number - if: github.event.workflow_run.event == 'pull_request' - run: | - cat pr_number.txt - PR_NUM=$(head -n1 pr_number.txt | awk '{print $3}') - echo "Found PR number: $PR_NUM" - echo "PR_NUMBER=$PR_NUM" >> $GITHUB_ENV - - - name: Get Additional PR Information + - name: Resolve PR metadata if: github.event.workflow_run.event == 'pull_request' env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + HEAD_SHA: ${{ github.event.workflow_run.head_sha }} + REPO: ${{ github.repository }} + # Trusted GitHub payload — not PR-controlled artifact content + PR_NUMBER: ${{ github.event.workflow_run.pull_requests[0].number }} run: | - PR_DATA=$(gh api "repos/${{ github.repository }}/pulls/${{ env.PR_NUMBER }}") - echo "PR_BASE=$(echo "$PR_DATA" | jq -r '.base.ref')" >> $GITHUB_ENV - echo "PR_HEAD=$(echo "$PR_DATA" | jq -r '.head.ref')" >> $GITHUB_ENV + set -euo pipefail - - name: Checkout PR branch - if: github.event.workflow_run.event == 'pull_request' - run: | - gh pr checkout ${{ env.PR_NUMBER }} || echo "::warning::Failed to checkout PR branch ${{ env.PR_NUMBER }}, this can happen if it was already merged and deleted." - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + if [[ -z "${PR_NUMBER}" ]]; then + PR_NUMBER=$(gh api \ + -H "Accept: application/vnd.github+json" \ + "repos/${REPO}/commits/${HEAD_SHA}/pulls" \ + --jq '.[0].number // empty') + fi + + if [[ ! "${PR_NUMBER}" =~ ^[0-9]+$ ]]; then + echo "::error::Unable to resolve a trusted PR number for head SHA ${HEAD_SHA}" + exit 1 + fi + + PR_DATA=$(gh api "repos/${REPO}/pulls/${PR_NUMBER}") + PR_BASE=$(jq -r '.base.ref' <<< "${PR_DATA}") + PR_HEAD=$(jq -r '.head.ref' <<< "${PR_DATA}") + + if [[ -z "${PR_BASE}" || "${PR_BASE}" == "null" || -z "${PR_HEAD}" || "${PR_HEAD}" == "null" ]]; then + echo "::error::Unable to resolve PR branch metadata for PR ${PR_NUMBER}" + exit 1 + fi + + { + echo "PR_NUMBER=${PR_NUMBER}" + echo "PR_BASE=${PR_BASE}" + echo "PR_HEAD=${PR_HEAD}" + } >> "${GITHUB_ENV}" + + echo "Resolved PR #${PR_NUMBER} (${PR_HEAD} -> ${PR_BASE})" - name: Prepare SonarCloud args if: hashFiles('packages/daemon/coverage/lcov.info') != '' shell: bash + env: + WORKFLOW_EVENT: ${{ github.event.workflow_run.event }} + COMMIT_SHA: ${{ github.event.workflow_run.head_sha }} + REPO_NAME: ${{ github.repository }} run: | - REPO_NAME="${{ github.repository }}" - COMMIT_SHA="${{ github.event.workflow_run.head_sha }}" + set -euo pipefail - IFS="/" read -r REPO_OWNER REPO_NAME_ONLY <<< "$REPO_NAME" + IFS="/" read -r REPO_OWNER REPO_NAME_ONLY <<< "${REPO_NAME}" SONAR_ARGS="-Dsonar.projectKey=${REPO_OWNER}_${REPO_NAME_ONLY} -Dsonar.organization=${REPO_OWNER}" - SONAR_ARGS="${SONAR_ARGS} -Dsonar.scm.revision=$COMMIT_SHA" + SONAR_ARGS="${SONAR_ARGS} -Dsonar.scm.revision=${COMMIT_SHA}" - WORKFLOW_EVENT="${{ github.event.workflow_run.event }}" - if [[ "$WORKFLOW_EVENT" == "pull_request" && -n "$PR_NUMBER" ]]; then - SONAR_ARGS="${SONAR_ARGS} -Dsonar.pullrequest.key=$PR_NUMBER" - SONAR_ARGS="${SONAR_ARGS} -Dsonar.pullrequest.branch=$PR_HEAD" - SONAR_ARGS="${SONAR_ARGS} -Dsonar.pullrequest.base=$PR_BASE" + if [[ "${WORKFLOW_EVENT}" == "pull_request" ]]; then + if [[ ! "${PR_NUMBER:-}" =~ ^[0-9]+$ ]]; then + echo "::error::PR_NUMBER is missing or invalid" + exit 1 + fi + SONAR_ARGS="${SONAR_ARGS} -Dsonar.pullrequest.key=${PR_NUMBER}" + SONAR_ARGS="${SONAR_ARGS} -Dsonar.pullrequest.branch=${PR_HEAD}" + SONAR_ARGS="${SONAR_ARGS} -Dsonar.pullrequest.base=${PR_BASE}" fi - echo "SONAR_ARGS=$SONAR_ARGS" >> $GITHUB_ENV + echo "SONAR_ARGS=${SONAR_ARGS}" >> "${GITHUB_ENV}" - name: Check for coverage files run: | diff --git a/sonar-project.properties b/sonar-project.properties index c80f187..1e5dae0 100644 --- a/sonar-project.properties +++ b/sonar-project.properties @@ -13,6 +13,8 @@ sonar.organization=redhat-developer sonar.projectKey=redhat-developer_abbenay sonar.sources=packages/daemon/src/,packages/vscode/src/ sonar.tests=packages/daemon/src/,packages/daemon/tests/,packages/vscode/src/test/ -sonar.exclusions=**/node_modules/**,**/dist/**,**/coverage/**,packages/proto-ts/**,packages/python/** +# Keep source/test sets disjoint: co-located *.test.ts live under source roots +# but must be classified only as tests (not main sources). +sonar.exclusions=**/node_modules/**,**/dist/**,**/coverage/**,packages/proto-ts/**,packages/python/**,**/*.test.ts sonar.test.inclusions=**/*.test.ts sonar.verbose=false From ebf0f24903f9b5fa049da361f6f66fc2cbb54c81 Mon Sep 17 00:00:00 2001 From: Hrithik-Gavankar Date: Tue, 11 Aug 2026 13:21:41 +0530 Subject: [PATCH 3/4] ci(sonar): pin sonar.host.url to prevent PR override of endpoint --- .github/workflows/finalize.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/finalize.yml b/.github/workflows/finalize.yml index 366f6c3..7e3a176 100644 --- a/.github/workflows/finalize.yml +++ b/.github/workflows/finalize.yml @@ -86,7 +86,9 @@ jobs: IFS="/" read -r REPO_OWNER REPO_NAME_ONLY <<< "${REPO_NAME}" - SONAR_ARGS="-Dsonar.projectKey=${REPO_OWNER}_${REPO_NAME_ONLY} -Dsonar.organization=${REPO_OWNER}" + # Pin endpoint; PRs must not override via sonar-project.properties + SONAR_ARGS="-Dsonar.host.url=https://sonarcloud.io" + SONAR_ARGS="${SONAR_ARGS} -Dsonar.projectKey=${REPO_OWNER}_${REPO_NAME_ONLY} -Dsonar.organization=${REPO_OWNER}" SONAR_ARGS="${SONAR_ARGS} -Dsonar.scm.revision=${COMMIT_SHA}" if [[ "${WORKFLOW_EVENT}" == "pull_request" ]]; then From 13f4d3be8ba639511114bb213de6ec152e3b9090 Mon Sep 17 00:00:00 2001 From: Hrithik-Gavankar Date: Tue, 11 Aug 2026 15:29:10 +0530 Subject: [PATCH 4/4] ci(sonar): update SonarCloud scan token key --- .github/workflows/finalize.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/finalize.yml b/.github/workflows/finalize.yml index 7e3a176..fcb343f 100644 --- a/.github/workflows/finalize.yml +++ b/.github/workflows/finalize.yml @@ -118,6 +118,6 @@ jobs: - name: SonarCloud Scan uses: SonarSource/sonarqube-scan-action@22918119ff8e1ca75a623e15c8296b6ea4fbe28f # v8 env: - SONAR_TOKEN: ${{ secrets.CICD_ORG_SONAR_TOKEN_CICD_BOT }} + SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} with: args: ${{ env.SONAR_ARGS }}