Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 53 additions & 1 deletion .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,33 @@ permissions:

# Based from https://docs.github.com/en/actions/use-cases-and-examples/building-and-testing/building-and-testing-net
jobs:
changes:
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: read
outputs:
backend: ${{ steps.filter.outputs.backend || 'true' }}
frontend: ${{ steps.filter.outputs.frontend || 'true' }}
steps:
- name: Checkout
if: github.event_name == 'pull_request'
uses: actions/checkout@v6

- name: Check for file changes
if: github.event_name == 'pull_request'
uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v3.0.2
id: filter
with:
filters: |
backend:
- 'backend/**'
- '.github/workflows/main.yml'
Comment thread
dgee2 marked this conversation as resolved.
Comment thread
dgee2 marked this conversation as resolved.
frontend:
- 'ui/**'
- 'open-api/**'
- '.github/workflows/main.yml'

dependency-review:
if: github.event_name == 'pull_request'
runs-on: ubuntu-latest
Expand All @@ -32,6 +59,10 @@ jobs:

backend-build:
name: Create OpenAPI spec
needs: changes
if: |
github.event_name != 'pull_request' ||
needs.changes.outputs.backend == 'true'
runs-on: ubuntu-latest
permissions:
contents: read
Expand All @@ -51,9 +82,11 @@ jobs:

- name: Restore
run: dotnet restore MenuApi.sln
working-directory: ./backend

- name: Build solution (generate OpenAPI)
run: dotnet build MenuApi.sln --configuration Release --no-restore
working-directory: ./backend

- name: Upload OpenAPI document
uses: actions/upload-artifact@v4
Expand All @@ -64,6 +97,10 @@ jobs:

backend-tests:
name: Backend tests
needs: changes
if: |
github.event_name != 'pull_request' ||
needs.changes.outputs.backend == 'true'
runs-on: ubuntu-latest
permissions:
contents: read
Expand All @@ -83,16 +120,23 @@ jobs:

- name: Restore
run: dotnet restore MenuApi.sln
working-directory: ./backend

- name: Build solution (generate OpenAPI)
run: dotnet build MenuApi.sln --configuration Release --no-restore
working-directory: ./backend

- name: Test with the dotnet CLI
run: dotnet test --project MenuApi.Tests/MenuApi.Tests.csproj --configuration Release --no-build
working-directory: ./backend
Comment thread
dgee2 marked this conversation as resolved.


backend-integration-tests:
name: Backend integration tests
needs: changes
if: |
github.event_name != 'pull_request' ||
needs.changes.outputs.backend == 'true'
runs-on: ubuntu-latest
permissions:
contents: read
Expand All @@ -112,9 +156,11 @@ jobs:

- name: Restore
run: dotnet restore MenuApi.sln
working-directory: ./backend

- name: Build solution (generate OpenAPI)
run: dotnet build MenuApi.sln --configuration Release --no-restore
working-directory: ./backend

- name: clean ssl cert
run: dotnet dev-certs https --clean
Expand All @@ -126,6 +172,7 @@ jobs:

- name: Test with the dotnet CLI
run: dotnet test --project MenuApi.Integration.Tests/MenuApi.Integration.Tests.csproj --configuration Release --no-build
Comment thread
dgee2 marked this conversation as resolved.
working-directory: ./backend
env:
parameters__Auth0TestClientId: ${{ vars.AUTH0_CLIENT_ID }}
parameters__Auth0TestClientSecret: ${{ secrets.AUTH0_CLIENT_SECRET }}
Expand All @@ -135,7 +182,11 @@ jobs:
frontend:
name: Frontend validation
runs-on: ubuntu-latest
needs: backend-build
needs: [changes, backend-build]
if: |
!cancelled() &&
(github.event_name != 'pull_request' || needs.changes.outputs.frontend == 'true') &&
(needs.backend-build.result == 'success' || needs.backend-build.result == 'skipped')
permissions:
contents: read
actions: read
Expand All @@ -145,6 +196,7 @@ jobs:
uses: actions/checkout@v6

- name: Download OpenAPI document
if: needs.backend-build.result == 'success'
uses: actions/download-artifact@v4
with:
name: openapi-spec
Expand Down
4 changes: 2 additions & 2 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"dotnet.defaultSolution": "MenuApi.sln",
"dotnet.defaultSolution": "backend/MenuApi.sln",
"sonarlint.connectedMode.project": {
"connectionId": "dgee2-github",
"projectKey": "dgee2_Menu"
}
}
}
35 changes: 18 additions & 17 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@

## Architecture

.NET Aspire distributed app (net10.0) for recipe/menu management. `Menu.AppHost` orchestrates all services:
.NET Aspire distributed app (net10.0) for recipe/menu management. Backend projects live under `backend/`, and `backend/Menu.AppHost` orchestrates all services:

- **MenuApi** – Minimal API (Auth0 JWT-secured). Endpoints defined via `MapGroup` extensions in `MenuApi/Recipes/`.
- **MenuDB** – EF Core `MenuDbContext` + entity definitions (`MenuDB/Data/`) + migrations (`MenuDB/Migrations/`).
- **MenuApi** – Minimal API (Auth0 JWT-secured). Endpoints defined via `MapGroup` extensions in `backend/MenuApi/Recipes/`.
- **MenuDB** – EF Core `MenuDbContext` + entity definitions (`backend/MenuDB/Data/`) + migrations (`backend/MenuDB/Migrations/`).
- **Menu.MigrationService** – BackgroundService that applies EF migrations on startup, then exits. The API (`MenuApi`) waits for this to complete before starting (`WaitForCompletion`).
- **Redis** – `AddRedis("cache")` resource for caching.
- **Menu.ServiceDefaults / Menu.ApiServiceDefaults** – Shared Aspire service defaults (OpenTelemetry, health checks, Swagger).
Expand All @@ -17,15 +17,15 @@ Three distinct model layers — never mix them:

| Layer | Namespace / Location | Purpose |
|---|---|---|
| **EF Entities** | `MenuDB/Data/` (e.g. `RecipeEntity`) | Database rows; configured in `MenuDbContext.OnModelCreating` |
| **DB Models** | `MenuApi/DBModel/` (e.g. `DBModel.Recipe`) | Intermediate records using Vogen value objects; returned by repositories |
| **ViewModels** | `MenuApi/ViewModel/` (e.g. `ViewModel.Recipe`, `NewRecipe`, `FullRecipe`) | API request/response DTOs |
| **EF Entities** | `backend/MenuDB/Data/` (e.g. `RecipeEntity`) | Database rows; configured in `MenuDbContext.OnModelCreating` |
| **DB Models** | `backend/MenuApi/DBModel/` (e.g. `DBModel.Recipe`) | Intermediate records using Vogen value objects; returned by repositories |
| **ViewModels** | `backend/MenuApi/ViewModel/` (e.g. `ViewModel.Recipe`, `NewRecipe`, `FullRecipe`) | API request/response DTOs |

Mapping between layers uses **Riok.Mapperly** (source-generated, zero-reflection) in `MenuApi/MappingProfiles/ViewModelMapper.cs`. When adding properties, update the `[MapProperty]` attributes there.
Mapping between layers uses **Riok.Mapperly** (source-generated, zero-reflection) in `backend/MenuApi/MappingProfiles/ViewModelMapper.cs`. When adding properties, update the `[MapProperty]` attributes there.

## Vogen Value Objects

Primitive types are wrapped with [Vogen](https://github.com/SteveDunn/Vogen) (`MenuApi/ValueObjects/`). Example: `RecipeId`, `RecipeName`, `IngredientAmount`. Assembly-wide defaults in `VogenDefaults.cs` enable EF Core value converters and Swagger mapping. When creating a new value object:
Primitive types are wrapped with [Vogen](https://github.com/SteveDunn/Vogen) (`backend/MenuApi/ValueObjects/`). Example: `RecipeId`, `RecipeName`, `IngredientAmount`. Assembly-wide defaults in `VogenDefaults.cs` enable EF Core value converters and Swagger mapping. When creating a new value object:

```csharp
[ValueObject<int>]
Expand All @@ -38,9 +38,10 @@ Repositories must use `.Value` to unwrap and `TypeName.From(x)` to wrap.

```bash
# Run the full stack (API + SQL container + migrations + UI)
cd backend
dotnet run --project Menu.AppHost

# EF migrations (always from solution root)
# EF migrations (always from the backend solution root)
dotnet ef migrations add <Name> --project MenuDB --startup-project MenuApi
dotnet ef migrations remove --project MenuDB --startup-project MenuApi

Expand All @@ -60,7 +61,7 @@ dotnet test MenuApi.Integration.Tests
## Code Style

- `TreatWarningsAsErrors` is enabled in Debug and Release for all projects.
- StyleCop is configured via `MenuApi/stylecop.json`.
- StyleCop is configured via `backend/MenuApi/stylecop.json`.
- `ConfigureAwait(false)` is used on all async calls in service/repository layers.
- `Program.cs` exposes a `public partial class Program` for integration test `WebApplicationFactory` compatibility.

Expand Down Expand Up @@ -138,10 +139,10 @@ pnpm storybook # Storybook dev server (port 6006)

## Adding a New API Endpoint

1. Add ViewModel DTOs in `MenuApi/ViewModel/`.
2. Add DB model records in `MenuApi/DBModel/` (if new data shapes are needed).
3. Add/update Mapperly mappings in `MenuApi/MappingProfiles/ViewModelMapper.cs`.
4. Add repository method (interface in `MenuApi/Repositories/I*Repository.cs`, impl in `*Repository.cs`).
5. Add service method (interface + impl in `MenuApi/Services/`).
6. Add the endpoint in the relevant `MenuApi/Recipes/*Api.cs` file using the `MapGroup` pattern.
7. Register new DI services in `MenuApi/Program.cs`.
1. Add ViewModel DTOs in `backend/MenuApi/ViewModel/`.
2. Add DB model records in `backend/MenuApi/DBModel/` (if new data shapes are needed).
3. Add/update Mapperly mappings in `backend/MenuApi/MappingProfiles/ViewModelMapper.cs`.
4. Add repository method (interface in `backend/MenuApi/Repositories/I*Repository.cs`, impl in `*Repository.cs`).
5. Add service method (interface + impl in `backend/MenuApi/Services/`).
6. Add the endpoint in the relevant `backend/MenuApi/Recipes/*Api.cs` file using the `MapGroup` pattern.
7. Register new DI services in `backend/MenuApi/Program.cs`.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
2 changes: 1 addition & 1 deletion MenuApi/MenuApi.csproj → backend/MenuApi/MenuApi.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@
</ItemGroup>

<PropertyGroup>
<OpenApiDocumentsDirectory>../open-api</OpenApiDocumentsDirectory>
<OpenApiDocumentsDirectory>../../open-api</OpenApiDocumentsDirectory>
<OpenApiGenerateDocumentsOptions>--file-name menu-api</OpenApiGenerateDocumentsOptions>
</PropertyGroup>
</Project>
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
163 changes: 163 additions & 0 deletions docs/ci-path-filters.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
# CI Path Filters

## Overview

The CI workflow uses conservative path filters to skip unaffected work while ensuring no required validation is missed for cross-stack changes.

## Implementation

The workflow uses the [dorny/paths-filter](https://github.com/dorny/paths-filter) action to detect which files have changed in pull requests and conditionally runs jobs based on those changes.

### Path Filter Configuration

#### Backend Jobs

Backend jobs (`backend-build`, `backend-tests`, `backend-integration-tests`) run when any of these paths change:

- `backend/**` - All backend projects, solution files, and backend-specific configuration grouped under the `backend/` directory
- `.github/workflows/main.yml` - The workflow file itself

#### Frontend Job

The frontend job (`frontend`) runs when any of these files change:

- `ui/**` - Any file in the frontend directory
- `open-api/**` - OpenAPI specification files (triggers type regeneration)
- `.github/workflows/main.yml` - The workflow file itself

## Behavior by Event Type

### Pull Requests

- **Changes detected**: Only jobs matching the changed files run
- **No changes detected**: Backend/frontend build and test jobs are skipped when no relevant files change
- **Workflow file changes**: Both frontend and backend jobs run (conservative approach)

### Push Events (main/master branches)

- **All jobs always run**: Path filters are only applied to pull requests
- This ensures complete validation on the main branches

### Workflow Dispatch

- **All jobs always run**: Manual triggers run complete validation

## Cross-Stack Scenarios

### Scenario: Backend-only changes (e.g., only files under `backend/`)

- ✅ Backend jobs run
- ❌ Frontend job is skipped
- The checked-in OpenAPI spec in the repository remains unchanged

### Scenario: Frontend-only changes (e.g., only `ui/` files)

- ❌ Backend jobs are skipped
- ✅ Frontend job runs
- The frontend uses the existing OpenAPI spec from the repository

### Scenario: OpenAPI contract changes

- ❌ Backend jobs are skipped (OpenAPI is generated by backend build)
- ✅ Frontend job runs to regenerate types
- **Note**: If the OpenAPI spec is out of sync with the backend, both backend and frontend files should be changed together

### Scenario: Both backend and frontend changes

- ✅ Backend jobs run
- ✅ Frontend job runs
- Normal full validation

### Scenario: Workflow file changes

- ✅ Backend jobs run
- ✅ Frontend job runs
- Conservative approach to ensure workflow changes don't break validation

## Frontend Job Dependencies

The frontend job has special dependency handling:

```yaml
needs: [changes, backend-build]
if: |
!cancelled() &&
(github.event_name != 'pull_request' || needs.changes.outputs.frontend == 'true') &&
(needs.backend-build.result == 'success' || needs.backend-build.result == 'skipped')
```

This allows the frontend to run even when backend-build is skipped (frontend-only changes), while ensuring it waits for backend-build when it does run.

### OpenAPI Artifact Handling

The "Download OpenAPI document" step in the frontend job is conditional:

```yaml
- name: Download OpenAPI document
if: needs.backend-build.result == 'success'
uses: actions/download-artifact@v4
```

- If backend-build runs and succeeds, the new OpenAPI spec is downloaded
- If backend-build is skipped, the existing OpenAPI spec from the repository is used

## Performance Benefits

Path filters improve CI throughput by:

1. **Reducing build time**: Backend builds (~2-3 minutes) are skipped for frontend-only PRs
2. **Reducing test time**: Backend tests (~1-2 minutes) are skipped for frontend-only PRs
3. **Reducing runner usage**: Skipped jobs don't consume GitHub Actions runner minutes

### Example Savings

- **Frontend-only PR**: Saves ~5-7 minutes (skips 3 backend jobs)
- **Backend-only PR**: Saves ~2-3 minutes (skips 1 frontend job)
- **Documentation-only PR**: Saves ~7-10 minutes (skips all build/test jobs, runs only `changes` and `dependency-review`)

Comment thread
dgee2 marked this conversation as resolved.
## Guardrails

The implementation includes several guardrails to prevent missing validation:

1. **Conservative structure**: Backend projects and backend-only config files live under `backend/`, so one path captures solution, project, and analyzer changes together
2. **Workflow file triggers both**: Changes to the workflow file trigger all jobs
3. **Always run on push**: All jobs run on pushes to main/master branches
4. **Dependency review still runs**: The dependency-review job always runs on PRs regardless of changed files

## Testing Path Filters

To test the path filters work correctly:

1. Create a PR with only backend changes (e.g., modify a `.cs` file)
- Verify backend jobs run and frontend job is skipped

2. Create a PR with only frontend changes (e.g., modify a `.vue` file)
- Verify frontend job runs and backend jobs are skipped

3. Create a PR with both backend and frontend changes
- Verify all jobs run

4. Create a PR with only documentation changes (e.g., modify a `.md` file)
- Verify the `changes` job runs, `dependency-review` runs, and backend/frontend jobs are skipped

## Troubleshooting

### Jobs unexpectedly skipped

- Check if the changed files match the path patterns
- Verify the PR is against the correct base branch
- Check if this is a push event (push events always run all jobs)

### Jobs not being skipped

- Ensure the event is a pull_request (not push)
- Check if the workflow file was changed (triggers all jobs)
- Verify the paths-filter action is running correctly in the changes job

### Frontend fails because OpenAPI spec is missing

This can happen if:
- The backend changed but wasn't included in the PR
- The OpenAPI spec in the repo is out of sync

**Solution**: Include both backend and frontend changes in the same PR when the OpenAPI contract changes.
Loading
Loading