From 78aa5797b990955f5c92a2d0d545013abcfacc10 Mon Sep 17 00:00:00 2001 From: Alban Auzeill Date: Mon, 3 Aug 2026 10:44:46 +0200 Subject: [PATCH] Investigate Windows GPG path bug (#186785) + CI reproducer convertToUnixPath() rewrites drive letters to MSYS form (R:\ -> /r/), which native GnuPG (Gpg4win) cannot resolve. Add investigation.md and a windows-latest reproducer workflow that installs native GnuPG and expects the action to fail at GPG signature verification. Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/issue-reproduced.yml | 84 +++++++++++++++++ investigation.md | 125 +++++++++++++++++++++++++ 2 files changed, 209 insertions(+) create mode 100644 .github/workflows/issue-reproduced.yml create mode 100644 investigation.md diff --git a/.github/workflows/issue-reproduced.yml b/.github/workflows/issue-reproduced.yml new file mode 100644 index 0000000..579c6a1 --- /dev/null +++ b/.github/workflows/issue-reproduced.yml @@ -0,0 +1,84 @@ +# Reproducer for: "sonarqube-scan-action GPG check not working on Windows runner" +# https://community.sonarsource.com/t/sonarqube-scan-action-gpg-check-not-working-on-windows-runner/186785 +# +# Root cause (see investigation.md): on Windows, convertToUnixPath() rewrites drive +# letters into MSYS/Git-Bash form (R:\... -> /r/...). The native GnuPG (Gpg4win) that +# the reporter has on PATH (C:\Program Files (x86)\GnuPG\bin\gpg.exe) cannot resolve +# that form, so GPG signature verification fails. +# +# This workflow reproduces the failure by installing native GnuPG and putting it first +# on PATH, matching the reporter's environment. It runs the local action (which executes +# the committed dist/index.js) and expects it to FAIL during signature verification. + +name: Reproduce GPG check failure on Windows + +on: + push: + +permissions: + contents: read + +jobs: + reproduce-windows-gpg-bug: + runs-on: windows-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Install native GnuPG (Gpg4win) and put it first on PATH + shell: pwsh + run: | + choco install gnupg -y --no-progress + # The reporter's gpg lives at C:\Program Files (x86)\GnuPG\bin\gpg.exe. + # Prepend the native GnuPG dir to PATH so `gpg` resolves to it (and NOT to + # the Git-for-Windows build, which would mask the bug by accepting /r/... paths). + $candidates = @( + "C:\Program Files (x86)\GnuPG\bin", + "C:\Program Files\GnuPG\bin" + ) + $gpgDir = $candidates | Where-Object { Test-Path (Join-Path $_ "gpg.exe") } | Select-Object -First 1 + if (-not $gpgDir) { Write-Error "Native GnuPG not found after install"; exit 1 } + Write-Host "Using native GnuPG from: $gpgDir" + Add-Content -Path $env:GITHUB_PATH -Value $gpgDir + + - name: Confirm `gpg` resolves to native GnuPG + shell: pwsh + run: | + $g = Get-Command gpg + Write-Host "gpg resolves to: $($g.Source)" + gpg --version + if ($g.Source -notmatch "GnuPG") { + Write-Error "Expected native GnuPG on PATH but got: $($g.Source)" + exit 1 + } + + - name: Root-cause demo — native gpg rejects an MSYS-style --homedir + shell: pwsh + continue-on-error: true + run: | + # convertToUnixPath() would turn e.g. C:\...\gpg-home into /c/.../gpg-home. + # Show that native GnuPG cannot use such a --homedir. + $msysHome = "/c/Users/runneradmin/gpg-home-demo" + Write-Host "Running: gpg --homedir '$msysHome' --batch --list-keys" + gpg --homedir "$msysHome" --batch --list-keys + Write-Host "native-gpg exit code with MSYS-style homedir: $LASTEXITCODE" + + - name: Run the action (expected to FAIL at GPG signature verification) + id: action + continue-on-error: true + # No SONAR_TOKEN / server needed: the failure happens during signature + # verification in installSonarScanner, before the scanner is ever invoked. + uses: ./ + with: + skipSignatureVerification: false + + - name: Assert the bug reproduced + shell: pwsh + run: | + Write-Host "Action step outcome: ${{ steps.action.outcome }}" + if ("${{ steps.action.outcome }}" -eq "failure") { + Write-Host "OK - Bug reproduced: the action failed on Windows with native GnuPG." + } else { + Write-Error "Bug did NOT reproduce (outcome: ${{ steps.action.outcome }}). The GPG path handling may have been fixed." + exit 1 + } diff --git a/investigation.md b/investigation.md new file mode 100644 index 0000000..22cfedf --- /dev/null +++ b/investigation.md @@ -0,0 +1,125 @@ +# Investigation: GPG check not working on Windows runner + +- **Reported:** https://community.sonarsource.com/t/sonarqube-scan-action-gpg-check-not-working-on-windows-runner/186785 +- **Affected version:** `SonarSource/sonarqube-scan-action@v8.2.1` +- **Environment:** Windows runner with native GnuPG (Gpg4win) at `C:\Program Files (x86)\GnuPG\bin\gpg.exe` +- **Verdict:** ✅ **Confirmed bug** in `src/main/gpg-verification.js` + +## Summary + +On Windows, the action converts every path it hands to `gpg` (the `--homedir`, the +signature file, and the ZIP file) into an **MSYS/Git-Bash-style path** such as +`/r/GHEC/Build_B1/_temp/gpg-957091c8`. That format is only understood by the +**Git-for-Windows** build of GPG. When the `gpg` on `PATH` is the **native GnuPG +(Gpg4win)** — as in the reporter's environment — those paths are meaningless, so GPG +cannot create/read its home directory, keyring, sockets, or lock files, and signature +verification fails. + +## Root cause + +`convertToUnixPath()` in `src/main/gpg-verification.js:106-118`: + +```js +export function convertToUnixPath(windowsPath) { + if (process.platform !== "win32") { + return windowsPath; + } + let unixPath = windowsPath.replaceAll('\\', "/"); + unixPath = unixPath.replace(/^([A-Za-z]):/, (match, drive) => { + return `/${drive.toLowerCase()}`; // R:\... -> /r/... + }); + return unixPath; +} +``` + +For the reporter's temp dir `R:\GHEC\Build_B1\_temp\gpg-957091c8` this produces: + +``` +R:\GHEC\Build_B1\_temp\gpg-957091c8 → /r/GHEC/Build_B1/_temp/gpg-957091c8 +``` + +which is exactly the path seen in the failure logs. + +The function's own comment states the assumption: + +> `GPG on Windows (from Git for Windows) expects Unix-style paths` — `src/main/gpg-verification.js:102` + +That assumption is wrong for the common case. There are two GPG builds on Windows: + +| GPG build | Understands `/r/GHEC/...` (MSYS) | Understands `R:/GHEC/...` (native) | +|---|---|---| +| Git for Windows / MSYS2 `gpg` | ✅ | ✅ | +| **Native GnuPG / Gpg4win** (`C:\Program Files (x86)\GnuPG\bin\gpg.exe`) | ❌ | ✅ | + +The MSYS drive-letter form (`/r/...`) is understood by *only one* of the two builds, +whereas the plain Windows form with forward slashes (`R:/...`) is understood by **both**. +By rewriting `R:` → `/r`, the action picks the form that breaks native GnuPG. + +## How the converted path is used + +All three call sites feed native GnuPG a path it cannot resolve: + +- `src/main/gpg-verification.js:165` — `--homedir` for `--recv-keys` (key import) +- `src/main/gpg-verification.js:247` — `--homedir` for `--verify` +- `src/main/gpg-verification.js:250-251` — the `.asc` signature path and the ZIP path + +## Mapping to the reported errors + +With `--homedir /r/GHEC/Build_B1/_temp/gpg-957091c8`, native GnuPG treats the argument +as a bogus location and cannot create/open its home dir. That directly explains every +symptom in the report: + +| Reported symptom | Explanation | +|---|---| +| `pubring.kbx` → *No such file or directory* | Keyring cannot be created/read under the bogus homedir | +| `can't create '…/gnupg_spawn_dirmngr_sentinel.lock'` → *The system cannot find the path specified* | Native Windows API cannot resolve `/r/...`; parent path does not exist | +| `can't connect to the dirmngr: No such file or directory` | `dirmngr` socket cannot be created under the bogus homedir | +| `Kein Dirmngr` (keyserver retrieval failed) | Consequence of dirmngr never starting → `--recv-keys` fails | + +The reporter's own suggested fix — use `R:/GHEC/Build_B1/_temp/gpg-957091c8` — matches +the analysis above. + +## Why it isn't caught by CI / existing tests + +- The unit tests in `src/main/__tests__/gpg-verification.test.js:132-163` only assert the + transformation itself (`C:\a\_temp\gpg-home` → `/c/a/_temp/gpg-home`). They **encode the + buggy behaviour as the expected behaviour**, so they pass while the real bug persists. +- GitHub-hosted `windows-latest` runners resolve `gpg` to the **Git-for-Windows** build, + which happens to accept `/r/...`. The bug only surfaces when the native GnuPG (Gpg4win) + is first on `PATH` — the reporter's self-hosted setup. This is why it escaped testing. + +## Suggested fix (not applied — investigation only) + +On Windows, do **not** rewrite the drive letter. Normalise separators only, keeping a +native Windows path with forward slashes, which both GPG builds accept: + +```js +export function convertToUnixPath(windowsPath) { + if (process.platform !== "win32") { + return windowsPath; + } + // Keep the drive letter (R:/...). Native GnuPG (Gpg4win) cannot resolve the + // MSYS "/r/..." form, and Git-for-Windows GPG understands "R:/..." too. + return windowsPath.replaceAll("\\", "/"); +} +``` + +The corresponding tests at `src/main/__tests__/gpg-verification.test.js:132-153` should be +updated to expect `C:/a/_temp/gpg-home` (and the function likely renamed, since it no +longer produces a Unix path). + +## Secondary observation (not the reported bug) + +`src/main/run-sonar-scanner.js:147` invokes `${scannerDir}/jre/bin/java` without a `.exe` +suffix and with forward slashes. This only runs when `SONAR_ROOT_CERT` is set (truststore +handling) and is unrelated to the GPG failure, but is worth a separate look for Windows +robustness. + +## Reproducer + +A CI reproducer is provided in `.github/workflows/issue-reproduced.yml`. It runs on +`windows-latest`, installs the **native GnuPG (Gpg4win)** and puts it first on `PATH` to +match the reporter's environment, then runs the action (`uses: ./`, which executes the +committed `dist/index.js`). It expects the action to **fail at GPG signature +verification**. It also includes a direct step showing native `gpg` rejecting an +MSYS-style `--homedir`, isolating the root cause. The workflow triggers `on: push`.