Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -102,3 +102,4 @@ jobs:
bash scripts/test-pr-review-workflow.sh
bash scripts/test-ci-workflow.sh
bash scripts/test-copilot-setup-steps-workflow.sh
bash scripts/test-sonarcloud-workflow.sh
112 changes: 112 additions & 0 deletions scripts/test-sonarcloud-workflow.sh
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

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Redirect this error message to stderr (>&2).

See more on https://sonarcloud.io/project/issues?id=petry-projects_TalkTerm&issues=AaAY7Rwq7WeOd2ADEMTZ&open=AaAY7Rwq7WeOd2ADEMTZ&pullRequest=441
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

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Redirect this error message to stderr (>&2).

See more on https://sonarcloud.io/project/issues?id=petry-projects_TalkTerm&issues=AaAY7Rwq7WeOd2ADEMTa&open=AaAY7Rwq7WeOd2ADEMTa&pullRequest=441
echo " does not fail the job before the retry runs."
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
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

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Redirect this error message to stderr (>&2).

See more on https://sonarcloud.io/project/issues?id=petry-projects_TalkTerm&issues=AaAY7Rwq7WeOd2ADEMTb&open=AaAY7Rwq7WeOd2ADEMTb&pullRequest=441
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
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
Comment thread
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
Comment thread
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
198 changes: 198 additions & 0 deletions scripts/test-sonarcloud-workflow.test.sh
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
Comment thread
don-petry marked this conversation as resolved.
set -euo pipefail

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
Comment thread
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
Loading