ci(DATAGO-120951): add sonarqube scanning for plugins (#82) #254
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: CI | |
| on: | |
| push: | |
| branches: | |
| - main | |
| pull_request: | |
| types: [opened, synchronize, reopened] | |
| permissions: | |
| contents: write | |
| pull-requests: write | |
| actions: write | |
| statuses: write | |
| checks: write | |
| repository-projects: read | |
| id-token: write | |
| concurrency: | |
| group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} | |
| cancel-in-progress: ${{ github.ref_name != github.event.repository.default_branch }} | |
| jobs: | |
| # Validate conventional commits for PRs | |
| validate-conventional-commit: | |
| name: "Validate Conventional Commit" | |
| runs-on: ubuntu-latest | |
| if: github.event_name == 'pull_request' && github.event.pull_request.user.login != 'dependabot[bot]' | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 | |
| with: | |
| fetch-depth: 0 | |
| - name: Validate PR Title | |
| uses: amannn/action-semantic-pull-request@e32d7e603df1aa1ba07e981f2a23455dee596825 # v5 | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| with: | |
| types: | | |
| feat | |
| fix | |
| docs | |
| style | |
| refactor | |
| perf | |
| test | |
| build | |
| ci | |
| chore | |
| deps | |
| revert | |
| requireScope: false | |
| disallowScopes: | | |
| release | |
| subjectPattern: ^.+$ | |
| subjectPatternError: | | |
| The subject "{subject}" found in the pull request title "{title}" | |
| didn't match the configured pattern. Please ensure that the subject | |
| is not empty. | |
| # Label PR based on changed files and determine which plugins to build | |
| label-pr: | |
| runs-on: ubuntu-24.04 | |
| outputs: | |
| all_plugins: ${{ steps.format-labels.outputs.all-plugins }} | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 | |
| with: | |
| fetch-depth: 0 | |
| - name: Label PR based on changes | |
| id: label-pr | |
| if: github.event_name == 'pull_request' | |
| uses: actions/labeler@8558fd74291d67161a8a78ce36a881fa63b766a9 # v5.0.0 | |
| with: | |
| repo-token: ${{ secrets.GITHUB_TOKEN }} | |
| sync-labels: true | |
| configuration-path: .github/pr_labeler.yaml | |
| - name: Get changed plugins in PR | |
| id: changed-pr | |
| if: github.event_name == 'pull_request' | |
| run: | | |
| # Get all changed files in the PR by comparing against the base branch | |
| # This compares the merge base to the PR head, capturing ALL changes in the PR | |
| BASE_SHA="${{ github.event.pull_request.base.sha }}" | |
| HEAD_SHA="${{ github.event.pull_request.head.sha }}" | |
| echo "Base SHA: $BASE_SHA" | |
| echo "Head SHA: $HEAD_SHA" | |
| # Get changed files between base and head of PR | |
| CHANGED_FILES=$(git diff --name-only "$BASE_SHA" "$HEAD_SHA") | |
| echo "Changed files in PR:" | |
| echo "$CHANGED_FILES" | |
| # Extract plugin directories (top-level directories starting with sam-) | |
| PLUGINS=$(echo "$CHANGED_FILES" | grep -E '^sam-[^/]+/' | cut -d'/' -f1 | sort -u | tr '\n' ',' | sed 's/,$//') | |
| echo "Changed plugins: $PLUGINS" | |
| echo "plugins=$PLUGINS" >> $GITHUB_OUTPUT | |
| - name: Get changed plugins on push | |
| id: changed-push | |
| if: github.event_name == 'push' | |
| run: | | |
| # Get changed files between HEAD and HEAD~1 | |
| CHANGED_FILES=$(git diff --name-only HEAD~1 HEAD 2>/dev/null || git diff --name-only $(git rev-list --max-parents=0 HEAD) HEAD) | |
| echo "Changed files: $CHANGED_FILES" | |
| # Extract plugin directories (top-level directories starting with sam-) | |
| PLUGINS=$(echo "$CHANGED_FILES" | grep -E '^sam-[^/]+/' | cut -d'/' -f1 | sort -u | tr '\n' ',' | sed 's/,$//') | |
| echo "Changed plugins: $PLUGINS" | |
| echo "plugins=$PLUGINS" >> $GITHUB_OUTPUT | |
| - name: Format plugins for matrix | |
| id: format-labels | |
| run: | | |
| if [ "${{ github.event_name }}" == "pull_request" ]; then | |
| # Use changed plugins detected from PR diff | |
| PLUGINS="${{ steps.changed-pr.outputs.plugins }}" | |
| else | |
| # Use changed plugins from push | |
| PLUGINS="${{ steps.changed-push.outputs.plugins }}" | |
| fi | |
| echo "Changed plugins: $PLUGINS" | |
| # Convert comma-separated list to JSON array of objects | |
| if [ -z "$PLUGINS" ]; then | |
| echo "all-plugins=[]" >> $GITHUB_OUTPUT | |
| exit 0 | |
| fi | |
| JSON="[" | |
| FIRST=true | |
| IFS=',' read -ra PLUGIN_ARRAY <<< "$PLUGINS" | |
| for plugin in "${PLUGIN_ARRAY[@]}"; do | |
| # Trim whitespace | |
| plugin=$(echo "$plugin" | xargs) | |
| if [ -z "$plugin" ]; then | |
| continue | |
| fi | |
| if [ "$FIRST" = true ]; then | |
| FIRST=false | |
| else | |
| JSON="$JSON," | |
| fi | |
| JSON="$JSON{\"plugin_directory\":\"$plugin\"}" | |
| done | |
| JSON="$JSON]" | |
| echo "Generated JSON: $JSON" | |
| echo "all-plugins=$JSON" >> $GITHUB_OUTPUT | |
| # Build and test each plugin that changed | |
| builds: | |
| needs: label-pr | |
| if: needs.label-pr.outputs.all_plugins != '[]' | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: ${{ fromJSON(needs.label-pr.outputs.all_plugins) }} | |
| name: Build Plugin - ${{ matrix.plugin_directory }} | |
| uses: ./.github/workflows/build-plugin.yaml | |
| with: | |
| plugin_directory: ${{ matrix.plugin_directory }} | |
| secrets: | |
| COMMIT_KEY: ${{ secrets.COMMIT_KEY }} | |
| FOSSA_API_KEY: ${{ secrets.FOSSA_API_KEY }} | |
| SONARQUBE_TOKEN: ${{ secrets.SONARQUBE_TOKEN }} | |
| SONARQUBE_HOST_URL: ${{ secrets.SONARQUBE_HOST_URL }} | |
| # Aggregate SonarQube Quality Gate results for all plugins | |
| sonarqube-quality-gate: | |
| name: SonarQube Quality Gate | |
| needs: [label-pr, builds] | |
| if: always() && github.event_name == 'pull_request' && needs.label-pr.outputs.all_plugins != '[]' | |
| runs-on: ubuntu-latest | |
| permissions: | |
| pull-requests: write | |
| checks: write | |
| steps: | |
| - name: Create Check Run | |
| id: create_check | |
| uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7.1.0 | |
| with: | |
| script: | | |
| const headSha = '${{ github.event.pull_request.head.sha }}'; | |
| console.log(`Creating SonarQube Quality Gate check run for SHA: ${headSha}`); | |
| const { data: checkRun } = await github.rest.checks.create({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| name: 'SonarQube Quality Gate', | |
| head_sha: headSha, | |
| status: 'in_progress', | |
| started_at: new Date().toISOString(), | |
| output: { | |
| title: 'SonarQube Quality Gate Check', | |
| summary: 'Checking quality gate status for all modified plugins...' | |
| } | |
| }); | |
| console.log(`Check run created with ID: ${checkRun.id}`); | |
| core.setOutput('check_run_id', checkRun.id); | |
| return checkRun.id; | |
| - name: Aggregate Quality Gate Results | |
| id: aggregate | |
| uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7.1.0 | |
| env: | |
| SONARQUBE_HOST_URL: ${{ secrets.SONARQUBE_HOST_URL }} | |
| with: | |
| script: | | |
| const plugins = JSON.parse('${{ needs.label-pr.outputs.all_plugins }}'); | |
| const prNumber = context.payload.pull_request.number; | |
| const headSha = '${{ github.event.pull_request.head.sha }}'; | |
| const sonarHostUrl = process.env.SONARQUBE_HOST_URL || 'https://sonarq.solace.com'; | |
| console.log(`Processing ${plugins.length} plugins`); | |
| console.log(`Head SHA: ${headSha}`); | |
| // Get all check runs for this commit | |
| const { data: checkRuns } = await github.rest.checks.listForRef({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| ref: headSha, | |
| per_page: 100 | |
| }); | |
| console.log(`Found ${checkRuns.check_runs.length} check runs total`); | |
| let allPassed = true; | |
| let pluginResults = []; | |
| for (const pluginObj of plugins) { | |
| const plugin = pluginObj.plugin_directory; | |
| const checkName = `SonarQube: ${plugin}`; | |
| // Find the check run for this plugin | |
| const checkRun = checkRuns.check_runs.find(run => run.name === checkName); | |
| let status = 'unknown'; | |
| let statusEmoji = '❓'; | |
| if (checkRun) { | |
| console.log(`Plugin: ${plugin}, Check conclusion: ${checkRun.conclusion}`); | |
| if (checkRun.conclusion === 'success') { | |
| status = 'passed'; | |
| statusEmoji = '✅'; | |
| } else if (checkRun.conclusion === 'failure') { | |
| status = 'failed'; | |
| statusEmoji = '❌'; | |
| allPassed = false; | |
| } else if (checkRun.conclusion === 'neutral' || checkRun.conclusion === 'skipped') { | |
| status = checkRun.conclusion; | |
| statusEmoji = '⏭️'; | |
| } else { | |
| status = checkRun.conclusion || 'unknown'; | |
| statusEmoji = '⚠️'; | |
| } | |
| } else { | |
| console.log(`Check run not found for ${plugin}`); | |
| statusEmoji = '⚠️'; | |
| } | |
| const sonarUrl = `${sonarHostUrl}dashboard?id=${context.repo.owner}_${plugin}&pullRequest=${prNumber}`; | |
| pluginResults.push({ | |
| plugin: plugin, | |
| status: status, | |
| statusEmoji: statusEmoji, | |
| url: sonarUrl | |
| }); | |
| console.log(`Plugin: ${plugin}, Status: ${status}`); | |
| } | |
| core.setOutput('all_passed', allPassed); | |
| core.setOutput('plugin_results', JSON.stringify(pluginResults)); | |
| return { allPassed, pluginResults }; | |
| - name: Update Check Run | |
| if: always() | |
| uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7.1.0 | |
| with: | |
| script: | | |
| const checkRunId = '${{ steps.create_check.outputs.check_run_id }}'; | |
| const allPassed = '${{ steps.aggregate.outputs.all_passed }}' === 'true'; | |
| const pluginResults = JSON.parse('${{ steps.aggregate.outputs.plugin_results }}'); | |
| console.log(`Updating check run ID: ${checkRunId}`); | |
| console.log(`All passed: ${allPassed}`); | |
| const conclusion = allPassed ? 'success' : 'failure'; | |
| const summary = allPassed | |
| ? '✅ All plugins passed SonarQube quality gate' | |
| : '❌ Some plugins failed SonarQube quality gate'; | |
| let outputText = '## SonarQube Quality Gate Results\n\n'; | |
| outputText += '| Plugin | Status | SonarQube |\n'; | |
| outputText += '|--------|--------|----------|\n'; | |
| for (const result of pluginResults) { | |
| const statusText = result.status === 'passed' ? 'Passed' : | |
| result.status === 'failed' ? 'Failed' : | |
| result.status.charAt(0).toUpperCase() + result.status.slice(1); | |
| outputText += `| \`${result.plugin}\` | ${result.statusEmoji} ${statusText} | [View Results](${result.url}) |\n`; | |
| } | |
| await github.rest.checks.update({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| check_run_id: checkRunId, | |
| status: 'completed', | |
| conclusion: conclusion, | |
| completed_at: new Date().toISOString(), | |
| output: { | |
| title: 'SonarQube Quality Gate Check', | |
| summary: summary, | |
| text: outputText | |
| } | |
| }); | |
| console.log(`Check run completed with conclusion: ${conclusion}`); | |
| - name: Comment on PR | |
| if: always() | |
| uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7.1.0 | |
| with: | |
| script: | | |
| const pluginResults = JSON.parse('${{ steps.aggregate.outputs.plugin_results }}'); | |
| const allPassed = '${{ steps.aggregate.outputs.all_passed }}' === 'true'; | |
| const prNumber = context.payload.pull_request.number; | |
| const header = allPassed | |
| ? '## ✅ SonarQube Quality Gate - All Passed' | |
| : '## ❌ SonarQube Quality Gate - Issues Found'; | |
| let tableRows = []; | |
| for (const result of pluginResults) { | |
| const statusText = result.status === 'passed' ? 'Passed' : | |
| result.status === 'failed' ? 'Failed' : | |
| result.status.charAt(0).toUpperCase() + result.status.slice(1); | |
| tableRows.push(`| \`${result.plugin}\` | ${result.statusEmoji} ${statusText} | [See analysis details on SonarQube](${result.url}) |`); | |
| } | |
| const body = [ | |
| header, | |
| '', | |
| '| Plugin | Quality Gate Status | Analysis |', | |
| '|--------|-------------------|----------|', | |
| ...tableRows, | |
| '', | |
| '---', | |
| '*Quality gate checks are run for each modified plugin. Click the SonarQube links above for detailed analysis.*' | |
| ].join('\n'); | |
| // Find and delete existing SonarQube comment | |
| const { data: comments } = await github.rest.issues.listComments({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: prNumber, | |
| }); | |
| const existingComment = comments.find(comment => | |
| comment.user.type === 'Bot' && | |
| comment.body.includes('SonarQube Quality Gate') | |
| ); | |
| if (existingComment) { | |
| console.log(`Deleting existing comment ${existingComment.id}`); | |
| await github.rest.issues.deleteComment({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| comment_id: existingComment.id | |
| }); | |
| } | |
| // Create new comment | |
| console.log(`Creating new SonarQube comment on PR #${prNumber}`); | |
| await github.rest.issues.createComment({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: prNumber, | |
| body: body | |
| }); | |
| # FOSSA scan for the entire repository (on main branch) | |
| fossa-scan: | |
| name: FOSSA Scan | |
| runs-on: ubuntu-latest | |
| if: github.ref_name == github.event.repository.default_branch | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 | |
| with: | |
| fetch-depth: 0 | |
| - name: FOSSA Analyze and Test | |
| uses: SolaceDev/solace-public-workflows/.github/actions/sca/sca-scan@main | |
| continue-on-error: true | |
| with: | |
| scanners: "fossa" | |
| additional_scan_params: | | |
| fossa.branch=${{ github.ref_name }} | |
| fossa.revision=${{ github.sha }} | |
| fossa_api_key: ${{ secrets.FOSSA_API_KEY }} | |
| - name: FOSSA Guard - Block on Licensing Violations | |
| uses: SolaceDev/solace-public-workflows/.github/actions/fossa-guard@main | |
| continue-on-error: true | |
| with: | |
| fossa_api_key: ${{ secrets.FOSSA_API_KEY }} | |
| fossa_project_id: "${{ github.repository_owner }}_${{ github.event.repository.name }}" | |
| fossa_branch: ${{ github.ref_name }} | |
| fossa_revision: ${{ github.sha }} | |
| fossa_category: licensing | |
| fossa_mode: BLOCK | |
| block_on: policy_conflict | |
| - name: FOSSA Guard - Block on Vulnerability Violations | |
| uses: SolaceDev/solace-public-workflows/.github/actions/fossa-guard@main | |
| continue-on-error: true | |
| with: | |
| fossa_api_key: ${{ secrets.FOSSA_API_KEY }} | |
| fossa_project_id: "${{ github.repository_owner }}_${{ github.event.repository.name }}" | |
| fossa_branch: ${{ github.ref_name }} | |
| fossa_revision: ${{ github.sha }} | |
| fossa_category: vulnerability | |
| fossa_mode: BLOCK | |
| block_on: critical,high | |
| # CI Status check - aggregates all job results | |
| ci-status: | |
| name: CI Status | |
| runs-on: ubuntu-latest | |
| needs: [label-pr, builds, sonarqube-quality-gate] | |
| if: always() | |
| steps: | |
| - name: Check CI status | |
| run: | | |
| if [[ "${{ needs.label-pr.outputs.all_plugins }}" == "[]" ]]; then | |
| echo "No plugins changed, skipping build and quality checks" | |
| elif [[ "${{ needs.builds.result }}" == "success" || "${{ needs.builds.result }}" == "skipped" ]]; then | |
| echo "Build jobs passed or were skipped" | |
| # Check SonarQube quality gate for PRs | |
| if [[ "${{ github.event_name }}" == "pull_request" ]]; then | |
| if [[ "${{ needs.sonarqube-quality-gate.result }}" == "success" || "${{ needs.sonarqube-quality-gate.result }}" == "skipped" ]]; then | |
| echo "SonarQube quality gate passed or was skipped" | |
| else | |
| echo "SonarQube quality gate failed" | |
| exit 1 | |
| fi | |
| fi | |
| else | |
| echo "Build jobs failed" | |
| exit 1 | |
| fi | |
| echo "All CI checks passed!" |