From c1b52418c66403409ff441141cb9525cd730c6db Mon Sep 17 00:00:00 2001 From: "Bart Segers (M16)" Date: Tue, 21 Apr 2026 16:00:15 +0200 Subject: [PATCH 01/10] docs: add repo layout simplification spec Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- ...-repo-simplification-file-layout-design.md | 198 ++++++++++++++++++ 1 file changed, 198 insertions(+) create mode 100644 docs/superpowers/specs/2026-04-21-repo-simplification-file-layout-design.md diff --git a/docs/superpowers/specs/2026-04-21-repo-simplification-file-layout-design.md b/docs/superpowers/specs/2026-04-21-repo-simplification-file-layout-design.md new file mode 100644 index 0000000..303800d --- /dev/null +++ b/docs/superpowers/specs/2026-04-21-repo-simplification-file-layout-design.md @@ -0,0 +1,198 @@ +# Repo simplification and file layout (design) + +## Context + +The repository already has a reasonable top-level shape, but it has started to drift in a few ways that make it harder to understand and maintain: + +- Runnable scripts live mostly under `scripts\`, but `docs\scripts\create-github-issues.ps1` introduces a second script location. +- Some documentation still refers to removed or superseded paths such as `scripts\azure\...`, while current operational entrypoints use `scripts\deploy.ps1`, `scripts\update.ps1`, and related root-level scripts. +- The repo contains multiple instruction surfaces (`README.md`, `docs\`, `AGENTS.md`, `.github\`, `.claude\`) that describe overlapping workflows. They are useful, but they can drift if they do not point to the same canonical paths and commands. + +The goal of this first spec is to simplify the repository structure without broad behavioral changes. It should establish a clearer layout that later script and documentation cleanup work can build on. + +## Goal + +Make the repository easier to navigate by giving scripts, documentation, and tool-specific instructions one clear home each. + +Success means: + +1. Every runnable script lives under one canonical `scripts\` folder. +2. `docs\` contains documentation only, not operational scripts. +3. `README.md`, `docs\`, `AGENTS.md`, `.github\`, and `.claude\` reference the same canonical paths and workflow names. +4. Stale references to removed structures such as `scripts\azure\...` are either updated or explicitly marked as historical content in archived design material. + +Non-goals: + +- Redesigning the application architecture. +- Changing deployment behavior in this spec. +- Rewriting all documentation content in one pass. +- Introducing a heavy taxonomy of script subfolders before the script count justifies it. + +## Options considered + +### Option 1 - One flat canonical `scripts\` folder *(chosen)* + +Keep all runnable scripts in the existing root `scripts\` folder. Move stray scripts into it. Documentation points to those scripts instead of embedding or hosting them. + +Pros: + +- Smallest change from current reality. +- Easiest rule for contributors: "if it runs, it lives in `scripts\`." +- Lowest churn while the repo is still early-stage. + +Cons: + +- The folder may become busy later if script count grows significantly. + +### Option 2 - Canonical `scripts\` folder with immediate subfolders by purpose + +Reorganize now into `scripts\deploy\`, `scripts\dev\`, `scripts\tooling\`, and similar categories. + +Pros: + +- Better long-term scaling if the script inventory grows rapidly. +- Slightly clearer categorization at a glance. + +Cons: + +- Premature structure for the current repo size. +- Creates more rename churn now, with little immediate user value. + +### Option 3 - Keep split script locations but document ownership rules + +Allow both `scripts\` and `docs\scripts\` as long as the distinction is documented. + +Pros: + +- Minimal file movement. + +Cons: + +- Preserves the ambiguity that this cleanup is meant to remove. +- Makes discoverability and maintenance worse over time. + +**Decision: Option 1.** + +## Design + +### Repository ownership rules + +The repository should use these simple ownership boundaries: + +- `scripts\` - all runnable scripts, whether for deployment, local setup, maintenance, coverage, or tooling. +- `docs\` - human-readable guidance only. Documentation may describe scripts, but must not be the canonical home for them. +- `docs\deployment\` - deployment and environment setup guidance. +- `docs\development\` - contributor setup, local development, testing, and tooling guidance. +- `docs\architecture\` - enduring architecture and system-shape documentation. +- `docs\superpowers\` - AI-generated specs and plans; useful as project history, but not the primary end-user documentation surface. +- `.github\` - GitHub-specific automation, templates, and instructions only. +- `.claude\` - Claude-specific overlays, local tooling, and backlog artifacts only. + +This keeps the repo easy to explain: code in `src\` and `tests\`, scripts in `scripts\`, docs in `docs\`, platform-specific overlays in `.github\` and `.claude\`. + +### Canonical script rule + +Every runnable script should live under `scripts\`. + +That includes: + +- deployment and teardown helpers, +- local development helpers, +- setup/bootstrap scripts, +- maintenance/tooling helpers such as issue creation or coverage generation. + +`docs\scripts\` should be removed as an active location. The current `docs\scripts\create-github-issues.ps1` should move to `scripts\create-github-issues.ps1`, and all references should be updated to point there. + +This is a lightweight **single source of truth** pattern for operational entrypoints: one script location, one canonical path, many docs that reference it. + +### Documentation layering + +Documentation should be intentionally layered instead of duplicated: + +- `README.md` should stay short and answer: what this project is, what is required to run it locally, and where to go next. +- `docs\deployment\` should hold deployment-specific details, including Azure and local container-based deployment guidance. +- `docs\development\` should hold contributor workflows such as local setup, GitHub setup, testing, and coverage. +- `AGENTS.md` should remain contributor/agent guidance for coding conventions and repo rules, but should not become the only place where operational workflows are documented. + +When a process needs both concise guidance and deeper guidance, `README.md` should link to the detailed doc instead of repeating it. + +### Alignment rules for `.github`, `.claude`, and docs + +This spec includes layout and alignment rules now, while deferring broader content cleanup to a later documentation-focused spec. + +The alignment rule is: + +1. Operational paths and script names are canonical in the repo tree itself. +2. Human-facing docs (`README.md`, `docs\`) should reference those canonical paths directly. +3. Tool-specific instruction files (`AGENTS.md`, `.github\copilot-instructions.md`, `.claude\CLAUDE.md`, related backlog notes) should reference the same paths and should not invent alternate structure descriptions. +4. Historical design docs under `docs\superpowers\` may mention old structures, but current user-facing docs must not. + +This means current drift such as references to `scripts\azure\...` should be removed from live docs. Historical specs and plans may remain as historical artifacts unless they are actively misleading a current workflow. + +### What this spec changes vs what it leaves for later + +This spec should drive: + +- moving stray scripts into `scripts\`, +- removing active duplicate script locations, +- updating current docs and instruction surfaces to reflect one canonical layout, +- defining ownership boundaries for repo surfaces. + +This spec should not yet decide: + +- whether `scripts\` eventually needs `deploy\`, `dev\`, or `tooling\` subfolders, +- how Azure deployment scripts should be functionally redesigned, +- how local deployment should be packaged for Raspberry Pi or other Docker hosts, +- how preflight checks and prerequisite validation should work inside `deploy.ps1`. + +Those are valid next specs, but they depend on this simplified layout first. + +## Planned file-level outcomes + +Expected outcomes from implementing this spec: + +- Move `docs\scripts\create-github-issues.ps1` to `scripts\create-github-issues.ps1`. +- Update `docs\development\github-setup.md` and any related references to point at the canonical script path. +- Audit current user-facing docs for references to old `scripts\azure\...` structure and replace them with current supported workflows. +- Update `README.md`, `AGENTS.md`, `.github\` guidance, and `.claude\` guidance where needed so they describe the same layout. +- Leave historical `docs\superpowers\specs\` and `docs\superpowers\plans\` content intact unless a specific file is being reused as active guidance. + +## Risks and mitigations + +### Risk: over-structuring too early + +Adding many new folders now could make the repo look cleaner on paper while making it harder to remember where things go. + +Mitigation: keep the rule simple now - one `scripts\` folder - and only introduce subfolders later if script volume creates real pain. + +### Risk: hidden references to old paths + +Docs and instruction files may still reference outdated paths even after the obvious files are updated. + +Mitigation: include a targeted cross-reference pass across `README.md`, `docs\`, `AGENTS.md`, `.github\`, and `.claude\` as part of implementation. + +### Risk: historical specs confuse contributors + +Older design docs may still mention removed structures. + +Mitigation: treat `docs\superpowers\` as historical design record, not canonical runtime documentation, and avoid linking current onboarding docs to outdated historical specs. + +## Testing and validation + +This structural cleanup does not require new testing tools. + +Validation for implementation should focus on: + +- all moved scripts remaining invocable from their new canonical paths, +- all user-facing documentation pointing to valid paths, +- no active documentation continuing to describe deprecated folder layouts. + +## Follow-on specs enabled by this design + +After this spec, the next logical specs are: + +1. Script standardization and PowerShell v5 compatibility. +2. Local install and deployment experience, including Docker-first local hosting guidance. +3. Azure `deploy.ps1` improvements, including prerequisite checks and workflow triggering. +4. Minimal documentation cleanup and prerequisite clarity. +5. Cross-surface consistency audit for repo guidance and automation instructions. From a45e3367e14c61b5f81998e45e18165c46fb57d9 Mon Sep 17 00:00:00 2001 From: "Bart Segers (M16)" Date: Tue, 21 Apr 2026 16:36:12 +0200 Subject: [PATCH 02/10] docs: add code simplification spec Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../2026-04-21-code-simplification-design.md | 259 ++++++++++++++++++ 1 file changed, 259 insertions(+) create mode 100644 docs/superpowers/specs/2026-04-21-code-simplification-design.md diff --git a/docs/superpowers/specs/2026-04-21-code-simplification-design.md b/docs/superpowers/specs/2026-04-21-code-simplification-design.md new file mode 100644 index 0000000..57cb9b3 --- /dev/null +++ b/docs/superpowers/specs/2026-04-21-code-simplification-design.md @@ -0,0 +1,259 @@ +# Code simplification (design) + +## Context + +The codebase is still small, which makes this a good time to simplify it before early structure hardens into long-term maintenance cost. + +The current architecture is directionally sound: controller-based ASP.NET Core API, MediatR in the application layer, EF Core in infrastructure, Blazor WebAssembly on the frontend, and a healthy test surface across API, application, infrastructure, UI, and E2E layers. + +The risk is not that the codebase is already deeply overengineered. The risk is that small pockets of ceremony accumulate faster than domain complexity: + +- startup and composition code in `src\Backend\AHKFlowApp.API\Program.cs` is dense and mixes several concerns inline, +- some abstractions are very thin and may only rename framework concepts without creating a meaningful seam, +- the frontend API client layer contains repeated request/result plumbing, +- tests include helpers and fixtures that may be useful, but should be pruned where they obscure intent more than they improve reuse. + +This first code-focused spec should simplify code in `src\` and noisy supporting code in `tests\`, while preserving the boundaries that still clearly support testability, clarity, or architecture. + +## Goal + +Reduce low-value complexity in the codebase without destabilizing the architectural boundaries that are already serving the project well. + +Success means: + +1. Common code paths are easier to read end-to-end. +2. Thin abstractions that do not add real value are removed or collapsed. +3. Startup, HTTP client, and test helper code becomes shorter and more obvious. +4. Any patterns kept or introduced are lightweight and reduce code rather than multiply it. +5. Small structure changes inside `src\` and `tests\` are allowed when they improve discoverability, but broad repo layout work stays out of scope. + +Non-goals: + +- redesigning the project’s Clean Architecture baseline, +- changing deployment or documentation workflows in this spec, +- chasing coverage percentages as the main objective, +- rewriting stable code just to make it look different. + +## Options considered + +### Option 1 - Targeted simplification *(chosen)* + +Remove low-value indirection, simplify hotspots, and allow small internal structure cleanup while preserving the boundaries that still clearly help with testing or architecture. + +Pros: + +- Delivers meaningful simplification without destabilizing the whole codebase. +- Fits the current repo maturity: enough code exists to trim ceremony, but not so much that a broad rewrite is justified. +- Keeps the cleanup focused on readability, not ideology. + +Cons: + +- Requires judgment calls on which abstractions still pay for themselves. +- Some inconsistencies may remain temporarily if they are lower-value than the main hotspots. + +### Option 2 - Conservative cleanup only + +Focus mainly on naming, formatting, short methods, and tiny refactors while preserving nearly all current abstractions and structure. + +Pros: + +- Lowest change risk. +- Easy to review. + +Cons: + +- Would leave the main sources of friction in place. +- Likely to disappoint the stated goal of making the code more concise. + +### Option 3 - Aggressive abstraction collapse + +Collapse most wrappers, interfaces, and intermediate layers unless they are absolutely required at runtime. + +Pros: + +- Maximum immediate reduction in moving parts. + +Cons: + +- Too risky for an early codebase that is still settling. +- Likely to erase boundaries that still support testing and future feature growth. +- Creates needless churn before the scripts and docs passes. + +**Decision: Option 1.** + +## Design + +### Simplification principles + +This cleanup should follow a few strict principles: + +- Prefer deleting code over moving code when the behavior is redundant. +- Prefer one obvious flow over multiple small indirections. +- Keep abstractions only when they create a real boundary, not when they merely wrap a framework type. +- Keep explicit mappings and explicit behavior where they improve correctness or readability. +- Avoid introducing new patterns unless they remove duplication and make the code easier to explain. + +In short: concise, but not clever. + +### Cleanup category 1 - Startup and composition + +`Program.cs` is a likely simplification hotspot because it currently mixes: + +- bootstrap logging, +- conditional Application Insights setup, +- controller and ProblemDetails configuration, +- service registration, +- development-only database migration logic, +- request pipeline setup, +- development-only browser launch behavior. + +The goal is not to hide all of this behind many extension methods. The goal is to make the composition root easier to scan. + +The preferred direction is: + +- keep `Program.cs` as the composition root, +- extract only cohesive setup blocks that materially improve readability, +- keep behavior discoverable near startup rather than scattering it across many files. + +Good candidates are small, single-purpose setup methods or extensions for areas that already have a natural boundary, such as API/OpenAPI setup or development-only startup behavior. Poor candidates are wrappers that just move a few lines without clarifying anything. + +### Cleanup category 2 - Abstraction pruning + +The codebase should review interfaces and wrappers with a high bar: + +- keep them when they protect a meaningful application boundary, +- remove them when they only rename a concrete framework dependency without real substitution value. + +Likely review targets include thin abstractions such as development environment wrappers and other one-property or one-method interfaces that exist only to pass framework state through a layer. + +This does **not** mean removing all abstractions. Some seams are still justified: + +- current-user access is a real boundary between HTTP context and application logic, +- database access boundaries may still be justified if they meaningfully isolate the application layer from infrastructure details, +- service abstractions that materially simplify testing or keep layers clean should remain. + +The rule is selective pruning, not a purity exercise. + +### Cleanup category 3 - Frontend service simplification + +The Blazor client layer should aim for a small, easy-to-follow shape: + +- page code should call clear client methods, +- shared HTTP and error handling should not be duplicated, +- the number of service interfaces should stay proportional to the actual complexity. + +The likely target is the request/result plumbing around `AhkFlowAppApiHttpClient` and `HotstringsApiClient`. This area may benefit from a small shared helper or a modest facade pattern, but only if it removes duplication without obscuring status handling or error mapping. + +What to avoid: + +- adding a generic service hierarchy, +- introducing a large base class just to share a few lines, +- splitting the API client surface into many tiny interfaces without need. + +### Cleanup category 4 - Mapping and DTO friction + +The project should keep explicit mapping rather than introducing mapper libraries. + +However, explicit mapping should still feel lightweight. This cleanup should review: + +- duplicate DTO shapes between frontend and backend where the duplication adds maintenance noise, +- mapping helpers that are justified versus ones that are too trivial to carry their own indirection cost, +- places where small mapping or response-shape cleanup would make the code easier to follow. + +The goal is not necessarily to unify all DTOs across the app, but to reduce friction where duplication no longer pays for the separation. + +### Cleanup category 5 - Test code simplification + +Test code is part of the codebase and should also stay concise. + +This spec should allow pruning or simplifying: + +- builders that do not save meaningful repetition, +- fixtures that hide test setup more than they clarify it, +- helper layers that make tests harder to read than inline setup would. + +At the same time, this cleanup should preserve helpers that clearly improve integration and end-to-end testing, especially where infrastructure setup is expensive or repetitive. + +The desired outcome is tests that read more directly while retaining the support code that materially reduces boilerplate. + +### Cleanup category 6 - Small internal structure cleanup + +This spec may include small structure changes inside `src\` and `tests\` when they simplify the code: + +- moving a file to a more obvious folder, +- co-locating tightly related classes, +- removing folders that contain only one trivial concept if the nesting adds noise. + +This spec should not do broad repo-layout work. That remains a separate follow-on effort for scripts and documentation. + +## Likely first-pass targets + +Based on the current code shape, likely early targets include: + +- `src\Backend\AHKFlowApp.API\Program.cs` +- `src\Backend\AHKFlowApp.Application\Abstractions\IDevEnvironment.cs` +- `src\Backend\AHKFlowApp.API\DevEnvironment.cs` +- `src\Frontend\AHKFlowApp.UI.Blazor\Services\AhkFlowAppApiHttpClient.cs` +- `src\Frontend\AHKFlowApp.UI.Blazor\Services\HotstringsApiClient.cs` +- test helpers or builders that are only lightly used or add indirection without enough payoff + +These are review targets, not automatic deletions. Each should be kept, simplified, or removed based on whether it reduces cognitive load in practice. + +## Lightweight patterns allowed + +This cleanup may use a few lightweight patterns where they reduce code and improve clarity: + +- **composition root cleanup** for startup wiring, +- **small, cohesive extension methods** for setup blocks with a real boundary, +- **facade-style client surface** in the frontend if it collapses duplicate HTTP plumbing, +- **single source of truth helpers** where multiple code paths are currently manually repeating the same transformation or status mapping. + +Patterns that should be avoided in this pass: + +- repository pattern, +- mapper frameworks, +- generic base hierarchies, +- speculative abstractions for future features. + +## Risks and mitigations + +### Risk: oversimplifying useful seams + +Some abstractions may look thin but still protect an important architectural or testing boundary. + +Mitigation: require each abstraction review to answer a simple question: what concrete cost do we pay if this seam disappears? If the answer is meaningful, keep it. + +### Risk: replacing explicit code with hidden indirection + +Startup cleanup in particular can become worse if code is pushed into many extension methods that hide behavior. + +Mitigation: only extract cohesive blocks that make `Program.cs` easier to scan, and keep behavior discoverable. + +### Risk: test readability regresses + +Removing helpers too aggressively can lead to repeated setup noise. + +Mitigation: simplify helpers selectively and optimize for direct, readable tests rather than minimal helper count. + +## Testing and validation + +This spec is about simplification, not only appearance, so implementation should validate behavior rather than assume safe refactors. + +Validation should focus on: + +- existing build and test workflows continuing to pass, +- no user-visible API behavior changing unintentionally, +- frontend pages and API client behavior preserving current error handling semantics, +- simplified tests remaining readable and representative of real behavior. + +Coverage improvement may happen as a byproduct where tests need to be clarified or added around changed code, but coverage thresholds are not the primary driver of this spec. + +## Follow-on specs enabled by this design + +After this spec, the next logical specs are: + +1. Repo simplification and file layout. +2. Script standardization and PowerShell v5 compatibility. +3. Local install and deployment experience. +4. Azure deployment automation and preflight checks. +5. Minimal documentation cleanup and cross-surface alignment. From 9ce91f7e397773fe70787f1912d4005232cec897 Mon Sep 17 00:00:00 2001 From: "Bart Segers (M16)" Date: Wed, 22 Apr 2026 08:54:33 +0200 Subject: [PATCH 03/10] fix: stop test utilities discovery crash Restore the test SDK and copy local lock-file assemblies so vstest can inspect AHKFlowApp.TestUtilities.dll without crashing during discovery while keeping the project marked as IsTestProject=false. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- tests/AHKFlowApp.TestUtilities/AHKFlowApp.TestUtilities.csproj | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/AHKFlowApp.TestUtilities/AHKFlowApp.TestUtilities.csproj b/tests/AHKFlowApp.TestUtilities/AHKFlowApp.TestUtilities.csproj index 39ff913..42be094 100644 --- a/tests/AHKFlowApp.TestUtilities/AHKFlowApp.TestUtilities.csproj +++ b/tests/AHKFlowApp.TestUtilities/AHKFlowApp.TestUtilities.csproj @@ -2,11 +2,13 @@ false false + true + From d5979f3a5e08ca5cd3780730e91a815336b662db Mon Sep 17 00:00:00 2001 From: "Bart Segers (M16)" Date: Wed, 22 Apr 2026 08:56:19 +0200 Subject: [PATCH 04/10] refactor: simplify startup and app environment Simplify API and Blazor startup wiring, remove the redundant DevEnvironment wrapper, and replace the old environment seam with a single application-owned AppEnvironment type while keeping the useful UI test seams intact. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/Backend/AHKFlowApp.API/DevEnvironment.cs | 5 - .../Extensions/ApiExtensions.cs | 16 ++ src/Backend/AHKFlowApp.API/Program.cs | 205 +++++++++--------- .../Abstractions/IDevEnvironment.cs | 6 - .../AHKFlowApp.Application/AppEnvironment.cs | 3 + .../Commands/Dev/SeedHotstringsCommand.cs | 3 +- src/Frontend/AHKFlowApp.UI.Blazor/Program.cs | 35 ++- .../SeedHotstringsCommandHandlerTests.cs | 9 +- 8 files changed, 140 insertions(+), 142 deletions(-) delete mode 100644 src/Backend/AHKFlowApp.API/DevEnvironment.cs delete mode 100644 src/Backend/AHKFlowApp.Application/Abstractions/IDevEnvironment.cs create mode 100644 src/Backend/AHKFlowApp.Application/AppEnvironment.cs diff --git a/src/Backend/AHKFlowApp.API/DevEnvironment.cs b/src/Backend/AHKFlowApp.API/DevEnvironment.cs deleted file mode 100644 index 20aa977..0000000 --- a/src/Backend/AHKFlowApp.API/DevEnvironment.cs +++ /dev/null @@ -1,5 +0,0 @@ -using AHKFlowApp.Application.Abstractions; - -namespace AHKFlowApp.API; - -internal sealed record DevEnvironment(bool IsDevelopment) : IDevEnvironment; diff --git a/src/Backend/AHKFlowApp.API/Extensions/ApiExtensions.cs b/src/Backend/AHKFlowApp.API/Extensions/ApiExtensions.cs index 561be4e..cb28234 100644 --- a/src/Backend/AHKFlowApp.API/Extensions/ApiExtensions.cs +++ b/src/Backend/AHKFlowApp.API/Extensions/ApiExtensions.cs @@ -71,4 +71,20 @@ internal static WebApplication UseSwaggerDocs(this WebApplication app) return app; } + + internal static WebApplication UseRootRedirect(this WebApplication app, string destination) + { + app.Use(async (context, next) => + { + if (context.Request.Path == "/") + { + context.Response.Redirect(destination); + return; + } + + await next(context); + }); + + return app; + } } diff --git a/src/Backend/AHKFlowApp.API/Program.cs b/src/Backend/AHKFlowApp.API/Program.cs index c8eda8f..28bcadc 100644 --- a/src/Backend/AHKFlowApp.API/Program.cs +++ b/src/Backend/AHKFlowApp.API/Program.cs @@ -31,29 +31,8 @@ WebApplicationBuilder builder = WebApplication.CreateBuilder(args); - // Application Insights — only when a connection string is configured (Test/Prod) string? appInsightsConnectionString = builder.Configuration["ApplicationInsights:ConnectionString"]; - if (!string.IsNullOrWhiteSpace(appInsightsConnectionString)) - { - builder.Services.AddApplicationInsightsTelemetry(options => - options.ConnectionString = appInsightsConnectionString); - } - - // Stage 2: Full logger configured from appsettings.json, with DI integration - builder.Services.AddSerilog((services, lc) => - { - lc.ReadFrom.Configuration(builder.Configuration) - .ReadFrom.Services(services) - .Enrich.FromLogContext() - .Enrich.WithProperty("Application", "AHKFlowApp.API"); - - // Route Serilog events to Application Insights when configured - if (!string.IsNullOrWhiteSpace(appInsightsConnectionString)) - { - TelemetryConfiguration telemetryConfig = services.GetRequiredService(); - lc.WriteTo.ApplicationInsights(telemetryConfig, TelemetryConverter.Traces); - } - }); + ConfigureObservability(builder, appInsightsConnectionString); // Start SQL Server in Docker if requested (for "https + Docker SQL" launch profile) if (builder.Environment.IsDevelopment() && @@ -62,76 +41,15 @@ DevDockerSqlServer.EnsureStarted(builder.Environment.ContentRootPath); } - builder.Services.AddProblemDetails(options => - options.CustomizeProblemDetails = ctx => - ctx.ProblemDetails.Extensions["traceId"] = ctx.HttpContext.TraceIdentifier); - - builder.Services.AddControllers() - .ConfigureApiBehaviorOptions(options => - options.InvalidModelStateResponseFactory = ctx => - { - var pd = new ValidationProblemDetails(ctx.ModelState) - { - Detail = "See the errors field for details.", - Instance = ctx.HttpContext.Request.Path, - Status = StatusCodes.Status422UnprocessableEntity, - Title = "One or more validation errors occurred." - }; - pd.Extensions["traceId"] = ctx.HttpContext.TraceIdentifier; - return new UnprocessableEntityObjectResult(pd) - { - ContentTypes = { "application/problem+json" } - }; - }); - - builder.Services.AddSingleton(TimeProvider.System); - builder.Services.AddSingleton(new DevEnvironment(builder.Environment.IsDevelopment())); - - if (builder.Environment.IsDevelopment()) - { - builder.Services.AddSwaggerDocs(); - } - builder.Services.AddApplication(); - builder.Services.AddInfrastructure(builder.Configuration); - builder.Services.AddHealthChecks() - .AddDbContextCheck( - name: "database", - failureStatus: HealthStatus.Unhealthy); - const string corsPolicyName = "AllowConfiguredOrigins"; string[] allowedOrigins = builder.Configuration.GetSection("Cors:AllowedOrigins").Get() ?? []; - builder.Services.AddConfiguredCors(allowedOrigins, corsPolicyName); - - builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) - .AddMicrosoftIdentityWebApi(builder.Configuration.GetSection("AzureAd")); - builder.Services.AddAuthorization(); - builder.Services.AddHttpContextAccessor(); - builder.Services.AddScoped(); + ConfigureServices(builder, allowedOrigins, corsPolicyName); WebApplication app = builder.Build(); - // Auto-apply migrations in Development (creates database if it doesn't exist) if (app.Environment.IsDevelopment()) { - await using AsyncServiceScope scope = app.Services.CreateAsyncScope(); - AppDbContext dbContext = scope.ServiceProvider.GetRequiredService(); - ILogger logger = scope.ServiceProvider.GetRequiredService>(); - try - { - logger.LogInformation("Applying database migrations..."); - await dbContext.Database.MigrateAsync(); - logger.LogInformation("Database migrations applied successfully."); - } - catch (SqlException ex) when (ex.Number == 1801) - { - // Database already exists (persisted Docker volume from a previous run) — migrations already applied - logger.LogInformation("Database already exists; skipping migration apply."); - } - catch (Exception ex) - { - logger.LogError(ex, "Error applying database migrations."); - throw; - } + await ApplyDevelopmentMigrationsAsync(app); } app.UseMiddleware(); @@ -159,27 +77,11 @@ if (app.Environment.IsDevelopment()) { app.UseSwaggerDocs(); - app.Use(async (context, next) => - { - if (context.Request.Path == "/") - { - context.Response.Redirect("/swagger"); - return; - } - await next(context); - }); + app.UseRootRedirect("/swagger"); } else { - app.Use(async (context, next) => - { - if (context.Request.Path == "/") - { - context.Response.Redirect("/health"); - return; - } - await next(context); - }); + app.UseRootRedirect("/health"); } if (allowedOrigins.Length > 0) @@ -225,3 +127,100 @@ { await Log.CloseAndFlushAsync(); } + +static void ConfigureObservability(WebApplicationBuilder builder, string? appInsightsConnectionString) +{ + if (!string.IsNullOrWhiteSpace(appInsightsConnectionString)) + { + builder.Services.AddApplicationInsightsTelemetry(options => + options.ConnectionString = appInsightsConnectionString); + } + + builder.Services.AddSerilog((services, lc) => + { + lc.ReadFrom.Configuration(builder.Configuration) + .ReadFrom.Services(services) + .Enrich.FromLogContext() + .Enrich.WithProperty("Application", "AHKFlowApp.API"); + + if (!string.IsNullOrWhiteSpace(appInsightsConnectionString)) + { + TelemetryConfiguration telemetryConfig = services.GetRequiredService(); + lc.WriteTo.ApplicationInsights(telemetryConfig, TelemetryConverter.Traces); + } + }); +} + +static void ConfigureServices(WebApplicationBuilder builder, string[] allowedOrigins, string corsPolicyName) +{ + builder.Services.AddProblemDetails(options => + options.CustomizeProblemDetails = ctx => + ctx.ProblemDetails.Extensions["traceId"] = ctx.HttpContext.TraceIdentifier); + + builder.Services.AddControllers() + .ConfigureApiBehaviorOptions(options => + options.InvalidModelStateResponseFactory = CreateValidationProblemResponse); + + builder.Services.AddSingleton(TimeProvider.System); + builder.Services.AddSingleton(new AppEnvironment(builder.Environment.IsDevelopment())); + + if (builder.Environment.IsDevelopment()) + { + builder.Services.AddSwaggerDocs(); + } + + builder.Services.AddApplication(); + builder.Services.AddInfrastructure(builder.Configuration); + builder.Services.AddHealthChecks() + .AddDbContextCheck( + name: "database", + failureStatus: HealthStatus.Unhealthy); + + builder.Services.AddConfiguredCors(allowedOrigins, corsPolicyName); + builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) + .AddMicrosoftIdentityWebApi(builder.Configuration.GetSection("AzureAd")); + builder.Services.AddAuthorization(); + builder.Services.AddHttpContextAccessor(); + builder.Services.AddScoped(); +} + +static IActionResult CreateValidationProblemResponse(ActionContext ctx) +{ + var pd = new ValidationProblemDetails(ctx.ModelState) + { + Detail = "See the errors field for details.", + Instance = ctx.HttpContext.Request.Path, + Status = StatusCodes.Status422UnprocessableEntity, + Title = "One or more validation errors occurred." + }; + + pd.Extensions["traceId"] = ctx.HttpContext.TraceIdentifier; + + return new UnprocessableEntityObjectResult(pd) + { + ContentTypes = { "application/problem+json" } + }; +} + +static async Task ApplyDevelopmentMigrationsAsync(WebApplication app) +{ + await using AsyncServiceScope scope = app.Services.CreateAsyncScope(); + AppDbContext dbContext = scope.ServiceProvider.GetRequiredService(); + ILogger logger = scope.ServiceProvider.GetRequiredService>(); + + try + { + logger.LogInformation("Applying database migrations..."); + await dbContext.Database.MigrateAsync(); + logger.LogInformation("Database migrations applied successfully."); + } + catch (SqlException ex) when (ex.Number == 1801) + { + logger.LogInformation("Database already exists; skipping migration apply."); + } + catch (Exception ex) + { + logger.LogError(ex, "Error applying database migrations."); + throw; + } +} diff --git a/src/Backend/AHKFlowApp.Application/Abstractions/IDevEnvironment.cs b/src/Backend/AHKFlowApp.Application/Abstractions/IDevEnvironment.cs deleted file mode 100644 index f44084c..0000000 --- a/src/Backend/AHKFlowApp.Application/Abstractions/IDevEnvironment.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace AHKFlowApp.Application.Abstractions; - -public interface IDevEnvironment -{ - bool IsDevelopment { get; } -} diff --git a/src/Backend/AHKFlowApp.Application/AppEnvironment.cs b/src/Backend/AHKFlowApp.Application/AppEnvironment.cs new file mode 100644 index 0000000..000172a --- /dev/null +++ b/src/Backend/AHKFlowApp.Application/AppEnvironment.cs @@ -0,0 +1,3 @@ +namespace AHKFlowApp.Application; + +public sealed record AppEnvironment(bool IsDevelopment); diff --git a/src/Backend/AHKFlowApp.Application/Commands/Dev/SeedHotstringsCommand.cs b/src/Backend/AHKFlowApp.Application/Commands/Dev/SeedHotstringsCommand.cs index 3aa1b08..1e04eb1 100644 --- a/src/Backend/AHKFlowApp.Application/Commands/Dev/SeedHotstringsCommand.cs +++ b/src/Backend/AHKFlowApp.Application/Commands/Dev/SeedHotstringsCommand.cs @@ -1,3 +1,4 @@ +using AHKFlowApp.Application; using AHKFlowApp.Application.Abstractions; using AHKFlowApp.Application.DTOs; using AHKFlowApp.Application.Mapping; @@ -15,7 +16,7 @@ internal sealed class SeedHotstringsCommandHandler( IAppDbContext db, ICurrentUser currentUser, TimeProvider clock, - IDevEnvironment env) + AppEnvironment env) : IRequestHandler>> { private static readonly (string Trigger, string Replacement, bool Ending, bool InsideWord)[] s_samples = diff --git a/src/Frontend/AHKFlowApp.UI.Blazor/Program.cs b/src/Frontend/AHKFlowApp.UI.Blazor/Program.cs index 623a80a..5e929e8 100644 --- a/src/Frontend/AHKFlowApp.UI.Blazor/Program.cs +++ b/src/Frontend/AHKFlowApp.UI.Blazor/Program.cs @@ -22,18 +22,10 @@ builder.Services.AddScoped(); // No bearer token needed — backend TestAuthHandler authenticates synthetically - builder.Services.AddHttpClient(client => - { - client.BaseAddress = new Uri(new Uri(builder.HostEnvironment.BaseAddress), apiBaseUrl); - client.Timeout = TimeSpan.FromSeconds(30); - }) + AddApiClient(new Uri(new Uri(builder.HostEnvironment.BaseAddress), apiBaseUrl)) .AddStandardResilienceHandler(); - builder.Services.AddHttpClient(client => - { - client.BaseAddress = new Uri(new Uri(builder.HostEnvironment.BaseAddress), apiBaseUrl); - client.Timeout = TimeSpan.FromSeconds(30); - }) + AddApiClient(new Uri(new Uri(builder.HostEnvironment.BaseAddress), apiBaseUrl)) .AddStandardResilienceHandler(); } else @@ -64,21 +56,24 @@ builder.Services.AddTransient(); - builder.Services.AddHttpClient(client => - { - client.BaseAddress = new Uri(apiBaseUrl); - client.Timeout = TimeSpan.FromSeconds(30); - }) + AddApiClient(new Uri(apiBaseUrl)) .AddHttpMessageHandler() .AddStandardResilienceHandler(); - builder.Services.AddHttpClient(client => - { - client.BaseAddress = new Uri(apiBaseUrl); - client.Timeout = TimeSpan.FromSeconds(30); - }) + AddApiClient(new Uri(apiBaseUrl)) .AddHttpMessageHandler() .AddStandardResilienceHandler(); } await builder.Build().RunAsync(); + +IHttpClientBuilder AddApiClient(Uri baseAddress) + where TClient : class + where TImplementation : class, TClient +{ + return builder.Services.AddHttpClient(client => + { + client.BaseAddress = baseAddress; + client.Timeout = TimeSpan.FromSeconds(30); + }); +} diff --git a/tests/AHKFlowApp.Application.Tests/Hotstrings/SeedHotstringsCommandHandlerTests.cs b/tests/AHKFlowApp.Application.Tests/Hotstrings/SeedHotstringsCommandHandlerTests.cs index 91628b1..19bdce5 100644 --- a/tests/AHKFlowApp.Application.Tests/Hotstrings/SeedHotstringsCommandHandlerTests.cs +++ b/tests/AHKFlowApp.Application.Tests/Hotstrings/SeedHotstringsCommandHandlerTests.cs @@ -1,3 +1,4 @@ +using AHKFlowApp.Application; using AHKFlowApp.Application.Abstractions; using AHKFlowApp.Application.Commands.Dev; using AHKFlowApp.Application.DTOs; @@ -6,7 +7,6 @@ using Ardalis.Result; using FluentAssertions; using Microsoft.EntityFrameworkCore; -using NSubstitute; using Xunit; namespace AHKFlowApp.Application.Tests.Hotstrings; @@ -14,12 +14,7 @@ namespace AHKFlowApp.Application.Tests.Hotstrings; [Collection("HotstringDb")] public sealed class SeedHotstringsCommandHandlerTests(HotstringDbFixture fx) { - private static IDevEnvironment DevEnv(bool isDev) - { - IDevEnvironment e = Substitute.For(); - e.IsDevelopment.Returns(isDev); - return e; - } + private static AppEnvironment DevEnv(bool isDev) => new(isDev); [Fact] public async Task Handle_InDevelopment_SeedsSamples() From fbf9b7ca10711ff3d3ed19c744e24cc9b412a0f4 Mon Sep 17 00:00:00 2001 From: "Bart Segers (M16)" Date: Wed, 22 Apr 2026 11:23:37 +0200 Subject: [PATCH 05/10] refactor: standardize PowerShell entry scripts Standardize supported script entrypoints on PowerShell 5.1, extract shared deployment helpers, and simplify fail-fast behavior for the main Azure scripts. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- scripts/Common.ps1 | 71 +++++++++++++++++++++++++++++++++++++ scripts/deploy.ps1 | 68 +++++++++++++---------------------- scripts/run-coverage.ps1 | 2 +- scripts/setup-dev-entra.ps1 | 2 +- scripts/setup-entra-app.ps1 | 2 +- scripts/teardown.ps1 | 22 +++--------- scripts/update.ps1 | 22 +++--------- 7 files changed, 109 insertions(+), 80 deletions(-) create mode 100644 scripts/Common.ps1 diff --git a/scripts/Common.ps1 b/scripts/Common.ps1 new file mode 100644 index 0000000..fa26e44 --- /dev/null +++ b/scripts/Common.ps1 @@ -0,0 +1,71 @@ +#Requires -Version 5.1 + +Set-StrictMode -Version Latest + +function Write-Step([string]$Message) +{ + Write-Host "`n==> $Message" -ForegroundColor Cyan +} + +function Write-Success([string]$Message) +{ + Write-Host " [OK] $Message" -ForegroundColor Green +} + +function Write-Warn([string]$Message) +{ + Write-Host " ! $Message" -ForegroundColor Yellow +} + +function Write-Fail([string]$Message) +{ + Write-Host "`n [FAIL] $Message" -ForegroundColor Red +} + +function Confirm-Command([string]$Name, [string]$InstallUrl) +{ + if (-not (Get-Command $Name -ErrorAction SilentlyContinue)) + { + Write-Fail "$Name is not installed." + Write-Host " Install from: $InstallUrl" -ForegroundColor Yellow + exit 1 + } + + Write-Success "$Name found" +} + +function Assert-AzureLogin() +{ + $null = az account show 2>&1 + if ($LASTEXITCODE -ne 0) + { + Write-Fail "Not logged into Azure. Run: az login" + exit 1 + } + + Write-Success "Azure login verified" +} + +function Assert-GitHubAuth() +{ + $null = gh auth status 2>&1 + if ($LASTEXITCODE -ne 0) + { + Write-Fail "GitHub CLI not authenticated. Run: gh auth login" + exit 1 + } + + Write-Success "GitHub CLI authenticated" +} + +function Read-KeyValueFile([string]$Path) +{ + $config = @{} + + Get-Content $Path | Where-Object { $_ -match '^\s*[^#]' -and $_ -match '=' } | ForEach-Object { + $parts = $_ -split '=', 2 + $config[$parts[0].Trim()] = $parts[1].Trim() + } + + return $config +} diff --git a/scripts/deploy.ps1 b/scripts/deploy.ps1 index 4f37d39..3ba6e82 100644 --- a/scripts/deploy.ps1 +++ b/scripts/deploy.ps1 @@ -1,4 +1,5 @@ -<# +#Requires -Version 5.1 +<# .SYNOPSIS Provisions an AHKFlowApp Azure environment and configures CI/CD. @@ -26,47 +27,33 @@ param( Set-StrictMode -Version Latest $ErrorActionPreference = 'Stop' - -# --------------------------------------------------------------------------- -# Helpers -# --------------------------------------------------------------------------- - -function Write-Step([string]$Message) { - Write-Host "`n==> $Message" -ForegroundColor Cyan -} - -function Write-Success([string]$Message) { - Write-Host " ✓ $Message" -ForegroundColor Green -} - -function Write-Warn([string]$Message) { - Write-Host " ! $Message" -ForegroundColor Yellow -} - -function Write-Fail([string]$Message) { - Write-Host "`n ✗ $Message" -ForegroundColor Red -} - -function Confirm-Command([string]$Name, [string]$InstallUrl) { - if (-not (Get-Command $Name -ErrorAction SilentlyContinue)) { - Write-Fail "$Name is not installed." - Write-Host " Install from: $InstallUrl" -ForegroundColor Yellow - throw "Missing prerequisite: $Name" - } - Write-Success "$Name found" -} +. (Join-Path $PSScriptRoot 'Common.ps1') function Invoke-Az { - $output = az @args 2>&1 - if ($LASTEXITCODE -ne 0) { - throw "az $($args -join ' ') failed:`n$output" + $prevEap = $ErrorActionPreference + $ErrorActionPreference = 'Continue' + try { + $output = az @args 2>&1 + if ($LASTEXITCODE -ne 0) { + throw "az $($args -join ' ') failed:`n$output" + } + } finally { + $ErrorActionPreference = $prevEap } + return $output } function Invoke-Az-Json { - $raw = az @args --output json 2>&1 - if ($LASTEXITCODE -ne 0) { throw "az $($args -join ' ') failed:`n$raw" } + $prevEap = $ErrorActionPreference + $ErrorActionPreference = 'Continue' + try { + $raw = az @args --output json 2>&1 + if ($LASTEXITCODE -ne 0) { throw "az $($args -join ' ') failed:`n$raw" } + } finally { + $ErrorActionPreference = $prevEap + } + return $raw | ConvertFrom-Json } @@ -120,16 +107,11 @@ try { Write-Success "Logged into Azure as $($account.user.name) (subscription: $($account.name))" } catch { Write-Fail "Not logged into Azure. Run: az login" - throw + Write-Host " Azure CLI account check failed. Resolve the Azure CLI issue above and rerun." -ForegroundColor Yellow + exit 1 } -# Verify gh auth -$ghStatus = gh auth status 2>&1 -if ($LASTEXITCODE -ne 0) { - Write-Fail "GitHub CLI not authenticated. Run: gh auth login" - throw "GitHub CLI not authenticated" -} -Write-Success "GitHub CLI authenticated" +Assert-GitHubAuth # Verify sqlcmd (optional — we fall back to portal instructions) $hasSqlcmd = [bool](Get-Command 'sqlcmd' -ErrorAction SilentlyContinue) diff --git a/scripts/run-coverage.ps1 b/scripts/run-coverage.ps1 index 5e761b6..2fa307a 100644 --- a/scripts/run-coverage.ps1 +++ b/scripts/run-coverage.ps1 @@ -1,4 +1,4 @@ -#Requires -Version 7.0 +#Requires -Version 5.1 <# .SYNOPSIS Run tests with coverage locally and generate HTML + JSON summary matching CI. diff --git a/scripts/setup-dev-entra.ps1 b/scripts/setup-dev-entra.ps1 index f347ddd..7d67c50 100644 --- a/scripts/setup-dev-entra.ps1 +++ b/scripts/setup-dev-entra.ps1 @@ -1,4 +1,4 @@ -#Requires -Version 7 +#Requires -Version 5.1 <# .SYNOPSIS Configures local dev Entra ID app registration and wires local config. diff --git a/scripts/setup-entra-app.ps1 b/scripts/setup-entra-app.ps1 index 2363713..16d190b 100644 --- a/scripts/setup-entra-app.ps1 +++ b/scripts/setup-entra-app.ps1 @@ -1,4 +1,4 @@ -#Requires -Version 7 +#Requires -Version 5.1 <# .SYNOPSIS Idempotent Entra ID app registration setup for AHKFlowApp. diff --git a/scripts/teardown.ps1 b/scripts/teardown.ps1 index bf92aea..daa43c7 100644 --- a/scripts/teardown.ps1 +++ b/scripts/teardown.ps1 @@ -1,4 +1,5 @@ -<# +#Requires -Version 5.1 +<# .SYNOPSIS Tears down an AHKFlowApp Azure environment. @@ -23,11 +24,7 @@ param( Set-StrictMode -Version Latest $ErrorActionPreference = 'Stop' - -function Write-Step([string]$Message) { Write-Host "`n==> $Message" -ForegroundColor Cyan } -function Write-Success([string]$Message) { Write-Host " ✓ $Message" -ForegroundColor Green } -function Write-Warn([string]$Message) { Write-Host " ! $Message" -ForegroundColor Yellow } -function Write-Fail([string]$Message) { Write-Host "`n ✗ $Message" -ForegroundColor Red } +. (Join-Path $PSScriptRoot 'Common.ps1') # Load saved config if (-not $Environment) { @@ -42,11 +39,7 @@ if (-not (Test-Path $EnvFile)) { exit 0 } -$config = @{} -Get-Content $EnvFile | Where-Object { $_ -match '^\s*[^#]' -and $_ -match '=' } | ForEach-Object { - $parts = $_ -split '=', 2 - $config[$parts[0].Trim()] = $parts[1].Trim() -} +$config = Read-KeyValueFile $EnvFile $ResourceGroup = $config['RESOURCE_GROUP'] $SqlAdminGroup = $config['SQL_ADMIN_GROUP'] @@ -71,12 +64,7 @@ if ($confirm -ne $ResourceGroup) { exit 0 } -# Verify az login -$null = az account show 2>&1 -if ($LASTEXITCODE -ne 0) { - Write-Fail "Not logged into Azure. Run: az login" - exit 1 -} +Assert-AzureLogin # --------------------------------------------------------------------------- # 1. Delete Azure resource group diff --git a/scripts/update.ps1 b/scripts/update.ps1 index 83f2979..01722dd 100644 --- a/scripts/update.ps1 +++ b/scripts/update.ps1 @@ -1,4 +1,5 @@ -<# +#Requires -Version 5.1 +<# .SYNOPSIS Updates AHKFlowApp to the latest release by pulling the newest container image. @@ -21,10 +22,7 @@ param( Set-StrictMode -Version Latest $ErrorActionPreference = 'Stop' - -function Write-Step([string]$Message) { Write-Host "`n==> $Message" -ForegroundColor Cyan } -function Write-Success([string]$Message) { Write-Host " ✓ $Message" -ForegroundColor Green } -function Write-Fail([string]$Message) { Write-Host "`n ✗ $Message" -ForegroundColor Red } +. (Join-Path $PSScriptRoot 'Common.ps1') # Load saved config if (-not $Environment) { @@ -39,11 +37,7 @@ if (-not (Test-Path $EnvFile)) { exit 1 } -$config = @{} -Get-Content $EnvFile | Where-Object { $_ -match '^\s*[^#]' -and $_ -match '=' } | ForEach-Object { - $parts = $_ -split '=', 2 - $config[$parts[0].Trim()] = $parts[1].Trim() -} +$config = Read-KeyValueFile $EnvFile $ResourceGroup = $config['RESOURCE_GROUP'] $AppServiceName = $config['APP_SERVICE_NAME'] @@ -52,13 +46,7 @@ $AppHostname = $config['APP_SERVICE_HOSTNAME'] Write-Step "Updating AHKFlowApp ($Environment)..." -# Verify az login -$null = az account show 2>&1 -if ($LASTEXITCODE -ne 0) { - Write-Fail "Not logged into Azure. Run: az login" - exit 1 -} -Write-Success "Azure login verified" +Assert-AzureLogin # Fetch the latest image tag from GHCR $Owner = $GitHubOrgRepo -split '/' | Select-Object -First 1 From 97a1532b25ce5f8755bc7851c7f9e22ff260cb23 Mon Sep 17 00:00:00 2001 From: "Bart Segers (M16)" Date: Wed, 22 Apr 2026 11:30:24 +0200 Subject: [PATCH 06/10] refactor: move issue script into scripts Move the backlog issue creation script into the canonical scripts folder, make the backlog path default to the repo backlog, and update live references to the new entrypoint. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../backlog/001-create-backlog-in-github.md | 2 +- docs/development/github-setup.md | 2 +- docs/scripts/create-github-issues.ps1 | 108 ------------ scripts/create-github-issues.ps1 | 160 ++++++++++++++++++ 4 files changed, 162 insertions(+), 110 deletions(-) delete mode 100644 docs/scripts/create-github-issues.ps1 create mode 100644 scripts/create-github-issues.ps1 diff --git a/.claude/backlog/001-create-backlog-in-github.md b/.claude/backlog/001-create-backlog-in-github.md index d05c381..cae5627 100644 --- a/.claude/backlog/001-create-backlog-in-github.md +++ b/.claude/backlog/001-create-backlog-in-github.md @@ -27,4 +27,4 @@ As a product owner, I want the backlog represented in GitHub so that work can be ## Notes / dependencies -- Use `docs/scripts/create-github-issues.ps1` to batch-create issues from backlog files via `gh` CLI. +- Use `scripts/create-github-issues.ps1` to batch-create issues from backlog files via `gh` CLI. diff --git a/docs/development/github-setup.md b/docs/development/github-setup.md index d00c89a..48d84fb 100644 --- a/docs/development/github-setup.md +++ b/docs/development/github-setup.md @@ -143,7 +143,7 @@ GitHub Projects v2 has limited built-in automation. Manage workflow manually: ### Batch Script -See [`docs/scripts/create-github-issues.ps1`](../scripts/create-github-issues.ps1) — reads backlog files from `.claude/backlog/`, detects type/epic/interface labels, and creates GitHub Issues via `gh` CLI. Supports dry-run (default) and `-Execute` flag. +See [`scripts/create-github-issues.ps1`](../../scripts/create-github-issues.ps1) — reads backlog files from `.claude/backlog/`, detects type/epic/interface labels, and creates GitHub Issues via `gh` CLI. Supports dry-run (default) and `-Execute` flag. ### Run diff --git a/docs/scripts/create-github-issues.ps1 b/docs/scripts/create-github-issues.ps1 deleted file mode 100644 index 5a2f781..0000000 --- a/docs/scripts/create-github-issues.ps1 +++ /dev/null @@ -1,108 +0,0 @@ -# Create GitHub Issues from Backlog -# Usage: -# .\scripts\create-github-issues.ps1 -BacklogPath "" # Dry run (default) -# .\scripts\create-github-issues.ps1 -BacklogPath "" -Execute # Create issues -# -# Example: -# .\create-github-issues.ps1 -BacklogPath "..\..\.claude\backlog" -# .\create-github-issues.ps1 -BacklogPath "..\..\.claude\backlog" -Execute - -param( - [switch]$Execute, - [Parameter(Mandatory=$true)] - [string]$BacklogPath -) - -# Epic name to label mapping -$epicMapping = @{ - "Backlog setup" = "epic: backlog setup" - "Initial project / solution" = "epic: initial project" - "Foundation" = "epic: foundation" - "Versioning" = "epic: versioning" - "Logging" = "epic: logging" - "Observability" = "epic: observability" - "CI/CD" = "epic: ci/cd" - "Authentication and authorization" = "epic: authentication" - "Hotstrings" = "epic: hotstrings" - "Hotkeys" = "epic: hotkeys" - "Profiles" = "epic: profiles" - "Script generation & download" = "epic: script generation" -} - -# Ensure a GitHub label exists, creating it if missing -function Ensure-Label { - param([string]$Name, [string]$Color, [string]$Description) - - $existing = gh label list --search $Name --json name | ConvertFrom-Json - if ($existing | Where-Object { $_.name -eq $Name }) { - return - } - - Write-Host " Creating label: $Name" -ForegroundColor DarkGray - if ($Execute) { - gh label create $Name --color $Color --description $Description 2>$null - } -} - -Write-Host "Ensuring labels exist..." -ForegroundColor Yellow - -foreach ($label in $epicMapping.Values) { - Ensure-Label -Name $label -Color "7B61FF" -Description "Epic grouping" -} -Ensure-Label -Name "api" -Color "0075ca" -Description "API layer" -Ensure-Label -Name "ui" -Color "e4e669" -Description "UI layer" -Ensure-Label -Name "cli" -Color "d93f0b" -Description "CLI layer" -Ensure-Label -Name "enhancement" -Color "a2eeef" -Description "New feature or request" - -Write-Host "" - -$backlogPath = $BacklogPath - -$files = Get-ChildItem -Path $backlogPath -Filter "*.md" | - Where-Object { $_.Name -ne "000-backlog-item-template.md" } | - Sort-Object Name - -foreach ($file in $files) { - $content = Get-Content $file.FullName -Raw - $firstLine = (Get-Content $file.FullName -First 1) -replace "^#\s*", "" - - $labels = @() - - # Type label - if ($content -match '\*\*Type\*\*:\s*Feature') { $labels += "enhancement" } - - # Epic label - if ($content -match '\*\*Epic\*\*:\s*(.+)') { - $epicName = $Matches[1].Trim() - if ($epicMapping.ContainsKey($epicName)) { - $labels += $epicMapping[$epicName] - } - } - - # Interface labels - if ($content -match '\*\*Interfaces\*\*:.*API') { $labels += "api" } - if ($content -match '\*\*Interfaces\*\*:.*UI') { $labels += "ui" } - if ($content -match '\*\*Interfaces\*\*:.*CLI') { $labels += "cli" } - - $labelArgs = ($labels | ForEach-Object { "--label `"$_`"" }) -join " " - - Write-Host "Creating: $firstLine" -ForegroundColor Cyan - Write-Host " Labels: $($labels -join ', ')" -ForegroundColor Gray - - $command = "gh issue create --title `"$firstLine`" --body-file `"$($file.FullName)`" $labelArgs" - - if ($Execute) { - Invoke-Expression $command - Write-Host " ✓ Created" -ForegroundColor Green - } - else { - Write-Host " [DRY RUN] $command" -ForegroundColor DarkGray - } -} - -if (-not $Execute) { - Write-Host "`nDry run complete. Run with -Execute to create issues." -ForegroundColor Yellow -} -else { - Write-Host "`nAll issues created!" -ForegroundColor Green -} diff --git a/scripts/create-github-issues.ps1 b/scripts/create-github-issues.ps1 new file mode 100644 index 0000000..912b260 --- /dev/null +++ b/scripts/create-github-issues.ps1 @@ -0,0 +1,160 @@ +#Requires -Version 5.1 +<# +.SYNOPSIS + Creates GitHub issues from backlog markdown files. + +.DESCRIPTION + Reads backlog files from .claude\backlog, ensures the expected labels exist, + and creates GitHub issues via the gh CLI. Runs as a dry run by default. + +.PARAMETER Execute + Creates issues for real instead of printing the gh commands. + +.PARAMETER BacklogPath + Optional path to the backlog folder. Defaults to .claude\backlog at the repo root. + +.EXAMPLE + .\scripts\create-github-issues.ps1 + +.EXAMPLE + .\scripts\create-github-issues.ps1 -Execute +#> +[CmdletBinding()] +param( + [switch]$Execute, + [string]$BacklogPath +) + +Set-StrictMode -Version Latest +$ErrorActionPreference = 'Stop' +. (Join-Path $PSScriptRoot 'Common.ps1') + +$repoRoot = Split-Path $PSScriptRoot -Parent +$resolvedBacklogPath = if ($BacklogPath) +{ + $BacklogPath +} +else +{ + Join-Path $repoRoot '.claude\backlog' +} + +if (-not (Test-Path $resolvedBacklogPath)) +{ + Write-Fail "Backlog path not found: $resolvedBacklogPath" + exit 1 +} + +Confirm-Command 'gh' 'https://cli.github.com/' +Assert-GitHubAuth + +$epicMapping = @{ + 'Backlog setup' = 'epic: backlog setup' + 'Initial project / solution' = 'epic: initial project' + 'Foundation' = 'epic: foundation' + 'Versioning' = 'epic: versioning' + 'Logging' = 'epic: logging' + 'Observability' = 'epic: observability' + 'CI/CD' = 'epic: ci/cd' + 'Authentication and authorization' = 'epic: authentication' + 'Hotstrings' = 'epic: hotstrings' + 'Hotkeys' = 'epic: hotkeys' + 'Profiles' = 'epic: profiles' + 'Script generation & download' = 'epic: script generation' +} + +function Ensure-Label +{ + param( + [string]$Name, + [string]$Color, + [string]$Description + ) + + $existing = gh label list --search $Name --json name | ConvertFrom-Json + if ($existing | Where-Object { $_.name -eq $Name }) + { + return + } + + Write-Host " Creating label: $Name" -ForegroundColor DarkGray + if ($Execute) + { + gh label create $Name --color $Color --description $Description 2>$null | Out-Null + } +} + +Write-Step 'Ensuring labels exist...' + +foreach ($label in $epicMapping.Values) +{ + Ensure-Label -Name $label -Color '7B61FF' -Description 'Epic grouping' +} + +Ensure-Label -Name 'api' -Color '0075ca' -Description 'API layer' +Ensure-Label -Name 'ui' -Color 'e4e669' -Description 'UI layer' +Ensure-Label -Name 'cli' -Color 'd93f0b' -Description 'CLI layer' +Ensure-Label -Name 'enhancement' -Color 'a2eeef' -Description 'New feature or request' + +$files = Get-ChildItem -Path $resolvedBacklogPath -Filter '*.md' | + Where-Object { $_.Name -ne '000-backlog-item-template.md' } | + Sort-Object Name + +foreach ($file in $files) +{ + $content = Get-Content $file.FullName -Raw + $firstLine = ($content -split '\r?\n', 2)[0] -replace '^#\s*', '' + $labels = @() + + if ($content -match '\*\*Type\*\*:\s*Feature') + { + $labels += 'enhancement' + } + + if ($content -match '\*\*Epic\*\*:\s*(.+)') + { + $epicName = $Matches[1].Trim() + if ($epicMapping.ContainsKey($epicName)) + { + $labels += $epicMapping[$epicName] + } + } + + if ($content -match '\*\*Interfaces\*\*:.*API') { $labels += 'api' } + if ($content -match '\*\*Interfaces\*\*:.*UI') { $labels += 'ui' } + if ($content -match '\*\*Interfaces\*\*:.*CLI') { $labels += 'cli' } + + Write-Host "Creating: $firstLine" -ForegroundColor Cyan + Write-Host " Labels: $($labels -join ', ')" -ForegroundColor Gray + + $issueArgs = @('issue', 'create', '--title', $firstLine, '--body-file', $file.FullName) + foreach ($label in $labels) + { + $issueArgs += @('--label', $label) + } + + if ($Execute) + { + & gh @issueArgs + if ($LASTEXITCODE -ne 0) + { + Write-Fail "Failed to create issue: $firstLine" + exit 1 + } + + Write-Success "Created" + } + else + { + Write-Host " [DRY RUN] gh $($issueArgs -join ' ')" -ForegroundColor DarkGray + } +} + +if (-not $Execute) +{ + Write-Host "`nDry run complete. Run with -Execute to create issues." -ForegroundColor Yellow +} +else +{ + Write-Host "`nAll issues created!" -ForegroundColor Green +} From ddb16135c86f302a98ff48ff54507d53ac4bde6c Mon Sep 17 00:00:00 2001 From: "Bart Segers (M16)" Date: Wed, 22 Apr 2026 11:47:33 +0200 Subject: [PATCH 07/10] feat: trigger frontend deployment after provisioning Add a .NET 10 SDK preflight check to deploy.ps1 and dispatch the deploy-frontend workflow after Azure provisioning finishes and GitHub configuration is in place. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- scripts/deploy.ps1 | 48 +++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 45 insertions(+), 3 deletions(-) diff --git a/scripts/deploy.ps1 b/scripts/deploy.ps1 index 3ba6e82..c6c04e6 100644 --- a/scripts/deploy.ps1 +++ b/scripts/deploy.ps1 @@ -87,6 +87,41 @@ function Try-Az-Json { } } +function Assert-DotNetSdkVersion([string]$RequiredMajorVersion) { + $installedSdks = dotnet --list-sdks 2>$null + if ($LASTEXITCODE -ne 0) { + Write-Fail 'Unable to determine installed .NET SDK versions.' + exit 1 + } + + $requiredPrefix = "$RequiredMajorVersion." + if (-not ($installedSdks | Where-Object { $_.StartsWith($requiredPrefix) })) { + Write-Fail ".NET SDK $RequiredMajorVersion.x is required." + Write-Host ' Install from: https://dotnet.microsoft.com/download/dotnet/10.0' -ForegroundColor Yellow + exit 1 + } + + Write-Success ".NET SDK $RequiredMajorVersion.x found" +} + +function Start-FrontendDeployment([string]$Repository, [string]$TargetEnvironment) { + Write-Step "Phase 8: Triggering frontend deployment workflow..." + + gh workflow run deploy-frontend.yml ` + --repo $Repository ` + --ref main ` + -f environment=$TargetEnvironment | Out-Null + + if ($LASTEXITCODE -ne 0) { + Write-Warn 'Could not trigger deploy-frontend.yml automatically.' + Write-Host " Run manually: gh workflow run deploy-frontend.yml --repo $Repository --ref main -f environment=$TargetEnvironment" -ForegroundColor Yellow + return $false + } + + Write-Success "Triggered deploy-frontend.yml for '$TargetEnvironment'" + return $true +} + # --------------------------------------------------------------------------- # Phase 1: Prerequisites # --------------------------------------------------------------------------- @@ -100,6 +135,7 @@ Write-Step "Phase 1: Checking prerequisites..." Confirm-Command 'az' 'https://learn.microsoft.com/cli/azure/install-azure-cli' Confirm-Command 'gh' 'https://cli.github.com/' Confirm-Command 'dotnet' 'https://dotnet.microsoft.com/download' +Assert-DotNetSdkVersion '10' # Verify az login try { @@ -617,7 +653,7 @@ az webapp cors add ` Write-Success "CORS configured for SWA frontend" # --------------------------------------------------------------------------- -# Phase 8: Save config + Summary +# Phase 8: Save config # --------------------------------------------------------------------------- Write-Step "Phase 8: Saving configuration..." @@ -650,6 +686,8 @@ APP_INSIGHTS_NAME=$AppInsightsName "@ | Set-Content -Path $EnvFileOut -Encoding UTF8 Write-Success "Config saved to scripts/.env.$Environment" +$frontendDeploymentTriggered = Start-FrontendDeployment -Repository $GitHubOrgRepo -TargetEnvironment $Environment + Write-Host "" Write-Host "==========================================================" -ForegroundColor Green Write-Host " AHKFlowApp ($Environment) -- Provisioning Complete!" -ForegroundColor Green @@ -660,8 +698,12 @@ Write-Host " Frontend : https://$SwaHostname" Write-Host " Resources : $ResourceGroup" Write-Host "" Write-Host " Next steps:" -ForegroundColor Cyan -Write-Host " 1. Push to 'main' to trigger GitHub Actions deploy" -Write-Host " 2. The first push will build and push the container image to GHCR." +if ($frontendDeploymentTriggered) { + Write-Host " 1. Frontend deploy workflow dispatched for '$Environment'." +} else { + Write-Host " 1. Trigger the frontend deploy workflow manually once ready." +} +Write-Host " 2. Push to 'main' to build and publish the API container image." Write-Host "" Write-Host " IMPORTANT: GHCR packages are private by default." -ForegroundColor Yellow Write-Host " After the first deploy-api.yml run, make the container image public:" From 6b13e80f65847fdfd5750fcb63935f29355ac790 Mon Sep 17 00:00:00 2001 From: "Bart Segers (M16)" Date: Wed, 22 Apr 2026 12:05:16 +0200 Subject: [PATCH 08/10] docs: align local and Azure workflows Trim the live docs down to the supported local and Azure paths, document the x64-only Docker limitation, and update deployment guidance to match the current deploy.ps1 behavior. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- AGENTS.md | 8 +- README.md | 22 ++- docs/deployment/entra-setup.md | 3 +- docs/deployment/getting-started.md | 19 +- docs/development/docker-setup.md | 4 + docs/environments.md | 296 ++--------------------------- 6 files changed, 52 insertions(+), 300 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index cb497b1..5be35f8 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -8,7 +8,7 @@ Blazor WebAssembly PWA frontend + ASP.NET Core Web API backend. Early stage — ## Tech Stack - **.NET 10.0** — all projects target `net10.0`; Microsoft.* packages use 10.x versions -- **EF Core** + SQL Server (LocalDB/Docker Compose/Azure SQL) with `EnableRetryOnFailure()` +- **EF Core** + SQL Server (Windows LocalDB/x64 Docker Compose/Azure SQL) with `EnableRetryOnFailure()` - **Blazor WebAssembly** PWA with MudBlazor 9.x and Azure AD (MSAL) authentication - **MediatR** (Jimmy Bogard) for CQRS — commands, queries, pipeline behaviors - **Ardalis.Result** for typed operation outcomes (handlers only) @@ -54,12 +54,12 @@ dotnet test tests/AHKFlowApp.API.Tests --configuration Release --verbosity norma dotnet test tests/AHKFlowApp.API.Tests --filter "FullyQualifiedName~HealthControllerTests" # Run API locally (recommended: Docker SQL on port 1433) -dotnet run --project src/Backend/AHKFlowApp.API --launch-profile "https + Docker SQL (Recommended)" +dotnet run --project src/Backend/AHKFlowApp.API --launch-profile "Docker SQL (Recommended)" # Run Blazor frontend (separate terminal) dotnet run --project src/Frontend/AHKFlowApp.UI.Blazor -# Full stack via Docker Compose (SQL Server + API) +# Full stack via Docker Compose (API + SQL Server on x64/amd64 hosts) docker compose up --build # EF Core migrations @@ -214,7 +214,7 @@ GitHub Actions workflows in `.github/workflows/`: **Environments:** - **DEV:** Local development environment (`ASPNETCORE_ENVIRONMENT=Development`) - - LocalDB or Docker SQL Server + - Windows LocalDB or x64 Docker SQL Server - No Azure resources required - Run locally with `dotnet run` - **TEST:** Azure pre-production environment (`ASPNETCORE_ENVIRONMENT=Test`) diff --git a/README.md b/README.md index f6d37f6..61b81fb 100644 --- a/README.md +++ b/README.md @@ -5,11 +5,16 @@ ### Prerequisites - [.NET 10 SDK](https://dotnet.microsoft.com/download/dotnet/10.0) -- SQL Server LocalDB (included with Visual Studio) or Docker +- One local database path: + - Windows + SQL Server LocalDB, or + - Docker on an x64/amd64 host for the bundled SQL Server compose stack +- Optional for Azure provisioning only: Windows PowerShell 5.1, Azure CLI, GitHub CLI + +> The bundled Docker stack is **not supported on Raspberry Pi / ARM64** because `docker-compose.yml` uses SQL Server 2022. ### Running Locally -**Option 1 — LocalDB:** +**Option 1 — Windows + LocalDB** ```bash # Apply migrations (after backlog item 007) @@ -18,13 +23,20 @@ dotnet ef database update \ --startup-project src/Backend/AHKFlowApp.API # Start API (http://localhost:5600, OpenAPI at /swagger/v1/swagger.json) -dotnet run --project src/Backend/AHKFlowApp.API --launch-profile "Docker SQL (Recommended)" +dotnet run --project src/Backend/AHKFlowApp.API --launch-profile "LocalDB SQL" # Start frontend in a separate terminal (http://localhost:5601) dotnet run --project src/Frontend/AHKFlowApp.UI.Blazor ``` -**Option 2 — Docker Compose (recommended):** +**Option 2 — Windows/x64 Docker SQL + local frontend** + +```bash +dotnet run --project src/Backend/AHKFlowApp.API --launch-profile "Docker SQL (Recommended)" +dotnet run --project src/Frontend/AHKFlowApp.UI.Blazor +``` + +**Option 3 — Docker Compose (API + SQL only, x64/amd64):** See `docs/development/docker-setup.md`. @@ -42,7 +54,7 @@ The application supports three distinct environments: | Environment | Description | ASPNETCORE_ENVIRONMENT | Deployment | |-------------|-------------|------------------------|------------| -| **DEV** | Local development | `Development` | Local machine (LocalDB or Docker SQL) | +| **DEV** | Local development | `Development` | Local machine (Windows LocalDB or x64 Docker SQL) | | **TEST** | Pre-production testing | `Test` | Azure (auto-deploy from `main` branch) | | **PROD** | Production | `Production` | Azure (manual deployment via workflow) | diff --git a/docs/deployment/entra-setup.md b/docs/deployment/entra-setup.md index a351639..559ce45 100644 --- a/docs/deployment/entra-setup.md +++ b/docs/deployment/entra-setup.md @@ -28,11 +28,10 @@ Handled automatically by the full provisioning script: Phase 3 sets up or updates the per-env app registration via `setup-entra-app.ps1` before Bicep runs. Later in the deployment, the script re-runs that setup only to refresh redirect URIs and write all three `AZURE_AD_*_{TEST|PROD}` GitHub Variables once the final app endpoints are known. The deploy workflows substitute these into `appsettings.{Test|Production}.json` at build time and inject them into App Service configuration on every deploy. -After `deploy.ps1` finishes, retrigger the API and frontend deploy workflows (they don't auto-run on `scripts/**` changes): +After `deploy.ps1` finishes, the script already dispatches `deploy-frontend.yml` for the selected environment. The API workflow still runs on the next push to `main`, or you can trigger it manually if you need to redeploy the current image: ```powershell gh workflow run deploy-api.yml --ref main -f environment=test -gh workflow run deploy-frontend.yml --ref main -f environment=test ``` ### Manual fallback diff --git a/docs/deployment/getting-started.md b/docs/deployment/getting-started.md index 599d7a5..e12a850 100644 --- a/docs/deployment/getting-started.md +++ b/docs/deployment/getting-started.md @@ -2,7 +2,7 @@ This guide walks you through provisioning your own AHKFlowApp instance on Azure. -**Prerequisites:** An Azure subscription. The deploy script will guide you through any additional tooling. +**Prerequisites:** Azure subscription, Windows PowerShell 5.1 or newer, .NET 10 SDK, Azure CLI, and GitHub CLI. `sqlcmd` is optional. ## Quick Start (Recommended) @@ -16,17 +16,16 @@ cd AHKFlowApp The script will: -1. Check that required tools are installed (Azure CLI, GitHub CLI) +1. Check that required tools are installed (.NET 10 SDK, Azure CLI, GitHub CLI) 2. Prompt for your environment name (`test` or `prod`), Azure region, and GitHub repository 3. Create an Azure resource group and provision all resources via Bicep -4. Set up Entra ID (SQL access, OIDC for GitHub Actions) +4. Set up Entra ID (app registration, SQL access, OIDC for GitHub Actions) 5. Create the SQL database user for the application 6. Configure GitHub secrets and variables so CI/CD works automatically -7. Save your configuration to `scripts/.env.{environment}` for future use +7. Trigger the frontend deploy workflow on GitHub +8. Save your configuration to `scripts/.env.{environment}` for future use -When done, push to `main` — GitHub Actions will deploy the API container and Blazor frontend automatically. - -> **Auth app registration not included in `deploy.ps1`.** After provisioning, run the Entra app registration setup separately — see [Entra ID Setup](#entra-id-app-registration-setup) below. +When done, the script queues the frontend deployment automatically. The API still deploys when `deploy-api.yml` runs, typically on the next push to `main`. ## What Gets Provisioned @@ -49,6 +48,8 @@ Estimated cost for the `test` environment: **~$15–25/month** (B1 App Service P The deploy script checks for these and will tell you how to install them if missing: +- **Windows PowerShell 5.1+** — included on supported Windows versions +- **.NET 10 SDK** — [install](https://dotnet.microsoft.com/download/dotnet/10.0) - **Azure CLI** — [install](https://learn.microsoft.com/en-us/cli/azure/install-azure-cli) - **GitHub CLI** (`gh`) — [install](https://cli.github.com) - **sqlcmd** (optional) — [install](https://learn.microsoft.com/en-us/sql/tools/sqlcmd/sqlcmd-utility). If absent, the script prints a SQL snippet to run in the Azure Portal instead. @@ -100,14 +101,14 @@ If you need to re-provision from GitHub Actions (e.g., after a Bicep change): ## Entra ID App Registration Setup -The `deploy.ps1` script provisions infrastructure but does **not** create the Entra ID app registration used for user authentication. Run this once per environment after provisioning: +`deploy.ps1` already creates or updates the Entra ID app registration for `test` and `prod`. Use `setup-entra-app.ps1` only as a manual fallback when you need to refresh redirect URIs or repair app-registration state without re-running the full provisioning flow: ```powershell .\scripts\setup-entra-app.ps1 -Environment test .\scripts\setup-entra-app.ps1 -Environment prod ``` -The script outputs the `ClientId`, `TenantId`, and `DefaultScope` values. Set them as GitHub Variables so the deploy workflows can inject them: +The script outputs the `ClientId`, `TenantId`, and `DefaultScope` values. If you run it manually, update the matching GitHub Variables so the deploy workflows can inject them: ```bash gh variable set AZURE_AD_TENANT_ID_TEST --body "" diff --git a/docs/development/docker-setup.md b/docs/development/docker-setup.md index 3969795..5ceaf7d 100644 --- a/docs/development/docker-setup.md +++ b/docs/development/docker-setup.md @@ -11,6 +11,10 @@ Access API at: http://localhost:5600/swagger SQL Server is available on: `localhost:1433` +> The bundled compose stack runs **API + SQL Server only**. Run the Blazor frontend separately with `dotnet run --project src/Frontend/AHKFlowApp.UI.Blazor`. +> +> The bundled compose stack is **x64/amd64 only**. It is not supported on Raspberry Pi / ARM64 because it uses `mcr.microsoft.com/mssql/server:2022-latest`. + ## Visual Studio Launch Profiles Launch profiles are defined in `src/Backend/AHKFlowApp.API/Properties/launchSettings.json`. diff --git a/docs/environments.md b/docs/environments.md index 1c7ce16..2aa69b7 100644 --- a/docs/environments.md +++ b/docs/environments.md @@ -2,289 +2,25 @@ ## Overview -AHKFlowApp supports three distinct environments with environment-specific configuration: +| Environment | ASPNETCORE_ENVIRONMENT | Hosting | Database | Deployment path | +|-------------|------------------------|---------|----------|-----------------| +| **DEV** | `Development` | Local machine | Windows LocalDB or x64 Docker SQL Server | Manual `dotnet run` / Docker Compose | +| **TEST** | `Test` | Azure | Azure SQL Database | Auto on push to `main` | +| **PROD** | `Production` | Azure | Azure SQL Database | Manual workflow dispatch | -| Environment | Name | ASPNETCORE_ENVIRONMENT | Location | Database | Deployment | -|-------------|------|------------------------|----------|----------|------------| -| **DEV** | Development | `Development` | Local machine | LocalDB or Docker SQL | Manual (`dotnet run`) | -| **TEST** | Test | `Test` | Azure | Azure SQL Database | Auto (push to `main`) | -| **PROD** | Production | `Production` | Azure | Azure SQL Database | Manual (workflow dispatch) | +## DEV -## DEV Environment (Local Development) +- **Windows LocalDB path:** `dotnet run --project src/Backend/AHKFlowApp.API --launch-profile "LocalDB SQL"` +- **Windows/x64 Docker SQL path:** `dotnet run --project src/Backend/AHKFlowApp.API --launch-profile "Docker SQL (Recommended)"` +- **Docker Compose path:** `docker compose up -d --build` starts **API + SQL Server only**; run the frontend separately with `dotnet run --project src/Frontend/AHKFlowApp.UI.Blazor` +- **Raspberry Pi / ARM64:** not supported for the bundled Docker stack because `docker-compose.yml` uses SQL Server 2022 -### Configuration +See [docs/development/docker-setup.md](development/docker-setup.md) and [docs/development/configuration-strategy.md](development/configuration-strategy.md). -- **ASPNETCORE_ENVIRONMENT**: `Development` -- **Database**: LocalDB (`(localdb)\mssqllocaldb`) or Docker SQL Server (port 1433) -- **Connection String**: Configured in `appsettings.Development.json` (not committed) or Docker Compose -- **CORS**: Allows `https://localhost:7601` and `http://localhost:5601` -- **Logging**: Debug level for application code, Information for EF Core queries +## TEST / PROD -### Running Locally +- Provision Azure resources with `.\scripts\deploy.ps1 -Environment test|prod` +- `deploy.ps1` creates or updates the Entra app registration, configures GitHub secrets/variables, and dispatches `deploy-frontend.yml` +- `deploy-api.yml` publishes the API container on push to `main` for **TEST** and on manual dispatch for **PROD** -**Option 1 — LocalDB:** -```bash -dotnet run --project src/Backend/AHKFlowApp.API --launch-profile https -``` - -**Option 2 — Docker SQL (Recommended):** -```bash -dotnet run --project src/Backend/AHKFlowApp.API --launch-profile "https + Docker SQL (Recommended)" -``` - -**Option 3 — Full Stack (Docker Compose):** -```bash -docker compose up --build -``` - -### URLs -- API: `http://localhost:5600` (single port for VS, docker-compose, Docker-only scenarios) -- Frontend: `http://localhost:5601` - -## TEST Environment (Azure Pre-Production) - -### Configuration - -- **ASPNETCORE_ENVIRONMENT**: `Test` (set via App Service application setting) -- **Database**: Azure SQL Database (`ahkflowapp-sql-test.database.windows.net`) -- **Authentication**: Entra ID (Managed Identity) — no SQL passwords -- **Connection String**: Set via App Service connection string configuration -- **CORS**: Configured to allow Static Web App URL -- **Logging**: Information level for application, Warning for framework - -### Azure Resources - -All resources are in the `rg-ahkflowapp-test` resource group: -- App Service: `ahkflowapp-api-test` -- SQL Server: `ahkflowapp-sql-test` -- SQL Database: `ahkflowapp-db` -- Static Web App: `ahkflowapp-swa-test` -- Key Vault: `ahkflowapp-kv-test` -- User-Assigned Managed Identity (deployer): `ahkflowapp-uami-deployer-test` -- User-Assigned Managed Identity (runtime): `ahkflowapp-uami-runtime-test` - -### Provisioning - -Run the Azure provisioning scripts with `ENVIRONMENT=test`: - -```bash -cd scripts/azure - -# Step 1: Verify prerequisites -cat 00-prerequisites.md - -# Step 2: Provision Azure resources -# Set ENVIRONMENT=test in 01-provision-azure.md and run commands - -# Step 3: Configure GitHub OIDC -# Set ENVIRONMENT=test in 02-configure-github-oidc.md and run commands -``` - -### Deployment - -**Automatic:** Push to `main` branch triggers `deploy-api.yml` and `deploy-frontend.yml` workflows. - -**Manual:** Use workflow dispatch in GitHub Actions. - -### GitHub Secrets & Variables - -**Secrets** (shared): -- `AZURE_TENANT_ID` -- `AZURE_SUBSCRIPTION_ID` -- `AZURE_CLIENT_ID_TEST` (deployer managed identity) -- `AZURE_STATIC_WEB_APPS_API_TOKEN_TEST` - -**Variables** (TEST-specific): -- `AZURE_RESOURCE_GROUP_TEST=rg-ahkflowapp-test` -- `APP_SERVICE_NAME_TEST=ahkflowapp-api-test` -- `SQL_SERVER_NAME_TEST=ahkflowapp-sql-test` -- `SQL_SERVER_FQDN_TEST=ahkflowapp-sql-test.database.windows.net` -- `SQL_DATABASE_NAME_TEST=ahkflowapp-db` - -### URLs -- API: `https://ahkflowapp-api-test.azurewebsites.net` -- API Health: `https://ahkflowapp-api-test.azurewebsites.net/health` -- Frontend: Get from `az staticwebapp show --name ahkflowapp-swa-test --query defaultHostname -o tsv` - -## PROD Environment (Azure Production) - -### Configuration - -- **ASPNETCORE_ENVIRONMENT**: `Production` (set via App Service application setting) -- **Database**: Azure SQL Database (`ahkflowapp-sql-prod.database.windows.net`) -- **Authentication**: Entra ID (Managed Identity) — no SQL passwords -- **Connection String**: Set via App Service connection string configuration -- **CORS**: Configured to allow Static Web App URL -- **Logging**: Warning level (minimal logging for performance) - -### Azure Resources - -All resources are in the `rg-ahkflowapp-prod` resource group: -- App Service: `ahkflowapp-api-prod` -- SQL Server: `ahkflowapp-sql-prod` -- SQL Database: `ahkflowapp-db` -- Static Web App: `ahkflowapp-swa-prod` -- Key Vault: `ahkflowapp-kv-prod` -- User-Assigned Managed Identity (deployer): `ahkflowapp-uami-deployer-prod` -- User-Assigned Managed Identity (runtime): `ahkflowapp-uami-runtime-prod` - -### Provisioning - -Run the Azure provisioning scripts with `ENVIRONMENT=prod`: - -```bash -cd scripts/azure - -# Step 1: Verify prerequisites -cat 00-prerequisites.md - -# Step 2: Provision Azure resources -# Set ENVIRONMENT=prod in 01-provision-azure.md and run commands - -# Step 3: Configure GitHub OIDC -# Set ENVIRONMENT=prod in 02-configure-github-oidc.md and run commands -``` - -### Deployment - -**Manual Only:** Use workflow dispatch in GitHub Actions: -1. Go to Actions → Deploy API (or Deploy Frontend) -2. Click "Run workflow" -3. Select environment: `prod` -4. Confirm and run - -### GitHub Secrets & Variables - -**Secrets** (shared): -- `AZURE_TENANT_ID` -- `AZURE_SUBSCRIPTION_ID` -- `AZURE_CLIENT_ID_PROD` (deployer managed identity) -- `AZURE_STATIC_WEB_APPS_API_TOKEN_PROD` - -**Variables** (PROD-specific): -- `AZURE_RESOURCE_GROUP_PROD=rg-ahkflowapp-prod` -- `APP_SERVICE_NAME_PROD=ahkflowapp-api-prod` -- `SQL_SERVER_NAME_PROD=ahkflowapp-sql-prod` -- `SQL_SERVER_FQDN_PROD=ahkflowapp-sql-prod.database.windows.net` -- `SQL_DATABASE_NAME_PROD=ahkflowapp-db` - -### URLs -- API: `https://ahkflowapp-api-prod.azurewebsites.net` -- API Health: `https://ahkflowapp-api-prod.azurewebsites.net/health` -- Frontend: Get from `az staticwebapp show --name ahkflowapp-swa-prod --query defaultHostname -o tsv` - -## Environment-Specific Configuration Files - -### API Backend - -``` -src/Backend/AHKFlowApp.API/ - appsettings.json # Base configuration (committed) - appsettings.Development.json # DEV overrides (committed, CORS only) - appsettings.Test.json # TEST overrides (committed) - appsettings.Production.json # PROD overrides (committed) -``` - -**Load order:** `appsettings.json` → `appsettings.{Environment}.json` → Environment variables → Azure App Configuration (future) - -### Frontend Blazor - -``` -src/Frontend/AHKFlowApp.UI.Blazor/wwwroot/ - appsettings.json # Base config (committed) - appsettings.Development.json # DEV overrides — localhost API (committed) - appsettings.Test.json # TEST overrides — TEST Azure API URL (committed) - appsettings.Production.json # PROD overrides — PROD Azure API URL (committed) -``` - -**Load order:** `appsettings.json` → `appsettings.{BlazorEnvironment}.json` - -The active environment is controlled by the `Blazor-Environment` HTTP header: -- TEST SWA: set to `Test` via `staticwebapp.config.json` -- PROD SWA: patched to `Production` by `deploy-frontend.yml` before publishing -- Local dev: set to `Development` automatically by the ASP.NET Core dev server - -No secrets are stored in frontend configuration — all values are public. - -## Switching Between Environments - -### Local Development → TEST -1. Ensure TEST environment is provisioned in Azure -2. Ensure GitHub secrets/variables are configured for TEST -3. Push to `main` branch or manually trigger workflow with environment=test - -### TEST → PROD -1. Provision PROD environment in Azure -2. Configure GitHub secrets/variables for PROD -3. Manually trigger Deploy API workflow with environment=prod -4. Manually trigger Deploy Frontend workflow with environment=prod - -## Environment Variable Reference - -### All Environments - -| Variable | DEV | TEST | PROD | -|----------|-----|------|------| -| `ASPNETCORE_ENVIRONMENT` | `Development` | `Test` | `Production` | -| `AZURE_CLIENT_ID` | (not set) | UAMI runtime client ID | UAMI runtime client ID | -| `WEBSITES_PORT` | (not set) | `8080` | `8080` | - -### Connection Strings - -**DEV:** -``` -Server=(localdb)\mssqllocaldb;Database=AHKFlowApp;Trusted_Connection=True;MultipleActiveResultSets=true -``` -or (Docker): -``` -Server=localhost,1433;Database=AHKFlowAppDb;User Id=sa;Password=AHKFlow_Dev!2026;TrustServerCertificate=True -``` - -**TEST/PROD (Azure, Entra ID auth):** -``` -Server=tcp:{SQL_SERVER_FQDN},1433;Database={SQL_DATABASE_NAME};Authentication=Active Directory Default;Encrypt=True;TrustServerCertificate=False;Connection Timeout=30; -``` - -## Troubleshooting - -### DEV: Database connection fails -- Ensure SQL Server LocalDB is installed or Docker is running -- Run `dotnet ef database update` to apply migrations - -### TEST/PROD: Deployment fails -- Check GitHub secrets and variables are set correctly with `_TEST` or `_PROD` suffix -- Verify Azure UAMI has correct RBAC permissions -- Check SQL firewall rules allow GitHub Actions runner IP - -### TEST/PROD: API returns 500 errors -- Check App Service logs: `az webapp log tail --name {APP_SERVICE_NAME} --resource-group {RESOURCE_GROUP}` -- Verify `ASPNETCORE_ENVIRONMENT` is set correctly in App Service configuration -- Check Application Insights for exceptions - -### Environment not loading correct appsettings -- Verify `ASPNETCORE_ENVIRONMENT` environment variable is set -- Check file exists: `appsettings.{Environment}.json` -- ASP.NET Core is case-sensitive for environment names (use `Development`, `Test`, `Production`) - -## Security Considerations - -### DEV -- SQL password in `docker-compose.yml` is for local development only -- Never commit real secrets to `appsettings.Development.json` -- Use `dotnet user-secrets` for sensitive local configuration - -### TEST/PROD -- All secrets stored in Azure Key Vault or App Service configuration -- SQL authentication uses Entra ID only (no SQL logins) -- Managed identities eliminate long-lived secrets -- HTTPS enforced via HSTS -- Connection strings never committed to source control - -## Next Steps - -1. **Set up DEV:** Clone repo, run `dotnet run` or `docker compose up` -2. **Provision TEST:** Run `scripts/azure/01-provision-azure.md` with `ENVIRONMENT=test` -3. **Configure CI/CD for TEST:** Run `scripts/azure/02-configure-github-oidc.md` with `ENVIRONMENT=test` -4. **Deploy to TEST:** Push to `main` or manually trigger workflow -5. **Provision PROD:** Repeat steps 2-3 with `ENVIRONMENT=prod` -6. **Deploy to PROD:** Manually trigger workflow with `environment=prod` +See [docs/deployment/getting-started.md](deployment/getting-started.md) and [docs/deployment/entra-setup.md](deployment/entra-setup.md). From f8b67ceb4832d01ad7beddef95568602b6594033 Mon Sep 17 00:00:00 2001 From: "Bart Segers (M16)" Date: Wed, 22 Apr 2026 13:59:41 +0200 Subject: [PATCH 09/10] docs: clarify API deploy next steps Differentiate TEST and PROD API deployment behavior after provisioning so deploy guidance matches deploy-api.yml trigger rules. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- docs/deployment/getting-started.md | 5 ++++- scripts/deploy.ps1 | 7 ++++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/docs/deployment/getting-started.md b/docs/deployment/getting-started.md index e12a850..87dca0f 100644 --- a/docs/deployment/getting-started.md +++ b/docs/deployment/getting-started.md @@ -25,7 +25,10 @@ The script will: 7. Trigger the frontend deploy workflow on GitHub 8. Save your configuration to `scripts/.env.{environment}` for future use -When done, the script queues the frontend deployment automatically. The API still deploys when `deploy-api.yml` runs, typically on the next push to `main`. +When done, the script queues the frontend deployment automatically. + +- **TEST:** the API deploys when `deploy-api.yml` is triggered by a qualifying push to `main` (`src/Backend/**`, `tests/**`, `Directory.*.props`, `global.json`, or `.github/workflows/deploy-api.yml`). +- **PROD:** the API deploy is manual — run `deploy-api.yml` with `environment=prod`. ## What Gets Provisioned diff --git a/scripts/deploy.ps1 b/scripts/deploy.ps1 index c6c04e6..2d9fad1 100644 --- a/scripts/deploy.ps1 +++ b/scripts/deploy.ps1 @@ -703,7 +703,12 @@ if ($frontendDeploymentTriggered) { } else { Write-Host " 1. Trigger the frontend deploy workflow manually once ready." } -Write-Host " 2. Push to 'main' to build and publish the API container image." +if ($Environment -eq 'test') { + Write-Host " 2. Push qualifying backend changes to 'main' to build and publish the API container image." + Write-Host " Auto-trigger paths: src/Backend/**, tests/**, Directory.*.props, global.json, .github/workflows/deploy-api.yml" +} else { + Write-Host " 2. Trigger deploy-api.yml manually with environment=prod to build and publish the API container image." +} Write-Host "" Write-Host " IMPORTANT: GHCR packages are private by default." -ForegroundColor Yellow Write-Host " After the first deploy-api.yml run, make the container image public:" From 299a4844ac0c5590506d0a5ece71a8da00b185aa Mon Sep 17 00:00:00 2001 From: "Bart Segers (M16)" Date: Wed, 22 Apr 2026 14:18:36 +0200 Subject: [PATCH 10/10] refactor: remove empty domain test project Drop the empty AHKFlowApp.Domain.Tests project from the solution and active guidance so verification no longer produces a no-tests warning. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .claude/skills/cck-testing/SKILL.md | 1 - .claude/skills/cck-verify/SKILL.md | 1 - AGENTS.md | 1 - AHKFlowApp.slnx | 1 - .../AHKFlowApp.Domain.Tests.csproj | 24 ------------------- 5 files changed, 28 deletions(-) delete mode 100644 tests/AHKFlowApp.Domain.Tests/AHKFlowApp.Domain.Tests.csproj diff --git a/.claude/skills/cck-testing/SKILL.md b/.claude/skills/cck-testing/SKILL.md index 337f9f3..05fd374 100644 --- a/.claude/skills/cck-testing/SKILL.md +++ b/.claude/skills/cck-testing/SKILL.md @@ -25,7 +25,6 @@ description: > tests/ AHKFlowApp.API.Tests/ # Integration tests — WebApplicationFactory + Testcontainers AHKFlowApp.Application.Tests/ # Validator unit tests + handler unit tests - AHKFlowApp.Domain.Tests/ # Domain entity logic unit tests AHKFlowApp.Infrastructure.Test/ # EF Core + migration tests with Testcontainers AHKFlowApp.UI.Blazor.Tests/ # Blazor component tests (bUnit) ``` diff --git a/.claude/skills/cck-verify/SKILL.md b/.claude/skills/cck-verify/SKILL.md index a5eaf61..cb6fe00 100644 --- a/.claude/skills/cck-verify/SKILL.md +++ b/.claude/skills/cck-verify/SKILL.md @@ -36,7 +36,6 @@ If tests fail: stop and fix before continuing. See the **build-fix** skill. ```bash dotnet test tests/AHKFlowApp.API.Tests --configuration Release --verbosity normal dotnet test tests/AHKFlowApp.Application.Tests --configuration Release --verbosity normal -dotnet test tests/AHKFlowApp.Domain.Tests --configuration Release --verbosity normal ``` ### Run a Single Test by Name diff --git a/AGENTS.md b/AGENTS.md index 5be35f8..80954cd 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -33,7 +33,6 @@ src/Frontend/ tests/ AHKFlowApp.API.Tests/ # API integration tests (WebApplicationFactory) AHKFlowApp.Application.Tests/ # Validator + service unit tests - AHKFlowApp.Domain.Tests/ # Domain logic unit tests AHKFlowApp.Infrastructure.Tests/ # Repository integration tests AHKFlowApp.UI.Blazor.Tests/ # Blazor component tests (bUnit) ``` diff --git a/AHKFlowApp.slnx b/AHKFlowApp.slnx index f1a2878..6b630df 100644 --- a/AHKFlowApp.slnx +++ b/AHKFlowApp.slnx @@ -12,7 +12,6 @@ - diff --git a/tests/AHKFlowApp.Domain.Tests/AHKFlowApp.Domain.Tests.csproj b/tests/AHKFlowApp.Domain.Tests/AHKFlowApp.Domain.Tests.csproj deleted file mode 100644 index 3dbf262..0000000 --- a/tests/AHKFlowApp.Domain.Tests/AHKFlowApp.Domain.Tests.csproj +++ /dev/null @@ -1,24 +0,0 @@ - - - false - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - -