From e64b1467d096bd3088dbbc36a552b609908badd2 Mon Sep 17 00:00:00 2001 From: Elmehdi Aitbrahim Date: Sat, 15 Aug 2026 16:57:09 -0400 Subject: [PATCH] ci(code-quality): skip cleanly when unconfigured, fail only when dispatched `code-quality` has failed on every merge to `main` since #272 added the `push` trigger. The cause is exactly what the job was written to report: Missing: SONAR_TOKEN SNYK_TOKEN ##[error]Process completed with exit code 1 Neither secret exists at repo or org level, so the preflight exits 1 and both scans are skipped. Nothing is broken in the code -- the workflow is correctly telling us it is not configured. WHY THAT ANSWER STOPPED BEING RIGHT. Failing loudly was correct when this ran weekly and on dispatch: an unconfigured repo produced one red X every Monday, and the message named the missing secrets instead of letting Sonar and Snyk fail with their own unhelpful auth errors. #272 added `push: [main]`, and the same behaviour now means `main` is permanently red for a condition that is not a defect. A red X on every merge is worse than a missing scan: it is a signal the reader learns to ignore, and it hides the next real failure. WHAT CHANGES. The preflight now reports rather than decides, and who asked determines the verdict: workflow_dispatch -> a human asked for a scan. Still FAILS, loudly. Silently doing nothing in response to a direct request is the worse outcome, so that path is unchanged. push / schedule -> nobody asked. Skips both scans, writes the missing secrets and where to get them to the run summary, and emits a ::notice. The build stays green. The two scan jobs are gated on a `configured` output rather than a condition written on them directly, because `secrets` cannot be referenced from a job-level `if:`. The skip is announced, never silent -- same standard the rest of this repo holds discovery to. A scan that did not run and says so is honest; one that quietly does nothing is not. Verified by running the preflight script directly in all three states: push without tokens exits 0 with `configured=false` and a correctly rendered summary; dispatch without tokens exits 1; either event with both tokens exits 0 with `configured=true`. This does not add the secrets. Once they exist the scans run and none of the above applies. Co-Authored-By: Claude Opus 5 (1M context) --- .github/workflows/code-quality.yml | 65 +++++++++++++++++++++++++----- 1 file changed, 54 insertions(+), 11 deletions(-) diff --git a/.github/workflows/code-quality.yml b/.github/workflows/code-quality.yml index e610df66..7ad96e41 100644 --- a/.github/workflows/code-quality.yml +++ b/.github/workflows/code-quality.yml @@ -3,9 +3,8 @@ name: Code Quality # SonarQube (static analysis + coverage) and Snyk (dependency vulnerabilities). # # ┌─ BEFORE THIS CAN RUN ───────────────────────────────────────────────────────────────────────── -# │ Two repository secrets must exist. As of this workflow landing, NEITHER DOES -- `gh secret -# │ list` is empty at both repo and org level, so every run will stop at the preflight job below -# │ until they are added: +# │ Two repository secrets must exist. As of 2026-08-16, NEITHER DOES -- `gh secret list` is empty +# │ at both repo and org level: # │ # │ SONAR_TOKEN -- from SonarQube ("My Account" -> Security -> Generate Token) # │ SNYK_TOKEN -- from Snyk (Account Settings -> Auth Token, or `snyk config get api`) @@ -14,6 +13,10 @@ name: Code Quality # │ `sonar-project.properties` at the repo root also carries a `sonar.projectKey` that must match # │ the project as it exists on the Sonar server, and a commented-out `sonar.organization` that # │ SonarQube Cloud requires -- read that file before the first run. +# │ +# │ UNTIL THEY EXIST, this workflow does NOT fail the build. It skips both scans and explains +# │ itself in the run summary -- unless a human DISPATCHED it, in which case it fails loudly, +# │ because silently ignoring a direct request is worse. See `preflight` for the full argument. # └─────────────────────────────────────────────────────────────────────────────────────────────── # # TRIGGERS. This was `workflow_dispatch` + `schedule` only, to avoid the automatic Actions spend a @@ -55,12 +58,31 @@ jobs: # authorized. Please check the property sonar.token", and Snyk exits with a generic auth error # from inside a container. Neither says "the repository secret does not exist", which is the # actual cause and the only thing the reader needs to know. This job says it once, up front. + # + # ⚠️ IT SAYS IT DIFFERENTLY DEPENDING ON WHO ASKED, and that asymmetry is the whole point: + # + # workflow_dispatch -> a human asked for a scan. If it cannot run, FAIL, loudly. Silently + # doing nothing in response to a direct request is the worse outcome. + # push / schedule -> nobody asked; the trigger fired on its own. If the tokens are not + # configured, SKIP cleanly and say so in the run summary. + # + # The second half exists because `push: [main]` was added in #272 when the repo went public and + # Actions minutes stopped being billed. Before that this workflow ran weekly, so an unconfigured + # repo produced one red X every Monday. On every merge, the same behaviour is a permanently red + # `main` that teaches the reader to ignore CI -- which costs more than the missing scan does. + # A skipped scan that announces itself is honest; a red X nobody reads is not. + # + # `secrets` cannot be referenced from a job-level `if:`, which is why this is an OUTPUT consumed + # by the two scan jobs rather than a condition written directly on them. preflight: name: Check required secrets runs-on: ubuntu-latest timeout-minutes: 5 + outputs: + configured: ${{ steps.check.outputs.configured }} steps: - name: Verify SONAR_TOKEN and SNYK_TOKEN are set + id: check env: SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }} @@ -69,18 +91,37 @@ jobs: MISSING="" [ -n "${SONAR_TOKEN:-}" ] || MISSING="$MISSING SONAR_TOKEN" [ -n "${SNYK_TOKEN:-}" ] || MISSING="$MISSING SNYK_TOKEN" - if [ -n "$MISSING" ]; then - echo "::error title=Missing repository secrets::This workflow cannot run until these secrets exist:$MISSING" - echo "Missing:$MISSING" + + if [ -z "$MISSING" ]; then + echo "configured=true" >> "$GITHUB_OUTPUT" + echo "SONAR_TOKEN and SNYK_TOKEN are both present." + exit 0 + fi + + echo "configured=false" >> "$GITHUB_OUTPUT" + + { + echo "### Code quality scans skipped" + echo + echo "These repository secrets do not exist, so SonarQube and Snyk cannot run:" echo - echo "Add them at:" - echo " https://github.com/${{ github.repository }}/settings/secrets/actions" + for s in $MISSING; do echo "- \`$s\`"; done echo - echo " SONAR_TOKEN -- SonarQube: My Account > Security > Generate Token" - echo " SNYK_TOKEN -- Snyk: Account Settings > Auth Token (or 'snyk config get api')" + echo "Add them at [Settings > Secrets > Actions](https://github.com/${{ github.repository }}/settings/secrets/actions):" + echo + echo "| secret | where to get it |" + echo "| --- | --- |" + echo "| \`SONAR_TOKEN\` | SonarQube: My Account > Security > Generate Token |" + echo "| \`SNYK_TOKEN\` | Snyk: Account Settings > Auth Token (or \`snyk config get api\`) |" + } >> "$GITHUB_STEP_SUMMARY" + + if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then + # A human asked for this scan. Refusing quietly would be worse than failing. + echo "::error title=Missing repository secrets::You dispatched this workflow, but it cannot run until these secrets exist:$MISSING" exit 1 fi - echo "SONAR_TOKEN and SNYK_TOKEN are both present." + + echo "::notice title=Code quality scans skipped::Not configured yet -- missing:$MISSING. This is not a build failure; see the run summary." # ONE whole-repo scan, not the reference's per-module matrix. The reference fans out over a # Node monorepo whose modules each have their own package.json, tsconfig and test run, and it @@ -93,6 +134,7 @@ jobs: sonarqube: name: SonarQube scan needs: preflight + if: needs.preflight.outputs.configured == 'true' runs-on: ubuntu-latest timeout-minutes: 20 steps: @@ -131,6 +173,7 @@ jobs: snyk: name: Snyk dependency scan needs: preflight + if: needs.preflight.outputs.configured == 'true' runs-on: ubuntu-latest timeout-minutes: 15 steps: