diff --git a/deploy/qa/docker-compose.qa.yml b/deploy/qa/docker-compose.qa.yml index 1f9cdb1..0cb9807 100644 --- a/deploy/qa/docker-compose.qa.yml +++ b/deploy/qa/docker-compose.qa.yml @@ -34,6 +34,20 @@ services: ports: - "${WEB_PORT:-6173}:80" + # Preflight migration check, run by deploy.sh via `--profile tools run --rm migrate` + # against the newly pulled image before the running stack is touched. Excluded from + # `up -d` by the tools profile. + migrate: + image: ${API_IMAGE}:latest + command: ["dotnet", "Novelly.Api.dll", "--migrate-only"] + environment: + ConnectionStrings__Novel: "Data Source=/data/novel.db" + volumes: + - novelly-data:/data + networks: + - novelly + profiles: ["tools"] + networks: novelly: name: novelly-net diff --git a/scripts/ci/deploy.sh b/scripts/ci/deploy.sh index 90c0dbc..ccc894a 100755 --- a/scripts/ci/deploy.sh +++ b/scripts/ci/deploy.sh @@ -19,6 +19,15 @@ COMPOSE="docker compose -f deploy/qa/docker-compose.qa.yml" log "Pulling latest :latest images" $COMPOSE pull +# Applies pending migrations against the live novelly-data volume using the new image, +# before the running (old-image) stack is touched. If a migration is broken, this fails +# here and the old containers keep serving traffic — `down`/`up` below never runs, so +# there is nothing to roll back. +log "Running preflight migration check" +if ! $COMPOSE --profile tools run --rm migrate; then + fail "migration failed against the new image; old novelly stack left running untouched" +fi + log "Recreating the novelly stack (data volume preserved)" $COMPOSE down $COMPOSE up -d diff --git a/src/Novelly.Api/Program.cs b/src/Novelly.Api/Program.cs index 34f6930..1e87e82 100644 --- a/src/Novelly.Api/Program.cs +++ b/src/Novelly.Api/Program.cs @@ -41,12 +41,30 @@ builder.Services.AddCors(options => options.AddDefaultPolicy(policy => policy .AllowAnyMethod() .AllowCredentials())); +var migrateOnly = args.Contains("--migrate-only"); + var app = builder.Build(); using (var scope = app.Services.CreateScope()) { var db = scope.ServiceProvider.GetRequiredService(); - await db.Database.MigrateAsync(); + + try + { + await db.Database.MigrateAsync(); + } + catch (Exception ex) + { + app.Logger.LogCritical(ex, "Database migration failed on startup"); + Environment.Exit(1); + } + + if (migrateOnly) + { + app.Logger.LogInformation("Migration complete, exiting ({MigrateOnlyFlag})", "--migrate-only"); + Environment.Exit(0); + } + await ServiceUser.EnsureSeededAsync(db, builder.Configuration[ServiceApiKeyAuthenticationHandler.ConfigurationKey], app.Logger); }