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
2 changes: 2 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ jobs:

steps:
- uses: actions/checkout@v4
with:
lfs: true

- name: Setup .NET
uses: actions/setup-dotnet@v4
Expand Down
113 changes: 100 additions & 13 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,37 @@ on:
permissions:
contents: write
packages: write
id-token: write

env:
DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true
DOTNET_NOLOGO: true

jobs:
release:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
lfs: true

- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: '10.0.x'

- name: Test
run: dotnet test FlexRender.slnx -c Release

publish-nuget:
needs: test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
lfs: true

- name: Setup .NET
uses: actions/setup-dotnet@v4
Expand All @@ -30,14 +48,8 @@ jobs:
id: version
run: echo "VERSION=${GITHUB_REF_NAME#v}" >> $GITHUB_OUTPUT

- name: Restore
run: dotnet restore FlexRender.slnx

- name: Build
run: dotnet build FlexRender.slnx --no-restore --configuration Release

- name: Test
run: dotnet test FlexRender.slnx --no-build --configuration Release
run: dotnet build FlexRender.slnx -c Release -p:Version=${{ steps.version.outputs.VERSION }}

- name: Pack NuGet packages
run: |
Expand All @@ -56,22 +68,97 @@ jobs:
--output ./artifacts
done

- name: Login to NuGet with trusted publishing
id: nuget-login
uses: nuget/login@v1
with:
user: ${{ secrets.NUGET_USER }}

- name: Publish to NuGet.org
env:
NUGET_API_KEY: ${{ secrets.NUGET_API_KEY }}
run: |
dotnet nuget push ./artifacts/*.nupkg \
--api-key "$NUGET_API_KEY" \
--api-key "${{ steps.nuget-login.outputs.NUGET_API_KEY }}" \
--source https://api.nuget.org/v3/index.json \
--skip-duplicate

dotnet nuget push ./artifacts/*.snupkg \
--api-key "$NUGET_API_KEY" \
--api-key "${{ steps.nuget-login.outputs.NUGET_API_KEY }}" \
--source https://api.nuget.org/v3/index.json \
--skip-duplicate

- name: Upload NuGet packages
uses: actions/upload-artifact@v4
with:
name: nuget-packages
path: ./artifacts/*.*nupkg

build-native:
needs: test
strategy:
matrix:
include:
- os: macos-latest
rid: osx-arm64
archive: tar.gz
- os: ubuntu-latest
rid: linux-x64
archive: tar.gz
- os: windows-latest
rid: win-x64
archive: zip
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
lfs: true

- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: '10.0.x'

- name: Extract version from tag
id: version
shell: bash
run: echo "VERSION=${GITHUB_REF_NAME#v}" >> $GITHUB_OUTPUT

- name: Publish native AOT binary
run: >
dotnet publish src/FlexRender.Cli/FlexRender.Cli.csproj
-c Release
-r ${{ matrix.rid }}
/p:PublishAot=true
-o ./publish

- name: Archive (Unix)
if: matrix.archive == 'tar.gz'
working-directory: ./publish
run: tar -czf ../flexrender-${{ steps.version.outputs.VERSION }}-${{ matrix.rid }}.tar.gz .

- name: Archive (Windows)
if: matrix.archive == 'zip'
shell: pwsh
run: Compress-Archive -Path ./publish/* -DestinationPath flexrender-${{ steps.version.outputs.VERSION }}-${{ matrix.rid }}.zip

- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: native-${{ matrix.rid }}
path: flexrender-*.*

release:
needs: [publish-nuget, build-native]
runs-on: ubuntu-latest
steps:
- name: Download all artifacts
uses: actions/download-artifact@v4
with:
path: ./release-assets
merge-multiple: true

- name: Create GitHub Release
uses: softprops/action-gh-release@v2
with:
generate_release_notes: true
files: ./artifacts/*.nupkg
files: ./release-assets/*
52 changes: 47 additions & 5 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -183,11 +183,6 @@ All new features and non-trivial changes must be developed in separate branches.
- **Types**: `feat`, `fix`, `refactor`, `build`, `test`, `docs`, `chore`
- **Do NOT merge into `main`** -- leave the feature branch as-is after completing work. Merging is done manually by the maintainer or via GitHub PR
- **Do NOT use git worktrees** -- work directly in the repository checkout. Worktrees add unnecessary complexity and cause issues with stash conflicts and asset path resolution
- **Before completing a branch** -- squash all commits into a single commit with a comprehensive message. This keeps the main branch history clean:
```bash
git reset --soft origin/main # Squash: soft reset to main
git commit -m "type(scope): description" # Single commit with full message
```

### Commits

Expand All @@ -199,6 +194,53 @@ Conventional Commits: `type(scope): description`

## CLI Tool

### Install as dotnet tool

```bash
dotnet tool install -g FlexRender.Cli
```

After installation the `flexrender` command is available globally:

```bash
flexrender render template.yaml -d data.json -o output.png
flexrender validate template.yaml
flexrender info template.yaml
flexrender watch template.yaml -d data.json -o preview.png
flexrender debug-layout template.yaml -d data.json
```

### Native AOT binary

The CLI is fully AOT-compatible. To build a standalone native binary (no .NET runtime required):

```bash
dotnet publish src/FlexRender.Cli -c Release -r <RID> /p:PublishAot=true
```

Common RIDs: `osx-arm64`, `osx-x64`, `linux-x64`, `linux-arm64`, `win-x64`.

### Distribution channels

| Channel | Install | Requires .NET? | Use case |
|---------|---------|---------------|----------|
| dotnet tool | `dotnet tool install -g FlexRender.Cli` | Yes | .NET developers |
| GitHub Release | Download binary from Releases | No | Everyone else (CI, Docker, non-.NET) |
| Build from source | `dotnet publish /p:PublishAot=true -r <RID>` | Build-time only | Custom builds |

Native AOT binaries are attached to every GitHub Release for: osx-arm64, linux-x64, win-x64.

### NuGet Trusted Publishing (OIDC)

The release workflow uses NuGet trusted publishing via OIDC instead of long-lived API keys. Setup:

1. Go to nuget.org -> username -> Trusted Publishing
2. Add policy: owner/repo = `<owner>/SkiaLayout`, workflow = `release.yml`, tag pattern = `v*`
3. Set `NUGET_USER` secret in GitHub repo settings (nuget.org username, not email)
4. Remove old `NUGET_API_KEY` secret after verifying trusted publishing works

### Run from source (development)

```bash
dotnet run --project src/FlexRender.Cli -- render template.yaml -d data.json -o output.png
dotnet run --project src/FlexRender.Cli -- validate template.yaml
Expand Down
2 changes: 1 addition & 1 deletion Directory.Build.props
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<Project>
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<TargetFrameworks>net8.0;net10.0</TargetFrameworks>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<LangVersion>latest</LangVersion>
Expand Down
12 changes: 9 additions & 3 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,16 @@
<PackageVersion Include="YamlDotNet" Version="16.3.0" />
<PackageVersion Include="SkiaSharp" Version="3.119.1" />
<PackageVersion Include="SkiaSharp.NativeAssets.Linux" Version="3.119.1" />
<PackageVersion Include="SkiaSharp.NativeAssets.macOS" Version="3.119.1" />
<PackageVersion Include="SkiaSharp.NativeAssets.Win32" Version="3.119.1" />
<PackageVersion Include="QRCoder" Version="1.7.0" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="10.0.2" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="10.0.2" />
<PackageVersion Include="Microsoft.Extensions.Options" Version="10.0.2" />
<!-- Microsoft.Extensions: version 10.x for net10.0, version 8.x for net8.0 -->
<PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="10.0.2" Condition="'$(TargetFramework)' == 'net10.0'" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="10.0.2" Condition="'$(TargetFramework)' == 'net10.0'" />
<PackageVersion Include="Microsoft.Extensions.Options" Version="10.0.2" Condition="'$(TargetFramework)' == 'net10.0'" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="8.0.2" Condition="'$(TargetFramework)' == 'net8.0'" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="8.0.1" Condition="'$(TargetFramework)' == 'net8.0'" />
<PackageVersion Include="Microsoft.Extensions.Options" Version="8.0.2" Condition="'$(TargetFramework)' == 'net8.0'" />
<PackageVersion Include="System.CommandLine" Version="2.0.2" />
<!-- Build dependencies -->
<PackageVersion Include="Microsoft.SourceLink.GitHub" Version="10.0.102" />
Expand Down
50 changes: 43 additions & 7 deletions llms-full.txt
Original file line number Diff line number Diff line change
Expand Up @@ -77,8 +77,8 @@ FlexRender.DependencyInjection (Microsoft.Extensions.DI)
|---------|-----------|---------------|
| FlexRender.Core | -- | (none) |
| FlexRender.Yaml | Core | YamlDotNet 16.3.0 |
| FlexRender.Skia | Core | SkiaSharp 2.88.9 |
| FlexRender.QrCode | Skia | QRCoder 1.6.0 |
| FlexRender.Skia | Core | SkiaSharp 3.119.1 |
| FlexRender.QrCode | Skia | QRCoder 1.7.0 |
| FlexRender.Barcode | Skia | (none) |
| FlexRender.DependencyInjection | Core, Yaml, Skia | Microsoft.Extensions.DI |
| FlexRender.MetaPackage | All | -- |
Expand Down Expand Up @@ -109,9 +109,9 @@ template: # Required: template metadata
name: "my-template" # Template name (string)
version: 1 # Template version (int)

fonts: # Optional: font definitions
- name: "main" # Font reference name
path: "fonts/Roboto.ttf" # File path, base64, embedded://, or http://
fonts: # Optional: font definitions (key = reference name, value = path)
default: "assets/fonts/Inter-Regular.ttf" # File path, base64, embedded://, or http://
bold: "assets/fonts/Inter-Bold.ttf"

canvas: # Required: canvas configuration
fixed: width # Which dimension is fixed (width|height|both|none)
Expand Down Expand Up @@ -538,7 +538,15 @@ services.AddFlexRender(builder => builder
var renderer = serviceProvider.GetRequiredService<IFlexRenderer>();
```

## CLI Commands
## CLI Tool

### Install as dotnet tool

```bash
dotnet tool install -g FlexRender.Cli
```

After installation the `flexrender` command is available globally:

```bash
flexrender render template.yaml -d data.json -o output.png
Expand All @@ -548,10 +556,38 @@ flexrender watch template.yaml -d data.json -o preview.png
flexrender debug-layout template.yaml -d data.json
```

### Native AOT binary

The CLI is fully AOT-compatible (`IsAotCompatible=true` on all projects, no reflection). Build a standalone native binary (no .NET runtime required):

```bash
dotnet publish src/FlexRender.Cli -c Release -r <RID> /p:PublishAot=true
```

Common RIDs: `osx-arm64`, `osx-x64`, `linux-x64`, `linux-arm64`, `win-x64`. The resulting binary is a single self-contained executable with no external dependencies (except native SkiaSharp libs).

### Run from source (development)

```bash
dotnet run --project src/FlexRender.Cli -- render template.yaml -d data.json -o output.png
```

Global options: `-v`/`--verbose`, `--fonts <dir>`, `--scale <float>`

The `--scale` option uses invariant culture -- always use `.` as the decimal separator (e.g., `--scale 2.0`).

Working directory matters: CLI resolves all relative paths (template, data, fonts, image src) from the current working directory.

### Distribution channels

| Channel | Install | Requires .NET? | Use case |
|---------|---------|---------------|----------|
| dotnet tool | `dotnet tool install -g FlexRender.Cli` | Yes | .NET developers |
| GitHub Release | Download binary from Releases | No | Everyone else (CI, Docker, non-.NET) |
| Build from source | `dotnet publish /p:PublishAot=true -r <RID>` | Build-time only | Custom builds |

Native AOT binaries are attached to every GitHub Release for: osx-arm64, linux-x64, win-x64.

## Coding Conventions

- **.NET 10**, C# latest, `Nullable=enable`, `TreatWarningsAsErrors=true`
Expand All @@ -575,7 +611,7 @@ Working directory matters: CLI resolves all relative paths (template, data, font
- **Naming**: `MethodUnderTest_Scenario_ExpectedResult` (e.g., `Parse_SimpleTextElement_ParsesCorrectly`)
- **Class naming**: `{ClassName}Tests`
- **Organization**: Mirrors source structure -- `Tests/Values/`, `Tests/Layout/`, `Tests/Parsing/`, etc.
- **Snapshot testing**: `SnapshotTestBase` with platform-specific golden images in `golden/{macos,linux,windows}/`. Pixel-by-pixel comparison with `colorThreshold=5`. Set `UPDATE_SNAPSHOTS=true` to regenerate
- **Snapshot testing**: `SnapshotTestBase` with golden images in `golden/`. Pixel-by-pixel comparison with `colorThreshold` and `maxDifferencePercent` (0.0 = exact match). Set `UPDATE_SNAPSHOTS=true` to regenerate
- **Pattern**: Arrange-Act-Assert

```bash
Expand Down
Loading