Skip to content

Commit c812dd5

Browse files
AdrianDeutschclaude
andcommitted
fix(cli): clean operational-failure handling — exit 3, no stack trace
End-user verification of the v0.7.0 binaries surfaced it: a transient registry failure (after the resilience pipeline gave up) blew an unhandled HttpRequestException stack trace at the user. A dead registry is an OPERATIONAL failure, not a policy verdict: - New ExitCodes.Unavailable = 3, distinct from PolicyViolation(1)/Usage(2), so CI can retry instead of failing the gate. - Program.cs wraps the dispatch: HttpRequestException/TaskCanceled/Timeout -> one clean stderr line + exit 3; Ctrl-C -> "Cancelled." + exit 3. Verified live: HTTPS_PROXY to a dead endpoint yields "error: an external service is unreachable — Connection refused" and exit 3. Tool 0.7.1. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 77455e0 commit c812dd5

4 files changed

Lines changed: 36 additions & 13 deletions

File tree

action.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ inputs:
4646
tool-version:
4747
description: 'The DepRadar.Tool version to install.'
4848
required: false
49-
default: '0.7.0'
49+
default: '0.7.1'
5050
tool-source:
5151
description: 'Extra NuGet source for the tool (e.g. a locally-packed nupkg directory). Defaults to NuGet.org.'
5252
required: false

src/DepRadar.Cli/DepRadar.Cli.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
<SelfContained Condition="'$(StandaloneBinary)' == 'true'">true</SelfContained>
1919
<EnableCompressionInSingleFile Condition="'$(StandaloneBinary)' == 'true'">true</EnableCompressionInSingleFile>
2020
<PackageId>DepRadar.Tool</PackageId>
21-
<Version>0.7.0</Version>
21+
<Version>0.7.1</Version>
2222
<Authors>Adrian Deutsch</Authors>
2323
<Description>Dependency-health CLI: scan a NuGet package or project for security, license and maintenance risk, fail CI on policy violations, and auto-fix vulnerable dependencies (incl. transitive) with a pull request.</Description>
2424
<PackageOutputPath>$(MSBuildThisFileDirectory)../../artifacts/nupkg</PackageOutputPath>

src/DepRadar.Cli/ExitCodes.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,4 +14,11 @@ internal static class ExitCodes
1414

1515
/// <summary>Bad usage, or nothing could be resolved.</summary>
1616
public const int Usage = 2;
17+
18+
/// <summary>
19+
/// An external service was unreachable (registry/OSV down, network cut) after the
20+
/// resilience pipeline gave up. Distinct from <see cref="PolicyViolation"/> so CI
21+
/// can retry instead of failing the gate.
22+
/// </summary>
23+
public const int Unavailable = 3;
1724
}

src/DepRadar.Cli/Program.cs

Lines changed: 27 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
using DepRadar.Cli;
22

33
// CI-friendly entry point: dispatch the verb and return a process exit code.
4-
// `depradar scan <target>` is the only verb today; --help prints usage.
54
using var cts = new CancellationTokenSource();
65
Console.CancelKeyPress += (_, e) =>
76
{
@@ -15,17 +14,34 @@
1514
return args.Length == 0 ? ExitCodes.Usage : ExitCodes.Ok;
1615
}
1716

18-
return args[0] switch
17+
// A dead registry/OSV after the resilience pipeline gave up is an OPERATIONAL failure,
18+
// not a policy verdict — report it cleanly (no stack trace) with its own exit code so
19+
// CI can retry instead of failing the gate.
20+
try
1921
{
20-
"scan" => await ScanCommand.RunAsync(args[1..], cts.Token),
21-
"diff" => await DiffCommand.RunAsync(args[1..], cts.Token),
22-
"fix" => await FixCommand.RunAsync(args[1..], cts.Token),
23-
"npm" => await NpmCommand.RunAsync(args[1..], cts.Token),
24-
"pypi" => await PyPiCommand.RunAsync(args[1..], cts.Token),
25-
"cargo" => await CargoCommand.RunAsync(args[1..], cts.Token),
26-
"go" => await GoCommand.RunAsync(args[1..], cts.Token),
27-
var unknown => Fail(unknown),
28-
};
22+
return args[0] switch
23+
{
24+
"scan" => await ScanCommand.RunAsync(args[1..], cts.Token),
25+
"diff" => await DiffCommand.RunAsync(args[1..], cts.Token),
26+
"fix" => await FixCommand.RunAsync(args[1..], cts.Token),
27+
"npm" => await NpmCommand.RunAsync(args[1..], cts.Token),
28+
"pypi" => await PyPiCommand.RunAsync(args[1..], cts.Token),
29+
"cargo" => await CargoCommand.RunAsync(args[1..], cts.Token),
30+
"go" => await GoCommand.RunAsync(args[1..], cts.Token),
31+
var unknown => Fail(unknown),
32+
};
33+
}
34+
catch (OperationCanceledException) when (cts.IsCancellationRequested)
35+
{
36+
Console.Error.WriteLine("Cancelled.");
37+
return ExitCodes.Unavailable;
38+
}
39+
catch (Exception exception) when (exception is HttpRequestException or TaskCanceledException or TimeoutException)
40+
{
41+
Console.Error.WriteLine($"error: an external service is unreachable — {exception.Message}");
42+
Console.Error.WriteLine("Check your network (or the registry/OSV status) and try again.");
43+
return ExitCodes.Unavailable;
44+
}
2945

3046
static int Fail(string verb)
3147
{

0 commit comments

Comments
 (0)