Skip to content
Open
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
5 changes: 4 additions & 1 deletion .gitattributes
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,7 @@
*.cs text

# Declare files that will always have CRLF line endings on checkout.
*.sln text eol=crlf
*.sln text eol=crlf

# GitHub Agentic Workflows lock files
.github/workflows/*.lock.yml linguist-generated=true merge=ours
1,515 changes: 1,515 additions & 0 deletions .github/workflows/api-sync-check.lock.yml

Large diffs are not rendered by default.

121 changes: 121 additions & 0 deletions .github/workflows/api-sync-check.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
---
emoji: 🔄
description: Weekly check to keep R3Ext APIs and features in sync with ReactiveUI and DynamicData. Identifies out-of-sync APIs and opens a pull request with the necessary updates.
on:
schedule: weekly on Monday
skip-if-match: 'is:pr is:open label:api-sync in:title "[api-sync]"'
permissions:
contents: read
issues: read
pull-requests: read
tools:
github:
mode: gh-proxy
toolsets: [default]
network:
allowed: [dotnet]
safe-outputs:
create-pull-request:
title-prefix: "[api-sync] "
labels: [api-sync, automation]
draft: true
reviewers: [copilot]
allowed-files:
- "R3Ext/**/*.cs"
- "R3.DynamicData/**/*.cs"
- "R3Ext.Bindings.SourceGenerator/**/*.cs"
- "R3Ext.Bindings.MauiTargets/**/*.cs"
- "docs/**/*.md"
- "README.md"
---

# API Sync Check — ReactiveUI & DynamicData

## Goal

Keep R3Ext aligned with the latest ReactiveUI and DynamicData APIs and features. Each week:

1. Inspect recent commits and releases in both upstream repos.
2. Compare against the current R3Ext implementation.
3. If gaps exist, produce a pull request with the necessary source updates.
4. If everything is already in sync, call `noop`.

## Context

- **R3Ext repo (this repo)** — a reactive MVVM + dynamic-data library built on R3.
- `R3Ext/` — ReactiveUI-compatible APIs: commands, interactions, bindings, MVVM helpers.
- `R3.DynamicData/` — DynamicData port: observable caches, lists, operators, change sets.
- **ReactiveUI upstream**: `https://github.com/reactiveui/reactiveui`
- **DynamicData upstream**: `https://github.com/reactivemarbles/DynamicData`

## Steps

### 1 — Gather upstream changes

Use `gh` to inspect each upstream repository.

For **ReactiveUI** (`reactiveui/reactiveui`):
- Fetch commits from the last 7 days on the default branch.
- Fetch the latest release tag and release notes.
- List recently modified public C# files to spot new or changed public APIs (classes, interfaces, public methods/properties).

For **DynamicData** (`reactivemarbles/DynamicData`):
- Fetch commits from the last 7 days on the default branch.
- Fetch the latest release tag and release notes.
- List recently modified public C# files to spot new or changed public APIs.

### 2 — Inspect the current R3Ext implementation

Read relevant files in this repo to understand the current state:
- `R3Ext/Commands/` — RxCommand implementations.
- `R3Ext/Interactions/` — RxInteraction implementations.
- `R3Ext/Bindings/` — binding helpers and source-generator hooks.
- `R3Ext/Extensions/` — observable extension methods.
- `R3.DynamicData/` — DynamicData port.

### 3 — Identify gaps

Compare the upstream public API surface against what R3Ext implements:

- **Missing APIs**: public types or members in the upstream that have no R3Ext equivalent.
- **Signature drift**: existing R3Ext methods whose signatures no longer match the upstream (parameter types, return types, overloads).
- **New features**: functionality added upstream (new operators, new command overloads, new change-set types) not yet present in R3Ext.
- **Removed/deprecated upstream APIs**: flag any that should also be deprecated in R3Ext.

Ignore internal, private, or test-only members. Ignore platform-specific code that does not apply to R3Ext's .NET / MAUI targets.

### 4 — Apply updates

For each identified gap:
- Add or update the corresponding C# source file(s) to bring R3Ext in line with the upstream API.
- Follow the existing code style: XML doc comments for public members, no reflection, AOT-safe patterns.
- Prefer additive changes (new overloads, new types) over breaking changes. If a breaking change is required, mark it with `// TODO: breaking change — <reason>` and note it in the PR body.
- Do not modify test files or benchmark files.

### 5 — Produce output

**If updates were made**: create a pull request using the `create-pull-request` safe output.

The PR body must include:

```
## Summary

Brief description of what changed and why.

## ReactiveUI changes
- List specific upstream commits or release notes that motivated each change.

## DynamicData changes
- List specific upstream commits or release notes that motivated each change.

## Breaking changes (if any)
- Describe any breaking changes or API removals.
```

**If nothing is out of sync**: call `noop` with a short message such as "R3Ext APIs are already in sync with ReactiveUI and DynamicData — no changes needed."

## Safe Outputs

- Use `create-pull-request` to open a draft PR when source changes are produced.
- Use `noop` when R3Ext is already in sync and no code changes are needed.
71 changes: 26 additions & 45 deletions .github/workflows/ci-cd.yml
Original file line number Diff line number Diff line change
Expand Up @@ -58,64 +58,45 @@ jobs:

- name: Restore dependencies (libraries and tests only)
run: |
# Avoid restoring the MAUI SampleApp on Linux runners (iOS/macCatalyst workloads not available)
dotnet restore R3Ext/R3Ext.csproj
dotnet restore R3Ext.Bindings.SourceGenerator/R3Ext.Bindings.SourceGenerator.csproj
dotnet restore R3Ext.Bindings.MauiTargets/R3Ext.Bindings.MauiTargets.csproj
dotnet restore R3.DynamicData/R3.DynamicData.csproj
# Auto-discover packable library projects; excludes tests, samples, and benchmarks
# (those set <IsPackable>false</IsPackable> or omit the property on executable/test targets).
# New shippable libraries are picked up automatically once they set <IsPackable>true</IsPackable>.
PACKABLE=$(find . -name "*.csproj" -not -path "*/obj/*" | xargs grep -l '<IsPackable>true</IsPackable>' | sort)
for proj in $PACKABLE; do
dotnet restore "$proj"
done
# Test projects are not packable but must be restored to run tests
dotnet restore R3Ext.Tests/R3Ext.Tests.csproj
dotnet restore R3.DynamicData.Tests/R3.DynamicData.Tests.csproj
dotnet restore R3Ext.DynamicData.Tests/R3Ext.DynamicData.Tests.csproj

- name: Build Libraries (exclude samples)
run: |
export DOTNET_USE_SHARED_COMPILATION=false
# Build source generator first (dependency for main library)
dotnet build R3Ext.Bindings.SourceGenerator/R3Ext.Bindings.SourceGenerator.csproj --configuration Release --no-restore \
/p:Version=${{ steps.gitversion.outputs.nuGetVersionV2 }} \
/p:AssemblyVersion=${{ steps.gitversion.outputs.assemblySemVer }} \
/p:FileVersion=${{ steps.gitversion.outputs.assemblySemFileVer }} \
/p:InformationalVersion=${{ steps.gitversion.outputs.informationalVersion }}

# Build main library (uses source generator)
dotnet build R3Ext/R3Ext.csproj --configuration Release --no-restore \
/p:Version=${{ steps.gitversion.outputs.nuGetVersionV2 }} \
/p:AssemblyVersion=${{ steps.gitversion.outputs.assemblySemVer }} \
/p:FileVersion=${{ steps.gitversion.outputs.assemblySemFileVer }} \
/p:InformationalVersion=${{ steps.gitversion.outputs.informationalVersion }}

# Build MAUI targets
dotnet build R3Ext.Bindings.MauiTargets/R3Ext.Bindings.MauiTargets.csproj --configuration Release --no-restore \
/p:Version=${{ steps.gitversion.outputs.nuGetVersionV2 }} \
/p:AssemblyVersion=${{ steps.gitversion.outputs.assemblySemVer }} \
/p:FileVersion=${{ steps.gitversion.outputs.assemblySemFileVer }} \
/p:InformationalVersion=${{ steps.gitversion.outputs.informationalVersion }}

# Build DynamicData library
dotnet build R3.DynamicData/R3.DynamicData.csproj --configuration Release --no-restore \
/p:Version=${{ steps.gitversion.outputs.nuGetVersionV2 }} \
/p:AssemblyVersion=${{ steps.gitversion.outputs.assemblySemVer }} \
/p:FileVersion=${{ steps.gitversion.outputs.assemblySemFileVer }} \
/p:InformationalVersion=${{ steps.gitversion.outputs.informationalVersion }}
# Alphabetical sort places R3Ext.Bindings.SourceGenerator before R3Ext (dot < slash),
# so the source generator is always built before the main library that references it.
# dotnet build also resolves project references automatically, so ordering is belt-and-suspenders.
PACKABLE=$(find . -name "*.csproj" -not -path "*/obj/*" | xargs grep -l '<IsPackable>true</IsPackable>' | sort)
for proj in $PACKABLE; do
dotnet build "$proj" --configuration Release --no-restore \
/p:Version=${{ steps.gitversion.outputs.nuGetVersionV2 }} \
/p:AssemblyVersion=${{ steps.gitversion.outputs.assemblySemVer }} \
/p:FileVersion=${{ steps.gitversion.outputs.assemblySemFileVer }} \
/p:InformationalVersion=${{ steps.gitversion.outputs.informationalVersion }}
done

- name: Test
run: |
export DOTNET_USE_SHARED_COMPILATION=false
dotnet test R3Ext.Tests/R3Ext.Tests.csproj --configuration Release --verbosity normal
dotnet test R3.DynamicData.Tests/R3.DynamicData.Tests.csproj --configuration Release --verbosity normal
dotnet test R3Ext.DynamicData.Tests/R3Ext.DynamicData.Tests.csproj --configuration Release --verbosity normal

- name: Pack NuGet packages
run: |
dotnet pack R3Ext/R3Ext.csproj --configuration Release --no-build --output ./artifacts \
/p:PackageVersion=${{ steps.gitversion.outputs.nuGetVersionV2 }}

dotnet pack R3Ext.Bindings.SourceGenerator/R3Ext.Bindings.SourceGenerator.csproj --configuration Release --no-build --output ./artifacts \
/p:PackageVersion=${{ steps.gitversion.outputs.nuGetVersionV2 }}

dotnet pack R3Ext.Bindings.MauiTargets/R3Ext.Bindings.MauiTargets.csproj --configuration Release --no-build --output ./artifacts \
/p:PackageVersion=${{ steps.gitversion.outputs.nuGetVersionV2 }}

dotnet pack R3.DynamicData/R3.DynamicData.csproj --configuration Release --no-build --output ./artifacts \
/p:PackageVersion=${{ steps.gitversion.outputs.nuGetVersionV2 }}
PACKABLE=$(find . -name "*.csproj" -not -path "*/obj/*" | xargs grep -l '<IsPackable>true</IsPackable>' | sort)
for proj in $PACKABLE; do
dotnet pack "$proj" --configuration Release --no-build --output ./artifacts \
/p:PackageVersion=${{ steps.gitversion.outputs.nuGetVersionV2 }}
done

- name: Upload artifacts
uses: actions/upload-artifact@v4
Expand Down
Loading
Loading