Skip to content

Commit ce57668

Browse files
fix: fail-fast config validation and post-deploy smoke test (#77)
* fix: fail-fast config validation and post-deploy smoke test (closes #75) Add startup validation to Program.cs that throws immediately if any required JWT config values are absent. This surfaces missing env vars as a clear error in Azure logs on boot rather than as silent 500s at runtime. Add a post-deploy smoke test to deploy.yml that retries /api/weatherforecasts up to 5 times (15s apart) after deployment. The workflow fails if the endpoint does not return HTTP 200, preventing a broken deploy from going undetected. Root cause documented in issue #76. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: add liveness/readiness health endpoints and smoke test Add /health/live (liveness, no DB check) and /health/ready (readiness, includes SQL Server check) using ASP.NET Core built-in health checks + AspNetCore.HealthChecks.SqlServer. The deploy smoke test hits /health/live so a sleeping serverless Azure SQL DB does not cause false failures. /health/ready is available for uptime monitoring and alerting. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * style: fix missing newline in Program.cs Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent db967e9 commit ce57668

3 files changed

Lines changed: 48 additions & 0 deletions

File tree

.github/workflows/deploy.yml

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,22 @@ jobs:
8080
app-name: ${{ secrets.AZURE_WEBAPP_NAME }}
8181
package: dist
8282

83+
- name: Smoke test
84+
run: |
85+
url="https://${{ secrets.AZURE_WEBAPP_NAME }}.azurewebsites.net/health/live"
86+
echo "Smoke testing $url"
87+
for i in 1 2 3 4 5; do
88+
status=$(curl -s -o /dev/null -w "%{http_code}" "$url")
89+
echo "Attempt $i: HTTP $status"
90+
if [ "$status" = "200" ]; then
91+
echo "Smoke test passed"
92+
exit 0
93+
fi
94+
sleep 15
95+
done
96+
echo "Smoke test failed — last status was $status"
97+
exit 1
98+
8399
# NOTE: Database migrations are not run automatically here.
84100
# If you use EF Core migrations, consider adding a step like:
85101
# dotnet ef database update --project apps/api/Api

apps/api/Api/Api.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
</PropertyGroup>
66

77
<ItemGroup>
8+
<PackageReference Include="AspNetCore.HealthChecks.SqlServer" Version="9.0.0" />
89
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="10.0.5" />
910
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="10.0.2" />
1011
<PackageReference Include="Microsoft.AspNetCore.SpaServices.Extensions" Version="10.0.2" />

apps/api/Api/Program.cs

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
using System;
2+
using System.Collections.Generic;
23
using System.IO;
4+
using System.Linq;
35
using System.Text;
46
using System.Threading.RateLimiting;
57
using Microsoft.AspNetCore.Authentication.JwtBearer;
@@ -29,6 +31,19 @@
2931
builder.Configuration["Jwt:Key"] = envJwtKey;
3032
}
3133

34+
// Fail fast: refuse to start if any required config is absent.
35+
// This surfaces missing env vars immediately in logs rather than as
36+
// cryptic 500s at runtime.
37+
var requiredConfig = new[] { "Jwt:Key", "Jwt:Issuer", "Jwt:Audience" };
38+
var missingConfig = requiredConfig
39+
.Where(key => string.IsNullOrEmpty(builder.Configuration[key]))
40+
.ToList();
41+
if (missingConfig.Count > 0)
42+
throw new InvalidOperationException(
43+
$"Required configuration values are missing: {string.Join(", ", missingConfig)}. " +
44+
"Set them as environment variables (e.g. JWT_KEY, Jwt__Issuer, Jwt__Audience) before starting the application."
45+
);
46+
3247
builder.Services
3348
.AddAuthentication(options =>
3449
{
@@ -69,6 +84,13 @@
6984

7085
builder.Services.AddScoped<TokenService>();
7186

87+
var connectionString = builder.Environment.IsDevelopment()
88+
? builder.Configuration.GetConnectionString("AZURE_SQL_CONNECTIONSTRING")
89+
: Environment.GetEnvironmentVariable("AZURE_SQL_CONNECTIONSTRING");
90+
91+
builder.Services.AddHealthChecks()
92+
.AddSqlServer(connectionString!, name: "sql", tags: ["ready"]);
93+
7294
builder.Services.AddEndpointsApiExplorer();
7395

7496
builder.Services.AddSwaggerGen(c =>
@@ -163,6 +185,15 @@
163185

164186
app.MapControllers();
165187

188+
// Liveness: app process is up — used by deploy smoke test.
189+
app.MapHealthChecks("/health/live", new Microsoft.AspNetCore.Diagnostics.HealthChecks.HealthCheckOptions
190+
{
191+
Predicate = _ => false, // exclude all tagged checks (e.g. SQL)
192+
});
193+
194+
// Readiness: app + dependencies (DB) — used for monitoring.
195+
app.MapHealthChecks("/health/ready");
196+
166197
// Custom JWT auth endpoints: login / refresh / logout
167198
app.MapAuthEndpoints(app.Environment.IsDevelopment());
168199

0 commit comments

Comments
 (0)