From d573c0c4e15f12f6e2c4ace3a657606c6836f2b8 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Thu, 22 Oct 2026 12:38:59 +0000 Subject: [PATCH 1/2] ci: warn when SONAR_TOKEN is within 7 days of expiry authentication/validate only reports valid/invalid. Query api/user_tokens/search for the 'github'-named user token's expirationDate and emit a ::warning:: 7 days before expiry so it can be rotated before CI/release jobs fail. Non-fatal: parse or reachability issues are informational only. --- .github/workflows/integrations-check.yml | 48 ++++++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/.github/workflows/integrations-check.yml b/.github/workflows/integrations-check.yml index a5af2814..3261df64 100644 --- a/.github/workflows/integrations-check.yml +++ b/.github/workflows/integrations-check.yml @@ -74,6 +74,54 @@ jobs: fi echo "OK: SONAR_TOKEN is valid and SonarCloud is reachable." + - name: Check SonarCloud token expiry + run: | + set -euo pipefail + # Warn (without failing) when the token is close to expiry so it can + # be rotated before CI/release jobs break. authentication/validate + # only reports valid/invalid, not the expiration date, so query the + # user tokens endpoint. Non-fatal: any reachability/parse issue is + # informational only - only the validate step above hard-fails. + body="$RUNNER_TEMP/sonar-tokens.json" + http_code="$(curl -sS -o "$body" -w '%{http_code}' \ + -u "$SONAR_TOKEN:" https://sonarcloud.io/api/user_tokens/search || true)" + echo "sonarcloud.io/api/user_tokens/search -> HTTP $http_code" + if [ "$http_code" != "200" ]; then + echo "Could not query user tokens (reachability/auth issue); skipping expiry check." + exit 0 + fi + # SONAR_TOKEN is the user token whose name contains "github" + # (case-insensitive, anywhere in the name). + match_count="$(jq '[.userTokens[] | select((.name | ascii_downcase) | contains("github"))] | length' "$body" 2>/dev/null || echo '')" + if [ -z "$match_count" ]; then + echo "Could not parse the user_tokens/search response; skipping expiry check." + exit 0 + fi + if [ "$match_count" = "0" ]; then + echo "No user token with 'github' in its name found; skipping expiry check." + exit 0 + fi + # Soonest expirationDate (YYYY-MM-DDTHH:MM:SSZ) among the matching + # tokens; tokens created without an expiry have no expirationDate. + expiry="$(jq -r '[.userTokens[] | select((.name | ascii_downcase) | contains("github")) | .expirationDate // empty] | sort | .[0] // empty' "$body")" + if [ -z "$expiry" ]; then + echo "The 'github' user token(s) have no expiration date; nothing to warn about." + exit 0 + fi + if ! expiry_epoch="$(date -d "$expiry" +%s 2>/dev/null)"; then + echo "Could not parse expirationDate '$expiry'; skipping expiry check." + exit 0 + fi + days_left=$(( (expiry_epoch - $(date +%s)) / 86400 )) + expiry_day="${expiry%%T*}" + if [ "$days_left" -lt 0 ]; then + echo "::warning::SONAR_TOKEN expired $((-days_left)) day(s) ago on $expiry_day; rotate it to restore CI/release jobs." + elif [ "$days_left" -le 7 ]; then + echo "::warning::SONAR_TOKEN expires in $days_left day(s) on $expiry_day; rotate it to avoid CI/release failures." + else + echo "OK: SONAR_TOKEN expires on $expiry_day ($days_left days remaining)." + fi + codecov: # Scheduled runs have no dispatch inputs, so always run both checks then. if: ${{ github.event.repository.fork == false && (github.event_name == 'schedule' || github.event.inputs.target == 'all' || github.event.inputs.target == 'codecov') }} From e414efb614132a9ac08533936633fb312b10d7cf Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Thu, 22 Oct 2026 12:49:16 +0000 Subject: [PATCH 2/2] ci: fail token-expiry check when expiry cannot be determined Match the diagnostic conventions of the sibling checks: empty SONAR_TOKEN, a non-200 or unparseable user_tokens/search response, a malformed expirationDate, and no 'github'-named token are real problems for a nightly token diagnostic (reachability/validity are already proven by the validate step), so they now hard-fail instead of silently passing. Only 'no expiration date' and '>7 days remaining' stay informational. --- .github/workflows/integrations-check.yml | 37 ++++++++++++++---------- 1 file changed, 22 insertions(+), 15 deletions(-) diff --git a/.github/workflows/integrations-check.yml b/.github/workflows/integrations-check.yml index 3261df64..8263e8dd 100644 --- a/.github/workflows/integrations-check.yml +++ b/.github/workflows/integrations-check.yml @@ -11,8 +11,9 @@ name: 'Integration connectivity check' # # It also runs on a nightly schedule so an expiring/revoked SONAR_TOKEN or # CODECOV_TOKEN surfaces as a failed run (and the usual GitHub failure -# notification) before it silently breaks a real CI/release run. Scheduled runs -# check all integrations. +# notification) before it silently breaks a real CI/release run. The sonarcloud +# job additionally warns once the token is within 7 days of its expiry. +# Scheduled runs check all integrations. # # Both jobs are skipped on forks (same convention as dependency-review.yml): # the secrets are absent there, so jobs would fail nightly on every fork. @@ -77,29 +78,35 @@ jobs: - name: Check SonarCloud token expiry run: | set -euo pipefail - # Warn (without failing) when the token is close to expiry so it can - # be rotated before CI/release jobs break. authentication/validate - # only reports valid/invalid, not the expiration date, so query the - # user tokens endpoint. Non-fatal: any reachability/parse issue is - # informational only - only the validate step above hard-fails. + if [ -z "${SONAR_TOKEN:-}" ]; then + echo "::error::SONAR_TOKEN secret is empty or not configured." + exit 1 + fi + # Warn ahead of expiry so the token can be rotated before CI/release + # jobs break: authentication/validate only reports valid/invalid, not + # the expiration date, so query the user tokens endpoint. Runs only on + # the upstream repo (job-level fork guard) after the validate step, so + # reachability and validity are already proven - anything that still + # prevents determining the expiry is a real problem and fails here. body="$RUNNER_TEMP/sonar-tokens.json" http_code="$(curl -sS -o "$body" -w '%{http_code}' \ -u "$SONAR_TOKEN:" https://sonarcloud.io/api/user_tokens/search || true)" echo "sonarcloud.io/api/user_tokens/search -> HTTP $http_code" if [ "$http_code" != "200" ]; then - echo "Could not query user tokens (reachability/auth issue); skipping expiry check." - exit 0 + echo "::error::user_tokens/search returned HTTP $http_code although the token validated; cannot determine its expiry." + exit 1 fi # SONAR_TOKEN is the user token whose name contains "github" # (case-insensitive, anywhere in the name). match_count="$(jq '[.userTokens[] | select((.name | ascii_downcase) | contains("github"))] | length' "$body" 2>/dev/null || echo '')" if [ -z "$match_count" ]; then - echo "Could not parse the user_tokens/search response; skipping expiry check." - exit 0 + echo "::error::Could not parse the user_tokens/search response; cannot determine the token expiry." + cat "$body" || true + exit 1 fi if [ "$match_count" = "0" ]; then - echo "No user token with 'github' in its name found; skipping expiry check." - exit 0 + echo "::error::No user token with 'github' in its name found; SONAR_TOKEN does not match the expected token or it was renamed." + exit 1 fi # Soonest expirationDate (YYYY-MM-DDTHH:MM:SSZ) among the matching # tokens; tokens created without an expiry have no expirationDate. @@ -109,8 +116,8 @@ jobs: exit 0 fi if ! expiry_epoch="$(date -d "$expiry" +%s 2>/dev/null)"; then - echo "Could not parse expirationDate '$expiry'; skipping expiry check." - exit 0 + echo "::error::Could not parse expirationDate '$expiry'; cannot determine the token expiry." + exit 1 fi days_left=$(( (expiry_epoch - $(date +%s)) / 86400 )) expiry_day="${expiry%%T*}"