From 5a1a8952e5f8cb3a60da6eb1e5f9e54a554fec48 Mon Sep 17 00:00:00 2001 From: YJack0000 Date: Mon, 24 Aug 2026 01:49:06 +0800 Subject: [PATCH] [feature] leave a sticky Quality Gate comment on PRs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit After every scan the workflow now posts (or updates in place) one PR comment with the Quality Gate verdict, any failed conditions, the open issues on the PR, and a link to the dashboard — so nobody has to open the Sonar UI to learn how the gate went. Best-effort: a comment that could not be posted never turns a green scan red. --- .github/workflows/sonar.yml | 88 +++++++++++++++++++++++++++++++++++++ 1 file changed, 88 insertions(+) diff --git a/.github/workflows/sonar.yml b/.github/workflows/sonar.yml index 3c35234..9069399 100644 --- a/.github/workflows/sonar.yml +++ b/.github/workflows/sonar.yml @@ -12,6 +12,11 @@ name: Sonar # and exclusions live in sonar-project.properties, which the scanner picks up # with no extra flag. +# The comment step below writes to the PR; everything else only reads. +permissions: + contents: read + pull-requests: write + on: workflow_dispatch: pull_request: @@ -165,3 +170,86 @@ jobs: ` new_lines=${m.new_lines ?? "(absent)"} ncloc=${m.ncloc ?? "?"}`, ); ' + + # Leave the scan result on the PR itself, so nobody has to open the + # Sonar UI to learn how the gate went. Sticky: the same comment is + # updated in place on every push, keyed by the hidden marker, so the PR + # never fills up with stale reports. Best-effort by design + # (continue-on-error + `|| true`): a comment that could not be posted + # must never turn a green scan red. + - name: Comment the result on the PR + if: always() && github.event_name == 'pull_request' + continue-on-error: true + env: + SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} + GH_TOKEN: ${{ github.token }} + PROJECT_KEY: pathorsAI_patchbay + PR_NUMBER: ${{ github.event.pull_request.number }} + REPO: ${{ github.repository }} + run: | + API=https://sonar.pathors.com/api + curl -sS -u "$SONAR_TOKEN:" \ + "$API/qualitygates/project_status?projectKey=$PROJECT_KEY&pullRequest=$PR_NUMBER" \ + > "$RUNNER_TEMP/gate.json" || true + curl -sS -u "$SONAR_TOKEN:" \ + "$API/issues/search?componentKeys=$PROJECT_KEY&pullRequest=$PR_NUMBER&resolved=false&ps=10" \ + > "$RUNNER_TEMP/issues.json" || true + + node -e ' + const fs = require("fs"); + const read = (n) => { + try { return JSON.parse(fs.readFileSync(`${process.env.RUNNER_TEMP}/${n}`, "utf8")); } + catch { return null; } + }; + const key = process.env.PROJECT_KEY; + const pr = process.env.PR_NUMBER; + const url = `https://sonar.pathors.com/dashboard?id=${encodeURIComponent(key)}&pullRequest=${pr}`; + + const status = read("gate.json")?.projectStatus; + if (!status) process.exit(0); // nothing worth posting + + const icon = status.status === "OK" ? "✅" : "❌"; + const verdict = status.status === "OK" ? "passed" : status.status; + const lines = [ + ``, + `### ${icon} SonarQube Quality Gate ${verdict} — [${key}](${url})`, + "", + ]; + + const conds = (status.conditions ?? []).filter((c) => c.status !== "OK"); + if (conds.length) { + lines.push("| failed condition | value | threshold |", "|---|---|---|"); + for (const c of conds) { + lines.push(`| ${c.metricKey} | ${c.actualValue} | ${c.comparator === "GT" ? "≤" : "≥"} ${c.errorThreshold} |`); + } + lines.push(""); + } + + const issues = read("issues.json"); + if (issues?.total != null) { + const n = issues.total; + lines.push(`**${n} open issue${n === 1 ? "" : "s"} on this PR**${n ? ":" : "."}`); + for (const i of issues.issues ?? []) { + const file = (i.component ?? "").split(":").pop(); + const loc = i.line ? `${file}:${i.line}` : file; + lines.push(`- **${i.severity}** \`${i.rule}\` — ${i.message} (\`${loc}\`)`); + } + const shown = (issues.issues ?? []).length; + if (n > shown) lines.push(`- …and ${n - shown} more on the [dashboard](${url}).`); + } else { + lines.push(`Details on the [dashboard](${url}).`); + } + fs.writeFileSync(`${process.env.RUNNER_TEMP}/comment.md`, lines.join("\n") + "\n"); + ' || exit 0 + + [ -f "$RUNNER_TEMP/comment.md" ] || exit 0 + MARKER="" + EXISTING=$(gh api "repos/$REPO/issues/$PR_NUMBER/comments" --paginate \ + --jq ".[] | select(.body | startswith(\"$MARKER\")) | .id" 2>/dev/null | head -1) + if [ -n "$EXISTING" ]; then + gh api -X PATCH "repos/$REPO/issues/comments/$EXISTING" \ + -F "body=@$RUNNER_TEMP/comment.md" > /dev/null || true + else + gh api -X POST "repos/$REPO/issues/$PR_NUMBER/comments" \ + -F "body=@$RUNNER_TEMP/comment.md" > /dev/null || true + fi