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

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 1a81ad7ce3
Signed by: louis9902
GPG key ID: 4B9DB28F826553BD
52 changed files with 1888 additions and 0 deletions

20
.editorconfig Normal file
View file

@ -0,0 +1,20 @@
root = true
[*]
max_line_length = 120
indent_style = tab
indent_size = 2
tab_width = 2
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
file_header_template = Copyright (c) The Geekeey Authors\nSPDX-License-Identifier: EUPL-1.2
[*.md]
indent_style = space
trim_trailing_whitespace = false
[*.yml]
indent_style = space
trim_trailing_whitespace = false

79
.forgejo/workflows/ci.yml Normal file
View file

@ -0,0 +1,79 @@
name: CI
on:
push:
branches: [main]
tags: ['*']
pull_request:
branches: [main]
enable-openid-connect: true
jobs:
build:
name: build
runs-on: debian-latest
container: mcr.microsoft.com/dotnet/sdk:10.0@sha256:0d99ebf1ccad15037b5c98c9fe8979d6b8dbac23812feae2277a0b277b5c5ffa
steps:
- name: checkout
uses: https://code.geekeey.de/actions/checkout@54953ce9d6c7d0a9cbd89d185c5b2c7515358c5f # 1
- name: dotnet pack
shell: bash
env:
TAG: ${{ forgejo.ref }}
TAG_NAME: ${{ forgejo.ref_name }}
run: |
set -euo pipefail
if [[ "$TAG" == refs/tags/* ]]; then
version="${TAG_NAME#v}"
if ! printf '%s' "$version" | grep -Eq '^[0-9]+\.[0-9]+\.[0-9]+$'; then
echo "::error::Tag '$TAG_NAME' is not a valid release version (expected MAJOR.MINOR.PATCH)"
exit 1
fi
export version
fi
dotnet pack arcade.slnx
- name: upload packages
uses: https://code.geekeey.de/actions/artifacts/push@ae8d20e174a8706675e6d54ebd3719a2802ea9dd # 1
with:
name: nuget-packages
path: artifacts/package/release
pattern: |
*.nupkg
*.snupkg
- name: dotnet test
run: dotnet test src/sdk.tests/Geekeey.Arcade.NET.Tests.csproj
- name: publish to nuget
if: startsWith(forgejo.ref, 'refs/tags/')
shell: bash
env:
AUDIENCE: "u:1:00000000-0000-0000-0000-000000000000"
run: |
set -euo pipefail
if [[ -z "${ACTIONS_ID_TOKEN_REQUEST_URL:-}" || -z "${ACTIONS_ID_TOKEN_REQUEST_TOKEN:-}" ]]; then
echo "::error::OpenID Connect is not enabled. Set enable-openid-connect: true on the workflow or job."
exit 1
fi
response=$(curl --fail --silent --show-error \
--header "Authorization: bearer ${ACTIONS_ID_TOKEN_REQUEST_TOKEN}" "${ACTIONS_ID_TOKEN_REQUEST_URL}&audience=${AUDIENCE}")
token=$(printf '%s' "${response}" | grep --only-matching '"value":"[^"]*"' | cut --delimiter '"' --fields 4)
if [[ -z "${token}" ]]; then
echo "::error::Failed to obtain an OpenID Connect token from Forgejo."
exit 1
fi
echo "::add-mask::${token}"
feed="${{ forgejo.server_url }}/api/packages/${{ forgejo.repository_owner }}/nuget/index.json"
dotnet nuget add source --name forgejo --username forgejo-publish --password "$token" --store-password-in-clear-text "$feed"
dotnet nuget push "artifacts/package/release/*.nupkg" "artifacts/package/release/*.snupkg" --source forgejo --skip-duplicate

4
.gitignore vendored Normal file
View file

@ -0,0 +1,4 @@
artifacts/
TestResults/
node_modules/
*.DotSettings.user

7
Directory.Build.props Normal file
View file

@ -0,0 +1,7 @@
<Project>
<Import Project="$(MSBuildThisFileDirectory)src/sdk/common/Common.props" />
<PropertyGroup>
<ArtifactsPath>$(MSBuildThisFileDirectory)artifacts</ArtifactsPath>
</PropertyGroup>
</Project>

3
Directory.Build.targets Normal file
View file

@ -0,0 +1,3 @@
<Project>
<Import Project="$(MSBuildThisFileDirectory)src/sdk/common/Common.targets" />
</Project>

8
Directory.Packages.props Normal file
View file

@ -0,0 +1,8 @@
<Project>
<PropertyGroup>
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
</PropertyGroup>
<ItemGroup>
<PackageVersion Include="TUnit" Version="1.75.0" />
</ItemGroup>
</Project>

1
README.md Normal file
View file

@ -0,0 +1 @@
# arcade

7
arcade.slnx Normal file
View file

@ -0,0 +1,7 @@
<Solution>
<Project Path="src/sdk/Sdk/Geekeey.Arcade.NET.Sdk/Geekeey.Arcade.NET.Sdk.csproj" />
<Project Path="src/sdk/Sdk/Geekeey.Arcade.NET.Sdk.Razor/Geekeey.Arcade.NET.Sdk.Razor.csproj" />
<Project Path="src/sdk/Sdk/Geekeey.Arcade.NET.Sdk.Test/Geekeey.Arcade.NET.Sdk.Test.csproj" />
<Project Path="src/sdk/Sdk/Geekeey.Arcade.NET.Sdk.Web/Geekeey.Arcade.NET.Sdk.Web.csproj" />
<Project Path="src/sdk.tests/Geekeey.Arcade.NET.Tests.csproj" />
</Solution>

11
global.json Normal file
View file

@ -0,0 +1,11 @@
{
"$schema": "https://www.schemastore.org/global.json",
"sdk": {
"version": "10.0.0",
"rollForward": "latestMinor"
},
"msbuild-sdks": {},
"test": {
"runner": "Microsoft.Testing.Platform"
}
}

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");

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