diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2f385d7..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,6 +62,14 @@ jobs: flags: daemon token: ${{ secrets.CODECOV_TOKEN }} + - 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 + build: needs: lint-and-test strategy: diff --git a/.github/workflows/finalize.yml b/.github/workflows/finalize.yml new file mode 100644 index 0000000..fcb343f --- /dev/null +++ b/.github/workflows/finalize.yml @@ -0,0 +1,123 @@ +--- +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: + 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 + with: + name: sonar-coverage + path: packages/daemon/coverage + github-token: ${{ secrets.GITHUB_TOKEN }} + run-id: ${{ github.event.workflow_run.id }} + + - 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: | + set -euo pipefail + + 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: | + set -euo pipefail + + IFS="/" read -r REPO_OWNER REPO_NAME_ONLY <<< "${REPO_NAME}" + + # 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 + 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}" + + - 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.SONAR_TOKEN }} + with: + args: ${{ env.SONAR_ARGS }} diff --git a/sonar-project.properties b/sonar-project.properties new file mode 100644 index 0000000..1e5dae0 --- /dev/null +++ b/sonar-project.properties @@ -0,0 +1,20 @@ +# 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/ +# 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