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
42 changes: 42 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,45 @@ jobs:
uses: DavidAnson/markdownlint-cli2-action@v24
with:
globs: "**/*.md"

build:
name: Build and test
runs-on: ubuntu-latest
timeout-minutes: 15
services:
postgres:
image: postgres:17
env:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: fluentcms_test
ports:
- 5432:5432
options: >-
--health-cmd "pg_isready -U postgres"
--health-interval 5s
--health-timeout 5s
--health-retries 10
env:
DOTNET_NOLOGO: true
DOTNET_CLI_TELEMETRY_OPTOUT: true
FLUENTCMS_TEST_POSTGRES: Host=localhost;Port=5432;Database=fluentcms_test;Username=postgres;Password=postgres
steps:
- name: Checkout
uses: actions/checkout@v7

- name: Set up .NET
uses: actions/setup-dotnet@v6
with:
global-json-file: global.json
cache: true
cache-dependency-path: Directory.Packages.props

- name: Restore
run: dotnet restore

- name: Build
run: dotnet build --no-restore --configuration Release

- name: Test
run: dotnet test --no-build --configuration Release
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,11 @@ artifacts/
*.swp
*.DotSettings.user

# Local SQLite databases
*.db
*.db-shm
*.db-wal

# Tests and coverage
TestResults/
*.trx
Expand Down
11 changes: 10 additions & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,16 @@ Product decisions live in `docs/requirements/00-decision-record.md`. Propose a c

## Building and testing

Not yet. The solution skeleton is the next piece of work after the repository bootstrap. Once it exists, this section will cover `dotnet build`, `dotnet test` and the Tailwind standalone step. Building will need the .NET 10 SDK and nothing else; Node is not required.
You need the .NET 10 SDK and nothing else; `global.json` pins the version and Node is not required.

```bash
dotnet build
dotnet test
```

Tests run against SQLite in-process. To run the PostgreSQL tests as well, set `FLUENTCMS_TEST_POSTGRES` to a connection string for an empty database, for example `Host=localhost;Port=5432;Database=fluentcms_test;Username=postgres;Password=postgres`. CI does this with a service container. Without the variable those tests are skipped.

To run the host, `dotnet run --project src/FluentCMS.Host` starts the web host on `http://localhost:5000` with a SQLite database in `fluentcms.db`, and `dotnet run --project src/FluentCMS.Host -- mcp` starts the stdio mode. Configuration comes from environment variables such as `Database__ConnectionString`, `Database__Provider` (`sqlite` or `postgresql`, inferred from the connection string when omitted) and `JobQueue__WorkerCount`, and from an optional `appsettings.json` next to the binary.

## Code style

Expand Down
13 changes: 13 additions & 0 deletions Directory.Build.props
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<Project>

<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<Deterministic>true</Deterministic>
<ContinuousIntegrationBuild Condition="'$(GITHUB_ACTIONS)' == 'true'">true</ContinuousIntegrationBuild>
<IsPackable>false</IsPackable>
</PropertyGroup>

</Project>
23 changes: 23 additions & 0 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<Project>

<PropertyGroup>
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
<CentralPackageTransitivePinningEnabled>false</CentralPackageTransitivePinningEnabled>
</PropertyGroup>

<ItemGroup Label="Runtime">
<PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="10.0.13" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="10.0.13" />
<PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="10.0.13" />
<PackageVersion Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="10.0.13" />
<PackageVersion Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="10.0.3" />
</ItemGroup>

<ItemGroup Label="Tests">
<PackageVersion Include="coverlet.collector" Version="6.0.4" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
<PackageVersion Include="xunit" Version="2.9.3" />
<PackageVersion Include="xunit.runner.visualstudio" Version="3.1.4" />
</ItemGroup>

</Project>
14 changes: 14 additions & 0 deletions FluentCMS.slnx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<Solution>
<Folder Name="/src/">
<Project Path="src/FluentCMS.Admin/FluentCMS.Admin.csproj" />
<Project Path="src/FluentCMS.Api/FluentCMS.Api.csproj" />
<Project Path="src/FluentCMS.Core/FluentCMS.Core.csproj" />
<Project Path="src/FluentCMS.Host/FluentCMS.Host.csproj" />
<Project Path="src/FluentCMS.Infrastructure/FluentCMS.Infrastructure.csproj" />
<Project Path="src/FluentCMS.Mcp/FluentCMS.Mcp.csproj" />
</Folder>
<Folder Name="/tests/">
<Project Path="tests/FluentCMS.IntegrationTests/FluentCMS.IntegrationTests.csproj" />
<Project Path="tests/FluentCMS.Tests/FluentCMS.Tests.csproj" />
</Folder>
</Solution>
7 changes: 7 additions & 0 deletions nuget.config
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<packageSources>
<clear />
<add key="nuget.org" value="https://api.nuget.org/v3/index.json" />
</packageSources>
</configuration>
12 changes: 12 additions & 0 deletions src/FluentCMS.Admin/AdminServiceCollectionExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
using Microsoft.Extensions.DependencyInjection;

namespace FluentCMS.Admin;

public static class AdminServiceCollectionExtensions
{
// The Blazor shell arrives with the admin shell issue; until then this registers nothing.
public static IServiceCollection AddFluentCmsAdmin(this IServiceCollection services)
{
return services;
}
}
11 changes: 11 additions & 0 deletions src/FluentCMS.Admin/FluentCMS.Admin.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<Project Sdk="Microsoft.NET.Sdk">

<ItemGroup>
<ProjectReference Include="..\FluentCMS.Core\FluentCMS.Core.csproj" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" />
</ItemGroup>

</Project>
12 changes: 12 additions & 0 deletions src/FluentCMS.Api/ApiServiceCollectionExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
using Microsoft.Extensions.DependencyInjection;

namespace FluentCMS.Api;

public static class ApiServiceCollectionExtensions
{
// The delivery API arrives with F-API; until then this registers nothing.
public static IServiceCollection AddFluentCmsApi(this IServiceCollection services)
{
return services;
}
}
11 changes: 11 additions & 0 deletions src/FluentCMS.Api/FluentCMS.Api.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<Project Sdk="Microsoft.NET.Sdk">

<ItemGroup>
<ProjectReference Include="..\FluentCMS.Core\FluentCMS.Core.csproj" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" />
</ItemGroup>

</Project>
12 changes: 12 additions & 0 deletions src/FluentCMS.Core/CoreServiceCollectionExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
using Microsoft.Extensions.DependencyInjection;

namespace FluentCMS.Core;

public static class CoreServiceCollectionExtensions
{
// The composition seam for domain and application services; each epic adds its registrations here.
public static IServiceCollection AddFluentCmsCore(this IServiceCollection services)
{
return services;
}
}
7 changes: 7 additions & 0 deletions src/FluentCMS.Core/FluentCMS.Core.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">

<ItemGroup>
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" />
</ItemGroup>

</Project>
16 changes: 16 additions & 0 deletions src/FluentCMS.Host/FluentCMS.Host.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<Project Sdk="Microsoft.NET.Sdk.Web">

<PropertyGroup>
<AssemblyName>fluentcms</AssemblyName>
<RootNamespace>FluentCMS.Host</RootNamespace>
</PropertyGroup>

<ItemGroup>
<ProjectReference Include="..\FluentCMS.Admin\FluentCMS.Admin.csproj" />
<ProjectReference Include="..\FluentCMS.Api\FluentCMS.Api.csproj" />
<ProjectReference Include="..\FluentCMS.Core\FluentCMS.Core.csproj" />
<ProjectReference Include="..\FluentCMS.Infrastructure\FluentCMS.Infrastructure.csproj" />
<ProjectReference Include="..\FluentCMS.Mcp\FluentCMS.Mcp.csproj" />
</ItemGroup>

</Project>
80 changes: 80 additions & 0 deletions src/FluentCMS.Host/FluentCmsHost.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
using FluentCMS.Admin;
using FluentCMS.Api;
using FluentCMS.Core;
using FluentCMS.Host.Jobs;
using FluentCMS.Infrastructure;
using FluentCMS.Mcp;
using Microsoft.Extensions.Logging.Console;

namespace FluentCMS.Host;

public static class FluentCmsHost
{
public static async Task<int> RunWebAsync(string[] args)
{
var app = BuildWebApplication(args);
await app.RunAsync();
return 0;
}

public static async Task<int> RunMcpAsync(string[] args)
{
using var host = BuildMcpHost(args);
await host.RunAsync();
return 0;
}

public static WebApplication BuildWebApplication(string[] args, Action<WebApplicationBuilder>? configure = null)
{
var builder = WebApplication.CreateBuilder(args);

ConfigureLogging(builder.Logging, logToStandardError: false);
AddFluentCms(builder.Services, builder.Configuration);
builder.Services.AddHealthChecks();
configure?.Invoke(builder);

var app = builder.Build();
app.MapHealthChecks("/health");
return app;
}

public static IHost BuildMcpHost(string[] args, Stream? standardInput = null, Action<HostApplicationBuilder>? configure = null)
{
var builder = Microsoft.Extensions.Hosting.Host.CreateApplicationBuilder(args);

ConfigureLogging(builder.Logging, logToStandardError: true);
AddFluentCms(builder.Services, builder.Configuration);
builder.Services.AddSingleton(standardInput is null
? StandardInputSource.Console
: new StandardInputSource(() => standardInput));
builder.Services.AddHostedService<StandardInputWatcher>();
configure?.Invoke(builder);

return builder.Build();
}

// One composition for both entry modes, so the stdio host opens the same database and runs the same services.
private static void AddFluentCms(IServiceCollection services, IConfiguration configuration)
{
services.AddFluentCmsCore();
services.AddFluentCmsInfrastructure(configuration);
services.AddFluentCmsMcp();
services.AddFluentCmsApi();
services.AddFluentCmsAdmin();

services.Configure<JobQueueOptions>(configuration.GetSection(JobQueueOptions.SectionName));
services.AddHostedService<JobQueueWorker>();
}

private static void ConfigureLogging(ILoggingBuilder logging, bool logToStandardError)
{
logging.ClearProviders();
logging.AddJsonConsole();

if (logToStandardError)
{
// Standard output belongs to the MCP transport, so every log line goes to standard error.
logging.Services.Configure<ConsoleLoggerOptions>(options => options.LogToStandardErrorThreshold = LogLevel.Trace);
}
}
}
10 changes: 10 additions & 0 deletions src/FluentCMS.Host/Jobs/JobQueueOptions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
namespace FluentCMS.Host.Jobs;

public sealed class JobQueueOptions
{
public const string SectionName = "JobQueue";

public int WorkerCount { get; set; } = 2;

public TimeSpan PollingInterval { get; set; } = TimeSpan.FromSeconds(5);
}
36 changes: 36 additions & 0 deletions src/FluentCMS.Host/Jobs/JobQueueWorker.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
using Microsoft.Extensions.Options;

namespace FluentCMS.Host.Jobs;

// The bounded worker pool from ADR-0010. There is no jobs table yet, so the workers only keep the polling rhythm.
public sealed class JobQueueWorker(IOptions<JobQueueOptions> options, ILogger<JobQueueWorker> logger) : BackgroundService
{
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
var settings = options.Value;
var workerCount = Math.Max(1, settings.WorkerCount);
var interval = settings.PollingInterval > TimeSpan.Zero ? settings.PollingInterval : TimeSpan.FromSeconds(1);

logger.LogInformation("Job queue started with {WorkerCount} worker(s), polling every {Interval}.", workerCount, interval);

var workers = Enumerable.Range(1, workerCount).Select(_ => IdleAsync(interval, stoppingToken));
await Task.WhenAll(workers);

logger.LogInformation("Job queue stopped.");
}

private static async Task IdleAsync(TimeSpan interval, CancellationToken stoppingToken)
{
try
{
while (!stoppingToken.IsCancellationRequested)
{
await Task.Delay(interval, stoppingToken);
}
}
catch (OperationCanceledException)
{
// Shutdown requested.
}
}
}
5 changes: 5 additions & 0 deletions src/FluentCMS.Host/Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
using FluentCMS.Host;

return args is ["mcp", ..]
? await FluentCmsHost.RunMcpAsync(args[1..])
: await FluentCmsHost.RunWebAsync(args);
Loading
Loading