feat: initialize Geekeey Arcade .NET SDK repository
All checks were successful
CI / build (push) Successful in 3m26s

Single-commit squash of the repository history containing the Arcade
.NET MSBuild SDK packages (base, Razor, Web, Test), their analyzer and
editorconfig configuration, test suites, and CI workflows.
This commit is contained in:
Louis Seubert 2026-10-23 23:23:40 +02:00
commit 5cbf26e8a1
Signed by: louis9902
GPG key ID: 4B9DB28F826553BD
43 changed files with 1905 additions and 0 deletions

View file

@ -0,0 +1,23 @@
// Copyright (c) The Geekeey Authors
// SPDX-License-Identifier: EUPL-1.2
namespace Geekeey.Arcade.NET.Tests;
public static class DirectoryExtensions
{
extension(Directory)
{
public static void Copy(string sourceDirectory, string destinationDirectory)
{
foreach (var directory in Directory.EnumerateDirectories(sourceDirectory, "*", SearchOption.AllDirectories))
{
Directory.CreateDirectory(Path.Combine(destinationDirectory, Path.GetRelativePath(sourceDirectory, directory)));
}
foreach (var file in Directory.EnumerateFiles(sourceDirectory, "*", SearchOption.AllDirectories))
{
File.Copy(file, Path.Combine(destinationDirectory, Path.GetRelativePath(sourceDirectory, file)));
}
}
}
}

View file

@ -0,0 +1,15 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
<OutputType>Exe</OutputType>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="TUnit" />
<Compile Remove="fixtures/**/*.cs" />
<None Remove="fixtures/**/*" />
<Content Include="fixtures/**/*" CopyToOutputDirectory="PreserveNewest" />
</ItemGroup>
</Project>

View file

@ -0,0 +1,163 @@
// Copyright (c) The Geekeey Authors
// SPDX-License-Identifier: EUPL-1.2
using System.Diagnostics;
using System.Text.RegularExpressions;
namespace Geekeey.Arcade.NET.Tests;
public sealed class SdkPackageIntegrationTests
{
private static string RepositoryPath
{
get
{
for (var directory = new DirectoryInfo(AppContext.BaseDirectory); directory is not null; directory = directory.Parent)
{
if (File.Exists(Path.Combine(directory.FullName, "global.json")))
{
return directory.FullName;
}
}
throw new InvalidOperationException("Could not locate the repository root containing global.json.");
}
}
private static string NuGetFeedPath { get; } = Path.Combine(RepositoryPath, "artifacts", "package", "release");
[Test]
[Arguments("Geekeey.Arcade.NET.Sdk", "base")]
[Arguments("Geekeey.Arcade.NET.Sdk.Razor", "razor")]
[Arguments("Geekeey.Arcade.NET.Sdk.Web", "web")]
public async Task SdkPackageCanBeConsumedFromAFeed(string packageId, string name)
{
// Arrange
var version = FindPackageVersion(packageId);
using var sut = await SdkTestProject.CreateAsync(name, version);
// Act
await sut.RunDotnetAsync($"restore --configfile \"{sut.NuGetConfigPath}\" --packages \"{sut.PackagesPath}\" --nologo");
await sut.RunDotnetAsync("build --no-restore --nologo");
await sut.RunDotnetAsync($"pack --no-restore --configuration Debug --output \"{sut.PackedPath}\" --nologo");
// Assert
await Assert.That(File.Exists(Path.Combine(sut.ProjectPath, "bin", "Debug", "net10.0", $"{sut.ProjectName}.dll"))).IsTrue();
await Assert.That(Directory.Exists(sut.PackedPath)).IsTrue();
await Assert.That(Directory.EnumerateFiles(sut.PackedPath, $"{sut.ProjectName}.*.nupkg").Any()).IsTrue();
}
[Test]
public async Task TestSdkPackageStoresTestOutputInArtifacts()
{
// Arrange
var version = FindPackageVersion("Geekeey.Arcade.NET.Sdk.Test");
using var sut = await SdkTestProject.CreateAsync("test", version);
await sut.RunDotnetAsync($"restore --configfile \"{sut.NuGetConfigPath}\" --packages \"{sut.PackagesPath}\" --nologo");
await sut.RunDotnetAsync("build --no-restore --nologo");
var outputDirectory = Path.Combine(sut.ProjectPath, "artifacts", "bin", sut.ProjectName, "debug");
await Assert.That(File.Exists(Path.Combine(outputDirectory, $"{sut.ProjectName}.dll"))).IsTrue();
// Act
var stdout = await sut.RunDotnetAsync("run --no-build --no-restore");
// Assert
await Assert.That(stdout.Contains("succeeded: 1", StringComparison.Ordinal)).IsTrue();
}
private static string FindPackageVersion(string packageId)
{
var pattern = $"""^{Regex.Escape(packageId)}\.(?<version>\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?)\.nupkg$""";
foreach (var package in Directory.EnumerateFiles(NuGetFeedPath, "*.nupkg"))
{
var match = Regex.Match(Path.GetFileName(package), pattern, RegexOptions.CultureInvariant);
if (match.Success)
{
return match.Groups["version"].Value;
}
}
throw new InvalidOperationException($"No current package found for {packageId}. Run dotnet pack first.");
}
private sealed class SdkTestProject : IDisposable
{
private SdkTestProject(string root)
{
RootPath = root;
ProjectPath = Path.Combine(RootPath, "Project");
PackagesPath = Path.Combine(RootPath, "packages");
PackedPath = Path.Combine(RootPath, "packed");
NuGetConfigPath = Path.Combine(RootPath, "NuGet.config");
}
public static async Task<SdkTestProject> CreateAsync(string name, string version)
{
var sut = new SdkTestProject(Path.Combine(Path.GetTempPath(), $"arcade-sdk-test-{Guid.NewGuid():N}"));
try
{
Directory.CreateDirectory(sut.ProjectPath);
Directory.Copy(Path.Combine(AppContext.BaseDirectory, "fixtures", name), sut.ProjectPath);
sut.ProjectFilePath = Directory.EnumerateFiles(sut.ProjectPath, "*.csproj").Single();
sut.ProjectName = Path.GetFileNameWithoutExtension(sut.ProjectFilePath);
var project = await File.ReadAllTextAsync(sut.ProjectFilePath);
await File.WriteAllTextAsync(sut.ProjectFilePath, project.Replace("{0}", version, StringComparison.Ordinal));
var contents = $"""
<configuration>
<packageSources>
<clear />
<add key="local" value="{Path.Combine(RepositoryPath, "artifacts", "package", "release")}" />
<add key="nuget.org" value="https://api.nuget.org/v3/index.json" />
</packageSources>
</configuration>
""";
await File.WriteAllTextAsync(sut.NuGetConfigPath, contents);
return sut;
}
catch
{
sut.Dispose();
throw;
}
}
public string RootPath { get; }
public string ProjectPath { get; }
public string ProjectFilePath { get; private set; } = "";
public string ProjectName { get; private set; } = "";
public string PackagesPath { get; }
public string PackedPath { get; }
public string NuGetConfigPath { get; }
public async Task<string> RunDotnetAsync(string arguments)
{
var startInfo = new ProcessStartInfo("dotnet", arguments)
{
WorkingDirectory = ProjectPath,
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false,
CreateNoWindow = true,
Environment = { ["NUGET_PACKAGES"] = PackagesPath, ["DOTNET_CLI_DO_NOT_USE_MSBUILD_SERVER"] = "1" }
};
var process = Process.Start(startInfo) ?? throw new InvalidOperationException("Could not start dotnet.");
var output = await process.StandardOutput.ReadToEndAsync();
var error = await process.StandardError.ReadToEndAsync();
await process.WaitForExitAsync();
return process.ExitCode is 0 ? output : throw new InvalidOperationException($"dotnet {arguments} failed with exit code {process.ExitCode}.\n{output}\n{error}");
}
public void Dispose()
{
Directory.Delete(RootPath, recursive: true);
}
}
}

View file

@ -0,0 +1,9 @@
<Project Sdk="Geekeey.Arcade.NET.Sdk/{0}">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<IsPackable>true</IsPackable>
<NoBuild>false</NoBuild>
<PackageId>BaseConsumer</PackageId>
<NoWarn>IDE0055</NoWarn>
</PropertyGroup>
</Project>

View file

@ -0,0 +1,4 @@
public static class EntryPoint
{
public static string Value { get; } = "ok";
}

View file

@ -0,0 +1,4 @@
public sealed class ComponentModel
{
public static string Value { get; } = "ok";
}

View file

@ -0,0 +1,8 @@
<Project Sdk="Geekeey.Arcade.NET.Sdk.Razor/{0}">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<PackageId>RazorConsumer</PackageId>
<StaticWebAssetsEnabled>false</StaticWebAssetsEnabled>
<NoWarn>IDE0055</NoWarn>
</PropertyGroup>
</Project>

View file

@ -0,0 +1,5 @@
<Project>
<PropertyGroup>
<ArtifactsPath>$(MSBuildProjectDirectory)/artifacts/</ArtifactsPath>
</PropertyGroup>
</Project>

View file

@ -0,0 +1,7 @@
using TUnit.Core;
public class ConsumerTests
{
[Test]
public void TestPasses() { }
}

View file

@ -0,0 +1,14 @@
<Project Sdk="Geekeey.Arcade.NET.Sdk.Test/{0}">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<IsPackable>true</IsPackable>
<NoBuild>false</NoBuild>
<PackageId>TestConsumer</PackageId>
<IsTestProject>true</IsTestProject>
<OutputType>Exe</OutputType>
<NoWarn>IDE0055</NoWarn>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="TUnit" Version="1.75.0" />
</ItemGroup>
</Project>

View file

@ -0,0 +1,3 @@
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/", () => "ok");

View file

@ -0,0 +1,10 @@
<Project Sdk="Geekeey.Arcade.NET.Sdk.Web/{0}">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<IsPackable>true</IsPackable>
<NoBuild>false</NoBuild>
<PackageId>WebConsumer</PackageId>
<OutputType>Exe</OutputType>
<NoWarn>IDE0055</NoWarn>
</PropertyGroup>
</Project>

25
src/sdk/README.md Normal file
View file

@ -0,0 +1,25 @@
# Geekeey.Arcade.NET.Sdk
Opinionated MSBuild SDKs for .NET projects with standardized build, analyzer, and editor configuration defaults:
- `Geekeey.Arcade.NET.Sdk` — base SDK
- `Geekeey.Arcade.NET.Sdk.Razor` — Razor class library and Blazor projects
- `Geekeey.Arcade.NET.Sdk.Web` — ASP.NET Core web projects
- `Geekeey.Arcade.NET.Sdk.Test` — test projects
## Packaging metadata
Set these properties in your project file or a directory-level props file. Unless stated otherwise, defaults apply only when the property is empty.
| Property | Default | Description |
| --- | --- | --- |
| `Authors` | `The Geekeey Team` | Default authors when no value is set. |
| `Copyright` | `Copyright (c) The Geekeey Team` | Default copyright when no value is set. |
| `PublishRepositoryUrl` | `true` | Publishes the repository URL in packages. |
| `IncludeSymbols` | `true` | Packs a symbol package. |
| `SymbolPackageFormat` | `snupkg` | Format of the symbol package. |
| `PackageReadmeFile` | `PACKAGE.md` when found | Packs a `PACKAGE.md` file from the project directory as the package readme. Not packed: `README.md` is for repository visitors. |
| `SearchReadmeFileAbove` | `false` | When `true`, searches parent directories for a `PACKAGE.md` to pack. |
| `PackageLicenseFile` | Detected license file | Packs and declares a `LICENSE.txt`, `LICENSE.md` or `LICENSE` file found in the project directory or a parent directory. Skipped when the project sets `PackageLicenseExpression` or `PackageLicenseFile`. |
Within this repository, each SDK project lives alongside its package contents in `Sdk/<PackageId>/`, so its `PACKAGE.md` is discovered automatically.

View file

@ -0,0 +1,19 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<NoBuild>true</NoBuild>
<IncludeBuildOutput>false</IncludeBuildOutput>
<SuppressDependenciesWhenPacking>true</SuppressDependenciesWhenPacking>
<IncludeSymbols>false</IncludeSymbols>
<Description>MSBuild SDK for Razor class library and Blazor projects with standardized build, analyzer, and editor configuration defaults.</Description>
</PropertyGroup>
<ItemGroup>
<None Include="Sdk.props" Pack="true" PackagePath="Sdk/Sdk.props" />
<None Include="Sdk.targets" Pack="true" PackagePath="Sdk/Sdk.targets" />
<None Include="Geekeey.Arcade.NET.Sdk.Razor.editorconfig" Pack="true" PackagePath="configuration/Geekeey.Arcade.NET.Sdk.Razor.editorconfig" />
<None Include="../../common/Common.props" Pack="true" PackagePath="common/Common.props" />
<None Include="../../common/Common.targets" Pack="true" PackagePath="common/Common.targets" />
<None Include="../../configuration/**/*" Pack="true" PackagePath="configuration/%(RecursiveDir)%(Filename)%(Extension)" />
</ItemGroup>
</Project>

View file

@ -0,0 +1,3 @@
# global_level must be higher than the base editorconfig files
is_global = true
global_level = 1

View file

@ -0,0 +1,29 @@
# Geekeey.Arcade.NET.Sdk.Razor
Opinionated MSBuild SDK for Razor class library and Blazor projects with standardized build, analyzer, and editor configuration defaults.
## Getting started
```xml
<Project Sdk="Geekeey.Arcade.NET.Sdk.Razor/1.0.0">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
</PropertyGroup>
</Project>
```
## What it provides
- Everything from the base `Geekeey.Arcade.NET.Sdk` (language, analyzer, editor, and packaging defaults), layered on top of the `Microsoft.NET.Sdk.Razor` SDK.
- Opinionated `.editorconfig` settings for Razor projects, applied automatically via `EditorConfigFiles` (including file-based programs when `FileBasedProgram=true`).
## Opt-outs
| Property | Effect |
| --- | --- |
| `EnableDefaultJsonSerializationOptions=false` | Skips the `System.Text.Json` nullable annotation defaults. |
| `IncludeDefaultBannedSymbols=false` | Skips the built-in `BannedSymbols.txt`. |
| `BannedNewtonsoftJsonSymbols=false` | Skips the built-in `BannedSymbols.NewtonsoftJson.txt`. |
| `SearchReadmeFileAbove=true` | Also searches parent directories for a `PACKAGE.md` to pack. |
See the [repository](https://code.geekeey.de/geekeey/arcade) for the full configuration.

View file

@ -0,0 +1,16 @@
<Project>
<PropertyGroup>
<_MustImportMicrosoftNETSdk Condition="'$(UsingMicrosoftNETSdk)' != 'true'">true</_MustImportMicrosoftNETSdk>
<CustomBeforeDirectoryBuildProps>$(CustomBeforeDirectoryBuildProps);$(MSBuildThisFileDirectory)../common/Common.props</CustomBeforeDirectoryBuildProps>
<BeforeMicrosoftNETSdkTargets>$(BeforeMicrosoftNETSdkTargets);$(MSBuildThisFileDirectory)/../common/Common.targets</BeforeMicrosoftNETSdkTargets>
</PropertyGroup>
<Import Project="Sdk.props" Sdk="Microsoft.NET.Sdk.Razor" Condition="'$(_MustImportMicrosoftNETSdk)' == 'true'" />
<Import Project="$(MSBuildThisFileDirectory)../common/Common.props" Condition="'$(_MustImportMicrosoftNETSdk)' != 'true'" />
<ItemGroup>
<EditorConfigFiles Include="$(MSBuildThisFileDirectory)../configuration/Geekeey.Arcade.NET.Sdk.Razor.editorconfig" />
<EditorConfigFiles Include="$(MSBuildThisFileDirectory)../configuration/Geekeey.Arcade.NET.FileBasedProgram.editorconfig" Condition="'$(FileBasedProgram)' == 'true'" />
</ItemGroup>
</Project>

View file

@ -0,0 +1,3 @@
<Project>
<Import Project="Sdk.targets" Sdk="Microsoft.NET.Sdk.Razor" Condition="'$(_MustImportMicrosoftNETSdk)' == 'true'" />
</Project>

View file

@ -0,0 +1,23 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<NoBuild>true</NoBuild>
<IncludeBuildOutput>false</IncludeBuildOutput>
<SuppressDependenciesWhenPacking>true</SuppressDependenciesWhenPacking>
<IncludeSymbols>false</IncludeSymbols>
<IsPackable>true</IsPackable>
<!-- The packaging project is not a testing platform application itself: the Microsoft.Testing.Platform
package flows to consumers and defaults 'IsTestingPlatformApplication' to true there. -->
<IsTestingPlatformApplication>false</IsTestingPlatformApplication>
<Description>MSBuild SDK for test projects with standardized build, analyzer, and editor configuration defaults.</Description>
</PropertyGroup>
<ItemGroup>
<None Include="Sdk.props" Pack="true" PackagePath="Sdk/Sdk.props" />
<None Include="Sdk.targets" Pack="true" PackagePath="Sdk/Sdk.targets" />
<None Include="Geekeey.Arcade.NET.Sdk.Test.editorconfig" Pack="true" PackagePath="configuration/Geekeey.Arcade.NET.Sdk.Test.editorconfig" />
<None Include="../../common/Common.props" Pack="true" PackagePath="common/Common.props" />
<None Include="../../common/Common.targets" Pack="true" PackagePath="common/Common.targets" />
<None Include="../../configuration/**/*" Pack="true" PackagePath="configuration/%(RecursiveDir)%(Filename)%(Extension)" />
</ItemGroup>
</Project>

View file

@ -0,0 +1,20 @@
# global_level must be higher than the base editorconfig files
is_global = true
global_level = 1
# Identifiers should not contain underscores (CA1707)
# https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/quality-rules/ca1707
dotnet_diagnostic.CA1707.severity = none
# Mark members as static (CA1822)
# https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/quality-rules/ca1822
dotnet_diagnostic.CA1822.severity = none
# Make method synchronous (IDE0390 and IDE0391)
# https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/style-rules/ide0390-ide0391
dotnet_diagnostic.IDE0390.severity = none
dotnet_diagnostic.IDE0391.severity = none
# Remove unused parameter (IDE0060)
# https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/style-rules/ide0060
dotnet_diagnostic.IDE0060.severity = none

Some files were not shown because too many files have changed in this diff Show more