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
12 changes: 12 additions & 0 deletions .config/dotnet-tools.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"version": 1,
"isRoot": true,
"tools": {
"dotnet-reportgenerator-globaltool": {
"version": "5.5.11",
"commands": [
"reportgenerator"
]
}
}
}
30 changes: 30 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
version: 2

updates:
- package-ecosystem: nuget
directory: /
schedule:
interval: monthly
timezone: America/Sao_Paulo
groups:
test-dependencies:
patterns:
- Microsoft.NET.Test.Sdk
- MSTest.*
- coverlet.*
open-pull-requests-limit: 5
commit-message:
prefix: build(deps)

- package-ecosystem: github-actions
directory: /
schedule:
interval: monthly
timezone: America/Sao_Paulo
groups:
actions:
patterns:
- "*"
open-pull-requests-limit: 5
commit-message:
prefix: build(actions)
108 changes: 108 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
name: CI

on:
push:
branches: [main]
pull_request:
branches: [main]
workflow_dispatch:

permissions:
contents: read

concurrency:
group: ci-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

env:
DOTNET_NOLOGO: true
DOTNET_CLI_TELEMETRY_OPTOUT: true

jobs:
build-test-pack:
name: Build, test and pack
runs-on: ubuntu-latest
timeout-minutes: 15

steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1

- name: Setup .NET
uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6.0.0
with:
dotnet-version: |
8.0.x
10.0.x
cache: true
cache-dependency-path: |
InterestExtension/packages.lock.json
InterestExtension.Tests/packages.lock.json

- name: Restore local tools
run: dotnet tool restore

- name: Restore dependencies
run: dotnet restore InterestExtension.sln --locked-mode

- name: Build
run: dotnet build InterestExtension.sln --configuration Release --no-restore -p:ContinuousIntegrationBuild=true

- name: Test with coverage
run: dotnet test InterestExtension.sln --configuration Release --no-build --settings coverlet.runsettings --logger "trx;LogFilePrefix=test-results" --results-directory TestResults --collect:"XPlat Code Coverage"

- name: Generate coverage report
run: dotnet tool run reportgenerator -reports:"TestResults/**/coverage.cobertura.xml" -targetdir:"artifacts/coverage" -reporttypes:"Html;Cobertura;MarkdownSummaryGithub"

- name: Add coverage to job summary
run: cat artifacts/coverage/SummaryGithub.md >> "$GITHUB_STEP_SUMMARY"

- name: Enforce coverage thresholds
shell: pwsh
run: |
[xml] $report = Get-Content artifacts/coverage/Cobertura.xml -Raw
$culture = [System.Globalization.CultureInfo]::InvariantCulture
$lineRate = [double]::Parse($report.coverage.'line-rate', $culture)
$branchRate = [double]::Parse($report.coverage.'branch-rate', $culture)

if ($lineRate -lt 0.95 -or $branchRate -lt 0.90) {
throw "Coverage is below the required 95% lines / 90% branches."
}

"Coverage gate passed: {0:P2} lines / {1:P2} branches." -f $lineRate, $branchRate |
Add-Content $env:GITHUB_STEP_SUMMARY

- name: Pack and validate
run: dotnet pack InterestExtension/InterestExtension.csproj --configuration Release --no-build --no-restore -p:ContinuousIntegrationBuild=true --output artifacts/packages

- name: Restore package smoke test
run: dotnet restore InterestExtension.PackageSmoke/InterestExtension.PackageSmoke.csproj --packages artifacts/smoke-packages --source artifacts/packages

- name: Test installed package on .NET 8
run: dotnet run --project InterestExtension.PackageSmoke/InterestExtension.PackageSmoke.csproj --framework net8.0 --no-restore -- artifacts/packages

- name: Test installed package on .NET 10
run: dotnet run --project InterestExtension.PackageSmoke/InterestExtension.PackageSmoke.csproj --framework net10.0 --no-restore -- artifacts/packages

- name: Upload package, test and coverage artifacts
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: ci-artifacts
path: |
artifacts/packages/*
artifacts/coverage/*
TestResults/**/*.trx
if-no-files-found: error
retention-days: 14

- name: Upload diagnostics after a failure
if: ${{ !cancelled() && failure() }}
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: diagnostics-${{ github.run_attempt }}
path: |
TestResults/**/*.trx
TestResults/**/coverage.cobertura.xml
artifacts/coverage/*
if-no-files-found: warn
retention-days: 7
51 changes: 0 additions & 51 deletions .github/workflows/main.yml

This file was deleted.

144 changes: 144 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
name: Publish NuGet

on:
release:
types: [published]

permissions:
contents: read

concurrency:
group: nuget-${{ github.event.release.tag_name }}
cancel-in-progress: false

env:
DOTNET_NOLOGO: true
DOTNET_CLI_TELEMETRY_OPTOUT: true

jobs:
validate:
name: Validate release artifact
if: ${{ !github.event.release.prerelease && !github.event.release.draft }}
runs-on: ubuntu-latest
timeout-minutes: 15
permissions:
contents: read
outputs:
version: ${{ steps.version.outputs.version }}

steps:
- name: Checkout release tag
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: ${{ github.event.release.tag_name }}
fetch-depth: 0

- name: Setup .NET
uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6.0.0
with:
dotnet-version: |
8.0.x
10.0.x
cache: true
cache-dependency-path: |
InterestExtension/packages.lock.json
InterestExtension.Tests/packages.lock.json

- name: Verify tag matches package version
id: version
shell: bash
env:
RELEASE_TAG: ${{ github.event.release.tag_name }}
run: |
[[ "$RELEASE_TAG" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]] || {
echo "Expected a stable vMAJOR.MINOR.PATCH tag, got $RELEASE_TAG"
exit 1
}

tag_version="${RELEASE_TAG#v}"
project_version="$(dotnet msbuild InterestExtension/InterestExtension.csproj -nologo -getProperty:PackageVersion | tr -d '\r')"

[[ "$tag_version" == "$project_version" ]] || {
echo "Tag version $tag_version does not match package version $project_version"
exit 1
}

echo "version=$tag_version" >> "$GITHUB_OUTPUT"

- name: Verify tagged commit is on main
shell: bash
run: |
git fetch --no-tags origin main:refs/remotes/origin/main
git merge-base --is-ancestor HEAD origin/main || {
echo "The release tag must point to a commit contained in main."
exit 1
}

- name: Restore dependencies
run: dotnet restore InterestExtension.sln --locked-mode

- name: Build
run: dotnet build InterestExtension.sln --configuration Release --no-restore -p:ContinuousIntegrationBuild=true

- name: Test
run: dotnet test InterestExtension.sln --configuration Release --no-build

- name: Pack and validate
run: dotnet pack InterestExtension/InterestExtension.csproj --configuration Release --no-build --no-restore -p:ContinuousIntegrationBuild=true --output artifacts/packages

- name: Restore package smoke test
run: dotnet restore InterestExtension.PackageSmoke/InterestExtension.PackageSmoke.csproj --packages artifacts/smoke-packages --source artifacts/packages

- name: Test installed package on .NET 8
run: dotnet run --project InterestExtension.PackageSmoke/InterestExtension.PackageSmoke.csproj --framework net8.0 --no-restore -- artifacts/packages

- name: Test installed package on .NET 10
run: dotnet run --project InterestExtension.PackageSmoke/InterestExtension.PackageSmoke.csproj --framework net10.0 --no-restore -- artifacts/packages

- name: Upload validated release artifact
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: InterestExtensions-${{ steps.version.outputs.version }}
path: artifacts/packages/*
if-no-files-found: error

publish:
name: Publish to NuGet.org
needs: validate
runs-on: ubuntu-latest
timeout-minutes: 5
environment: nuget.org
permissions:
contents: read
id-token: write

steps:
- name: Setup .NET
uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6.0.0
with:
dotnet-version: 10.0.x

- name: Download validated release artifact
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: InterestExtensions-${{ needs.validate.outputs.version }}
path: artifacts/packages

- name: Verify downloaded package files
shell: bash
env:
VERSION: ${{ needs.validate.outputs.version }}
run: |
test -f "artifacts/packages/InterestExtensions.$VERSION.nupkg"
test -f "artifacts/packages/InterestExtensions.$VERSION.snupkg"

- name: Authenticate to NuGet.org
id: nuget
uses: NuGet/login@8d196754b4036150537f80ac539e15c2f1028841 # v1.2.0
with:
user: ${{ vars.NUGET_USER }}

- name: Publish package
env:
PACKAGE_PATH: artifacts/packages/InterestExtensions.${{ needs.validate.outputs.version }}.nupkg
run: dotnet nuget push "$PACKAGE_PATH" --api-key "${{ steps.nuget.outputs.NUGET_API_KEY }}" --source https://api.nuget.org/v3/index.json
41 changes: 41 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# Changelog

All notable changes to this project are documented in this file. The project follows [Semantic Versioning](https://semver.org/).

## [3.0.0] - 2026-08-16

### Added

- `netstandard2.0` package asset while retaining the optimized `net8.0` asset.
- .NET 8 and .NET 10 test execution.
- Annual overload for `CalculateCompoundInterestAmount`.
- XML API documentation, portable symbols, Source Link metadata, and `.snupkg` generation.
- Package validation against 2.0.1.
- Reproducible CI artifacts, coverage reporting, locked dependencies, and monthly Dependabot updates.
- Safe NuGet release workflow based on a published GitHub Release and Trusted Publishing.

### Changed

- Compound calculations now use decimal exponentiation instead of converting through `double`.
- Documentation now defines `interestRate` as a nominal annual fractional rate and `period` as whole years, matching the existing formulas.
- Daily compounding is explicitly documented as 365 compounding periods per year.
- Package version is now published only through the dedicated release workflow; pushes to `main` never publish.

### Fixed

- All public methods now reject negative principal, interest rate, and period values consistently.
- `CalculateCompoundInterestAmount` now rejects undefined `InterestPeriod` values instead of silently treating them as yearly.
- Package contents now include the XML documentation promised by the README.

### Breaking changes

- Invalid calls that previously returned a value can now throw `ArgumentOutOfRangeException`.
- Valid compound calculations can differ in their least significant decimal digits because the `double` conversion was removed.
- Consumers that depend on exact 2.x behavior must remain on 2.0.1 until they have reviewed these changes.

## [2.0.1] - 2025-05-25

- Fixed the build workflow and added Coverlet-based test coverage collection.

[3.0.0]: https://github.com/louresb/InterestExtensions/compare/v2.0.1...v3.0.0
[2.0.1]: https://github.com/louresb/InterestExtensions/tree/v2.0.1
Loading