From 959024240099929e35b0e947c3a36f2904c28cc7 Mon Sep 17 00:00:00 2001 From: Morten Andersen Date: Fri, 4 Sep 2026 10:03:18 +0200 Subject: [PATCH 1/3] fix: coerce sonar-non-blocking to boolean in continue-on-error continue-on-error: ${{ inputs.sonar-non-blocking }} fails workflow template validation ("Unexpected value ''") whenever a caller doesn't explicitly set the boolean input and relies on its declared default (true) - e.g. service-ocpp's pull-request.yml just does `uses: .../pull-request-kotlin.yml@main` with no `sonar-non-blocking:` in `with:`. The step then hard-fails the job instead of being non-blocking, defeating the whole point of the input (#320/#321). Comparing with `== true` forces a real boolean result regardless of how the underlying input value serializes, fixing all three affected steps (Tailscale, the SonarQube reachability probe, and the scan/ upload step itself) in both pull-request-kotlin.yml and sonar-cloud.yml, which share the same pattern. Observed failing in monta-app/service-ocpp#3083: https://github.com/monta-app/service-ocpp/actions/runs/33848732898/job/100946486591 Co-Authored-By: Claude Sonnet 5 --- .github/workflows/pull-request-kotlin.yml | 6 +++--- .github/workflows/sonar-cloud.yml | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/pull-request-kotlin.yml b/.github/workflows/pull-request-kotlin.yml index 571e69f..2157b8b 100644 --- a/.github/workflows/pull-request-kotlin.yml +++ b/.github/workflows/pull-request-kotlin.yml @@ -204,7 +204,7 @@ jobs: - name: Tailscale id: tailscale if: ${{ !inputs.skip-sonar && env.TAILSCALE_AUTHKEY != '' }} - continue-on-error: ${{ inputs.sonar-non-blocking }} + continue-on-error: ${{ inputs.sonar-non-blocking == true }} uses: tailscale/github-action@780049a30b6ff5c378a9e7b389d15ece7a204888 # v4.1.3 with: authkey: ${{ env.TAILSCALE_AUTHKEY }} @@ -220,7 +220,7 @@ jobs: # relay path, so the scan starts on a connection we know works. - name: Wait for SonarQube to be reachable over the tailnet if: ${{ !inputs.skip-sonar && steps.tailscale.outcome != 'failure' }} - continue-on-error: ${{ inputs.sonar-non-blocking }} + continue-on-error: ${{ inputs.sonar-non-blocking == true }} shell: bash env: SONAR_HOST_URL: https://sonarqube.vpn.internal.monta.app/ @@ -258,7 +258,7 @@ jobs: # != 'failure' (not == 'success') keeps the SonarCloud path when Tailscale is skipped. - name: Upload results to SonarQube if: ${{ !inputs.skip-sonar && steps.tailscale.outcome != 'failure' }} - continue-on-error: ${{ inputs.sonar-non-blocking }} + continue-on-error: ${{ inputs.sonar-non-blocking == true }} env: GHL_USERNAME: ${{ secrets.GHL_USERNAME }} GHL_PASSWORD: ${{ secrets.GHL_PASSWORD }} diff --git a/.github/workflows/sonar-cloud.yml b/.github/workflows/sonar-cloud.yml index b6df652..3fe44af 100644 --- a/.github/workflows/sonar-cloud.yml +++ b/.github/workflows/sonar-cloud.yml @@ -103,7 +103,7 @@ jobs: - name: Tailscale id: tailscale if: ${{ env.TAILSCALE_AUTHKEY != '' }} - continue-on-error: ${{ inputs.sonar-non-blocking }} + continue-on-error: ${{ inputs.sonar-non-blocking == true }} uses: tailscale/github-action@780049a30b6ff5c378a9e7b389d15ece7a204888 # v4.1.3 with: authkey: ${{ env.TAILSCALE_AUTHKEY }} @@ -128,7 +128,7 @@ jobs: # relay path, so the scan starts on a connection we know works. - name: Wait for SonarQube to be reachable over the tailnet if: ${{ steps.tailscale.outcome != 'failure' && env.TAILSCALE_AUTHKEY != '' }} - continue-on-error: ${{ inputs.sonar-non-blocking }} + continue-on-error: ${{ inputs.sonar-non-blocking == true }} shell: bash env: SONAR_HOST_URL: https://sonarqube.vpn.internal.monta.app/ @@ -166,7 +166,7 @@ jobs: # != 'failure' (not == 'success') keeps the SonarCloud path when Tailscale is skipped. - name: Analyze with SonarQube if: ${{ steps.tailscale.outcome != 'failure' }} - continue-on-error: ${{ inputs.sonar-non-blocking }} + continue-on-error: ${{ inputs.sonar-non-blocking == true }} env: GHL_USERNAME: ${{ secrets.GHL_USERNAME }} GHL_PASSWORD: ${{ secrets.GHL_PASSWORD }} From 48fd12dcaa2dc5eb00f0e12eeb9468e7b08ca78d Mon Sep 17 00:00:00 2001 From: Morten Andersen Date: Fri, 4 Sep 2026 10:40:48 +0200 Subject: [PATCH 2/3] fix: drop blob:none from the test job's checkout (breaks Sonar blame) filter: blob:none was added to both the lint and test job checkouts in #327 to speed up checkout. It's safe for lint (no blame needed), but the test job's checkout feeds the Sonar step, and SonarQube's git-blame runs on JGit - which, unlike native git, can't lazily fetch a blob that a partial clone filtered out. Blame walks a changed file's full history across ancestor commits, needing blobs well beyond the checked-out HEAD tree; under blob:none those come back missing, and JGit throws org.eclipse.jgit.errors.MissingObjectException instead of fetching them on demand the way real git would. sonar-cloud.yml never had this filter and doesn't hit the issue - confirms it's specific to this checkout. Observed failing in monta-app/service-ocpp#3083: https://github.com/monta-app/service-ocpp/actions/runs/33848732898/job/100946486591 (consistently the same blob, across multiple runs of that PR) Co-Authored-By: Claude Sonnet 5 --- .github/workflows/pull-request-kotlin.yml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/.github/workflows/pull-request-kotlin.yml b/.github/workflows/pull-request-kotlin.yml index 2157b8b..af667d0 100644 --- a/.github/workflows/pull-request-kotlin.yml +++ b/.github/workflows/pull-request-kotlin.yml @@ -146,13 +146,18 @@ jobs: TAILSCALE_AUTHKEY: ${{ secrets.TAILSCALE_AUTHKEY }} steps: # Checkout + # No blob filter here (unlike the lint job): this checkout feeds the Sonar step below, + # and SonarQube's git-blame runs on JGit, which - unlike native git - can't lazily fetch + # a blob it filtered out of a partial clone. blame walks a changed file's history across + # ancestor commits, so it needs blobs beyond just the checked-out HEAD tree; with + # `filter: blob:none` those come back missing and blame throws + # org.eclipse.jgit.errors.MissingObjectException instead of fetching them on demand. - name: Checkout uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6 with: # to check out the actual pull request commit, not the merge commit ref: ${{ github.event.pull_request.head.sha }} fetch-depth: 0 - filter: blob:none - name: Set up JDK uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5 with: From 7cf2e80de0fbe3e712b3f3ab7f71247ff679a5a7 Mon Sep 17 00:00:00 2001 From: Morten Andersen Date: Fri, 4 Sep 2026 11:29:16 +0200 Subject: [PATCH 3/3] fix: correct sonar-non-blocking workaround to target the actual bug My earlier `== true` fix was applied to all 3 continue-on-error steps in each file, but that was wrong in two ways: 1. It was unnecessary for 2 of them. "Tailscale" (a plain JS action) and "Wait for SonarQube to be reachable" (a `run:` step) were never broken - only "Upload results to SonarQube" / "Analyze with SonarQube" (both `uses:` the gradle-multi-module *composite* action) hit the actual bug. Reverted those two back to the original bare `${{ inputs.sonar-non-blocking }}`. 2. For the step that IS broken, `== true` doesn't fix it correctly - it just stops the crash. Per actions/runner#2418, continue-on-error on a step that also `uses:` a composite action resolves `inputs.*` against the *composite action's own* inputs schema, not the calling workflow's - so `inputs.sonar-non-blocking` is always null there, regardless of what the caller actually passed. `null == true` is a valid boolean (false), so the template error goes away, but the step would now ALWAYS hard-fail on a Sonar/Tailscale problem - exactly what sonar-non-blocking is supposed to prevent. Fixed properly by mirroring the input into a job-level env var (SONAR_NON_BLOCKING) and reading env.SONAR_NON_BLOCKING == 'true' in continue-on-error for just that one step. env isn't part of a composite action's own schema, so it resolves in the correct (calling workflow) scope regardless of which step consumes it. Co-Authored-By: Claude Sonnet 5 --- .github/workflows/pull-request-kotlin.yml | 13 ++++++++++--- .github/workflows/sonar-cloud.yml | 13 ++++++++++--- 2 files changed, 20 insertions(+), 6 deletions(-) diff --git a/.github/workflows/pull-request-kotlin.yml b/.github/workflows/pull-request-kotlin.yml index af667d0..ba11e7c 100644 --- a/.github/workflows/pull-request-kotlin.yml +++ b/.github/workflows/pull-request-kotlin.yml @@ -144,6 +144,13 @@ jobs: timeout-minutes: ${{ inputs.test-timeout-minutes }} env: TAILSCALE_AUTHKEY: ${{ secrets.TAILSCALE_AUTHKEY }} + # actions/runner#2418: continue-on-error: ${{ inputs.X }} resolves against the wrong + # (composite action's own) inputs schema on a step that also `uses:` a composite action, + # always yielding null - "Unexpected value ''" (template validation) rather than the + # caller's actual value. Mirroring the input into env here and reading env.SONAR_NON_BLOCKING + # instead sidesteps that scope confusion for the one step below affected by it + # ("Upload results to SonarQube", which uses the gradle-multi-module composite action). + SONAR_NON_BLOCKING: ${{ inputs.sonar-non-blocking }} steps: # Checkout # No blob filter here (unlike the lint job): this checkout feeds the Sonar step below, @@ -209,7 +216,7 @@ jobs: - name: Tailscale id: tailscale if: ${{ !inputs.skip-sonar && env.TAILSCALE_AUTHKEY != '' }} - continue-on-error: ${{ inputs.sonar-non-blocking == true }} + continue-on-error: ${{ inputs.sonar-non-blocking }} uses: tailscale/github-action@780049a30b6ff5c378a9e7b389d15ece7a204888 # v4.1.3 with: authkey: ${{ env.TAILSCALE_AUTHKEY }} @@ -225,7 +232,7 @@ jobs: # relay path, so the scan starts on a connection we know works. - name: Wait for SonarQube to be reachable over the tailnet if: ${{ !inputs.skip-sonar && steps.tailscale.outcome != 'failure' }} - continue-on-error: ${{ inputs.sonar-non-blocking == true }} + continue-on-error: ${{ inputs.sonar-non-blocking }} shell: bash env: SONAR_HOST_URL: https://sonarqube.vpn.internal.monta.app/ @@ -263,7 +270,7 @@ jobs: # != 'failure' (not == 'success') keeps the SonarCloud path when Tailscale is skipped. - name: Upload results to SonarQube if: ${{ !inputs.skip-sonar && steps.tailscale.outcome != 'failure' }} - continue-on-error: ${{ inputs.sonar-non-blocking == true }} + continue-on-error: ${{ env.SONAR_NON_BLOCKING == 'true' }} env: GHL_USERNAME: ${{ secrets.GHL_USERNAME }} GHL_PASSWORD: ${{ secrets.GHL_PASSWORD }} diff --git a/.github/workflows/sonar-cloud.yml b/.github/workflows/sonar-cloud.yml index 3fe44af..c3c2896 100644 --- a/.github/workflows/sonar-cloud.yml +++ b/.github/workflows/sonar-cloud.yml @@ -71,6 +71,13 @@ jobs: timeout-minutes: 30 env: TAILSCALE_AUTHKEY: ${{ secrets.TAILSCALE_AUTHKEY }} + # actions/runner#2418: continue-on-error: ${{ inputs.X }} resolves against the wrong + # (composite action's own) inputs schema on a step that also `uses:` a composite action, + # always yielding null - "Unexpected value ''" (template validation) rather than the + # caller's actual value. Mirroring the input into env here and reading env.SONAR_NON_BLOCKING + # instead sidesteps that scope confusion for the one step below affected by it + # ("Analyze with SonarQube", which uses the gradle-multi-module composite action). + SONAR_NON_BLOCKING: ${{ inputs.sonar-non-blocking }} steps: - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6 with: @@ -103,7 +110,7 @@ jobs: - name: Tailscale id: tailscale if: ${{ env.TAILSCALE_AUTHKEY != '' }} - continue-on-error: ${{ inputs.sonar-non-blocking == true }} + continue-on-error: ${{ inputs.sonar-non-blocking }} uses: tailscale/github-action@780049a30b6ff5c378a9e7b389d15ece7a204888 # v4.1.3 with: authkey: ${{ env.TAILSCALE_AUTHKEY }} @@ -128,7 +135,7 @@ jobs: # relay path, so the scan starts on a connection we know works. - name: Wait for SonarQube to be reachable over the tailnet if: ${{ steps.tailscale.outcome != 'failure' && env.TAILSCALE_AUTHKEY != '' }} - continue-on-error: ${{ inputs.sonar-non-blocking == true }} + continue-on-error: ${{ inputs.sonar-non-blocking }} shell: bash env: SONAR_HOST_URL: https://sonarqube.vpn.internal.monta.app/ @@ -166,7 +173,7 @@ jobs: # != 'failure' (not == 'success') keeps the SonarCloud path when Tailscale is skipped. - name: Analyze with SonarQube if: ${{ steps.tailscale.outcome != 'failure' }} - continue-on-error: ${{ inputs.sonar-non-blocking == true }} + continue-on-error: ${{ env.SONAR_NON_BLOCKING == 'true' }} env: GHL_USERNAME: ${{ secrets.GHL_USERNAME }} GHL_PASSWORD: ${{ secrets.GHL_PASSWORD }}