diff --git a/.github/workflows/sonar-ci-artifacts.yaml b/.github/workflows/sonar-ci-artifacts.yaml new file mode 100644 index 0000000000..b4c6313e15 --- /dev/null +++ b/.github/workflows/sonar-ci-artifacts.yaml @@ -0,0 +1,72 @@ +name: CI test report +on: + push: + branches: + - main + pull_request: + types: [opened, synchronize, reopened] + +jobs: + check-changes: + runs-on: ubuntu-latest + outputs: + code: ${{ steps.filter.outputs.code }} + steps: + - uses: actions/checkout@v4 + - uses: dorny/paths-filter@d1c1ffe0248fe513906c8e24db8ea791d46f8590 #v3.0.3 + id: filter + with: + filters: | + code: + - '!docs/**' + + build: + runs-on: ubuntu-latest + needs: check-changes + if: needs.check-changes.outputs.code == 'true' + steps: + + - name: Checkout repo + uses: actions/checkout@v4 + with: + # Disabling shallow clones is recommended for improving the relevancy of reporting + fetch-depth: 0 + + - name: Set up Go + uses: ./.github/actions/setup-go-kpt + with: + install-kpt: "false" + + - name: Run unit tests to generate coverage report + id: test + run: make test + + - name: Archive test results + if: steps.test.outcome == 'success' + uses: actions/upload-artifact@v4 + with: + name: coverage-report + path: ./coverage.out + + - name: Save PR number to file + if: github.event_name == 'pull_request' + run: echo ${{ github.event.number }} > PR_NUMBER.txt + + - name: Archive PR number + if: github.event_name == 'pull_request' + uses: actions/upload-artifact@v4 + with: + name: PR_NUMBER + path: PR_NUMBER.txt + + test-summary: + name: Test Summary + if: always() + needs: [check-changes, build] + runs-on: ubuntu-latest + steps: + - run: exit 1 + if: | + needs.check-changes.outputs.code == 'true' && + (needs.build.result == 'failure' || needs.build.result == 'cancelled') + - run: echo "Tests passed or skipped" diff --git a/.github/workflows/sonarcloud.yml b/.github/workflows/sonarcloud.yml new file mode 100644 index 0000000000..0038f9beec --- /dev/null +++ b/.github/workflows/sonarcloud.yml @@ -0,0 +1,138 @@ +name: SonarCloud analysis + +on: + workflow_run: + workflows: [CI test report] + types: [completed] + +jobs: + check-artifacts: + runs-on: ubuntu-latest + if: github.event.workflow_run.conclusion == 'success' + permissions: + actions: read + outputs: + has-artifacts: ${{ steps.check.outputs.has-artifacts }} + steps: + - name: Check for coverage artifact + id: check + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + with: + script: | + const artifacts = await github.rest.actions.listWorkflowRunArtifacts({ + owner: context.repo.owner, + repo: context.repo.repo, + run_id: ${{ github.event.workflow_run.id }} + }); + const hasCoverage = artifacts.data.artifacts.some(a => a.name === 'coverage-report'); + core.setOutput('has-artifacts', hasCoverage); + + sonarqube: + needs: check-artifacts + if: needs.check-artifacts.outputs.has-artifacts == 'true' + runs-on: ubuntu-latest + permissions: + actions: read + contents: read + pull-requests: read + steps: + - name: Download PR number artifact + if: github.event.workflow_run.event == 'pull_request' + uses: dawidd6/action-download-artifact@b6e2e70617bc3265edd6dab6c906732b2f1ae151 # v21 + with: + workflow: CI test report + run_id: ${{ github.event.workflow_run.id }} + name: PR_NUMBER + + - name: Read PR_NUMBER.txt + if: github.event.workflow_run.event == 'pull_request' + id: pr_number + uses: juliangruber/read-file-action@271ff311a4947af354c6abcd696a306553b9ec18 # v1.1.8 + with: + path: ./PR_NUMBER.txt + + - name: Request GitHub API for PR data + if: github.event.workflow_run.event == 'pull_request' + uses: octokit/request-action@b91aabaa861c777dcdb14e2387e30eddf04619ae # v3.0.0 + id: get_pr_data + with: + route: GET /repos/{full_name}/pulls/{number} + number: ${{ steps.pr_number.outputs.content }} + full_name: ${{ github.event.repository.full_name }} + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract PR metadata + if: github.event.workflow_run.event == 'pull_request' + id: pr_meta + env: + PR_DATA: ${{ steps.get_pr_data.outputs.data }} + run: | + echo "number=$(echo "$PR_DATA" | jq -r '.number')" >> "$GITHUB_OUTPUT" + echo "head_ref=$(echo "$PR_DATA" | jq -r '.head.ref')" >> "$GITHUB_OUTPUT" + echo "base_ref=$(echo "$PR_DATA" | jq -r '.base.ref')" >> "$GITHUB_OUTPUT" + + # Use SHA for checkout — immune to branch-name injection. + # allow-unsafe-pr-checkout is safe here: we only scan (no script execution + # from fork code) and persist-credentials is false. + - name: Checkout PR head + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + repository: ${{ github.event.workflow_run.head_repository.full_name }} + ref: ${{ github.event.workflow_run.head_sha }} + fetch-depth: 0 + persist-credentials: false + allow-unsafe-pr-checkout: true + + # Branch names passed via env (not expression interpolation in run:) + - name: Checkout base branch + if: github.event.workflow_run.event == 'pull_request' + env: + BASE_REF: ${{ steps.pr_meta.outputs.base_ref }} + HEAD_SHA: ${{ github.event.workflow_run.head_sha }} + CLONE_URL: ${{ github.event.repository.clone_url }} + run: | + git remote add upstream "$CLONE_URL" + git fetch upstream + git checkout -B "$BASE_REF" "upstream/$BASE_REF" + git checkout "$HEAD_SHA" + git clean -ffdx && git reset --hard HEAD + + - name: Download coverage artifact + uses: dawidd6/action-download-artifact@b6e2e70617bc3265edd6dab6c906732b2f1ae151 # v21 + with: + workflow: CI test report + run_id: ${{ github.event.workflow_run.id }} + name: coverage-report + use_unzip: true + + - name: Fix Go module paths in coverage + run: | + sed -i 's|github.com/kptdev/kpt|.|g' coverage.out + + - name: SonarQube Scan on PR + if: github.event.workflow_run.event == 'pull_request' + uses: SonarSource/sonarqube-scan-action@713881670b6b3676cda39549040e2d88c70d582e # v8.2.0 + env: + SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} + with: + args: + -Dsonar.projectKey=kptdev_kpt + -Dsonar.organization=kptdev + -Dproject.settings=sonar-project.properties + -Dsonar.pullrequest.key=${{ steps.pr_meta.outputs.number }} + -Dsonar.pullrequest.branch=${{ steps.pr_meta.outputs.head_ref }} + -Dsonar.pullrequest.base=${{ steps.pr_meta.outputs.base_ref }} + + - name: SonarCloud Scan on push + if: >- + github.event.workflow_run.event == 'push' && + github.event.workflow_run.head_repository.full_name == github.event.repository.full_name + uses: SonarSource/sonarqube-scan-action@713881670b6b3676cda39549040e2d88c70d582e # v8.2.0 + env: + SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} + with: + args: + -Dsonar.projectKey=kptdev_kpt + -Dsonar.organization=kptdev + -Dproject.settings=sonar-project.properties diff --git a/sonar-project.properties b/sonar-project.properties new file mode 100644 index 0000000000..dbcefa1d0e --- /dev/null +++ b/sonar-project.properties @@ -0,0 +1,21 @@ +# Required metadata +sonar.projectKey=kptdev_kpt +sonar.projectName=kpt +sonar.organization=kptdev + +sonar.language=go + +# Path to your Go source code +sonar.sources=pkg, func, commands, internal, run, api + +# Exclude files if needed +sonar.exclusions=**/test/**, **/examples/*, **/scripts/*, **/*_test.go, **/testing*, **/generated/**, **/testdata/**, **/*zz_generated.* + +# To include test coverage reports (optional) +#sonar.tests=./ +sonar.test.inclusions=**/*_test.go +sonar.coverage.exclusions=**/test/**, **/*_test.go, **/testing*, **/api/** +sonar.go.tests.reportPaths=report.xml +sonar.go.coverage.reportPaths=coverage.out +# To exclude duplicated blocks from CPD (Copy-Paste Detection) +sonar.cpd.exclusions=**/*_test.go, **/test/**, **/testing**, **/api/**