apksigner verify --print-certs prints Signer #1 certificate DN: CN=ClinicLive Demo, O=ClinicLive, C=ZZ, the signature is valid — and the script that called it reports failure. On Windows, the exit code you are reading is usually not apksigner's.

apksigner.bat ends by calling Java, so it returns Java's exit code, and Java returns 0 for a valid signature — re-tested on build-tools 35 and 36 from cmd, PowerShell and Git Bash, on the release and debug APKs. What turns a good verify into "failed" is the shell around it: a PowerShell pipeline that stops reading early (| Select-Object -First 5) closes Java's output pipe and reports -1; Windows PowerShell 5.1 turns anything a native tool writes to stderr into error records when you redirect with 2>&1, so $? is false while $LASTEXITCODE is 0. Assert on the output, and take the exit code from a plain, unpiped call.

The error

The same APK, twice, in Windows PowerShell 5.1:

PS> & $apksigner verify --print-certs $apk
Signer #1 certificate DN: CN=ClinicLive Demo, O=ClinicLive, C=ZZ
Signer #1 certificate SHA-256 digest: 761ae52e...
PS> $LASTEXITCODE
0

PS> & $apksigner verify --verbose --print-certs $apk | Select-Object -First 6
Verifies
Verified using v1 scheme (JAR signing): false
Verified using v2 scheme (APK Signature Scheme v2): true
/* ... */
PS> $LASTEXITCODE
-1

Why it happens

The wrapper is short: it finds java.exe, collects the arguments, and its last line is call "%java_exe%" %javaOpts% -jar "%jarpath%" %params%. A batch file's exit code is the last command's, so the wrapper reports whatever apksigner.jar reported. There is nothing in it to invent a failure.

The shell can. When a downstream cmdlet such as Select-Object -First has what it needs, PowerShell ends the pipeline and the native process loses its stdout; the run is recorded as -1 even though the certificates already printed. Separately, Windows PowerShell 5.1 wraps each stderr line of a native command in a NativeCommandError when stderr is redirected, so any harness that judges by $? — or a script host that reports "exit 1" whenever an error record appeared — calls a clean run a failure. The wrapper's own comments add a third oddity: cmd.exe splits --name=value into two arguments, which apksigner tolerates but other tools may not.

The fix

# take the exit code from an unpiped, unredirected call
& "$sdk\build-tools\36.0.0\apksigner.bat" verify --print-certs $apk
if ($LASTEXITCODE -ne 0) { throw "signature check failed" }

# or skip cmd.exe entirely — same exit code, no batch-file argument mangling
java -jar "$sdk\build-tools\36.0.0\lib\apksigner.jar" verify --print-certs $apk

Better still, assert on what you actually care about. Capture the output and require the line that names your signer, or run with --verbose and require its first line, Verifies. An exit code tells you a process ended; the DN line tells you who signed the APK.

Where it bit us

Season three, Part 11 (tag pocket-11 in the repo). The release publish produced a signed APK and AAB, the harness that ran the verify printed the demo distinguished name and then reported the step as failed. The build log blamed the batch wrapper. Re-running the identical verify on the identical APK afterwards returned 0 every way we tried, so the failure reading belonged to the shell that captured it — which is the part's own lesson, sharpened: an exit code is a claim, not a fact, and a claim made by a pipeline is weaker than one made by the tool.

Frequently asked

Why does my script say apksigner failed when the signature is valid?
Usually because the shell, not apksigner, produced the failure. A PowerShell pipeline that stops reading early reports -1 for the native command, and Windows PowerShell 5.1 turns redirected stderr output into error records that make $? false even when the exit code is 0. Call apksigner without a pipe or redirection and read $LASTEXITCODE.
Does apksigner.bat return a different exit code from apksigner.jar?
No. The batch file ends with a call to java -jar apksigner.jar, so its exit code is Java's. On a valid signature both return 0. Calling the jar directly only removes cmd.exe's argument handling, such as splitting --name=value into two arguments.
How should CI check an APK signature without trusting the exit code alone?
Capture the output of apksigner verify --print-certs and require the Signer certificate DN line to match the signer you expect, or run with --verbose and require the first line to read Verifies. Then also fail the step on a non-zero exit code from an unpiped call.

More decoded errors in the Fixes category; the release build this came from starts at From Prompt to Pocket, Part 1.