|
| 1 | +name: SonarCloud Bulk Accept |
| 2 | + |
| 3 | +# Marks remaining open code smells in well-defined buckets as |
| 4 | +# Accepted (formerly "Won't Fix") with a deliberate comment, so the |
| 5 | +# Sonar issue list reflects only smells we actually want to act on. |
| 6 | +# |
| 7 | +# Trigger manually from the Actions tab — never runs on push/PR. The |
| 8 | +# rule + filter pairs are explicit; adding a new bucket means editing |
| 9 | +# this file (visible in PR review) rather than mass-suppressing in code. |
| 10 | +# |
| 11 | +# Each bucket sends ONE bulk_change call to the SonarCloud API: |
| 12 | +# POST /api/issues/bulk_change |
| 13 | +# issues=<comma-separated keys> |
| 14 | +# do_transition=accept |
| 15 | +# comment=<bucket-specific justification> |
| 16 | +# |
| 17 | +# Why "Accepted" and not "False Positive": these are real findings |
| 18 | +# under their respective rules — we just don't intend to act on them. |
| 19 | +# False Positive is reserved for cases where the rule has misfired |
| 20 | +# (only godre:S8239 in shutdown handling here qualifies). |
| 21 | + |
| 22 | +on: |
| 23 | + workflow_dispatch: |
| 24 | + inputs: |
| 25 | + dry_run: |
| 26 | + description: "Print buckets and counts without calling the API" |
| 27 | + type: boolean |
| 28 | + default: true |
| 29 | + |
| 30 | +jobs: |
| 31 | + bulk-accept: |
| 32 | + runs-on: ubuntu-latest |
| 33 | + permissions: |
| 34 | + contents: read |
| 35 | + env: |
| 36 | + SONAR_HOST: https://sonarcloud.io |
| 37 | + SONAR_PROJECT: RandomCodeSpace_ctm |
| 38 | + SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} |
| 39 | + steps: |
| 40 | + - name: Verify SONAR_TOKEN |
| 41 | + run: | |
| 42 | + if [ -z "${SONAR_TOKEN}" ]; then |
| 43 | + echo "::error::SONAR_TOKEN secret is not set" |
| 44 | + exit 1 |
| 45 | + fi |
| 46 | +
|
| 47 | + - name: Install jq |
| 48 | + run: sudo apt-get update -qq && sudo apt-get install -y -qq jq |
| 49 | + |
| 50 | + - name: Process buckets |
| 51 | + env: |
| 52 | + DRY_RUN: ${{ inputs.dry_run }} |
| 53 | + run: | |
| 54 | + set -euo pipefail |
| 55 | +
|
| 56 | + # Helper: fetch all open CODE_SMELL issue keys matching a filter |
| 57 | + # (rule + optional path glob via componentKeys). Paginates. |
| 58 | + fetch_keys() { |
| 59 | + local rules="$1" |
| 60 | + local file_filter="${2:-}" |
| 61 | + local page=1 |
| 62 | + local keys=() |
| 63 | + while :; do |
| 64 | + local url="${SONAR_HOST}/api/issues/search?componentKeys=${SONAR_PROJECT}&types=CODE_SMELL&statuses=OPEN,CONFIRMED,REOPENED&rules=${rules}&ps=500&p=${page}" |
| 65 | + if [ -n "${file_filter}" ]; then |
| 66 | + url="${url}&files=${file_filter}" |
| 67 | + fi |
| 68 | + local resp |
| 69 | + resp=$(curl -sSf -u "${SONAR_TOKEN}:" "${url}") |
| 70 | + local batch |
| 71 | + batch=$(echo "${resp}" | jq -r '.issues[].key') |
| 72 | + if [ -z "${batch}" ]; then break; fi |
| 73 | + while IFS= read -r k; do keys+=("$k"); done <<< "${batch}" |
| 74 | + local total |
| 75 | + total=$(echo "${resp}" | jq -r '.total') |
| 76 | + local fetched=$(( page * 500 )) |
| 77 | + if [ "${fetched}" -ge "${total}" ]; then break; fi |
| 78 | + page=$(( page + 1 )) |
| 79 | + done |
| 80 | + (IFS=,; echo "${keys[*]}") |
| 81 | + } |
| 82 | +
|
| 83 | + # Helper: bulk-accept a set of keys with a comment. |
| 84 | + bulk_accept() { |
| 85 | + local label="$1" |
| 86 | + local keys="$2" |
| 87 | + local comment="$3" |
| 88 | + if [ -z "${keys}" ]; then |
| 89 | + echo "[${label}] no matching issues — skipping" |
| 90 | + return |
| 91 | + fi |
| 92 | + local count |
| 93 | + count=$(echo "${keys}" | tr ',' '\n' | wc -l) |
| 94 | + echo "[${label}] ${count} issues" |
| 95 | + if [ "${DRY_RUN}" = "true" ]; then |
| 96 | + echo "[${label}] DRY_RUN — not calling bulk_change" |
| 97 | + return |
| 98 | + fi |
| 99 | + # SonarCloud bulk_change accepts at most ~500 keys per call. |
| 100 | + # Split into chunks of 400 to stay safe. |
| 101 | + local chunk |
| 102 | + local rest="${keys}" |
| 103 | + while [ -n "${rest}" ]; do |
| 104 | + chunk=$(echo "${rest}" | cut -d',' -f1-400) |
| 105 | + rest=$(echo "${rest}" | cut -d',' -f401- || true) |
| 106 | + if [ "${rest}" = "${chunk}" ]; then rest=""; fi |
| 107 | + curl -sSf -u "${SONAR_TOKEN}:" -X POST \ |
| 108 | + --data-urlencode "issues=${chunk}" \ |
| 109 | + --data-urlencode "do_transition=accept" \ |
| 110 | + --data-urlencode "comment=${comment}" \ |
| 111 | + "${SONAR_HOST}/api/issues/bulk_change" > /dev/null |
| 112 | + done |
| 113 | + echo "[${label}] accepted" |
| 114 | + } |
| 115 | +
|
| 116 | + # ───────────────────────────────────────────────────────────── |
| 117 | + # Bucket 1: typescript:S6759 — "Mark props as read-only" |
| 118 | + # The codebase deliberately does not adopt Readonly<Props>; this |
| 119 | + # is a project-wide style choice, not a per-component miss. |
| 120 | + KEYS=$(fetch_keys "typescript:S6759") |
| 121 | + bulk_accept "S6759 readonly-props" "${KEYS}" \ |
| 122 | + "Project style: props interfaces are not wrapped in Readonly<>. Deliberate — accepted." |
| 123 | +
|
| 124 | + # Bucket 2: typescript:S6819 — "Use <output> instead of role=status" |
| 125 | + # The role=status pattern is acceptable and used consistently |
| 126 | + # for transient status text; <output> is not adopted project-wide. |
| 127 | + KEYS=$(fetch_keys "typescript:S6819") |
| 128 | + bulk_accept "S6819 role-status" "${KEYS}" \ |
| 129 | + "role=status is the established pattern for transient status text; <output> not adopted. Accepted." |
| 130 | +
|
| 131 | + # Bucket 3: typescript:S3358 — nested ternaries |
| 132 | + # All remaining occurrences are inline JSX render expressions |
| 133 | + # where extracting helpers would harm readability. |
| 134 | + KEYS=$(fetch_keys "typescript:S3358") |
| 135 | + bulk_accept "S3358 nested-ternary" "${KEYS}" \ |
| 136 | + "Inline JSX render — extracting a helper hurts readability more than the nesting. Accepted." |
| 137 | +
|
| 138 | + # Bucket 4: typescript:S6571 — redundant union members |
| 139 | + # Most are deliberate "string | undefined" / "T | null" shapes |
| 140 | + # used as explicit escape hatches at API boundaries. |
| 141 | + KEYS=$(fetch_keys "typescript:S6571") |
| 142 | + bulk_accept "S6571 redundant-type" "${KEYS}" \ |
| 143 | + "Union members are intentional escape hatches at API boundaries. Accepted." |
| 144 | +
|
| 145 | + # Bucket 5: typescript:S6754 — useState destructuring style |
| 146 | + # The chosen form (no destructuring of the setter) is intentional |
| 147 | + # in a couple of one-shot setters; not worth churn. |
| 148 | + KEYS=$(fetch_keys "typescript:S6754") |
| 149 | + bulk_accept "S6754 useState-style" "${KEYS}" \ |
| 150 | + "Chosen form is intentional for these one-shot setters. Accepted." |
| 151 | +
|
| 152 | + # Bucket 6: typescript:S6479 — array-index keys |
| 153 | + # Used only where the list is statically ordered (timestamps in |
| 154 | + # row keys, doctor checks). React reconciliation is unaffected. |
| 155 | + KEYS=$(fetch_keys "typescript:S6479") |
| 156 | + bulk_accept "S6479 array-index-key" "${KEYS}" \ |
| 157 | + "Lists are append-only with stable per-row prefixes; index suffix is fine. Accepted." |
| 158 | +
|
| 159 | + # Bucket 7: typescript:S3735 — `void` operator |
| 160 | + # We use `void` to discard an awaited Promise result intentionally |
| 161 | + # (fire-and-forget within useEffect / event handlers). |
| 162 | + KEYS=$(fetch_keys "typescript:S3735") |
| 163 | + bulk_accept "S3735 void-operator" "${KEYS}" \ |
| 164 | + "Fire-and-forget Promise in event handler / useEffect; void is the documented escape. Accepted." |
| 165 | +
|
| 166 | + # Bucket 8: typescript:S1874 + javascript:S1874 — use of deprecated APIs |
| 167 | + # The deprecations flagged are in third-party libs (react-router 6→7 |
| 168 | + # transition residue) where the migration target also fires Sonar. |
| 169 | + KEYS=$(fetch_keys "typescript:S1874,javascript:S1874") |
| 170 | + bulk_accept "S1874 deprecation" "${KEYS}" \ |
| 171 | + "Deprecation is in transitional library API; migration tracked separately. Accepted." |
| 172 | +
|
| 173 | + # Bucket 9: typescript:S7763 — re-export shorthand |
| 174 | + # Existing shape is more grep-able for the codebase's small surface; |
| 175 | + # the rule's preferred form is fine but not worth churn. |
| 176 | + KEYS=$(fetch_keys "typescript:S7763") |
| 177 | + bulk_accept "S7763 export-from" "${KEYS}" \ |
| 178 | + "Existing form is intentional for symbol grep clarity. Accepted." |
| 179 | +
|
| 180 | + # Bucket 10: typescript:S7718 — prefer Set#has over Array#includes |
| 181 | + # Inputs are O(<10) — Set construction overhead exceeds savings. |
| 182 | + KEYS=$(fetch_keys "typescript:S7718") |
| 183 | + bulk_accept "S7718 set-has" "${KEYS}" \ |
| 184 | + "Lookup arrays have <10 elements; Array#includes is faster. Accepted." |
| 185 | +
|
| 186 | + # Bucket 11: typescript:S6772 — "ambiguous spacing" |
| 187 | + # Remaining occurrences are inside <code>/<span> tag trees where |
| 188 | + # the chosen form is intentional. Reliability-impact ones already fixed. |
| 189 | + KEYS=$(fetch_keys "typescript:S6772") |
| 190 | + bulk_accept "S6772 ambiguous-spacing" "${KEYS}" \ |
| 191 | + "Spacing is intentional inside the affected text/code spans. Accepted." |
| 192 | +
|
| 193 | + # Bucket 12: godre:S8205 — named struct types |
| 194 | + # Anonymous struct types are intentional in test scaffolding and |
| 195 | + # request-decode shapes that aren't reused. |
| 196 | + KEYS=$(fetch_keys "godre:S8205") |
| 197 | + bulk_accept "S8205 named-struct" "${KEYS}" \ |
| 198 | + "One-shot decode/scratch structs; naming would scatter the type. Accepted." |
| 199 | +
|
| 200 | + # Bucket 13: godre:S8196 — interface naming |
| 201 | + # Existing names are domain-aligned (InputSessionSource, ProjRefresher). |
| 202 | + # Renaming would touch a wide blast radius for a stylistic nit. |
| 203 | + KEYS=$(fetch_keys "godre:S8196") |
| 204 | + bulk_accept "S8196 interface-name" "${KEYS}" \ |
| 205 | + "Names are domain-aligned and tested; rename has too broad a blast radius. Accepted." |
| 206 | +
|
| 207 | + # Bucket 14: godre:S8193 — receiver naming |
| 208 | + # Receiver names are short and consistent within each type; |
| 209 | + # the rule's "first-letter" preference doesn't add value here. |
| 210 | + KEYS=$(fetch_keys "godre:S8193") |
| 211 | + bulk_accept "S8193 receiver-name" "${KEYS}" \ |
| 212 | + "Receiver names are consistent within each type. Accepted." |
| 213 | +
|
| 214 | + # Bucket 15: godre:S8242 — context.Context as struct field |
| 215 | + # Used in a long-lived daemon component where ctx genuinely lives |
| 216 | + # on the struct (cancellation propagates through the lifecycle). |
| 217 | + KEYS=$(fetch_keys "godre:S8242") |
| 218 | + bulk_accept "S8242 ctx-field" "${KEYS}" \ |
| 219 | + "Daemon-scoped ctx travels with the struct's lifecycle. Accepted." |
| 220 | +
|
| 221 | + # Bucket 16: go:S107 + go:S117 — too many params / variable name |
| 222 | + # Existing shape mirrors HTTP handler / cobra signatures. |
| 223 | + KEYS=$(fetch_keys "go:S107,go:S117") |
| 224 | + bulk_accept "S107/S117 signature" "${KEYS}" \ |
| 225 | + "Signature mirrors handler / cobra contracts. Accepted." |
| 226 | +
|
| 227 | + # Bucket 17: typescript:S6582 — optional chain |
| 228 | + # Already fixed where applicable; remaining are intentional |
| 229 | + # truthiness checks (e.g. `&& obj.field` where obj is required). |
| 230 | + KEYS=$(fetch_keys "typescript:S6582") |
| 231 | + bulk_accept "S6582 optional-chain" "${KEYS}" \ |
| 232 | + "Remaining occurrences are intentional truthiness checks on required fields. Accepted." |
| 233 | +
|
| 234 | + # Bucket 18: typescript:S4624 — nested template literals |
| 235 | + # Used for compact JSX label composition; collapsing harms clarity. |
| 236 | + KEYS=$(fetch_keys "typescript:S4624") |
| 237 | + bulk_accept "S4624 nested-template" "${KEYS}" \ |
| 238 | + "Compact JSX label composition; collapsing harms clarity. Accepted." |
| 239 | +
|
| 240 | + # Bucket 19: typescript:S6822 — implicit list role (remaining only) |
| 241 | + # Reliability-impact occurrences fixed in code; remaining list |
| 242 | + # elements are inside scrollable card bodies where the parent |
| 243 | + # treats them as decorative. |
| 244 | + KEYS=$(fetch_keys "typescript:S6822") |
| 245 | + bulk_accept "S6822 implicit-list" "${KEYS}" \ |
| 246 | + "Remaining list elements are decorative within scrollable card bodies. Accepted." |
| 247 | +
|
| 248 | + # Bucket 20: typescript:S1871 — duplicate case body |
| 249 | + # The duplicate clauses document distinct semantic categories |
| 250 | + # that happen to dispatch to the same code path. |
| 251 | + KEYS=$(fetch_keys "typescript:S1871") |
| 252 | + bulk_accept "S1871 duplicate-case" "${KEYS}" \ |
| 253 | + "Cases document distinct semantic categories sharing one code path. Accepted." |
| 254 | +
|
| 255 | + # ───────────────────────────────────────────────────────────── |
| 256 | + # Bucket 21: ALL remaining smells in *_test.go / *.test.ts(x) |
| 257 | + # Test code is intentionally dense (table-driven cases, mock |
| 258 | + # plumbing, deep ternaries to express expected outputs). The |
| 259 | + # cognitive-complexity / readonly / etc. rules are noise here. |
| 260 | + test_keys=$(curl -sSf -u "${SONAR_TOKEN}:" \ |
| 261 | + "${SONAR_HOST}/api/issues/search?componentKeys=${SONAR_PROJECT}&types=CODE_SMELL&statuses=OPEN,CONFIRMED,REOPENED&ps=500" \ |
| 262 | + | jq -r '.issues[] | select(.component | test("(_test\\.go|\\.test\\.tsx?)$")) | .key' \ |
| 263 | + | paste -sd, -) |
| 264 | + bulk_accept "test-file smells" "${test_keys}" \ |
| 265 | + "Test code: table-driven density / mock plumbing / explicit ternaries are by design. Accepted." |
| 266 | +
|
| 267 | + # ───────────────────────────────────────────────────────────── |
| 268 | + # FALSE POSITIVE bucket — the rule has misfired here. |
| 269 | + # Keep this list explicit; do not append-only. |
| 270 | + fp_keys=$(fetch_keys "godre:S8239") |
| 271 | + if [ -n "${fp_keys}" ]; then |
| 272 | + count=$(echo "${fp_keys}" | tr ',' '\n' | wc -l) |
| 273 | + echo "[S8239 false-positive] ${count} issues" |
| 274 | + if [ "${DRY_RUN}" != "true" ]; then |
| 275 | + curl -sSf -u "${SONAR_TOKEN}:" -X POST \ |
| 276 | + --data-urlencode "issues=${fp_keys}" \ |
| 277 | + --data-urlencode "do_transition=falsepositive" \ |
| 278 | + --data-urlencode "comment=Shutdown handler: the parent ctx is already Done at this point (we just received from <-ctx.Done()), so deriving from it would give a zero-grace shutdown. context.Background() is required for the grace deadline." \ |
| 279 | + "${SONAR_HOST}/api/issues/bulk_change" > /dev/null |
| 280 | + echo "[S8239 false-positive] marked" |
| 281 | + fi |
| 282 | + fi |
| 283 | +
|
| 284 | + - name: Summary |
| 285 | + if: always() |
| 286 | + run: | |
| 287 | + echo "Run with dry_run=false to actually apply the transitions." |
| 288 | + echo "Re-run after the next Sonar scan to clean up any new findings in the same buckets." |
0 commit comments