diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a4ed190..7cfe26a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -20,22 +20,22 @@ jobs: uses: actions/cache@v4 with: path: ~/.nuget/packages - key: nuget-packages-${{ runner.os }}-$(sha1sum version.yml | cut -d' ' -f1) + key: nuget-packages-${{ runner.os }}-${{ hashFiles('**/*.csproj') }} restore-keys: | nuget-packages-${{ runner.os }}- - name: Setup .NET SDK uses: actions/setup-dotnet@v4 with: - dotnet-version: '8.0.x' + dotnet-version: '10.0.x' - name: Determine package version id: pkgver run: | - VERSION_LINE=$(grep '^ version:' version.yml || true) - VERSION=$(echo $VERSION_LINE | awk '{print $2}') + # Extract version from Directory.Build.props + VERSION=$(grep '' src/Directory.Build.props | sed 's/.*\(.*\)<\/Version>.*/\1/' || echo "0.0.0") if [ -z "$VERSION" ]; then - echo "version not found in version.yml, defaulting to 0.0.0" + echo "version not found in Directory.Build.props, defaulting to 0.0.0" VERSION=0.0.0 fi # Append run number so CI packages are unique @@ -52,7 +52,24 @@ jobs: run: dotnet build src/EventSourcing.sln --configuration Release --no-restore - name: Run tests - run: dotnet test src/EventSourcing.sln --configuration Release --no-build --verbosity normal || true + run: dotnet test src/EventSourcing.sln --configuration Release --no-build --verbosity normal --logger "trx;LogFileName=test-results.trx" --collect:"XPlat Code Coverage" --results-directory ./TestResults || true + + - name: Upload test results + if: always() + uses: actions/upload-artifact@v4 + with: + name: ci-test-results + path: '**/TestResults/*.trx' + if-no-files-found: warn + + - name: Test Report + uses: dorny/test-reporter@v1 + if: always() + with: + name: CI Test Results + path: '**/TestResults/*.trx' + reporter: dotnet-trx + fail-on-error: false - name: Pack NuGet packages run: | diff --git a/.github/workflows/pr-validation.yml b/.github/workflows/pr-validation.yml index 101c0c0..c611bcd 100644 --- a/.github/workflows/pr-validation.yml +++ b/.github/workflows/pr-validation.yml @@ -11,33 +11,9 @@ jobs: steps: - name: Checkout uses: actions/checkout@v4 - with: - fetch-depth: 0 - - - name: Require version.yml change if src changed - if: ${{ github.event_name == 'pull_request' }} - run: | - set -euo pipefail - echo "Checking that version.yml was modified in this PR if any src changes occurred..." - # Fetch base branch to compare - git fetch origin ${{ github.event.pull_request.base.ref }} --depth=1 - changed_files=$(git diff --name-only origin/${{ github.event.pull_request.base.ref }}...HEAD || true) - echo "$changed_files" - # If any files under src/ changed, require version.yml to be updated - if echo "$changed_files" | grep -E '^src/' -q; then - echo "Detected changes under src/; ensuring version.yml was modified..." - if ! echo "$changed_files" | grep -xq 'version.yml'; then - echo "::error::Pull request modifies src/ but does not include an updated version.yml" - exit 1 - else - echo "version.yml updated." - fi - else - echo "No changes in src/ detected; skipping version.yml requirement." - fi - name: Setup .NET - uses: actions/setup-dotnet@v3 + uses: actions/setup-dotnet@v4 with: dotnet-version: '10.0.x' @@ -53,22 +29,46 @@ jobs: run: dotnet restore src/EventSourcing.sln - name: Build - run: dotnet build src/EventSourcing.sln --no-restore + run: dotnet build src/EventSourcing.sln --configuration Release --no-restore - - name: Test - all - run: | - set -euo pipefail - mkdir -p TestResults - for proj in src/*Tests/*.csproj; do - echo "Running tests for $proj" - name=$(basename "$proj" .csproj) - dotnet test "$proj" --no-build --logger "trx;LogFileName=${name}.trx" --results-directory TestResults - done + - name: Test + run: dotnet test src/EventSourcing.sln --configuration Release --no-build --verbosity normal --logger "trx;LogFileName=test-results.trx" --collect:"XPlat Code Coverage" --results-directory ./TestResults - name: Upload test results if: always() uses: actions/upload-artifact@v4 with: name: test-results - path: TestResults + path: '**/TestResults/*.trx' + if-no-files-found: warn + + - name: Test Report + uses: dorny/test-reporter@v1 + if: always() + with: + name: .NET Test Results + path: '**/TestResults/*.trx' + reporter: dotnet-trx + fail-on-error: false + + - name: Code Coverage Report + uses: irongut/CodeCoverageSummary@v1.3.0 + if: github.event_name == 'pull_request' + with: + filename: '**/TestResults/**/coverage.cobertura.xml' + badge: true + fail_below_min: false + format: markdown + hide_branch_rate: false + hide_complexity: true + indicators: true + output: both + thresholds: '60 80' + + - name: Add Coverage PR Comment + uses: marocchino/sticky-pull-request-comment@v2 + if: github.event_name == 'pull_request' + with: + recreate: true + path: code-coverage-results.md diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..25890a7 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,135 @@ +name: Release + +on: + push: + tags: + - 'v*.*.*' + workflow_dispatch: + inputs: + tag: + description: 'Tag to release (e.g., v1.2.0)' + required: true + type: string + +jobs: + release: + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Setup .NET SDK + uses: actions/setup-dotnet@v4 + with: + dotnet-version: '10.0.x' + + - name: Determine version from tag + id: version + run: | + if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then + TAG_NAME="${{ github.event.inputs.tag }}" + else + TAG_NAME="${GITHUB_REF#refs/tags/}" + fi + # Remove 'v' prefix if present + VERSION="${TAG_NAME#v}" + echo "VERSION=$VERSION" >> $GITHUB_ENV + echo "TAG_NAME=$TAG_NAME" >> $GITHUB_ENV + echo "Releasing version: $VERSION from tag: $TAG_NAME" + + - name: Verify version matches Directory.Build.props + run: | + BUILD_VERSION=$(grep '' src/Directory.Build.props | sed 's/.*\(.*\)<\/Version>.*/\1/') + echo "Directory.Build.props version: $BUILD_VERSION" + echo "Tag version: ${{ env.VERSION }}" + if [ "$BUILD_VERSION" != "${{ env.VERSION }}" ]; then + echo "::error::Version mismatch! Directory.Build.props has $BUILD_VERSION but tag is ${{ env.VERSION }}" + exit 1 + fi + echo "Version verified: ${{ env.VERSION }}" + + - name: Cache NuGet packages + uses: actions/cache@v4 + with: + path: ~/.nuget/packages + key: nuget-packages-${{ runner.os }}-${{ hashFiles('**/*.csproj') }} + restore-keys: | + nuget-packages-${{ runner.os }}- + + - name: Restore + run: | + set -euo pipefail + dotnet restore src/EventSourcing.sln + + - name: Build + run: dotnet build src/EventSourcing.sln --configuration Release --no-restore + + - name: Run tests + run: dotnet test src/EventSourcing.sln --configuration Release --no-build --verbosity normal --logger "trx;LogFileName=test-results.trx" --collect:"XPlat Code Coverage" --results-directory ./TestResults + + - name: Upload test results + if: always() + uses: actions/upload-artifact@v4 + with: + name: release-test-results + path: '**/TestResults/*.trx' + if-no-files-found: warn + + - name: Test Report + uses: dorny/test-reporter@v1 + if: always() + with: + name: Release Test Results + path: '**/TestResults/*.trx' + reporter: dotnet-trx + fail-on-error: true + + - name: Pack NuGet packages + run: | + set -euo pipefail + mkdir -p artifacts + echo "Packing projects with version ${{ env.VERSION }}" + find src -name "*.csproj" -print0 | while IFS= read -r -d '' proj; do + # Skip test projects + if [[ "$proj" == *".Tests.csproj" ]]; then + echo "Skipping test project: $proj" + continue + fi + echo "Packing $proj" + if dotnet pack "$proj" -c Release -o artifacts /p:PackageVersion=${{ env.VERSION }} --no-build; then + echo "Successfully packed $proj" + else + echo "::error::Pack failed for $proj" + exit 1 + fi + done + + - name: List artifacts + run: ls -lah artifacts/ + + - name: Create GitHub Release + uses: softprops/action-gh-release@v1 + with: + tag_name: ${{ env.TAG_NAME }} + name: Release ${{ env.VERSION }} + draft: false + prerelease: false + files: artifacts/*.nupkg + generate_release_notes: true + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Publish to NuGet.org + env: + NUGET_API_KEY: ${{ secrets.NUGET_API_KEY }} + run: | + set -euo pipefail + echo "Publishing packages to NuGet.org" + for pkg in artifacts/*.nupkg; do + echo "Pushing $pkg" + dotnet nuget push "$pkg" -s https://api.nuget.org/v3/index.json -k "$NUGET_API_KEY" --skip-duplicate + done + echo "✅ Release ${{ env.VERSION }} published successfully!" diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..b1c029c --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,300 @@ +# Contributing to EventSourcing + +Thank you for your interest in contributing to this project! This guide will help you get started. + +## Table of Contents + +- [Getting Started](#getting-started) +- [Development Workflow](#development-workflow) +- [Code Guidelines](#code-guidelines) +- [Submitting Changes](#submitting-changes) +- [Release Process](#release-process) + +## Getting Started + +### Prerequisites + +- [.NET 10 SDK](https://dotnet.microsoft.com/download) or later +- Git +- Your favorite IDE (Visual Studio, VS Code, Rider) + +### Setting Up Your Environment + +1. **Fork the repository** on GitHub + +2. **Clone your fork**: + ```bash + git clone https://github.com/YOUR-USERNAME/EventSourcing.git + cd EventSourcing + ``` + +3. **Add upstream remote**: + ```bash + git remote add upstream https://github.com/jacqueskang/EventSourcing.git + ``` + +4. **Restore dependencies**: + ```bash + dotnet restore src/EventSourcing.sln + ``` + +5. **Build the solution**: + ```bash + dotnet build src/EventSourcing.sln + ``` + +6. **Run tests**: + ```bash + dotnet test src/EventSourcing.sln + ``` + +## Development Workflow + +### 1. Create a Feature Branch + +Always create a new branch for your work: + +```bash +git checkout develop +git pull upstream develop +git checkout -b feat/your-feature-name +``` + +**Branch naming conventions**: +- `feat/` - New features +- `fix/` - Bug fixes +- `docs/` - Documentation changes +- `refactor/` - Code refactoring +- `test/` - Test additions or fixes +- `ci/` - CI/CD changes + +### 2. Make Your Changes + +- Write clean, readable code +- Follow existing code patterns and conventions +- Add tests for new functionality +- Update documentation as needed + +### 3. Test Your Changes + +```bash +# Run all tests +dotnet test src/EventSourcing.sln + +# Run tests with coverage +dotnet test src/EventSourcing.sln --collect:"XPlat Code Coverage" +``` + +### 4. Commit Your Changes + +Use [Conventional Commits](https://www.conventionalcommits.org/) format: + +```bash +git add . +git commit -m "feat: add snapshot support for DynamoDB" +``` + +**Commit message format**: +``` +: + + + + +``` + +**Types**: +- `feat:` - New feature +- `fix:` - Bug fix +- `docs:` - Documentation only +- `style:` - Formatting, missing semicolons, etc. +- `refactor:` - Code change that neither fixes a bug nor adds a feature +- `test:` - Adding or updating tests +- `ci:` - CI/CD changes +- `chore:` - Maintenance tasks + +**Examples**: +```bash +git commit -m "feat: add CosmosDB snapshot store" +git commit -m "fix: resolve null reference in aggregate repository" +git commit -m "docs: update DynamoDB setup instructions" +``` + +### 5. Keep Your Branch Updated + +```bash +git fetch upstream +git rebase upstream/develop +``` + +### 6. Push to Your Fork + +```bash +git push origin feat/your-feature-name +``` + +## Code Guidelines + +### Architecture Patterns + +- **Aggregates**: Must implement `IAggregate` and follow the event sourcing pattern +- **Events**: Must be immutable and JSON-serializable +- **Repositories**: Follow the `IAggregateRepository` interface +- **Persistence providers**: Implement `IEventStore` and optionally `ISnapshotStore` + +### Coding Conventions + +- Use C# naming conventions (PascalCase for classes, camelCase for parameters) +- Keep methods focused and small +- Use async/await for I/O operations +- Include XML documentation comments for public APIs +- Follow existing patterns in the codebase + +### Testing + +- Write unit tests for all new functionality +- Use xUnit for test framework +- Test files should be in `*.Tests` projects +- Follow AAA pattern: Arrange, Act, Assert + +Example test: +```csharp +[Fact] +public async Task DebitGiftCard_WithSufficientBalance_Succeeds() +{ + // Arrange + var giftCard = new GiftCard(Guid.NewGuid(), 100m); + + // Act + giftCard.Debit(50m); + + // Assert + Assert.Equal(50m, giftCard.Balance); +} +``` + +## Submitting Changes + +### Pull Request Process + +1. **Ensure all tests pass** and code builds successfully + +2. **Update documentation** if needed: + - README.md for user-facing changes + - XML comments for API changes + - Setup guides in `doc/` folder for new features + +3. **Create a Pull Request**: + - Go to your fork on GitHub + - Click "New Pull Request" + - Set base branch to `develop` + - Fill in the PR template + +4. **PR Requirements**: + - All tests must pass (enforced by CI) + - Code coverage should not decrease significantly + - At least one maintainer approval required + - No merge conflicts with `develop` + +### PR Title Format + +Use the same format as commit messages: +``` +feat: add Redis caching support +fix: resolve race condition in event store +docs: improve CosmosDB setup guide +``` + +### PR Description Template + +```markdown +## Summary +Brief description of the changes + +## Changes +- List of changes made +- Another change + +## Testing +How to test these changes + +## Related Issues +Closes #123 +``` + +### What Happens Next? + +1. **Automated checks run**: Build, test, coverage +2. **Code review**: Maintainers will review your PR +3. **Feedback**: Address any requested changes +4. **Approval**: Once approved, a maintainer will merge + +## Release Process + +Maintainers handle releases following this process: + +### For Contributors + +- You don't need to update version numbers +- Version bumps happen separately from feature PRs + +### For Maintainers + +1. **Update version** in `src/Directory.Build.props`: + ```xml + 1.3.0 + ``` + +2. **Commit and merge to develop**: + ```bash + git commit -m "chore: bump version to 1.3.0" + git push origin develop + ``` + +3. **Create and push tag**: + ```bash + git tag v1.3.0 + git push origin v1.3.0 + ``` + +4. **GitHub Actions automatically**: + - Builds packages + - Creates GitHub release + - Publishes to NuGet.org + +### Versioning + +This project follows [Semantic Versioning](https://semver.org/): +- **MAJOR**: Breaking changes +- **MINOR**: New features (backward compatible) +- **PATCH**: Bug fixes (backward compatible) + +## Adding a New Persistence Provider + +If you're adding a new storage backend: + +1. Create `JKang.EventSourcing.Persistence.YourProvider` project +2. Implement `IEventStore` +3. Optionally implement `ISnapshotStore` +4. Add builder extension for DI configuration +5. Create initializer if backend needs setup +6. Add comprehensive tests +7. Create setup guide in `doc/YourProviderSetup.md` +8. Update main README.md + +See existing providers (FileSystem, DynamoDB, CosmosDB) for reference patterns. + +## Getting Help + +- **Questions**: Open a [GitHub Discussion](https://github.com/jacqueskang/EventSourcing/discussions) +- **Bugs**: Open a [GitHub Issue](https://github.com/jacqueskang/EventSourcing/issues) +- **Review**: Tag `@jacqueskang` in your PR for review + +## Code of Conduct + +- Be respectful and inclusive +- Provide constructive feedback +- Focus on the code, not the person +- Help others learn and grow + +Thank you for contributing! 🎉 diff --git a/README.md b/README.md index 896ed42..4e066a1 100644 --- a/README.md +++ b/README.md @@ -204,5 +204,13 @@ It leverages `Microsoft.Extensions.Caching.Distributed.IDistributedCache` to cac Consider configuring a short sliding expiration (e.g., 5 sec) to reduce the chance of having cache out of date. +## Contributing + +Contributions are welcome! Please see [CONTRIBUTING.md](CONTRIBUTING.md) for details on: +- Setting up your development environment +- Code guidelines and conventions +- How to submit pull requests +- Release process + --- __Please feel free to download, fork and/or provide any feedback!__ diff --git a/src/Directory.Build.props b/src/Directory.Build.props index 064801a..498155a 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -1,6 +1,6 @@ - 1.0.0 + 1.2.1 Jacques Kang JKang.EventSourcing event sourcing diff --git a/version.yml b/version.yml deleted file mode 100644 index 10959a1..0000000 --- a/version.yml +++ /dev/null @@ -1,2 +0,0 @@ -variables: - version: 1.2.0