Repository navigation
feat: implement issue #439 — [Fleet Monitor] petry-projects/TalkTerm — .github/workflows/sonarcloud.yml #441
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 1 commit
74205dd
eded3a1
202ed8c
9dd2f0f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,112 @@ | ||
| #!/usr/bin/env bash | ||
| # Regression guard for sonarcloud.yml. Locks the flaky-scan resilience invariant | ||
| # whose loss inflates the Fleet Monitor failure rate (#439): | ||
| # | ||
| # The SonarCloud analysis endpoint is occasionally flaky, so a single transient | ||
| # blip must not fail the job. sonarcloud.yml mitigates this — per its own header | ||
| # convention — by running the scan with `continue-on-error: true` and following | ||
| # it with a retry step guarded on the first scan's `outcome == 'failure'`. If | ||
| # that pair silently drifts away (someone deletes the retry, or drops | ||
| # continue-on-error), every transient blip becomes a hard failure again and the | ||
| # workflow degrades. This guard fails the build the moment that happens. | ||
| # | ||
| # It also asserts every third-party action stays SHA-pinned (never a mutable tag), | ||
| # matching the workflow's own "SHA-pin every third-party action" convention. | ||
| # | ||
| # Accepts an optional workflow path (default: .github/workflows/sonarcloud.yml) so | ||
| # the checks can be exercised against fixtures — see test-sonarcloud-workflow.test.sh. | ||
| # Run: bash scripts/test-sonarcloud-workflow.sh | ||
| set -euo pipefail | ||
|
|
||
| WORKFLOW="${1:-.github/workflows/sonarcloud.yml}" | ||
| SCAN_ACTION="SonarSource/sonarqube-scan-action" | ||
| PASS=true | ||
|
|
||
| echo "=== test-sonarcloud-workflow ===" | ||
|
|
||
| # ── Check 0: yq is available ─────────────────────────────────────────────── | ||
| if ! command -v yq &> /dev/null; then | ||
| echo "FAIL: 'yq' is required to parse YAML safely but was not found." | ||
| exit 1 | ||
| fi | ||
| echo "PASS: 'yq' is available" | ||
|
|
||
| # ── Check 1: file exists ─────────────────────────────────────────────────── | ||
| if [[ ! -f "$WORKFLOW" ]]; then | ||
| echo "FAIL: $WORKFLOW not found" | ||
| exit 1 | ||
| fi | ||
| echo "PASS: $WORKFLOW exists" | ||
|
|
||
| # ── Check 2: the primary scan runs with continue-on-error: true ──────────── | ||
| # The primary scan is the SonarSource scan step marked continue-on-error so a | ||
| # transient endpoint blip is swallowed rather than failing the job outright. | ||
| if ! primary_ce=$(yq " | ||
| [ .jobs[].steps[] | ||
| | select((.uses // \"\") | test(\"^${SCAN_ACTION}@\")) | ||
| | select(.[\"continue-on-error\"] == true) ] | length | ||
| " "$WORKFLOW" 2>/dev/null); then | ||
| echo "FAIL: yq failed to parse $WORKFLOW. Please check if it is valid YAML." | ||
| exit 1 | ||
| fi | ||
| if [[ "$primary_ce" -lt 1 ]]; then | ||
| echo "FAIL: no '${SCAN_ACTION}' scan step with 'continue-on-error: true' in $WORKFLOW" | ||
|
Check warning on line 53 in scripts/test-sonarcloud-workflow.sh
|
||
| echo " The first scan must be continue-on-error so a flaky endpoint blip" | ||
|
Check warning on line 54 in scripts/test-sonarcloud-workflow.sh
|
||
| echo " does not fail the job before the retry runs." | ||
| PASS=false | ||
| else | ||
| echo "PASS: primary scan step is 'continue-on-error: true' in $WORKFLOW" | ||
|
Check warning on line 58 in scripts/test-sonarcloud-workflow.sh
|
||
| fi | ||
|
|
||
| # ── Check 3: a retry scan step is guarded on the primary's failure ───────── | ||
| # The retry re-runs the scan only when the primary reported failure, expressed as | ||
| # `steps.<id>.outcome == 'failure'` in its `if:` condition. This is what turns a | ||
| # single flaky blip into a self-healing run instead of a red build. | ||
| if ! retry_guards=$(yq " | ||
| [ .jobs[].steps[] | ||
| | select((.uses // \"\") | test(\"^${SCAN_ACTION}@\")) | ||
| | select((.[\"continue-on-error\"] // false) != true) | ||
| | select((.if // \"\") | test(\"outcome *== *'failure'\")) ] | length | ||
|
coderabbitai[bot] marked this conversation as resolved.
Outdated
don-petry marked this conversation as resolved.
Outdated
|
||
| " "$WORKFLOW" 2>/dev/null); then | ||
| echo "FAIL: yq failed to parse $WORKFLOW. Please check if it is valid YAML." | ||
| exit 1 | ||
| fi | ||
| if [[ "$retry_guards" -lt 1 ]]; then | ||
| echo "FAIL: no '${SCAN_ACTION}' retry step guarded on \"outcome == 'failure'\" in $WORKFLOW" | ||
| echo " Add a second scan step with:" | ||
| echo " if: ... && steps.<primary-id>.outcome == 'failure'" | ||
| echo " so a transient first-scan failure is retried instead of failing CI." | ||
| PASS=false | ||
| else | ||
| echo "PASS: failure-guarded retry scan step present in $WORKFLOW" | ||
| fi | ||
|
|
||
| # ── Check 4: every third-party action is SHA-pinned ──────────────────────── | ||
| # A mutable tag (e.g. @v8.2.1) lets an upstream change alter behavior without a | ||
| # commit here — a supply-chain and flakiness risk. Require a 40-hex commit SHA. | ||
| if ! mapfile -t uses < <(yq '.jobs[].steps[] | .uses // "" ' "$WORKFLOW" 2>/dev/null); then | ||
| echo "FAIL: yq failed to parse $WORKFLOW. Please check if it is valid YAML." | ||
| exit 1 | ||
| fi | ||
| unpinned="" | ||
| for ref in "${uses[@]}"; do | ||
| [[ -z "$ref" || "$ref" == "null" ]] && continue | ||
| if [[ ! "$ref" =~ @[0-9a-f]{40}$ ]]; then | ||
| unpinned+=" - ${ref}"$'\n' | ||
| fi | ||
| done | ||
|
don-petry marked this conversation as resolved.
|
||
| if [[ -n "$unpinned" ]]; then | ||
| echo "FAIL: unpinned action(s) in $WORKFLOW (SHA-pin every third-party action):" | ||
| printf '%s' "$unpinned" | ||
| PASS=false | ||
| else | ||
| echo "PASS: all third-party actions are SHA-pinned in $WORKFLOW" | ||
| fi | ||
|
|
||
| echo "" | ||
| if [[ "$PASS" == "true" ]]; then | ||
| echo "All checks passed." | ||
| else | ||
| echo "One or more checks failed." | ||
| exit 1 | ||
| fi | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,198 @@ | ||
| #!/usr/bin/env bash | ||
| # test-sonarcloud-workflow.test.sh — portable tests for the sonarcloud.yml | ||
| # regression guard (scripts/test-sonarcloud-workflow.sh). Verifies the guard | ||
| # enforces the flaky-scan resilience invariants that keep the SonarCloud | ||
| # workflow's Fleet Monitor failure rate low (#439): the primary scan step must | ||
| # be `continue-on-error: true` and be followed by a retry step guarded on that | ||
| # step's `outcome == 'failure'`, and every third-party action must be SHA-pinned. | ||
| # No bats dependency: the guard is driven as a subprocess against temporary | ||
| # fixture workflows. | ||
| # Run: bash scripts/test-sonarcloud-workflow.test.sh | ||
|
don-petry marked this conversation as resolved.
|
||
| set -euo pipefail | ||
|
|
||
| SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" | ||
|
don-petry marked this conversation as resolved.
Outdated
|
||
| GUARD="${SCRIPT_DIR}/test-sonarcloud-workflow.sh" | ||
|
|
||
| fails=0 | ||
| pass() { | ||
| local desc="$1" | ||
| echo "ok - $desc" | ||
| } | ||
| fail() { | ||
| local desc="$1" | ||
| echo "FAIL - $desc" | ||
| fails=$((fails + 1)) | ||
| } | ||
|
|
||
| # The guard uses yq to parse YAML (its Check 0). Without yq it exits early and | ||
| # these fixtures cannot be exercised, so skip cleanly rather than report noise. | ||
| if ! command -v yq >/dev/null 2>&1; then | ||
| echo "SKIP: 'yq' not installed — cannot exercise the guard" | ||
| exit 0 | ||
| fi | ||
|
|
||
| if ! TMP="$(mktemp -d)"; then | ||
| echo "FAIL: Failed to create temporary directory" >&2 | ||
| exit 1 | ||
| fi | ||
| trap 'rm -rf "$TMP"' EXIT | ||
|
|
||
| # ── Fixture fragments ────────────────────────────────────────────────────── | ||
| # A valid header shared by every fixture: top-level permissions, per-ref | ||
| # concurrency, and a checkout with fetch-depth: 0 (SHA-pinned). | ||
| write_header() { | ||
| local file="$1" | ||
| cat > "$file" <<'YAML' | ||
| name: SonarCloud Analysis | ||
| on: | ||
| push: | ||
| branches: [main] | ||
| pull_request: | ||
| branches: [main] | ||
| permissions: {} | ||
| concurrency: | ||
| group: ${{ github.workflow }}-${{ github.ref }} | ||
| cancel-in-progress: true | ||
| jobs: | ||
| sonarcloud: | ||
| name: SonarCloud | ||
| runs-on: ubuntu-latest | ||
| permissions: | ||
| contents: read | ||
| pull-requests: read | ||
| env: | ||
| SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} | ||
| steps: | ||
| - name: Checkout repository | ||
| uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 | ||
| with: | ||
| fetch-depth: 0 | ||
| YAML | ||
| } | ||
|
|
||
| # The correct pair: a continue-on-error primary scan (id: sonar) followed by a | ||
| # retry guarded on that step's outcome == 'failure'. Both SHA-pinned. | ||
| append_good_scan() { | ||
| local file="$1" | ||
| cat >> "$file" <<'YAML' | ||
| - name: SonarCloud Scan | ||
| id: sonar | ||
| if: env.SONAR_TOKEN != '' | ||
| uses: SonarSource/sonarqube-scan-action@22918119ff8e1ca75a623e15c8296b6ea4fbe28f # v8.2.1 | ||
| continue-on-error: true | ||
| - name: SonarCloud Scan (retry) | ||
| if: env.SONAR_TOKEN != '' && steps.sonar.outcome == 'failure' | ||
| uses: SonarSource/sonarqube-scan-action@22918119ff8e1ca75a623e15c8296b6ea4fbe28f # v8.2.1 | ||
| YAML | ||
| } | ||
|
|
||
| # A single scan step that is NOT continue-on-error: a transient endpoint blip | ||
| # fails the job outright (the exact regression this guard prevents). | ||
| append_scan_without_continue_on_error() { | ||
| local file="$1" | ||
| cat >> "$file" <<'YAML' | ||
| - name: SonarCloud Scan | ||
| id: sonar | ||
| if: env.SONAR_TOKEN != '' | ||
| uses: SonarSource/sonarqube-scan-action@22918119ff8e1ca75a623e15c8296b6ea4fbe28f # v8.2.1 | ||
| - name: SonarCloud Scan (retry) | ||
| if: env.SONAR_TOKEN != '' && steps.sonar.outcome == 'failure' | ||
| uses: SonarSource/sonarqube-scan-action@22918119ff8e1ca75a623e15c8296b6ea4fbe28f # v8.2.1 | ||
| YAML | ||
| } | ||
|
|
||
| # A continue-on-error primary scan with NO retry: the first blip is swallowed and | ||
| # the analysis never runs — the resilience is only half present. | ||
| append_scan_without_retry() { | ||
| local file="$1" | ||
| cat >> "$file" <<'YAML' | ||
| - name: SonarCloud Scan | ||
| id: sonar | ||
| if: env.SONAR_TOKEN != '' | ||
| uses: SonarSource/sonarqube-scan-action@22918119ff8e1ca75a623e15c8296b6ea4fbe28f # v8.2.1 | ||
| continue-on-error: true | ||
| YAML | ||
| } | ||
|
|
||
| # Correct scan pair but the retry action is pinned to a mutable tag, not a SHA. | ||
| append_good_scan_unpinned_retry() { | ||
| local file="$1" | ||
| cat >> "$file" <<'YAML' | ||
| - name: SonarCloud Scan | ||
| id: sonar | ||
| if: env.SONAR_TOKEN != '' | ||
| uses: SonarSource/sonarqube-scan-action@22918119ff8e1ca75a623e15c8296b6ea4fbe28f # v8.2.1 | ||
| continue-on-error: true | ||
| - name: SonarCloud Scan (retry) | ||
| if: env.SONAR_TOKEN != '' && steps.sonar.outcome == 'failure' | ||
| uses: SonarSource/sonarqube-scan-action@v8.2.1 | ||
| YAML | ||
| } | ||
|
|
||
| run_guard() { | ||
| local file="$1" | ||
| bash "$GUARD" "$file" >/dev/null 2>&1 | ||
| } | ||
|
|
||
| # ── Case 1: the real workflow is accepted ────────────────────────────────── | ||
| real="${SCRIPT_DIR}/../.github/workflows/sonarcloud.yml" | ||
| if run_guard "$real"; then | ||
| pass "the checked-in .github/workflows/sonarcloud.yml is accepted" | ||
| else | ||
| fail "the checked-in .github/workflows/sonarcloud.yml should be ACCEPTED" | ||
| fi | ||
|
|
||
| # ── Case 2: a valid synthesized workflow is accepted ─────────────────────── | ||
| good="${TMP}/sonar-good.yml" | ||
| write_header "$good" | ||
| append_good_scan "$good" | ||
| if run_guard "$good"; then | ||
| pass "continue-on-error primary scan + failure-guarded retry is accepted" | ||
| else | ||
| fail "a valid continue-on-error + retry workflow should be ACCEPTED" | ||
| fi | ||
|
|
||
| # ── Case 3: scan missing continue-on-error is rejected ───────────────────── | ||
| noce="${TMP}/sonar-no-continue.yml" | ||
| write_header "$noce" | ||
| append_scan_without_continue_on_error "$noce" | ||
| if run_guard "$noce"; then | ||
| fail "scan step without continue-on-error should be REJECTED" | ||
| else | ||
| pass "scan step without continue-on-error is rejected" | ||
| fi | ||
|
|
||
| # ── Case 4: missing failure-guarded retry is rejected ────────────────────── | ||
| noretry="${TMP}/sonar-no-retry.yml" | ||
| write_header "$noretry" | ||
| append_scan_without_retry "$noretry" | ||
| if run_guard "$noretry"; then | ||
| fail "workflow without a failure-guarded retry step should be REJECTED" | ||
| else | ||
| pass "workflow without a failure-guarded retry step is rejected" | ||
| fi | ||
|
|
||
| # ── Case 5: an unpinned (tag) action is rejected ─────────────────────────── | ||
| unpinned="${TMP}/sonar-unpinned.yml" | ||
| write_header "$unpinned" | ||
| append_good_scan_unpinned_retry "$unpinned" | ||
| if run_guard "$unpinned"; then | ||
| fail "workflow with a tag-pinned action should be REJECTED" | ||
| else | ||
| pass "workflow with a tag-pinned action is rejected" | ||
| fi | ||
|
|
||
| # ── Case 6: a missing workflow file fails cleanly ────────────────────────── | ||
| if run_guard "${TMP}/does-not-exist.yml"; then | ||
| fail "a missing workflow file should be REJECTED" | ||
| else | ||
| pass "a missing workflow file is rejected" | ||
| fi | ||
|
|
||
| echo "" | ||
| if [[ "$fails" -eq 0 ]]; then | ||
| echo "All tests passed." | ||
| exit 0 | ||
| fi | ||
| echo "$fails test(s) failed." >&2 | ||
| exit 1 | ||
Uh oh!
There was an error while loading. Please reload this page.