diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index cbe6a7f..a4478dc 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -11,20 +11,12 @@ jobs:
steps:
- name: Checkout repository
- uses: actions/checkout@v4
+ uses: actions/checkout@v6
- name: Setup .NET
- uses: actions/setup-dotnet@v3
+ uses: actions/setup-dotnet@v5
with:
- dotnet-version: '8.0.x'
-
- - name: Add Garage Group NuGet Source
- run: >
- dotnet nuget add source ${{ vars.GG_NUGET_SOURCE_URL }}
- -n garage
- -u ${{ secrets.GG_NUGET_SOURCE_USER_NAME }}
- -p ${{ secrets.GG_NUGET_SOURCE_USER_PASSWORD }}
- --store-password-in-clear-text
+ dotnet-version: '10.0.x'
- name: Restore
run: dotnet restore
diff --git a/.github/workflows/push.yml b/.github/workflows/push.yml
index aa31b81..6cf4a72 100644
--- a/.github/workflows/push.yml
+++ b/.github/workflows/push.yml
@@ -6,6 +6,10 @@ on:
types:
- created
+permissions:
+ contents: read
+ packages: write
+
jobs:
build-and-push-docker-image:
name: Build and push Docker image
@@ -13,44 +17,36 @@ jobs:
steps:
- name: Checkout repository
- uses: actions/checkout@v4
+ uses: actions/checkout@v6
- name: Set output
id: vars
- run: echo ::set-output name=tag::${GITHUB_REF#refs/*/}
+ run: echo "tag=${GITHUB_REF#refs/*/}" >> "$GITHUB_OUTPUT"
- name: Setup .NET
- uses: actions/setup-dotnet@v3
+ uses: actions/setup-dotnet@v5
with:
- dotnet-version: '8.0.x'
+ dotnet-version: '10.0.x'
- name: Set up Docker Buildx
- uses: docker/setup-buildx-action@v2
-
- - name: Add Garage Group NuGet Source
- run: >
- dotnet nuget add source ${{ vars.GG_NUGET_SOURCE_URL }}
- -n garage
- -u ${{ secrets.GG_NUGET_SOURCE_USER_NAME }}
- -p ${{ secrets.GG_NUGET_SOURCE_USER_PASSWORD }}
- --store-password-in-clear-text
+ uses: docker/setup-buildx-action@v4
- name: Publish Console.csproj
run: dotnet publish ./src/*/Console.csproj -c Release -o './publish'
- - name: Login to Github Packages
- uses: docker/login-action@v2
+ - name: Login to GitHub Container Registry
+ uses: docker/login-action@v4
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build image and push to GitHub Container Registry
- uses: docker/build-push-action@v4
+ uses: docker/build-push-action@v7
with:
context: .
file: ./Dockerfile
tags: |
ghcr.io/garagegroup/gg-ping-health-check:${{ steps.vars.outputs.tag }}
ghcr.io/garagegroup/gg-ping-health-check:latest
- push: true
+ push: true
\ No newline at end of file
diff --git a/Dockerfile b/Dockerfile
index be63961..534db88 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,4 +1,4 @@
-FROM mcr.microsoft.com/dotnet/runtime:8.0
+FROM mcr.microsoft.com/dotnet/runtime:10.0
WORKDIR /app
COPY ./publish ./
diff --git a/Platform.Ping.sln b/Platform.Ping.sln
deleted file mode 100644
index 881cf1a..0000000
--- a/Platform.Ping.sln
+++ /dev/null
@@ -1,28 +0,0 @@
-
-Microsoft Visual Studio Solution File, Format Version 12.00
-# Visual Studio Version 17
-VisualStudioVersion = 17.0.31903.59
-MinimumVisualStudioVersion = 10.0.40219.1
-Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".sln", ".sln", "{8CA9B029-557C-4F01-97BA-27DA6634F956}"
- ProjectSection(SolutionItems) = preProject
- action.yml = action.yml
- Dockerfile = Dockerfile
- EndProjectSection
-EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Console", "src\Console\Console.csproj", "{933BFA35-3FFF-434B-82A5-0CB3DF1AB979}"
-EndProject
-Global
- GlobalSection(SolutionConfigurationPlatforms) = preSolution
- Debug|Any CPU = Debug|Any CPU
- Release|Any CPU = Release|Any CPU
- EndGlobalSection
- GlobalSection(SolutionProperties) = preSolution
- HideSolutionNode = FALSE
- EndGlobalSection
- GlobalSection(ProjectConfigurationPlatforms) = postSolution
- {933BFA35-3FFF-434B-82A5-0CB3DF1AB979}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {933BFA35-3FFF-434B-82A5-0CB3DF1AB979}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {933BFA35-3FFF-434B-82A5-0CB3DF1AB979}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {933BFA35-3FFF-434B-82A5-0CB3DF1AB979}.Release|Any CPU.Build.0 = Release|Any CPU
- EndGlobalSection
-EndGlobal
diff --git a/Platform.Ping.slnx b/Platform.Ping.slnx
new file mode 100644
index 0000000..3369505
--- /dev/null
+++ b/Platform.Ping.slnx
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/action.yml b/action.yml
index 615e476..4e68075 100644
--- a/action.yml
+++ b/action.yml
@@ -1,17 +1,21 @@
name: 'gg-ping-health-check'
-description: 'Ping web service health check'
+description: 'Checks the health status of a web service by pinging its health check endpoint.'
inputs:
health_check_url:
- description: 'Web service health check URL'
+ description: 'The URL of the web service health check endpoint.'
required: true
contains:
- description: 'Expected message'
+ description: 'The expected text or pattern that must be present in the health check response.'
required: false
+ is_end_of_line_match:
+ description: 'Indicates whether the expected value should match only at the end of a line.'
+ required: false
+ default: 'false'
retry_delay_in_seconds:
- description: 'Retry delay in seconds'
+ description: 'The delay, in seconds, between retry attempts.'
required: false
max_attempts:
- description: 'Max number of attempts'
+ description: 'The maximum number of health check attempts before failing.'
required: false
runs:
using: 'composite'
@@ -20,6 +24,7 @@ runs:
docker run --rm \
-e In__HealthCheckUrl="${{ inputs.health_check_url }}" \
-e In__ContainedMessage="${{ inputs.contains }}" \
+ -e In__IsEndOfLineMatch="${{ inputs.is_end_of_line_match }}" \
-e In__RetryDelayInSeconds="${{ inputs.retry_delay_in_seconds }}" \
-e In__MaxAttempts="${{ inputs.max_attempts }}" \
ghcr.io/garagegroup/gg-ping-health-check:latest
diff --git a/src/Console/Application.cs b/src/Console/Application.cs
index e4ce01a..2bebddd 100644
--- a/src/Console/Application.cs
+++ b/src/Console/Application.cs
@@ -10,5 +10,6 @@ internal static Dependency> UsePingHandler()
=>
PrimaryHandler.UseStandardSocketsHttpHandler()
.UseLogging("PingHandler")
+ .UseHttpApi()
.Map>(PingHandler.Resolve);
}
\ No newline at end of file
diff --git a/src/Console/Console.csproj b/src/Console/Console.csproj
index 0dfc0ca..cdd2b95 100644
--- a/src/Console/Console.csproj
+++ b/src/Console/Console.csproj
@@ -1,12 +1,13 @@
- net8.0
+ net10.0
Exe
disable
enable
false
true
+ $(NoWarn);IDE0130;CA1859;CA1873
GarageGroup.Platform.Ping
GarageGroup.Platform.Ping.Console
@@ -15,8 +16,10 @@
-
-
+
+
+
+
\ No newline at end of file
diff --git a/src/Console/Handler/Contract/IPingHandler.cs b/src/Console/Handler.Contract/IPingHandler.cs
similarity index 100%
rename from src/Console/Handler/Contract/IPingHandler.cs
rename to src/Console/Handler.Contract/IPingHandler.cs
diff --git a/src/Console/Handler/Contract/PingIn.cs b/src/Console/Handler.Contract/PingIn.cs
similarity index 55%
rename from src/Console/Handler/Contract/PingIn.cs
rename to src/Console/Handler.Contract/PingIn.cs
index 0259ae6..41b4cc7 100644
--- a/src/Console/Handler/Contract/PingIn.cs
+++ b/src/Console/Handler.Contract/PingIn.cs
@@ -1,13 +1,13 @@
-using System;
-
-namespace GarageGroup.Platform.Ping;
+namespace GarageGroup.Platform.Ping;
internal sealed record class PingIn
{
- public Uri? HealthCheckUrl { get; init; }
+ public string? HealthCheckUrl { get; init; }
public string? ContainedMessage { get; init; }
+ public bool? IsEndOfLineMatch { get; init; }
+
public int? RetryDelayInSeconds { get; init; }
public int? MaxAttempts { get; init; }
diff --git a/src/Console/Handler/Handler.Handler.cs b/src/Console/Handler/Handler.Handler.cs
index 98569fd..53ca6ea 100644
--- a/src/Console/Handler/Handler.Handler.cs
+++ b/src/Console/Handler/Handler.Handler.cs
@@ -1,5 +1,5 @@
using System;
-using System.Net.Http;
+using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using GarageGroup.Infra;
@@ -9,11 +9,12 @@ namespace GarageGroup.Platform.Ping;
partial class PingHandler
{
- public async ValueTask>> HandleAsync(PingIn? input, CancellationToken cancellationToken)
+ public async ValueTask>> HandleAsync(
+ PingIn? input, CancellationToken cancellationToken)
{
- if (input?.HealthCheckUrl is null)
+ if (string.IsNullOrWhiteSpace(input?.HealthCheckUrl))
{
- return Failure.Create(HandlerFailureCode.Persistent, "HealthCheckUrl must be specified");
+ return Failure.Create(HandlerFailureCode.Persistent, "HealthCheckUrl must be specified.");
}
var retryDelayInSeconds = input.RetryDelayInSeconds > 0 ? input.RetryDelayInSeconds.Value : DefaultRetryDelayInSeconds;
@@ -21,8 +22,8 @@ public async ValueTask>> HandleAsync(Pi
for (var i = 0; i < maxAttempts; i++)
{
- logger.LogInformation("Attempt {attemmpt} of {maxAttempts}", i + 1, maxAttempts);
- var result = await PingAsync(input, cancellationToken).ConfigureAwait(false);
+ logger.LogInformation("Attempt {attemmpt} of {maxAttempts}.", i + 1, maxAttempts);
+ var result = await InnerPingAsync(input, cancellationToken);
if (result.IsSuccess)
{
@@ -32,43 +33,51 @@ public async ValueTask>> HandleAsync(Pi
var failure = result.FailureOrThrow();
logger.LogError(failure.SourceException, "{failureMessage}", failure.FailureMessage);
- logger.LogInformation("Delay {delay}s", retryDelayInSeconds);
- await Task.Delay(retryDelayInSeconds * 1000, cancellationToken).ConfigureAwait(false);
+ logger.LogInformation("Delay {delay}s.", retryDelayInSeconds);
+ await Task.Delay(retryDelayInSeconds * 1000, cancellationToken);
}
- return Failure.Create(HandlerFailureCode.Transient, "All attempts were unsuccessful");
+ return Failure.Create(HandlerFailureCode.Transient, "All attempts were unsuccessful.");
}
- private async ValueTask>> PingAsync(PingIn input, CancellationToken cancellationToken)
+ private ValueTask>> InnerPingAsync(PingIn input, CancellationToken cancellationToken)
+ =>
+ AsyncPipeline.Pipe(
+ input, cancellationToken)
+ .Pipe(
+ static @in => new HttpSendIn(HttpVerb.Get, @in.HealthCheckUrl.OrEmpty()))
+ .PipeValue(
+ httpApi.SendAsync)
+ .Map(
+ static success => success.Body.Content?.ToString(),
+ static failure => failure.ToStandardFailure("An unsuccessful HTTP request:").WithFailureCode(default))
+ .Forward(
+ response => VerifyResponse(response.OrEmpty(), input));
+
+ private static Result> VerifyResponse(string response, PingIn input)
{
- try
+ Console.WriteLine(response);
+ if (string.IsNullOrWhiteSpace(input.ContainedMessage))
{
- using var httpClient = new HttpClient(httpMessageHandler, disposeHandler: false);
- using var httpResponse = await httpClient.GetAsync(input.HealthCheckUrl, cancellationToken).ConfigureAwait(false);
-
- var httpResponseBody = await httpResponse.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);
- logger.LogInformation("{responseBody}", httpResponseBody);
-
- if (httpResponse.IsSuccessStatusCode is false)
- {
- return Failure.Create("An unsuccessful HTTP request");
- }
-
- if (string.IsNullOrWhiteSpace(input.ContainedMessage))
- {
- return Result.Success(default);
- }
+ return Result.Success(default);
+ }
- if (httpResponseBody.Contains(input.ContainedMessage, StringComparison.InvariantCulture))
+ if (input.IsEndOfLineMatch is not true)
+ {
+ if (response.Contains(input.ContainedMessage, StringComparison.InvariantCultureIgnoreCase))
{
return Result.Success(default);
}
- return Failure.Create($"Response does not contain required message '{input.ContainedMessage}'");
+ return Failure.Create($"Response does not contain required message '{input.ContainedMessage}'.");
}
- catch (OperationCanceledException ex)
+
+ var pattern = Regex.Escape(input.ContainedMessage) + "$";
+ if (Regex.IsMatch(response, pattern, RegexOptions.Multiline | RegexOptions.IgnoreCase | RegexOptions.CultureInvariant))
{
- return ex.ToFailure("An HTTP request was canceled");
+ return Result.Success(default);
}
+
+ return Failure.Create($"Response does not contain strict required message '{input.ContainedMessage}'.");
}
}
\ No newline at end of file
diff --git a/src/Console/Handler/PingHandler.cs b/src/Console/Handler/PingHandler.cs
index 51c83ef..b88e873 100644
--- a/src/Console/Handler/PingHandler.cs
+++ b/src/Console/Handler/PingHandler.cs
@@ -1,5 +1,5 @@
using System;
-using System.Net.Http;
+using GarageGroup.Infra;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
@@ -7,13 +7,13 @@ namespace GarageGroup.Platform.Ping;
internal sealed partial class PingHandler : IPingHandler
{
- public static PingHandler Resolve(IServiceProvider serviceProvider, HttpMessageHandler httpMessageHandler)
+ public static PingHandler Resolve(IServiceProvider serviceProvider, IHttpApi httpApi)
{
ArgumentNullException.ThrowIfNull(serviceProvider);
- ArgumentNullException.ThrowIfNull(httpMessageHandler);
+ ArgumentNullException.ThrowIfNull(httpApi);
return new(
- httpMessageHandler: httpMessageHandler,
+ httpApi: httpApi,
logger: serviceProvider.GetRequiredService().CreateLogger());
}
@@ -21,13 +21,13 @@ public static PingHandler Resolve(IServiceProvider serviceProvider, HttpMessageH
private const int DefaultMaxAttempts = 3;
- private readonly HttpMessageHandler httpMessageHandler;
+ private readonly IHttpApi httpApi;
private readonly ILogger logger;
- private PingHandler(HttpMessageHandler httpMessageHandler, ILogger logger)
+ private PingHandler(IHttpApi httpApi, ILogger logger)
{
- this.httpMessageHandler = httpMessageHandler;
+ this.httpApi = httpApi;
this.logger = logger;
}
}
\ No newline at end of file
diff --git a/src/Console/Program.cs b/src/Console/Program.cs
index c1268b6..febe31d 100644
--- a/src/Console/Program.cs
+++ b/src/Console/Program.cs
@@ -3,9 +3,9 @@
namespace GarageGroup.Platform.Ping;
-public static class Program
+static class Program
{
- public static Task Main(string[] args)
+ static Task Main(string[] args)
=>
- Application.UsePingHandler().RunConsoleAsync("In", args);
+ Application.UsePingHandler().UseConsoleRunner("In", args).RunAsync();
}
\ No newline at end of file
diff --git a/src/Console/appsettings.json b/src/Console/appsettings.json
index 054f959..dbae1d8 100644
--- a/src/Console/appsettings.json
+++ b/src/Console/appsettings.json
@@ -3,6 +3,7 @@
"In": {
"HealthCheckUrl": "",
"ContainedMessage": "",
+ "IsEndOfLineMatch": false,
"RetryDelayInSeconds": 5,
"MaxAttempts": 3
}