(feature): retarget MaksIT.Dapr to .NET 10 and release v2.0.0
This commit is contained in:
parent
d07fc13b56
commit
5399408978
26
.vscode/launch.json
vendored
Normal file
26
.vscode/launch.json
vendored
Normal file
@ -0,0 +1,26 @@
|
||||
{
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
// Use IntelliSense to find out which attributes exist for C# debugging
|
||||
// Use hover for the description of the existing attributes
|
||||
// For further information visit https://github.com/dotnet/vscode-csharp/blob/main/debugger-launchjson.md
|
||||
"name": ".NET Core Launch (console)",
|
||||
"type": "coreclr",
|
||||
"request": "launch",
|
||||
"preLaunchTask": "build",
|
||||
// If you have changed target frameworks, make sure to update the program path.
|
||||
"program": "${workspaceFolder}/src/MaksIT.Dapr.Tests/bin/Debug/net10.0/MaksIT.Dapr.Tests.dll",
|
||||
"args": [],
|
||||
"cwd": "${workspaceFolder}/src/MaksIT.Dapr.Tests",
|
||||
// For more information about the 'console' field, see https://aka.ms/VSCode-CS-LaunchJson-Console
|
||||
"console": "internalConsole",
|
||||
"stopAtEntry": false
|
||||
},
|
||||
{
|
||||
"name": ".NET Core Attach",
|
||||
"type": "coreclr",
|
||||
"request": "attach"
|
||||
}
|
||||
]
|
||||
}
|
||||
41
.vscode/tasks.json
vendored
Normal file
41
.vscode/tasks.json
vendored
Normal file
@ -0,0 +1,41 @@
|
||||
{
|
||||
"version": "2.0.0",
|
||||
"tasks": [
|
||||
{
|
||||
"label": "build",
|
||||
"command": "dotnet",
|
||||
"type": "process",
|
||||
"args": [
|
||||
"build",
|
||||
"${workspaceFolder}/src/MaksIT.Dapr.Tests/MaksIT.Dapr.Tests.csproj",
|
||||
"/property:GenerateFullPaths=true",
|
||||
"/consoleloggerparameters:NoSummary;ForceNoAlign"
|
||||
],
|
||||
"problemMatcher": "$msCompile"
|
||||
},
|
||||
{
|
||||
"label": "publish",
|
||||
"command": "dotnet",
|
||||
"type": "process",
|
||||
"args": [
|
||||
"publish",
|
||||
"${workspaceFolder}/src/MaksIT.Dapr.Tests/MaksIT.Dapr.Tests.csproj",
|
||||
"/property:GenerateFullPaths=true",
|
||||
"/consoleloggerparameters:NoSummary;ForceNoAlign"
|
||||
],
|
||||
"problemMatcher": "$msCompile"
|
||||
},
|
||||
{
|
||||
"label": "watch",
|
||||
"command": "dotnet",
|
||||
"type": "process",
|
||||
"args": [
|
||||
"watch",
|
||||
"run",
|
||||
"--project",
|
||||
"${workspaceFolder}/src/MaksIT.Dapr.Tests/MaksIT.Dapr.Tests.csproj"
|
||||
],
|
||||
"problemMatcher": "$msCompile"
|
||||
}
|
||||
]
|
||||
}
|
||||
45
CHANGELOG.md
Normal file
45
CHANGELOG.md
Normal file
@ -0,0 +1,45 @@
|
||||
# Changelog
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## v2.0.0 - 2026-02-22
|
||||
|
||||
### Added
|
||||
- Dedicated test project (`MaksIT.Dapr.Tests`) with coverage for publisher and state-store service behavior.
|
||||
- Repository-level utility modules and scripts under `utils/` for release automation, coverage badge generation, and tagged-commit maintenance.
|
||||
|
||||
### Changed
|
||||
- Upgraded target framework to `.NET 10` (`net10.0`).
|
||||
- Updated core dependencies to Dapr `1.16.1`, `MaksIT.Core` `1.6.4`, and `MaksIT.Results` `2.0.0`.
|
||||
- Migrated solution definition from `MaksIT.Dapr.sln` to `MaksIT.Dapr.slnx`, including test project wiring.
|
||||
- NuGet packaging now includes `CHANGELOG.md` and coverage badge assets.
|
||||
|
||||
### Removed
|
||||
- Legacy root-level release scripts (`Release-NuGetPackage.*`) in favor of the `utils/Release-NuGetPackage/` flow.
|
||||
|
||||
<!--
|
||||
Template for new releases:
|
||||
|
||||
## v1.x.x
|
||||
|
||||
### Added
|
||||
- New features
|
||||
|
||||
### Changed
|
||||
- Changes in existing functionality
|
||||
|
||||
### Deprecated
|
||||
- Soon-to-be removed features
|
||||
|
||||
### Removed
|
||||
- Removed features
|
||||
|
||||
### Fixed
|
||||
- Bug fixes
|
||||
|
||||
### Security
|
||||
- Security improvements
|
||||
-->
|
||||
117
CONTRIBUTING.md
Normal file
117
CONTRIBUTING.md
Normal file
@ -0,0 +1,117 @@
|
||||
# Contributing to MaksIT.Dapr
|
||||
|
||||
Thank you for your interest in contributing to `MaksIT.Dapr`.
|
||||
|
||||
## Getting Started
|
||||
|
||||
1. Fork the repository.
|
||||
2. Clone your fork locally.
|
||||
3. Create a feature branch.
|
||||
4. Implement and test your changes.
|
||||
5. Submit a pull request to `main`.
|
||||
|
||||
## Development Setup
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- .NET 8 SDK or later
|
||||
- Git
|
||||
- PowerShell 7+ (recommended for utility scripts)
|
||||
|
||||
### Build
|
||||
|
||||
```bash
|
||||
cd src
|
||||
dotnet build MaksIT.Dapr.slnx
|
||||
```
|
||||
|
||||
### Test
|
||||
|
||||
```bash
|
||||
cd src
|
||||
dotnet test MaksIT.Dapr.Tests
|
||||
```
|
||||
|
||||
## Commit Message Format
|
||||
|
||||
Use:
|
||||
|
||||
```text
|
||||
(type): description
|
||||
```
|
||||
|
||||
### Commit Types
|
||||
|
||||
| Type | Description |
|
||||
|------|-------------|
|
||||
| `(feature):` | New feature or enhancement |
|
||||
| `(bugfix):` | Bug fix |
|
||||
| `(refactor):` | Refactoring without behavior change |
|
||||
| `(chore):` | Maintenance tasks (dependencies, tooling, docs) |
|
||||
|
||||
### Guidelines
|
||||
|
||||
- Use lowercase in the description.
|
||||
- Keep it concise and specific.
|
||||
- Do not end with a period.
|
||||
|
||||
## Pull Request Checklist
|
||||
|
||||
1. Ensure build and tests pass.
|
||||
2. Update `README.md` if behavior or usage changed.
|
||||
3. Update `CHANGELOG.md` under the target version.
|
||||
4. Keep changes scoped and explain rationale in the PR description.
|
||||
|
||||
## Versioning
|
||||
|
||||
This project follows [Semantic Versioning](https://semver.org/):
|
||||
|
||||
- **MAJOR**: breaking API changes
|
||||
- **MINOR**: backward-compatible features
|
||||
- **PATCH**: backward-compatible fixes
|
||||
|
||||
## Utility Scripts
|
||||
|
||||
Scripts are located under `utils/`.
|
||||
|
||||
### Generate Coverage Badges
|
||||
|
||||
Runs tests with coverage and generates SVG badges in `assets/badges/`.
|
||||
|
||||
```powershell
|
||||
.\utils\Generate-CoverageBadges\Generate-CoverageBadges.ps1
|
||||
```
|
||||
|
||||
Configuration: `utils/Generate-CoverageBadges/scriptsettings.json`
|
||||
|
||||
### Release NuGet Package
|
||||
|
||||
Builds, tests, packs, and publishes to NuGet and GitHub release flows.
|
||||
|
||||
```powershell
|
||||
.\utils\Release-NuGetPackage\Release-NuGetPackage.ps1
|
||||
```
|
||||
|
||||
Prerequisites:
|
||||
|
||||
- Docker Desktop (for Linux test validation)
|
||||
- GitHub CLI (`gh`)
|
||||
- environment variable `NUGET_MAKS_IT`
|
||||
- environment variable `GITHUB_MAKS_IT_COM`
|
||||
|
||||
Configuration: `utils/Release-NuGetPackage/scriptsettings.json`
|
||||
|
||||
### Force Amend Tagged Commit
|
||||
|
||||
Amends the latest tagged commit and force-pushes updated branch and tag.
|
||||
|
||||
```powershell
|
||||
.\utils\Force-AmendTaggedCommit\Force-AmendTaggedCommit.ps1
|
||||
.\utils\Force-AmendTaggedCommit\Force-AmendTaggedCommit.ps1 -DryRun
|
||||
```
|
||||
|
||||
Warning: this rewrites git history.
|
||||
|
||||
## License
|
||||
|
||||
By contributing, you agree that your contributions are licensed under the terms in `LICENSE.md`.
|
||||
@ -1,6 +1,6 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2024 - 2025 Maksym Sadovnychyy (MAKS-IT)
|
||||
Copyright (c) 2024 - 2026 Maksym Sadovnychyy (MAKS-IT)
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
||||
57
README.md
57
README.md
@ -1,26 +1,27 @@
|
||||
# MaksIT.Dapr
|
||||
|
||||
  
|
||||
|
||||
This repository hosts the `maksit-dapr` project, which utilizes [Dapr](https://dapr.io/) (Distributed Application Runtime) to facilitate building and managing microservices with ease. The project focuses on implementing a robust, scalable solution leveraging Dapr's building blocks and abstractions.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Overview](#overview)
|
||||
- [Features](#features)
|
||||
- [Getting Started](#getting-started)
|
||||
- [Prerequisites](#prerequisites)
|
||||
- [MaksIT.Dapr](#maksitdapr)
|
||||
- [Table of Contents](#table-of-contents)
|
||||
- [Overview](#overview)
|
||||
- [Features](#features)
|
||||
- [Getting Started](#getting-started)
|
||||
- [Installation](#installation)
|
||||
- [Configuration](#configuration)
|
||||
- [Usage](#usage)
|
||||
- [Running the Project](#running-the-project)
|
||||
- [Environment Variables](#environment-variables)
|
||||
- [Testing](#testing)
|
||||
- [Deployment](#deployment)
|
||||
- [Contributing](#contributing)
|
||||
- [License](#license)
|
||||
- [Usage](#usage)
|
||||
- [Registering Dapr Services](#registering-dapr-services)
|
||||
- [Injecting and Using Dapr Services](#injecting-and-using-dapr-services)
|
||||
- [Contributing](#contributing)
|
||||
- [Contact](#contact)
|
||||
- [License](#license)
|
||||
|
||||
## Overview
|
||||
|
||||
`maksit-dapr` serves as a foundational project to explore and implement Dapr-based microservices, demonstrating the integration of Dapr’s pub-sub, bindings, state management, and other building blocks in a distributed system environment.
|
||||
`maksit-dapr` serves as a foundational project to explore and implement Dapr-based microservices, demonstrating the integration of Dapr<EFBFBD>s pub-sub, bindings, state management, and other building blocks in a distributed system environment.
|
||||
|
||||
## Features
|
||||
|
||||
@ -176,32 +177,4 @@ If you have any questions or need further assistance, feel free to reach out:
|
||||
|
||||
## License
|
||||
|
||||
This project is licensed under the MIT License. See the full license text below.
|
||||
|
||||
---
|
||||
|
||||
### MIT License
|
||||
|
||||
```
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2024 Maksym Sadovnychyy (MAKS-IT)
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
```
|
||||
See `LICENSE.md`.
|
||||
21
assets/badges/coverage-branches.svg
Normal file
21
assets/badges/coverage-branches.svg
Normal file
@ -0,0 +1,21 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="147.5" height="20" role="img" aria-label="Branch Coverage: 50%">
|
||||
<title>Branch Coverage: 50%</title>
|
||||
<linearGradient id="s" x2="0" y2="100%">
|
||||
<stop offset="0" stop-color="#bbb" stop-opacity=".1"/>
|
||||
<stop offset="1" stop-opacity=".1"/>
|
||||
</linearGradient>
|
||||
<clipPath id="r">
|
||||
<rect width="147.5" height="20" rx="3" fill="#fff"/>
|
||||
</clipPath>
|
||||
<g clip-path="url(#r)">
|
||||
<rect width="107.5" height="20" fill="#555"/>
|
||||
<rect x="107.5" width="40" height="20" fill="#a4a61d"/>
|
||||
<rect width="147.5" height="20" fill="url(#s)"/>
|
||||
</g>
|
||||
<g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" text-rendering="geometricPrecision" font-size="11">
|
||||
<text aria-hidden="true" x="53.75" y="15" fill="#010101" fill-opacity=".3">Branch Coverage</text>
|
||||
<text x="53.75" y="14" fill="#fff">Branch Coverage</text>
|
||||
<text aria-hidden="true" x="127.5" y="15" fill="#010101" fill-opacity=".3">50%</text>
|
||||
<text x="127.5" y="14" fill="#fff">50%</text>
|
||||
</g>
|
||||
</svg>
|
||||
21
assets/badges/coverage-lines.svg
Normal file
21
assets/badges/coverage-lines.svg
Normal file
@ -0,0 +1,21 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="137" height="20" role="img" aria-label="Line Coverage: 62.1%">
|
||||
<title>Line Coverage: 62.1%</title>
|
||||
<linearGradient id="s" x2="0" y2="100%">
|
||||
<stop offset="0" stop-color="#bbb" stop-opacity=".1"/>
|
||||
<stop offset="1" stop-opacity=".1"/>
|
||||
</linearGradient>
|
||||
<clipPath id="r">
|
||||
<rect width="137" height="20" rx="3" fill="#fff"/>
|
||||
</clipPath>
|
||||
<g clip-path="url(#r)">
|
||||
<rect width="94.5" height="20" fill="#555"/>
|
||||
<rect x="94.5" width="42.5" height="20" fill="#97ca00"/>
|
||||
<rect width="137" height="20" fill="url(#s)"/>
|
||||
</g>
|
||||
<g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" text-rendering="geometricPrecision" font-size="11">
|
||||
<text aria-hidden="true" x="47.25" y="15" fill="#010101" fill-opacity=".3">Line Coverage</text>
|
||||
<text x="47.25" y="14" fill="#fff">Line Coverage</text>
|
||||
<text aria-hidden="true" x="115.75" y="15" fill="#010101" fill-opacity=".3">62.1%</text>
|
||||
<text x="115.75" y="14" fill="#fff">62.1%</text>
|
||||
</g>
|
||||
</svg>
|
||||
21
assets/badges/coverage-methods.svg
Normal file
21
assets/badges/coverage-methods.svg
Normal file
@ -0,0 +1,21 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="147.5" height="20" role="img" aria-label="Method Coverage: 60%">
|
||||
<title>Method Coverage: 60%</title>
|
||||
<linearGradient id="s" x2="0" y2="100%">
|
||||
<stop offset="0" stop-color="#bbb" stop-opacity=".1"/>
|
||||
<stop offset="1" stop-opacity=".1"/>
|
||||
</linearGradient>
|
||||
<clipPath id="r">
|
||||
<rect width="147.5" height="20" rx="3" fill="#fff"/>
|
||||
</clipPath>
|
||||
<g clip-path="url(#r)">
|
||||
<rect width="107.5" height="20" fill="#555"/>
|
||||
<rect x="107.5" width="40" height="20" fill="#97ca00"/>
|
||||
<rect width="147.5" height="20" fill="url(#s)"/>
|
||||
</g>
|
||||
<g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" text-rendering="geometricPrecision" font-size="11">
|
||||
<text aria-hidden="true" x="53.75" y="15" fill="#010101" fill-opacity=".3">Method Coverage</text>
|
||||
<text x="53.75" y="14" fill="#fff">Method Coverage</text>
|
||||
<text aria-hidden="true" x="127.5" y="15" fill="#010101" fill-opacity=".3">60%</text>
|
||||
<text x="127.5" y="14" fill="#fff">60%</text>
|
||||
</g>
|
||||
</svg>
|
||||
50
src/MaksIT.Dapr.Tests/DaprPublisherServiceTests.cs
Normal file
50
src/MaksIT.Dapr.Tests/DaprPublisherServiceTests.cs
Normal file
@ -0,0 +1,50 @@
|
||||
using Dapr.Client;
|
||||
using MaksIT.Dapr.Services;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Moq;
|
||||
|
||||
namespace MaksIT.Dapr.Tests;
|
||||
|
||||
public class DaprPublisherServiceTests {
|
||||
[Fact]
|
||||
public async Task PublishEventAsync_ReturnsOk_WhenPublishSucceeds() {
|
||||
var clientMock = new Mock<DaprClient>();
|
||||
clientMock
|
||||
.Setup(x => x.PublishEventAsync(
|
||||
It.IsAny<string>(),
|
||||
It.IsAny<string>(),
|
||||
It.IsAny<object>(),
|
||||
It.IsAny<CancellationToken>()))
|
||||
.Returns(Task.CompletedTask);
|
||||
|
||||
var service = new DaprPublisherService(
|
||||
Mock.Of<ILogger<DaprPublisherService>>(),
|
||||
clientMock.Object);
|
||||
object payload = new { Name = "payload" };
|
||||
|
||||
var result = await service.PublishEventAsync("pubsub", "topic", payload);
|
||||
|
||||
Assert.True(result.IsSuccess);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task PublishEventAsync_ReturnsInternalServerError_WhenPublishFails() {
|
||||
var clientMock = new Mock<DaprClient>();
|
||||
clientMock
|
||||
.Setup(x => x.PublishEventAsync(
|
||||
It.IsAny<string>(),
|
||||
It.IsAny<string>(),
|
||||
It.IsAny<object>(),
|
||||
It.IsAny<CancellationToken>()))
|
||||
.ThrowsAsync(new InvalidOperationException("publish failed"));
|
||||
|
||||
var service = new DaprPublisherService(
|
||||
Mock.Of<ILogger<DaprPublisherService>>(),
|
||||
clientMock.Object);
|
||||
object payload = new { Name = "payload" };
|
||||
|
||||
var result = await service.PublishEventAsync("pubsub", "topic", payload);
|
||||
|
||||
Assert.False(result.IsSuccess);
|
||||
}
|
||||
}
|
||||
95
src/MaksIT.Dapr.Tests/DaprStateStoreServiceTests.cs
Normal file
95
src/MaksIT.Dapr.Tests/DaprStateStoreServiceTests.cs
Normal file
@ -0,0 +1,95 @@
|
||||
using Dapr.Client;
|
||||
using MaksIT.Dapr.Services;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Moq;
|
||||
|
||||
namespace MaksIT.Dapr.Tests;
|
||||
|
||||
public class DaprStateStoreServiceTests {
|
||||
[Fact]
|
||||
public async Task SetStateAsync_ReturnsOk_WhenSaveSucceeds() {
|
||||
var clientMock = new Mock<DaprClient>();
|
||||
clientMock
|
||||
.Setup(x => x.SaveStateAsync(
|
||||
It.IsAny<string>(),
|
||||
It.IsAny<string>(),
|
||||
It.IsAny<string>(),
|
||||
It.IsAny<StateOptions>(),
|
||||
It.IsAny<IReadOnlyDictionary<string, string>>(),
|
||||
It.IsAny<CancellationToken>()))
|
||||
.Returns(Task.CompletedTask);
|
||||
|
||||
var service = new DaprStateStoreService(
|
||||
Mock.Of<ILogger<DaprStateStoreService>>(),
|
||||
clientMock.Object);
|
||||
|
||||
var result = await service.SetStateAsync("store", "key", "value");
|
||||
|
||||
Assert.True(result.IsSuccess);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetStateAsync_ReturnsOk_WhenStateExists() {
|
||||
var clientMock = new Mock<DaprClient>();
|
||||
clientMock
|
||||
.Setup(x => x.GetStateAsync<string?>(
|
||||
It.IsAny<string>(),
|
||||
It.IsAny<string>(),
|
||||
It.IsAny<ConsistencyMode?>(),
|
||||
It.IsAny<IReadOnlyDictionary<string, string>>(),
|
||||
It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync("value");
|
||||
|
||||
var service = new DaprStateStoreService(
|
||||
Mock.Of<ILogger<DaprStateStoreService>>(),
|
||||
clientMock.Object);
|
||||
|
||||
var result = await service.GetStateAsync<string>("store", "key");
|
||||
|
||||
Assert.True(result.IsSuccess);
|
||||
Assert.Equal("value", result.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetStateAsync_ReturnsNotFound_WhenStateIsNull() {
|
||||
var clientMock = new Mock<DaprClient>();
|
||||
clientMock
|
||||
.Setup(x => x.GetStateAsync<string?>(
|
||||
It.IsAny<string>(),
|
||||
It.IsAny<string>(),
|
||||
It.IsAny<ConsistencyMode?>(),
|
||||
It.IsAny<IReadOnlyDictionary<string, string>>(),
|
||||
It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync((string?)null);
|
||||
|
||||
var service = new DaprStateStoreService(
|
||||
Mock.Of<ILogger<DaprStateStoreService>>(),
|
||||
clientMock.Object);
|
||||
|
||||
var result = await service.GetStateAsync<string>("store", "key");
|
||||
|
||||
Assert.False(result.IsSuccess);
|
||||
Assert.Null(result.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task DeleteStateAsync_ReturnsInternalServerError_WhenDeleteFails() {
|
||||
var clientMock = new Mock<DaprClient>();
|
||||
clientMock
|
||||
.Setup(x => x.DeleteStateAsync(
|
||||
It.IsAny<string>(),
|
||||
It.IsAny<string>(),
|
||||
It.IsAny<StateOptions>(),
|
||||
It.IsAny<IReadOnlyDictionary<string, string>>(),
|
||||
It.IsAny<CancellationToken>()))
|
||||
.ThrowsAsync(new InvalidOperationException("delete failed"));
|
||||
|
||||
var service = new DaprStateStoreService(
|
||||
Mock.Of<ILogger<DaprStateStoreService>>(),
|
||||
clientMock.Object);
|
||||
|
||||
var result = await service.DeleteStateAsync("store", "key");
|
||||
|
||||
Assert.False(result.IsSuccess);
|
||||
}
|
||||
}
|
||||
29
src/MaksIT.Dapr.Tests/MaksIT.Dapr.Tests.csproj
Normal file
29
src/MaksIT.Dapr.Tests/MaksIT.Dapr.Tests.csproj
Normal file
@ -0,0 +1,29 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<IsPackable>false</IsPackable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="coverlet.collector" Version="8.0.0">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.0.1" />
|
||||
<PackageReference Include="Moq" Version="4.*" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="3.*" />
|
||||
<PackageReference Include="xunit.v3" Version="3.*" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Using Include="Xunit" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\MaksIT.Dapr\MaksIT.Dapr.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@ -1,25 +0,0 @@
|
||||
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio Version 17
|
||||
VisualStudioVersion = 17.11.35327.3
|
||||
MinimumVisualStudioVersion = 10.0.40219.1
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MaksIT.Dapr", "MaksIT.Dapr\MaksIT.Dapr.csproj", "{D6A8FD32-11E6-422E-9C33-B2D302B87562}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
Release|Any CPU = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||
{D6A8FD32-11E6-422E-9C33-B2D302B87562}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{D6A8FD32-11E6-422E-9C33-B2D302B87562}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{D6A8FD32-11E6-422E-9C33-B2D302B87562}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{D6A8FD32-11E6-422E-9C33-B2D302B87562}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
SolutionGuid = {D4625BDB-1AC6-42DA-9D7A-537D368EFE01}
|
||||
EndGlobalSection
|
||||
EndGlobal
|
||||
9
src/MaksIT.Dapr.slnx
Normal file
9
src/MaksIT.Dapr.slnx
Normal file
@ -0,0 +1,9 @@
|
||||
<Solution>
|
||||
<Configurations>
|
||||
<Platform Name="Any CPU" />
|
||||
<Platform Name="x64" />
|
||||
<Platform Name="x86" />
|
||||
</Configurations>
|
||||
<Project Path="MaksIT.Dapr.Tests/MaksIT.Dapr.Tests.csproj" />
|
||||
<Project Path="MaksIT.Dapr/MaksIT.Dapr.csproj" />
|
||||
</Solution>
|
||||
@ -1,13 +1,13 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
|
||||
<!-- NuGet package metadata -->
|
||||
<PackageId>MaksIT.Dapr</PackageId>
|
||||
<Version>1.0.8</Version>
|
||||
<Version>2.0.0</Version>
|
||||
<Authors>Maksym Sadovnychyy</Authors>
|
||||
<Company>MAKS-IT</Company>
|
||||
<Product>MaksIT.Dapr</Product>
|
||||
@ -21,16 +21,21 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Include="../../README.md" Pack="true" PackagePath="" />
|
||||
<None Include="../../LICENSE.md" Pack="true" PackagePath="" />
|
||||
<PackageReference Include="Dapr.Actors.AspNetCore" Version="1.16.1" />
|
||||
<PackageReference Include="Dapr.AspNetCore" Version="1.16.1" />
|
||||
<PackageReference Include="Dapr.Workflow" Version="1.16.1" />
|
||||
<PackageReference Include="MaksIT.Core" Version="1.6.4" />
|
||||
<PackageReference Include="MaksIT.Results" Version="2.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Dapr.Actors.AspNetCore" Version="1.16.0" />
|
||||
<PackageReference Include="Dapr.AspNetCore" Version="1.16.0" />
|
||||
<PackageReference Include="Dapr.Workflow" Version="1.16.0" />
|
||||
<PackageReference Include="MaksIT.Core" Version="1.5.1" />
|
||||
<PackageReference Include="MaksIT.Results" Version="1.1.0" />
|
||||
<None Include="..\..\LICENSE.md" Pack="true" PackagePath="\" Link="LICENSE.md" />
|
||||
<None Include="..\..\README.md" Pack="true" PackagePath="\" Link="README.md" />
|
||||
<None Include="..\..\CHANGELOG.md" Pack="true" PackagePath="\" Link="CHANGELOG.md" />
|
||||
|
||||
<None Include="..\..\assets\badges\**\*" Link="assets\badges\%(RecursiveDir)%(Filename)%(Extension)">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@ -1,9 +0,0 @@
|
||||
@echo off
|
||||
|
||||
REM Change directory to the location of the script
|
||||
cd /d %~dp0
|
||||
|
||||
REM Invoke the PowerShell script (Release-NuGetPackage.ps1) in the same directory
|
||||
powershell -ExecutionPolicy Bypass -File "%~dp0Release-NuGetPackage.ps1"
|
||||
|
||||
pause
|
||||
@ -1,46 +0,0 @@
|
||||
# Retrieve the API key from the environment variable
|
||||
$apiKey = $env:NUGET_MAKS_IT
|
||||
if (-not $apiKey) {
|
||||
Write-Host "Error: API key not found in environment variable NUGET_MAKS_IT."
|
||||
exit 1
|
||||
}
|
||||
|
||||
# NuGet source
|
||||
$nugetSource = "https://api.nuget.org/v3/index.json"
|
||||
|
||||
# Define paths
|
||||
$solutionDir = Split-Path -Parent $MyInvocation.MyCommand.Path
|
||||
$projectDir = "$solutionDir\MaksIT.Dapr"
|
||||
$outputDir = "$projectDir\bin\Release"
|
||||
|
||||
# Clean previous builds
|
||||
Write-Host "Cleaning previous builds..."
|
||||
dotnet clean $projectDir -c Release
|
||||
|
||||
# Build the project
|
||||
Write-Host "Building the project..."
|
||||
dotnet build $projectDir -c Release
|
||||
|
||||
# Pack the NuGet package
|
||||
Write-Host "Packing the project..."
|
||||
dotnet pack $projectDir -c Release --no-build
|
||||
|
||||
# Look for the .nupkg file
|
||||
$packageFile = Get-ChildItem -Path $outputDir -Filter "*.nupkg" -Recurse | Sort-Object LastWriteTime -Descending | Select-Object -First 1
|
||||
|
||||
if ($packageFile) {
|
||||
Write-Host "Package created successfully: $($packageFile.FullName)"
|
||||
|
||||
# Push the package to NuGet
|
||||
Write-Host "Pushing the package to NuGet..."
|
||||
dotnet nuget push $packageFile.FullName -k $apiKey -s $nugetSource --skip-duplicate
|
||||
|
||||
if ($LASTEXITCODE -eq 0) {
|
||||
Write-Host "Package pushed successfully."
|
||||
} else {
|
||||
Write-Host "Failed to push the package."
|
||||
}
|
||||
} else {
|
||||
Write-Host "Package creation failed. No .nupkg file found."
|
||||
exit 1
|
||||
}
|
||||
@ -1,49 +0,0 @@
|
||||
#!/bin/sh
|
||||
|
||||
# Retrieve the API key from the environment variable
|
||||
apiKey=$NUGET_MAKS_IT
|
||||
if [ -z "$apiKey" ]; then
|
||||
echo "Error: API key not found in environment variable NUGET_MAKS_IT."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# NuGet source
|
||||
nugetSource="https://api.nuget.org/v3/index.json"
|
||||
|
||||
# Define paths
|
||||
scriptDir=$(dirname "$0")
|
||||
solutionDir=$(realpath "$scriptDir")
|
||||
projectDir="$solutionDir/MaksIT.Dapr"
|
||||
outputDir="$projectDir/bin/Release"
|
||||
|
||||
# Clean previous builds
|
||||
echo "Cleaning previous builds..."
|
||||
dotnet clean "$projectDir" -c Release
|
||||
|
||||
# Build the project
|
||||
echo "Building the project..."
|
||||
dotnet build "$projectDir" -c Release
|
||||
|
||||
# Pack the NuGet package
|
||||
echo "Packing the project..."
|
||||
dotnet pack "$projectDir" -c Release --no-build
|
||||
|
||||
# Look for the .nupkg file
|
||||
packageFile=$(find "$outputDir" -name "*.nupkg" -print0 | xargs -0 ls -t | head -n 1)
|
||||
|
||||
if [ -n "$packageFile" ]; then
|
||||
echo "Package created successfully: $packageFile"
|
||||
|
||||
# Push the package to NuGet
|
||||
echo "Pushing the package to NuGet..."
|
||||
dotnet nuget push "$packageFile" -k "$apiKey" -s "$nugetSource" --skip-duplicate
|
||||
|
||||
if [ $? -eq 0 ]; then
|
||||
echo "Package pushed successfully."
|
||||
else
|
||||
echo "Failed to push the package."
|
||||
fi
|
||||
else
|
||||
echo "Package creation failed. No .nupkg file found."
|
||||
exit 1
|
||||
fi
|
||||
@ -0,0 +1,3 @@
|
||||
@echo off
|
||||
powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0Force-AmendTaggedCommit.ps1"
|
||||
pause
|
||||
246
utils/Force-AmendTaggedCommit/Force-AmendTaggedCommit.ps1
Normal file
246
utils/Force-AmendTaggedCommit/Force-AmendTaggedCommit.ps1
Normal file
@ -0,0 +1,246 @@
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Amends the latest commit, recreates its associated tag, and force pushes both to remote.
|
||||
|
||||
.DESCRIPTION
|
||||
This script performs the following operations:
|
||||
1. Gets the last commit and verifies it has an associated tag
|
||||
2. Stages all pending changes
|
||||
3. Amends the latest commit (keeps existing message)
|
||||
4. Deletes and recreates the tag on the amended commit
|
||||
5. Force pushes the branch and tag to remote
|
||||
|
||||
All configuration is in scriptsettings.json.
|
||||
|
||||
.PARAMETER DryRun
|
||||
If specified, shows what would be done without making changes.
|
||||
|
||||
.EXAMPLE
|
||||
.\Force-AmendTaggedCommit.ps1
|
||||
|
||||
.EXAMPLE
|
||||
.\Force-AmendTaggedCommit.ps1 -DryRun
|
||||
|
||||
.NOTES
|
||||
CONFIGURATION (scriptsettings.json):
|
||||
- git.remote: Remote name to push to (default: "origin")
|
||||
- git.confirmBeforeAmend: Prompt before amending (default: true)
|
||||
- git.confirmWhenNoChanges: Prompt if no pending changes (default: true)
|
||||
#>
|
||||
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[Parameter(Mandatory = $false)]
|
||||
[switch]$DryRun
|
||||
)
|
||||
|
||||
# Get the directory of the current script (for loading settings and relative paths)
|
||||
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
|
||||
$utilsDir = Split-Path $scriptDir -Parent
|
||||
|
||||
#region Import Modules
|
||||
|
||||
# Import shared ScriptConfig module (settings loading + dependency checks)
|
||||
$scriptConfigModulePath = Join-Path $utilsDir "ScriptConfig.psm1"
|
||||
if (-not (Test-Path $scriptConfigModulePath)) {
|
||||
Write-Error "ScriptConfig module not found at: $scriptConfigModulePath"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Import shared GitTools module (git operations used by this script)
|
||||
$gitToolsModulePath = Join-Path $utilsDir "GitTools.psm1"
|
||||
if (-not (Test-Path $gitToolsModulePath)) {
|
||||
Write-Error "GitTools module not found at: $gitToolsModulePath"
|
||||
exit 1
|
||||
}
|
||||
|
||||
$loggingModulePath = Join-Path $utilsDir "Logging.psm1"
|
||||
if (-not (Test-Path $loggingModulePath)) {
|
||||
Write-Error "Logging module not found at: $loggingModulePath"
|
||||
exit 1
|
||||
}
|
||||
|
||||
Import-Module $scriptConfigModulePath -Force
|
||||
Import-Module $loggingModulePath -Force
|
||||
Import-Module $gitToolsModulePath -Force
|
||||
|
||||
#endregion
|
||||
|
||||
#region Helpers
|
||||
|
||||
function Select-PreferredHeadTag {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string[]]$Tags
|
||||
)
|
||||
|
||||
# Pick the latest tag on HEAD by git's own ordering (no tag-name parsing assumptions).
|
||||
$ordered = (& git tag --points-at HEAD --sort=-creatordate 2>$null)
|
||||
if ($LASTEXITCODE -eq 0 -and $ordered) {
|
||||
$orderedTags = @($ordered | Where-Object { -not [string]::IsNullOrWhiteSpace($_) } | ForEach-Object { $_.Trim() })
|
||||
if ($orderedTags.Count -gt 0) {
|
||||
return $orderedTags[0]
|
||||
}
|
||||
}
|
||||
|
||||
# Fallback: keep script functional even if sorting is unavailable.
|
||||
return $Tags[0]
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Load Settings
|
||||
|
||||
$settings = Get-ScriptSettings -ScriptDir $scriptDir
|
||||
|
||||
#endregion
|
||||
|
||||
#region Configuration
|
||||
|
||||
# Git configuration with safe defaults when settings are omitted
|
||||
$Remote = if ($settings.git.remote) { $settings.git.remote } else { "origin" }
|
||||
$ConfirmBeforeAmend = if ($null -ne $settings.git.confirmBeforeAmend) { $settings.git.confirmBeforeAmend } else { $true }
|
||||
$ConfirmWhenNoChanges = if ($null -ne $settings.git.confirmWhenNoChanges) { $settings.git.confirmWhenNoChanges } else { $true }
|
||||
|
||||
#endregion
|
||||
|
||||
#region Validate CLI Dependencies
|
||||
|
||||
Assert-Command git
|
||||
|
||||
#endregion
|
||||
|
||||
#region Main
|
||||
|
||||
Write-Log -Level "INFO" -Message "========================================"
|
||||
Write-Log -Level "INFO" -Message "Force Amend Tagged Commit Script"
|
||||
Write-Log -Level "INFO" -Message "========================================"
|
||||
|
||||
if ($DryRun) {
|
||||
Write-Log -Level "WARN" -Message "*** DRY RUN MODE - No changes will be made ***"
|
||||
}
|
||||
|
||||
#region Preflight
|
||||
|
||||
# 1. Detect current branch
|
||||
$Branch = Get-CurrentBranch
|
||||
|
||||
# 2. Read HEAD commit details
|
||||
Write-LogStep "Getting last commit..."
|
||||
$CommitMessage = Get-HeadCommitMessage
|
||||
$CommitHash = Get-HeadCommitHash -Short
|
||||
Write-Log -Level "INFO" -Message "Commit: $CommitHash - $CommitMessage"
|
||||
|
||||
# 3. Ensure HEAD has at least one tag
|
||||
Write-LogStep "Finding tag on last commit..."
|
||||
$tags = Get-HeadTags
|
||||
if ($tags.Count -eq 0) {
|
||||
Write-Error "No tag found on the last commit ($CommitHash). This script requires the last commit to have an associated tag."
|
||||
exit 1
|
||||
}
|
||||
|
||||
# If multiple tags exist, choose the latest one on HEAD by git ordering.
|
||||
if ($tags.Count -gt 1) {
|
||||
Write-Log -Level "WARN" -Message "Multiple tags found on HEAD: $($tags -join ', ')"
|
||||
}
|
||||
$TagName = Select-PreferredHeadTag -Tags $tags
|
||||
Write-Log -Level "OK" -Message "Found tag: $TagName"
|
||||
|
||||
# 4. Inspect pending changes before amend
|
||||
Write-LogStep "Checking pending changes..."
|
||||
$Status = Get-GitStatusShort
|
||||
if (-not [string]::IsNullOrWhiteSpace($Status)) {
|
||||
Write-Log -Level "INFO" -Message "Pending changes:"
|
||||
$Status -split "`r?`n" | ForEach-Object { Write-Log -Level "INFO" -Message " $_" }
|
||||
}
|
||||
else {
|
||||
Write-Log -Level "WARN" -Message "No pending changes found"
|
||||
if ($ConfirmWhenNoChanges -and -not $DryRun) {
|
||||
$confirm = Read-Host "`n No changes to amend. Continue to recreate tag and force push? (y/N)"
|
||||
if ($confirm -ne 'y' -and $confirm -ne 'Y') {
|
||||
Write-Log -Level "WARN" -Message "Aborted by user"
|
||||
exit 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# 5. Show operation summary and request explicit confirmation
|
||||
Write-Log -Level "INFO" -Message "----------------------------------------"
|
||||
Write-Log -Level "INFO" -Message "Summary of operations:"
|
||||
Write-Log -Level "INFO" -Message "----------------------------------------"
|
||||
Write-Log -Level "INFO" -Message "Branch: $Branch"
|
||||
Write-Log -Level "INFO" -Message "Commit: $CommitHash"
|
||||
Write-Log -Level "INFO" -Message "Tag: $TagName"
|
||||
Write-Log -Level "INFO" -Message "Remote: $Remote"
|
||||
Write-Log -Level "INFO" -Message "----------------------------------------"
|
||||
|
||||
if ($ConfirmBeforeAmend -and -not $DryRun) {
|
||||
$confirm = Read-Host " Proceed with amend and force push? (y/N)"
|
||||
if ($confirm -ne 'y' -and $confirm -ne 'Y') {
|
||||
Write-Log -Level "WARN" -Message "Aborted by user"
|
||||
exit 0
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Amend And Push
|
||||
|
||||
# 6. Stage all changes to include them in amended commit
|
||||
Write-LogStep "Staging all changes..."
|
||||
if (-not $DryRun) {
|
||||
Add-AllChanges
|
||||
}
|
||||
Write-Log -Level "OK" -Message "All changes staged"
|
||||
|
||||
# 7. Amend HEAD commit while preserving commit message
|
||||
Write-LogStep "Amending commit..."
|
||||
if (-not $DryRun) {
|
||||
Update-HeadCommitNoEdit
|
||||
}
|
||||
Write-Log -Level "OK" -Message "Commit amended"
|
||||
|
||||
# 8. Move existing local tag to the amended commit
|
||||
Write-LogStep "Deleting local tag '$TagName'..."
|
||||
if (-not $DryRun) {
|
||||
Remove-LocalTag -Tag $TagName
|
||||
}
|
||||
Write-Log -Level "OK" -Message "Local tag deleted"
|
||||
|
||||
# 9. Recreate the same tag on new HEAD
|
||||
Write-LogStep "Recreating tag '$TagName' on amended commit..."
|
||||
if (-not $DryRun) {
|
||||
New-LocalTag -Tag $TagName
|
||||
}
|
||||
Write-Log -Level "OK" -Message "Tag recreated"
|
||||
|
||||
# 10. Force push updated branch history
|
||||
Write-LogStep "Force pushing branch '$Branch' to $Remote..."
|
||||
if (-not $DryRun) {
|
||||
Push-BranchToRemote -Branch $Branch -Remote $Remote -Force
|
||||
}
|
||||
Write-Log -Level "OK" -Message "Branch force pushed"
|
||||
|
||||
# 11. Force push moved tag
|
||||
Write-LogStep "Force pushing tag '$TagName' to $Remote..."
|
||||
if (-not $DryRun) {
|
||||
Push-TagToRemote -Tag $TagName -Remote $Remote -Force
|
||||
}
|
||||
Write-Log -Level "OK" -Message "Tag force pushed"
|
||||
|
||||
#endregion
|
||||
|
||||
#region Summary
|
||||
|
||||
Write-Log -Level "OK" -Message "========================================"
|
||||
Write-Log -Level "OK" -Message "Operation completed successfully!"
|
||||
Write-Log -Level "OK" -Message "========================================"
|
||||
|
||||
# Show resulting HEAD commit after amend
|
||||
Write-Log -Level "INFO" -Message "Final state:"
|
||||
$finalLog = Get-HeadCommitOneLine
|
||||
Write-Log -Level "INFO" -Message $finalLog
|
||||
|
||||
#endregion
|
||||
|
||||
#endregion
|
||||
18
utils/Force-AmendTaggedCommit/scriptsettings.json
Normal file
18
utils/Force-AmendTaggedCommit/scriptsettings.json
Normal file
@ -0,0 +1,18 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$comment": "Configuration for Force-AmendTaggedCommit.ps1",
|
||||
|
||||
"git": {
|
||||
"remote": "origin",
|
||||
"confirmBeforeAmend": true,
|
||||
"confirmWhenNoChanges": true
|
||||
},
|
||||
|
||||
"_comments": {
|
||||
"git": {
|
||||
"remote": "Remote name used for force-pushing branch and tag",
|
||||
"confirmBeforeAmend": "Ask for confirmation before amend + force-push operations",
|
||||
"confirmWhenNoChanges": "Ask for confirmation when there are no pending changes to amend"
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,3 @@
|
||||
@echo off
|
||||
powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0Generate-CoverageBadges.ps1"
|
||||
pause
|
||||
232
utils/Generate-CoverageBadges/Generate-CoverageBadges.ps1
Normal file
232
utils/Generate-CoverageBadges/Generate-CoverageBadges.ps1
Normal file
@ -0,0 +1,232 @@
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Runs tests, collects coverage, and generates SVG badges for README.
|
||||
|
||||
.DESCRIPTION
|
||||
This script runs unit tests via TestRunner.psm1, then generates shields.io-style
|
||||
SVG badges for line, branch, and method coverage.
|
||||
Optional HTML report generation is controlled by scriptsettings.json (openReport).
|
||||
|
||||
Configuration is stored in scriptsettings.json:
|
||||
- openReport : Generate and open full HTML report (true/false)
|
||||
- paths.testProject : Relative path to test project
|
||||
- paths.badgesDir : Relative path to badges output directory
|
||||
- badges : Array of badges to generate (name, label, metric)
|
||||
- colorThresholds : Coverage percentages for badge colors
|
||||
|
||||
Badge colors based on coverage:
|
||||
- brightgreen (>=80%), green (>=60%), yellowgreen (>=40%)
|
||||
- yellow (>=20%), orange (>=10%), red (<10%)
|
||||
If openReport is true, ReportGenerator is required:
|
||||
dotnet tool install -g dotnet-reportgenerator-globaltool
|
||||
|
||||
.EXAMPLE
|
||||
.\Generate-CoverageBadges.ps1
|
||||
Runs tests and generates coverage badges (and optionally HTML report if configured).
|
||||
|
||||
.OUTPUTS
|
||||
SVG badge files in the configured badges directory.
|
||||
|
||||
.NOTES
|
||||
Author: MaksIT
|
||||
Requires: .NET SDK, Coverlet (included in test project)
|
||||
#>
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
# Get the directory of the current script (for loading settings and relative paths)
|
||||
$ScriptDir = $PSScriptRoot
|
||||
$UtilsDir = Split-Path $ScriptDir -Parent
|
||||
|
||||
#region Import Modules
|
||||
|
||||
# Import TestRunner module (executes tests and collects coverage metrics)
|
||||
$testRunnerModulePath = Join-Path $UtilsDir "TestRunner.psm1"
|
||||
if (-not (Test-Path $testRunnerModulePath)) {
|
||||
Write-Error "TestRunner module not found at: $testRunnerModulePath"
|
||||
exit 1
|
||||
}
|
||||
Import-Module $testRunnerModulePath -Force
|
||||
|
||||
# Import shared ScriptConfig module (settings + command validation helpers)
|
||||
$scriptConfigModulePath = Join-Path $UtilsDir "ScriptConfig.psm1"
|
||||
if (-not (Test-Path $scriptConfigModulePath)) {
|
||||
Write-Error "ScriptConfig module not found at: $scriptConfigModulePath"
|
||||
exit 1
|
||||
}
|
||||
Import-Module $scriptConfigModulePath -Force
|
||||
|
||||
# Import shared Logging module (timestamped/aligned output)
|
||||
$loggingModulePath = Join-Path $UtilsDir "Logging.psm1"
|
||||
if (-not (Test-Path $loggingModulePath)) {
|
||||
Write-Error "Logging module not found at: $loggingModulePath"
|
||||
exit 1
|
||||
}
|
||||
Import-Module $loggingModulePath -Force
|
||||
|
||||
#endregion
|
||||
|
||||
#region Load Settings
|
||||
|
||||
$Settings = Get-ScriptSettings -ScriptDir $ScriptDir
|
||||
|
||||
$thresholds = $Settings.colorThresholds
|
||||
|
||||
#endregion
|
||||
|
||||
#region Configuration
|
||||
|
||||
# Runtime options from settings
|
||||
$OpenReport = if ($null -ne $Settings.openReport) { [bool]$Settings.openReport } else { $false }
|
||||
|
||||
# Resolve configured paths to absolute paths
|
||||
$TestProjectPath = [System.IO.Path]::GetFullPath((Join-Path $ScriptDir $Settings.paths.testProject))
|
||||
$BadgesDir = [System.IO.Path]::GetFullPath((Join-Path $ScriptDir $Settings.paths.badgesDir))
|
||||
|
||||
# Ensure badges directory exists
|
||||
if (-not (Test-Path $BadgesDir)) {
|
||||
New-Item -ItemType Directory -Path $BadgesDir | Out-Null
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Helpers
|
||||
|
||||
# Maps a coverage percentage to a shields.io color using configured thresholds.
|
||||
function Get-BadgeColor {
|
||||
param([double]$percentage)
|
||||
|
||||
if ($percentage -ge $thresholds.brightgreen) { return "brightgreen" }
|
||||
if ($percentage -ge $thresholds.green) { return "green" }
|
||||
if ($percentage -ge $thresholds.yellowgreen) { return "yellowgreen" }
|
||||
if ($percentage -ge $thresholds.yellow) { return "yellow" }
|
||||
if ($percentage -ge $thresholds.orange) { return "orange" }
|
||||
return "red"
|
||||
}
|
||||
|
||||
# Builds a shields.io-like SVG badge string for one metric.
|
||||
function New-Badge {
|
||||
param(
|
||||
[string]$label,
|
||||
[string]$value,
|
||||
[string]$color
|
||||
)
|
||||
|
||||
# Calculate widths (approximate character width of 6.5px for the font)
|
||||
$labelWidth = [math]::Max(($label.Length * 6.5) + 10, 50)
|
||||
$valueWidth = [math]::Max(($value.Length * 6.5) + 10, 40)
|
||||
$totalWidth = $labelWidth + $valueWidth
|
||||
$labelX = $labelWidth / 2
|
||||
$valueX = $labelWidth + ($valueWidth / 2)
|
||||
|
||||
$colorMap = @{
|
||||
"brightgreen" = "#4c1"
|
||||
"green" = "#97ca00"
|
||||
"yellowgreen" = "#a4a61d"
|
||||
"yellow" = "#dfb317"
|
||||
"orange" = "#fe7d37"
|
||||
"red" = "#e05d44"
|
||||
}
|
||||
$hexColor = $colorMap[$color]
|
||||
if (-not $hexColor) { $hexColor = "#9f9f9f" }
|
||||
|
||||
return @"
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="$totalWidth" height="20" role="img" aria-label="$label`: $value">
|
||||
<title>$label`: $value</title>
|
||||
<linearGradient id="s" x2="0" y2="100%">
|
||||
<stop offset="0" stop-color="#bbb" stop-opacity=".1"/>
|
||||
<stop offset="1" stop-opacity=".1"/>
|
||||
</linearGradient>
|
||||
<clipPath id="r">
|
||||
<rect width="$totalWidth" height="20" rx="3" fill="#fff"/>
|
||||
</clipPath>
|
||||
<g clip-path="url(#r)">
|
||||
<rect width="$labelWidth" height="20" fill="#555"/>
|
||||
<rect x="$labelWidth" width="$valueWidth" height="20" fill="$hexColor"/>
|
||||
<rect width="$totalWidth" height="20" fill="url(#s)"/>
|
||||
</g>
|
||||
<g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" text-rendering="geometricPrecision" font-size="11">
|
||||
<text aria-hidden="true" x="$labelX" y="15" fill="#010101" fill-opacity=".3">$label</text>
|
||||
<text x="$labelX" y="14" fill="#fff">$label</text>
|
||||
<text aria-hidden="true" x="$valueX" y="15" fill="#010101" fill-opacity=".3">$value</text>
|
||||
<text x="$valueX" y="14" fill="#fff">$value</text>
|
||||
</g>
|
||||
</svg>
|
||||
"@
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Main
|
||||
|
||||
#region Test And Coverage
|
||||
|
||||
$coverage = Invoke-TestsWithCoverage -TestProjectPath $TestProjectPath -KeepResults:$OpenReport
|
||||
if (-not $coverage.Success) {
|
||||
Write-Error "Tests failed: $($coverage.Error)"
|
||||
exit 1
|
||||
}
|
||||
|
||||
Write-Log -Level "OK" -Message "Tests passed!"
|
||||
|
||||
$metrics = @{
|
||||
"line" = $coverage.LineRate
|
||||
"branch" = $coverage.BranchRate
|
||||
"method" = $coverage.MethodRate
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Generate Badges
|
||||
|
||||
Write-LogStep -Message "Generating coverage badges..."
|
||||
|
||||
foreach ($badge in $Settings.badges) {
|
||||
$metricValue = $metrics[$badge.metric]
|
||||
$color = Get-BadgeColor $metricValue
|
||||
$svg = New-Badge -label $badge.label -value "$metricValue%" -color $color
|
||||
$path = Join-Path $BadgesDir $badge.name
|
||||
$svg | Out-File -FilePath $path -Encoding utf8
|
||||
Write-Log -Level "OK" -Message "$($badge.name): $($badge.label) = $metricValue%"
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Summary
|
||||
|
||||
Write-Log -Level "INFO" -Message "Coverage Summary:"
|
||||
Write-Log -Level "INFO" -Message "Line Coverage: $($coverage.LineRate)%"
|
||||
Write-Log -Level "INFO" -Message "Branch Coverage: $($coverage.BranchRate)%"
|
||||
Write-Log -Level "INFO" -Message "Method Coverage: $($coverage.MethodRate)% ($($coverage.CoveredMethods) of $($coverage.TotalMethods) methods)"
|
||||
Write-Log -Level "OK" -Message "Badges generated in: $BadgesDir"
|
||||
Write-Log -Level "STEP" -Message "Commit the badges/ folder to update README."
|
||||
|
||||
#endregion
|
||||
|
||||
#region Optional Html Report
|
||||
|
||||
if ($OpenReport -and $coverage.CoverageFile) {
|
||||
Write-LogStep -Message "Generating HTML report..."
|
||||
Assert-Command reportgenerator
|
||||
|
||||
$ResultsDir = Split-Path (Split-Path $coverage.CoverageFile -Parent) -Parent
|
||||
$ReportDir = Join-Path $ResultsDir "report"
|
||||
|
||||
$reportGenArgs = @(
|
||||
"-reports:$($coverage.CoverageFile)"
|
||||
"-targetdir:$ReportDir"
|
||||
"-reporttypes:Html"
|
||||
)
|
||||
& reportgenerator @reportGenArgs
|
||||
|
||||
$IndexFile = Join-Path $ReportDir "index.html"
|
||||
if (Test-Path $IndexFile) {
|
||||
Start-Process $IndexFile
|
||||
}
|
||||
|
||||
Write-Log -Level "INFO" -Message "TestResults kept for HTML report viewing."
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#endregion
|
||||
44
utils/Generate-CoverageBadges/scriptsettings.json
Normal file
44
utils/Generate-CoverageBadges/scriptsettings.json
Normal file
@ -0,0 +1,44 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft-07/schema",
|
||||
"title": "Generate Coverage Badges Script Settings",
|
||||
"description": "Configuration for Generate-CoverageBadges.ps1 script",
|
||||
"openReport": false,
|
||||
"paths": {
|
||||
"testProject": "..\\..\\src\\MaksIT.Dapr.Tests",
|
||||
"badgesDir": "..\\..\\assets\\badges"
|
||||
},
|
||||
"badges": [
|
||||
{
|
||||
"name": "coverage-lines.svg",
|
||||
"label": "Line Coverage",
|
||||
"metric": "line"
|
||||
},
|
||||
{
|
||||
"name": "coverage-branches.svg",
|
||||
"label": "Branch Coverage",
|
||||
"metric": "branch"
|
||||
},
|
||||
{
|
||||
"name": "coverage-methods.svg",
|
||||
"label": "Method Coverage",
|
||||
"metric": "method"
|
||||
}
|
||||
],
|
||||
"colorThresholds": {
|
||||
"brightgreen": 80,
|
||||
"green": 60,
|
||||
"yellowgreen": 40,
|
||||
"yellow": 20,
|
||||
"orange": 10,
|
||||
"red": 0
|
||||
},
|
||||
"_comments": {
|
||||
"openReport": "If true, generate and open full HTML coverage report (requires reportgenerator tool).",
|
||||
"paths": {
|
||||
"testProject": "Relative path to test project used by TestRunner.",
|
||||
"badgesDir": "Relative path where SVG coverage badges are written."
|
||||
},
|
||||
"badges": "List of output badges. Each entry maps a metric key (line|branch|method) to filename and label.",
|
||||
"colorThresholds": "Coverage percentage thresholds used to pick badge colors."
|
||||
}
|
||||
}
|
||||
265
utils/GitTools.psm1
Normal file
265
utils/GitTools.psm1
Normal file
@ -0,0 +1,265 @@
|
||||
#
|
||||
# Shared Git helpers for utility scripts.
|
||||
#
|
||||
|
||||
function Import-LoggingModuleInternal {
|
||||
if (Get-Command Write-Log -ErrorAction SilentlyContinue) {
|
||||
return
|
||||
}
|
||||
|
||||
$modulePath = Join-Path $PSScriptRoot "Logging.psm1"
|
||||
if (Test-Path $modulePath) {
|
||||
Import-Module $modulePath -Force
|
||||
}
|
||||
}
|
||||
|
||||
function Write-GitToolsLogInternal {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$Message,
|
||||
|
||||
[Parameter(Mandatory = $false)]
|
||||
[ValidateSet("INFO", "OK", "WARN", "ERROR", "STEP", "DEBUG")]
|
||||
[string]$Level = "INFO"
|
||||
)
|
||||
|
||||
Import-LoggingModuleInternal
|
||||
|
||||
if (Get-Command Write-Log -ErrorAction SilentlyContinue) {
|
||||
Write-Log -Level $Level -Message $Message
|
||||
return
|
||||
}
|
||||
|
||||
Write-Host $Message -ForegroundColor Gray
|
||||
}
|
||||
|
||||
# Internal:
|
||||
# Purpose:
|
||||
# - Execute a git command and enforce fail-fast error handling.
|
||||
function Invoke-GitInternal {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string[]]$Arguments,
|
||||
|
||||
[Parameter(Mandatory = $false)]
|
||||
[switch]$CaptureOutput,
|
||||
|
||||
[Parameter(Mandatory = $false)]
|
||||
[string]$ErrorMessage = "Git command failed"
|
||||
)
|
||||
|
||||
if ($CaptureOutput) {
|
||||
$output = & git @Arguments 2>&1
|
||||
$exitCode = $LASTEXITCODE
|
||||
if ($exitCode -ne 0) {
|
||||
Write-Error "$ErrorMessage (exit code: $exitCode)"
|
||||
exit 1
|
||||
}
|
||||
|
||||
if ($null -eq $output) {
|
||||
return ""
|
||||
}
|
||||
|
||||
return ($output -join "`n").Trim()
|
||||
}
|
||||
|
||||
& git @Arguments
|
||||
$exitCode = $LASTEXITCODE
|
||||
if ($exitCode -ne 0) {
|
||||
Write-Error "$ErrorMessage (exit code: $exitCode)"
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
|
||||
# Used by:
|
||||
# - utils/Release-NuGetPackage/Release-NuGetPackage.ps1
|
||||
# - utils/Force-AmendTaggedCommit/Force-AmendTaggedCommit.ps1
|
||||
# Purpose:
|
||||
# - Resolve and print the current branch name.
|
||||
function Get-CurrentBranch {
|
||||
Write-GitToolsLogInternal -Level "STEP" -Message "Detecting current branch..."
|
||||
|
||||
$branch = Invoke-GitInternal -Arguments @("rev-parse", "--abbrev-ref", "HEAD") -CaptureOutput -ErrorMessage "Could not determine current branch"
|
||||
Write-GitToolsLogInternal -Level "OK" -Message "Branch: $branch"
|
||||
return $branch
|
||||
}
|
||||
|
||||
# Used by:
|
||||
# - utils/Release-NuGetPackage/Release-NuGetPackage.ps1
|
||||
# - utils/Force-AmendTaggedCommit/Force-AmendTaggedCommit.ps1
|
||||
# Purpose:
|
||||
# - Return `git status --short` output for pending-change checks.
|
||||
function Get-GitStatusShort {
|
||||
return Invoke-GitInternal -Arguments @("status", "--short") -CaptureOutput -ErrorMessage "Failed to get git status"
|
||||
}
|
||||
|
||||
# Used by:
|
||||
# - utils/Release-NuGetPackage/Release-NuGetPackage.ps1
|
||||
# Purpose:
|
||||
# - Get exact tag name attached to HEAD (release flow).
|
||||
function Get-CurrentCommitTag {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$Version
|
||||
)
|
||||
|
||||
Write-GitToolsLogInternal -Level "STEP" -Message "Checking for tag on current commit..."
|
||||
$tag = Invoke-GitInternal -Arguments @("describe", "--tags", "--exact-match", "HEAD") -CaptureOutput -ErrorMessage "No tag found on current commit. Create a tag: git tag v$Version"
|
||||
return $tag
|
||||
}
|
||||
|
||||
# Used by:
|
||||
# - utils/Force-AmendTaggedCommit/Force-AmendTaggedCommit.ps1
|
||||
# Purpose:
|
||||
# - Get all tag names pointing at HEAD.
|
||||
function Get-HeadTags {
|
||||
$tagsRaw = Invoke-GitInternal -Arguments @("tag", "--points-at", "HEAD") -CaptureOutput -ErrorMessage "Failed to list tags on HEAD"
|
||||
|
||||
if ([string]::IsNullOrWhiteSpace($tagsRaw)) {
|
||||
return @()
|
||||
}
|
||||
|
||||
return @($tagsRaw -split "`r?`n" | Where-Object { -not [string]::IsNullOrWhiteSpace($_) } | ForEach-Object { $_.Trim() })
|
||||
}
|
||||
|
||||
# Used by:
|
||||
# - utils/Release-NuGetPackage/Release-NuGetPackage.ps1
|
||||
# Purpose:
|
||||
# - Check whether a given tag exists on the remote.
|
||||
function Test-RemoteTagExists {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$Tag,
|
||||
|
||||
[Parameter(Mandatory = $false)]
|
||||
[string]$Remote = "origin"
|
||||
)
|
||||
|
||||
$remoteTag = Invoke-GitInternal -Arguments @("ls-remote", "--tags", $Remote, $Tag) -CaptureOutput -ErrorMessage "Failed to check remote tag existence"
|
||||
return [bool]$remoteTag
|
||||
}
|
||||
|
||||
# Used by:
|
||||
# - utils/Release-NuGetPackage/Release-NuGetPackage.ps1
|
||||
# - utils/Force-AmendTaggedCommit/Force-AmendTaggedCommit.ps1
|
||||
# Purpose:
|
||||
# - Push tag to remote (optionally with `--force`).
|
||||
function Push-TagToRemote {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$Tag,
|
||||
|
||||
[Parameter(Mandatory = $false)]
|
||||
[string]$Remote = "origin",
|
||||
|
||||
[Parameter(Mandatory = $false)]
|
||||
[switch]$Force
|
||||
)
|
||||
|
||||
$pushArgs = @("push")
|
||||
if ($Force) {
|
||||
$pushArgs += "--force"
|
||||
}
|
||||
$pushArgs += @($Remote, $Tag)
|
||||
|
||||
Invoke-GitInternal -Arguments $pushArgs -ErrorMessage "Failed to push tag $Tag to remote $Remote"
|
||||
}
|
||||
|
||||
# Used by:
|
||||
# - utils/Force-AmendTaggedCommit/Force-AmendTaggedCommit.ps1
|
||||
# Purpose:
|
||||
# - Push branch to remote (optionally with `--force`).
|
||||
function Push-BranchToRemote {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$Branch,
|
||||
|
||||
[Parameter(Mandatory = $false)]
|
||||
[string]$Remote = "origin",
|
||||
|
||||
[Parameter(Mandatory = $false)]
|
||||
[switch]$Force
|
||||
)
|
||||
|
||||
$pushArgs = @("push")
|
||||
if ($Force) {
|
||||
$pushArgs += "--force"
|
||||
}
|
||||
$pushArgs += @($Remote, $Branch)
|
||||
|
||||
Invoke-GitInternal -Arguments $pushArgs -ErrorMessage "Failed to push branch $Branch to remote $Remote"
|
||||
}
|
||||
|
||||
# Used by:
|
||||
# - utils/Force-AmendTaggedCommit/Force-AmendTaggedCommit.ps1
|
||||
# Purpose:
|
||||
# - Get HEAD commit hash.
|
||||
function Get-HeadCommitHash {
|
||||
param(
|
||||
[Parameter(Mandatory = $false)]
|
||||
[switch]$Short
|
||||
)
|
||||
|
||||
$format = if ($Short) { "--format=%h" } else { "--format=%H" }
|
||||
return Invoke-GitInternal -Arguments @("log", "-1", $format) -CaptureOutput -ErrorMessage "Failed to get HEAD commit hash"
|
||||
}
|
||||
|
||||
# Used by:
|
||||
# - utils/Force-AmendTaggedCommit/Force-AmendTaggedCommit.ps1
|
||||
# Purpose:
|
||||
# - Get HEAD commit subject line.
|
||||
function Get-HeadCommitMessage {
|
||||
return Invoke-GitInternal -Arguments @("log", "-1", "--format=%s") -CaptureOutput -ErrorMessage "Failed to get HEAD commit message"
|
||||
}
|
||||
|
||||
# Used by:
|
||||
# - utils/Force-AmendTaggedCommit/Force-AmendTaggedCommit.ps1
|
||||
# Purpose:
|
||||
# - Stage all changes (tracked, untracked, deletions).
|
||||
function Add-AllChanges {
|
||||
Invoke-GitInternal -Arguments @("add", "-A") -ErrorMessage "Failed to stage changes"
|
||||
}
|
||||
|
||||
# Used by:
|
||||
# - utils/Force-AmendTaggedCommit/Force-AmendTaggedCommit.ps1
|
||||
# Purpose:
|
||||
# - Amend HEAD commit and keep existing commit message.
|
||||
function Update-HeadCommitNoEdit {
|
||||
Invoke-GitInternal -Arguments @("commit", "--amend", "--no-edit") -ErrorMessage "Failed to amend commit"
|
||||
}
|
||||
|
||||
# Used by:
|
||||
# - utils/Force-AmendTaggedCommit/Force-AmendTaggedCommit.ps1
|
||||
# Purpose:
|
||||
# - Delete local tag.
|
||||
function Remove-LocalTag {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$Tag
|
||||
)
|
||||
|
||||
Invoke-GitInternal -Arguments @("tag", "-d", $Tag) -ErrorMessage "Failed to delete local tag"
|
||||
}
|
||||
|
||||
# Used by:
|
||||
# - utils/Force-AmendTaggedCommit/Force-AmendTaggedCommit.ps1
|
||||
# Purpose:
|
||||
# - Create local tag.
|
||||
function New-LocalTag {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$Tag
|
||||
)
|
||||
|
||||
Invoke-GitInternal -Arguments @("tag", $Tag) -ErrorMessage "Failed to create tag"
|
||||
}
|
||||
|
||||
# Used by:
|
||||
# - utils/Force-AmendTaggedCommit/Force-AmendTaggedCommit.ps1
|
||||
# Purpose:
|
||||
# - Get HEAD one-line commit info.
|
||||
function Get-HeadCommitOneLine {
|
||||
return Invoke-GitInternal -Arguments @("log", "-1", "--oneline") -CaptureOutput -ErrorMessage "Failed to read final commit state"
|
||||
}
|
||||
|
||||
Export-ModuleMember -Function Get-CurrentBranch, Get-GitStatusShort, Get-CurrentCommitTag, Get-HeadTags, Test-RemoteTagExists, Push-TagToRemote, Push-BranchToRemote, Get-HeadCommitHash, Get-HeadCommitMessage, Add-AllChanges, Update-HeadCommitNoEdit, Remove-LocalTag, New-LocalTag, Get-HeadCommitOneLine
|
||||
67
utils/Logging.psm1
Normal file
67
utils/Logging.psm1
Normal file
@ -0,0 +1,67 @@
|
||||
function Get-LogTimestampInternal {
|
||||
return (Get-Date).ToString("yyyy-MM-dd HH:mm:ss")
|
||||
}
|
||||
|
||||
function Get-LogColorInternal {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$Level
|
||||
)
|
||||
|
||||
switch ($Level.ToUpperInvariant()) {
|
||||
"OK" { return "Green" }
|
||||
"INFO" { return "Gray" }
|
||||
"WARN" { return "Yellow" }
|
||||
"ERROR" { return "Red" }
|
||||
"STEP" { return "Cyan" }
|
||||
"DEBUG" { return "DarkGray" }
|
||||
default { return "White" }
|
||||
}
|
||||
}
|
||||
|
||||
function Write-Log {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$Message,
|
||||
|
||||
[Parameter(Mandatory = $false)]
|
||||
[ValidateSet("INFO", "OK", "WARN", "ERROR", "STEP", "DEBUG")]
|
||||
[string]$Level = "INFO",
|
||||
|
||||
[Parameter(Mandatory = $false)]
|
||||
[switch]$NoTimestamp
|
||||
)
|
||||
|
||||
$levelToken = "[$($Level.ToUpperInvariant())]"
|
||||
$padding = " " * [Math]::Max(1, (10 - $levelToken.Length))
|
||||
$prefix = if ($NoTimestamp) { "" } else { "[$(Get-LogTimestampInternal)] " }
|
||||
$line = "$prefix$levelToken$padding$Message"
|
||||
|
||||
Write-Host $line -ForegroundColor (Get-LogColorInternal -Level $Level)
|
||||
}
|
||||
|
||||
function Write-LogStep {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$Message
|
||||
)
|
||||
|
||||
Write-Log -Level "STEP" -Message $Message
|
||||
}
|
||||
|
||||
function Write-LogStepResult {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[ValidateSet("OK", "FAIL")]
|
||||
[string]$Status,
|
||||
|
||||
[Parameter(Mandatory = $false)]
|
||||
[string]$Message
|
||||
)
|
||||
|
||||
$level = if ($Status -eq "FAIL") { "ERROR" } else { "OK" }
|
||||
$text = if ([string]::IsNullOrWhiteSpace($Message)) { $Status } else { $Message }
|
||||
Write-Log -Level $level -Message $text
|
||||
}
|
||||
|
||||
Export-ModuleMember -Function Write-Log, Write-LogStep, Write-LogStepResult
|
||||
3
utils/Release-NuGetPackage/Release-NuGetPackage.bat
Normal file
3
utils/Release-NuGetPackage/Release-NuGetPackage.bat
Normal file
@ -0,0 +1,3 @@
|
||||
@echo off
|
||||
powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0Release-NuGetPackage.ps1"
|
||||
pause
|
||||
751
utils/Release-NuGetPackage/Release-NuGetPackage.ps1
Normal file
751
utils/Release-NuGetPackage/Release-NuGetPackage.ps1
Normal file
@ -0,0 +1,751 @@
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Release script for MaksIT.Core NuGet package and GitHub release.
|
||||
|
||||
.DESCRIPTION
|
||||
This script automates the release process for MaksIT.Core library.
|
||||
The script is IDEMPOTENT - you can safely re-run it if any step fails.
|
||||
It will skip already-completed steps (NuGet and GitHub) and only create what's missing.
|
||||
|
||||
Features:
|
||||
- Validates environment and prerequisites
|
||||
- Checks if version already exists on NuGet.org (skips if released)
|
||||
- Checks if GitHub release exists (skips if released)
|
||||
- Scans for vulnerable packages (security check)
|
||||
- Builds and tests the project (Windows + Linux via Docker)
|
||||
- Collects code coverage with Coverlet (threshold enforcement optional)
|
||||
- Generates test result artifacts (TRX format) and coverage reports
|
||||
- Displays test results with pass/fail counts and coverage percentage
|
||||
- Publishes to NuGet.org
|
||||
- Creates a GitHub release with changelog and NuGet package assets
|
||||
- Shows timing summary for all steps
|
||||
|
||||
.REQUIREMENTS
|
||||
Environment Variables:
|
||||
- NUGET_MAKS_IT : NuGet.org API key for publishing packages
|
||||
- GITHUB_MAKS_IT_COM : GitHub Personal Access Token (needs 'repo' scope)
|
||||
|
||||
Tools (Required):
|
||||
- dotnet CLI : For building, testing, and packing
|
||||
- git : For version control operations
|
||||
- gh (GitHub CLI) : For creating GitHub releases
|
||||
- docker : For cross-platform Linux testing
|
||||
|
||||
.WORKFLOW
|
||||
1. VALIDATION PHASE
|
||||
- Check required environment variables (NuGet key, GitHub token)
|
||||
- Check required tools are installed (dotnet, git, gh, docker)
|
||||
- Verify no uncommitted changes in working directory
|
||||
- Authenticate GitHub CLI
|
||||
|
||||
2. VERSION & RELEASE CHECK PHASE (Idempotent)
|
||||
- Read latest version from CHANGELOG.md
|
||||
- Find commit with matching version tag
|
||||
- Validate tag is on configured release branch (from scriptsettings.json)
|
||||
- Check if already released on NuGet.org (mark for skip if yes)
|
||||
- Check if GitHub release exists (mark for skip if yes)
|
||||
- Read target framework from MaksIT.Core.csproj
|
||||
- Extract release notes from CHANGELOG.md for current version
|
||||
|
||||
3. SECURITY SCAN
|
||||
- Check for vulnerable packages (dotnet list package --vulnerable)
|
||||
- Fail or warn based on $failOnVulnerabilities setting
|
||||
|
||||
4. BUILD & TEST PHASE
|
||||
- Clean previous builds (delete bin/obj folders)
|
||||
- Restore NuGet packages
|
||||
- Windows: Build main project -> Build test project -> Run tests with coverage
|
||||
- Analyze code coverage (fail if below threshold when configured)
|
||||
- Linux (Docker): Build main project -> Build test project -> Run tests (TRX report)
|
||||
- Rebuild for Windows (Docker may overwrite bin/obj)
|
||||
- Create NuGet package (.nupkg) and symbols (.snupkg)
|
||||
- All steps are timed for performance tracking
|
||||
|
||||
5. CONFIRMATION PHASE
|
||||
- Display release summary
|
||||
- Prompt user for confirmation before proceeding
|
||||
|
||||
6. NUGET RELEASE PHASE (Idempotent)
|
||||
- Skip if version already exists on NuGet.org
|
||||
- Otherwise, push package to NuGet.org
|
||||
|
||||
7. GITHUB RELEASE PHASE (Idempotent)
|
||||
- Skip if release already exists
|
||||
- Push tag to remote if not already there
|
||||
- Create GitHub release with:
|
||||
* Release notes from CHANGELOG.md
|
||||
* .nupkg and .snupkg as downloadable assets
|
||||
|
||||
8. COMPLETION PHASE
|
||||
- Display timing summary for all steps
|
||||
- Display test results summary
|
||||
- Display success summary with links
|
||||
- Open NuGet and GitHub release pages in browser
|
||||
- TODO: Email notification (template provided)
|
||||
- TODO: Package signing (template provided)
|
||||
|
||||
.USAGE
|
||||
Before running:
|
||||
1. Ensure Docker Desktop is running (for Linux tests)
|
||||
2. Update version in MaksIT.Core.csproj
|
||||
3. Run .\Generate-Changelog.ps1 to update CHANGELOG.md and LICENSE.md
|
||||
4. Review and commit all changes
|
||||
5. Create version tag: git tag v1.x.x
|
||||
6. Run: .\Release-NuGetPackage.ps1
|
||||
|
||||
Note: The script finds the commit with the tag matching CHANGELOG.md version.
|
||||
You can run it from any branch/commit - it releases the tagged commit.
|
||||
|
||||
Re-run release (idempotent - skips NuGet/GitHub if already released):
|
||||
.\Release-NuGetPackage.ps1
|
||||
|
||||
Generate changelog and update LICENSE year:
|
||||
.\Generate-Changelog.ps1
|
||||
|
||||
.CONFIGURATION
|
||||
All settings are stored in scriptsettings.json:
|
||||
- qualityGates: Coverage threshold, vulnerability checks
|
||||
- packageSigning: Code signing certificate configuration
|
||||
- emailNotification: SMTP settings for release notifications
|
||||
|
||||
.NOTES
|
||||
Author: Maksym Sadovnychyy (MAKS-IT)
|
||||
Repository: https://github.com/MAKS-IT-COM/maksit-core
|
||||
#>
|
||||
|
||||
# No parameters - behavior is controlled by current branch (configured in scriptsettings.json):
|
||||
# - dev branch -> Local build only (no tag required, uncommitted changes allowed)
|
||||
# - release branch -> Full release to GitHub (tag required, clean working directory)
|
||||
|
||||
# Get the directory of the current script (for loading settings and relative paths)
|
||||
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
|
||||
|
||||
#region Import Modules
|
||||
|
||||
# Import TestRunner module
|
||||
$utilsDir = Split-Path $scriptDir -Parent
|
||||
|
||||
$testRunnerModulePath = Join-Path $utilsDir "TestRunner.psm1"
|
||||
if (-not (Test-Path $testRunnerModulePath)) {
|
||||
Write-Error "TestRunner module not found at: $testRunnerModulePath"
|
||||
exit 1
|
||||
}
|
||||
|
||||
Import-Module $testRunnerModulePath -Force
|
||||
|
||||
# Import ScriptConfig module
|
||||
$scriptConfigModulePath = Join-Path $utilsDir "ScriptConfig.psm1"
|
||||
if (-not (Test-Path $scriptConfigModulePath)) {
|
||||
Write-Error "ScriptConfig module not found at: $scriptConfigModulePath"
|
||||
exit 1
|
||||
}
|
||||
|
||||
Import-Module $scriptConfigModulePath -Force
|
||||
|
||||
# Import Logging module
|
||||
$loggingModulePath = Join-Path $utilsDir "Logging.psm1"
|
||||
if (-not (Test-Path $loggingModulePath)) {
|
||||
Write-Error "Logging module not found at: $loggingModulePath"
|
||||
exit 1
|
||||
}
|
||||
|
||||
Import-Module $loggingModulePath -Force
|
||||
|
||||
|
||||
# Import GitTools module
|
||||
$gitToolsModulePath = Join-Path $utilsDir "GitTools.psm1"
|
||||
if (-not (Test-Path $gitToolsModulePath)) {
|
||||
Write-Error "GitTools module not found at: $gitToolsModulePath"
|
||||
exit 1
|
||||
}
|
||||
|
||||
Import-Module $gitToolsModulePath -Force
|
||||
|
||||
#endregion
|
||||
|
||||
#region Load Settings
|
||||
$settings = Get-ScriptSettings -ScriptDir $scriptDir
|
||||
|
||||
#endregion
|
||||
|
||||
#region Environment Helpers
|
||||
|
||||
# Resolve environment variable in Process -> User -> Machine order.
|
||||
function Get-EnvVarValue {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$Name
|
||||
)
|
||||
|
||||
$processValue = [System.Environment]::GetEnvironmentVariable($Name, [System.EnvironmentVariableTarget]::Process)
|
||||
if (-not [string]::IsNullOrWhiteSpace($processValue)) {
|
||||
return $processValue
|
||||
}
|
||||
|
||||
$userValue = [System.Environment]::GetEnvironmentVariable($Name, [System.EnvironmentVariableTarget]::User)
|
||||
if (-not [string]::IsNullOrWhiteSpace($userValue)) {
|
||||
return $userValue
|
||||
}
|
||||
|
||||
$machineValue = [System.Environment]::GetEnvironmentVariable($Name, [System.EnvironmentVariableTarget]::Machine)
|
||||
if (-not [string]::IsNullOrWhiteSpace($machineValue)) {
|
||||
return $machineValue
|
||||
}
|
||||
|
||||
return $null
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Configuration
|
||||
|
||||
# GitHub configuration
|
||||
$githubReleseEnabled = $settings.github.enabled
|
||||
$githubTokenEnvVar = $settings.github.githubToken
|
||||
$githubToken = Get-EnvVarValue -Name $githubTokenEnvVar
|
||||
|
||||
# NuGet configuration
|
||||
$nugetReleseEnabled = $settings.nuget.enabled
|
||||
$nugetApiKeyEnvVar = $settings.nuget.nugetApiKey
|
||||
$nugetApiKey = Get-EnvVarValue -Name $nugetApiKeyEnvVar
|
||||
$nugetSource = if ($settings.nuget.source) { $settings.nuget.source } else { "https://api.nuget.org/v3/index.json" }
|
||||
|
||||
# Paths from settings (resolve relative to script directory)
|
||||
$csprojPaths = @()
|
||||
$rawCsprojPaths = @()
|
||||
|
||||
if ($settings.paths.csprojPaths) {
|
||||
if ($settings.paths.csprojPaths -is [System.Collections.IEnumerable] -and -not ($settings.paths.csprojPaths -is [string])) {
|
||||
$rawCsprojPaths += $settings.paths.csprojPaths
|
||||
}
|
||||
else {
|
||||
$rawCsprojPaths += $settings.paths.csprojPaths
|
||||
}
|
||||
}
|
||||
else {
|
||||
Write-Error "No csproj path configured. Set 'paths.csprojPaths' (preferred) or 'paths.csprojPath' in scriptsettings.json."
|
||||
exit 1
|
||||
}
|
||||
|
||||
foreach ($path in $rawCsprojPaths) {
|
||||
if ([string]::IsNullOrWhiteSpace($path)) {
|
||||
continue
|
||||
}
|
||||
|
||||
$resolvedPath = [System.IO.Path]::GetFullPath((Join-Path $scriptDir $path))
|
||||
$csprojPaths += $resolvedPath
|
||||
}
|
||||
|
||||
if ($csprojPaths.Count -eq 0) {
|
||||
Write-Error "No valid csproj paths configured in scriptsettings.json."
|
||||
exit 1
|
||||
}
|
||||
|
||||
$testResultsDir = [System.IO.Path]::GetFullPath((Join-Path $scriptDir $settings.paths.testResultsDir))
|
||||
$releaseDir = [System.IO.Path]::GetFullPath((Join-Path $scriptDir $settings.paths.releaseDir))
|
||||
$changelogPath = [System.IO.Path]::GetFullPath((Join-Path $scriptDir $settings.paths.changelogPath))
|
||||
$testProjectPath = [System.IO.Path]::GetFullPath((Join-Path $scriptDir $settings.paths.testProject))
|
||||
|
||||
# Release naming pattern
|
||||
$zipNamePattern = $settings.release.zipNamePattern
|
||||
$releaseTitlePattern = $settings.release.releaseTitlePattern
|
||||
|
||||
# Branch configuration
|
||||
$releaseBranch = $settings.branches.release
|
||||
$devBranch = $settings.branches.dev
|
||||
|
||||
#endregion
|
||||
|
||||
#region Helpers
|
||||
|
||||
# Helper: extract a csproj property (first match)
|
||||
function Get-CsprojPropertyValue {
|
||||
param(
|
||||
[Parameter(Mandatory=$true)][xml]$csproj,
|
||||
[Parameter(Mandatory=$true)][string]$propertyName
|
||||
)
|
||||
|
||||
$propNode = $csproj.Project.PropertyGroup |
|
||||
Where-Object { $_.$propertyName } |
|
||||
Select-Object -First 1
|
||||
|
||||
if ($propNode) {
|
||||
return $propNode.$propertyName
|
||||
}
|
||||
|
||||
return $null
|
||||
}
|
||||
|
||||
# Helper: check for uncommitted changes
|
||||
function Assert-WorkingTreeClean {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[bool]$IsReleaseBranch
|
||||
)
|
||||
|
||||
$gitStatus = Get-GitStatusShort
|
||||
if ($gitStatus) {
|
||||
if ($IsReleaseBranch) {
|
||||
Write-Error "Working directory has uncommitted changes. Commit or stash them before releasing."
|
||||
Write-Log -Level "WARN" -Message "Uncommitted files:"
|
||||
$gitStatus | ForEach-Object { Write-Log -Level "WARN" -Message " $_" }
|
||||
exit 1
|
||||
}
|
||||
else {
|
||||
Write-Log -Level "WARN" -Message " Uncommitted changes detected (allowed on dev branch)."
|
||||
}
|
||||
}
|
||||
else {
|
||||
Write-Log -Level "OK" -Message " Working directory is clean."
|
||||
}
|
||||
}
|
||||
|
||||
# Helper: read versions from csproj files
|
||||
function Get-CsprojVersions {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string[]]$CsprojPaths
|
||||
)
|
||||
|
||||
Write-Log -Level "INFO" -Message "Reading version(s) from csproj(s)..."
|
||||
$projectVersions = @{}
|
||||
|
||||
foreach ($projPath in $CsprojPaths) {
|
||||
if (-not (Test-Path $projPath -PathType Leaf)) {
|
||||
Write-Error "Csproj file not found at: $projPath"
|
||||
exit 1
|
||||
}
|
||||
|
||||
if ([System.IO.Path]::GetExtension($projPath) -ne ".csproj") {
|
||||
Write-Error "Configured path is not a .csproj file: $projPath"
|
||||
exit 1
|
||||
}
|
||||
|
||||
[xml]$csproj = Get-Content $projPath
|
||||
$version = Get-CsprojPropertyValue -csproj $csproj -propertyName "Version"
|
||||
|
||||
if (-not $version) {
|
||||
Write-Error "Version not found in $projPath"
|
||||
exit 1
|
||||
}
|
||||
|
||||
$projectVersions[$projPath] = $version
|
||||
Write-Log -Level "OK" -Message " $([System.IO.Path]::GetFileName($projPath)): $version"
|
||||
}
|
||||
|
||||
return $projectVersions
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Validate CLI Dependencies
|
||||
|
||||
Assert-Command dotnet
|
||||
Assert-Command git
|
||||
Assert-Command docker
|
||||
# gh command check deferred until after branch detection (only needed on release branch)
|
||||
|
||||
#endregion
|
||||
|
||||
#region Main
|
||||
|
||||
Write-Log -Level "STEP" -Message "=================================================="
|
||||
Write-Log -Level "STEP" -Message "RELEASE BUILD"
|
||||
Write-Log -Level "STEP" -Message "=================================================="
|
||||
|
||||
#region Preflight
|
||||
|
||||
$isDevBranch = $false
|
||||
$isReleaseBranch = $false
|
||||
|
||||
# 1. Detect current branch and determine release mode
|
||||
$currentBranch = Get-CurrentBranch
|
||||
|
||||
$isDevBranch = $currentBranch -eq $devBranch
|
||||
$isReleaseBranch = $currentBranch -eq $releaseBranch
|
||||
|
||||
if (-not $isDevBranch -and -not $isReleaseBranch) {
|
||||
Write-Error "Releases can only be created from '$releaseBranch' or '$devBranch' branches. Current branch: $currentBranch"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# 2. Check for uncommitted changes (required on release branch, allowed on dev)
|
||||
Assert-WorkingTreeClean -IsReleaseBranch:$isReleaseBranch
|
||||
|
||||
# 3. Get version from csproj (source of truth)
|
||||
$projectVersions = Get-CsprojVersions -CsprojPaths $csprojPaths
|
||||
|
||||
# Use the first project's version as the release version
|
||||
$version = $projectVersions[$csprojPaths[0]]
|
||||
|
||||
# 4. Handle tag based on branch
|
||||
if ($isReleaseBranch) {
|
||||
# Release branch: tag is required and must match version
|
||||
$tag = Get-CurrentCommitTag -Version $version
|
||||
|
||||
if ($tag -notmatch '^v(\d+\.\d+\.\d+)$') {
|
||||
Write-Error "Tag '$tag' does not match expected format 'vX.Y.Z' (e.g., v$version)."
|
||||
exit 1
|
||||
}
|
||||
|
||||
$tagVersion = $Matches[1]
|
||||
|
||||
if ($tagVersion -ne $version) {
|
||||
Write-Error "Tag version ($tagVersion) does not match csproj version ($version)."
|
||||
Write-Log -Level "WARN" -Message " Either update the tag or the csproj version."
|
||||
exit 1
|
||||
}
|
||||
|
||||
Write-Log -Level "OK" -Message " Tag found: $tag (matches csproj)"
|
||||
}
|
||||
else {
|
||||
# Dev branch: no tag required, use version from csproj
|
||||
$tag = "v$version"
|
||||
Write-Log -Level "INFO" -Message " Using version from csproj (no tag required on dev)."
|
||||
}
|
||||
|
||||
# 5. Verify CHANGELOG.md has matching version entry
|
||||
Write-Log -Level "INFO" -Message "Verifying CHANGELOG.md..."
|
||||
if (-not (Test-Path $changelogPath)) {
|
||||
Write-Error "CHANGELOG.md not found at: $changelogPath"
|
||||
exit 1
|
||||
}
|
||||
|
||||
$changelog = Get-Content $changelogPath -Raw
|
||||
|
||||
if ($changelog -notmatch '##\s+v(\d+\.\d+\.\d+)') {
|
||||
Write-Error "No version entry found in CHANGELOG.md"
|
||||
exit 1
|
||||
}
|
||||
|
||||
$changelogVersion = $Matches[1]
|
||||
|
||||
if ($changelogVersion -ne $version) {
|
||||
Write-Error "Csproj version ($version) does not match latest CHANGELOG.md version ($changelogVersion)."
|
||||
Write-Log -Level "WARN" -Message " Update CHANGELOG.md or the csproj version."
|
||||
exit 1
|
||||
}
|
||||
|
||||
Write-Log -Level "OK" -Message " CHANGELOG.md version matches: v$changelogVersion"
|
||||
|
||||
|
||||
|
||||
Write-Log -Level "OK" -Message "All pre-flight checks passed!"
|
||||
|
||||
#endregion
|
||||
|
||||
#region Test
|
||||
|
||||
Write-Log -Level "STEP" -Message "Running tests..."
|
||||
|
||||
# Run tests using TestRunner module
|
||||
$testResult = Invoke-TestsWithCoverage -TestProjectPath $testProjectPath -ResultsDirectory $testResultsDir -Silent
|
||||
|
||||
if (-not $testResult.Success) {
|
||||
Write-Error "Tests failed. Release aborted."
|
||||
Write-Log -Level "ERROR" -Message " Error: $($testResult.Error)"
|
||||
exit 1
|
||||
}
|
||||
|
||||
Write-Log -Level "OK" -Message " All tests passed!"
|
||||
Write-Log -Level "INFO" -Message " Line Coverage: $($testResult.LineRate)%"
|
||||
Write-Log -Level "INFO" -Message " Branch Coverage: $($testResult.BranchRate)%"
|
||||
Write-Log -Level "INFO" -Message " Method Coverage: $($testResult.MethodRate)%"
|
||||
|
||||
#endregion
|
||||
|
||||
#region Build And Publish
|
||||
|
||||
# 7. Prepare release directory
|
||||
if (!(Test-Path $releaseDir)) {
|
||||
New-Item -ItemType Directory -Path $releaseDir | Out-Null
|
||||
}
|
||||
|
||||
|
||||
# 8. Pack NuGet package and resolve produced .nupkg/.snupkg files
|
||||
$packageProjectPath = $csprojPaths[0]
|
||||
Write-Log -Level "STEP" -Message "Packing NuGet package..."
|
||||
dotnet pack $packageProjectPath -c Release -o $releaseDir --nologo `
|
||||
-p:IncludeSymbols=true `
|
||||
-p:SymbolPackageFormat=snupkg
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
Write-Error "dotnet pack failed for $packageProjectPath."
|
||||
exit 1
|
||||
}
|
||||
|
||||
$packageFile = Get-ChildItem -Path $releaseDir -Filter "*.nupkg" |
|
||||
Where-Object {
|
||||
$_.Name -like "*$version*.nupkg" -and
|
||||
$_.Name -notlike "*.symbols.nupkg" -and
|
||||
$_.Name -notlike "*.snupkg"
|
||||
} |
|
||||
Sort-Object LastWriteTime -Descending |
|
||||
Select-Object -First 1
|
||||
|
||||
if (-not $packageFile) {
|
||||
Write-Error "Could not locate generated NuGet package for version $version in: $releaseDir"
|
||||
exit 1
|
||||
}
|
||||
|
||||
Write-Log -Level "OK" -Message " Package ready: $($packageFile.FullName)"
|
||||
|
||||
# Find the symbols package if available
|
||||
$symbolsPackageFile = Get-ChildItem -Path $releaseDir -Filter "*.snupkg" |
|
||||
Where-Object { $_.Name -like "*$version*.snupkg" } |
|
||||
Sort-Object LastWriteTime -Descending |
|
||||
Select-Object -First 1
|
||||
|
||||
if ($symbolsPackageFile) {
|
||||
Write-Log -Level "OK" -Message " Symbols package ready: $($symbolsPackageFile.FullName)"
|
||||
}
|
||||
else {
|
||||
Write-Log -Level "WARN" -Message " Symbols package (.snupkg) not found for version $version."
|
||||
}
|
||||
|
||||
# 9. Create release archive with NuGet package artifacts
|
||||
Write-Log -Level "STEP" -Message "Creating release archive..."
|
||||
$resolvedZipNamePattern = if ([string]::IsNullOrWhiteSpace($zipNamePattern)) { "release-{version}.zip" } else { $zipNamePattern }
|
||||
$zipFileName = $resolvedZipNamePattern -replace '\{version\}', $version
|
||||
$zipPath = Join-Path $releaseDir $zipFileName
|
||||
|
||||
if (Test-Path $zipPath) {
|
||||
Remove-Item $zipPath -Force
|
||||
}
|
||||
|
||||
$archiveArtifacts = @($packageFile.FullName)
|
||||
if ($symbolsPackageFile) {
|
||||
$archiveArtifacts += $symbolsPackageFile.FullName
|
||||
}
|
||||
|
||||
Compress-Archive -Path $archiveArtifacts -DestinationPath $zipPath -CompressionLevel Optimal -Force
|
||||
|
||||
if (-not (Test-Path $zipPath)) {
|
||||
Write-Error "Failed to create release archive at: $zipPath"
|
||||
exit 1
|
||||
}
|
||||
|
||||
Write-Log -Level "OK" -Message " Release archive ready: $zipPath"
|
||||
|
||||
# 10. Extract release notes from CHANGELOG.md
|
||||
Write-Log -Level "STEP" -Message "Extracting release notes..."
|
||||
$pattern = "(?ms)^##\s+v$([regex]::Escape($version))\b.*?(?=^##\s+v\d+\.\d+\.\d+|\Z)"
|
||||
$match = [regex]::Match($changelog, $pattern)
|
||||
|
||||
if (-not $match.Success) {
|
||||
Write-Error "Changelog entry for version $version not found."
|
||||
exit 1
|
||||
}
|
||||
|
||||
$releaseNotes = $match.Value.Trim()
|
||||
Write-Log -Level "OK" -Message " Release notes extracted."
|
||||
|
||||
# 11. Get repository info
|
||||
$remoteUrl = git config --get remote.origin.url
|
||||
if ($LASTEXITCODE -ne 0 -or -not $remoteUrl) {
|
||||
Write-Error "Could not determine git remote origin URL."
|
||||
exit 1
|
||||
}
|
||||
|
||||
if ($remoteUrl -match "[:/](?<owner>[^/]+)/(?<repo>[^/.]+)(\.git)?$") {
|
||||
$owner = $matches['owner']
|
||||
$repoName = $matches['repo']
|
||||
$repo = "$owner/$repoName"
|
||||
} else {
|
||||
Write-Error "Could not parse GitHub repo from remote URL: $remoteUrl"
|
||||
exit 1
|
||||
}
|
||||
|
||||
$releaseName = $releaseTitlePattern -replace '\{version\}', $version
|
||||
|
||||
Write-Log -Level "STEP" -Message "Release Summary:"
|
||||
Write-Log -Level "INFO" -Message " Repository: $repo"
|
||||
Write-Log -Level "INFO" -Message " Tag: $tag"
|
||||
Write-Log -Level "INFO" -Message " Title: $releaseName"
|
||||
|
||||
# 12. Check if tag is pushed to remote (skip on dev branch)
|
||||
|
||||
if (-not $isDevBranch) {
|
||||
|
||||
Write-Log -Level "STEP" -Message "Verifying tag is pushed to remote..."
|
||||
$remoteTagExists = Test-RemoteTagExists -Tag $tag -Remote "origin"
|
||||
if (-not $remoteTagExists) {
|
||||
Write-Log -Level "WARN" -Message " Tag $tag not found on remote. Pushing..."
|
||||
Push-TagToRemote -Tag $tag -Remote "origin"
|
||||
}
|
||||
else {
|
||||
Write-Log -Level "OK" -Message " Tag exists on remote."
|
||||
}
|
||||
|
||||
|
||||
|
||||
# Release to GitHub
|
||||
if ($githubReleseEnabled) {
|
||||
|
||||
Write-Log -Level "STEP" -Message " Release branch ($releaseBranch) - will publish to GitHub."
|
||||
Assert-Command gh
|
||||
|
||||
# 6. Check GitHub authentication
|
||||
|
||||
Write-Log -Level "INFO" -Message "Checking GitHub authentication..."
|
||||
if (-not $githubToken) {
|
||||
Write-Error "GitHub token is not set. Set '$githubTokenEnvVar' and rerun."
|
||||
exit 1
|
||||
}
|
||||
|
||||
$previousGhTokenForAuth = $env:GH_TOKEN
|
||||
$env:GH_TOKEN = $githubToken
|
||||
$authTest = & gh api user 2>$null
|
||||
|
||||
if ($LASTEXITCODE -ne 0 -or -not $authTest) {
|
||||
if ($null -ne $previousGhTokenForAuth) {
|
||||
$env:GH_TOKEN = $previousGhTokenForAuth
|
||||
}
|
||||
else {
|
||||
Remove-Item Env:GH_TOKEN -ErrorAction SilentlyContinue
|
||||
}
|
||||
Write-Error "GitHub CLI authentication failed. GitHub token may be invalid or missing repo scope."
|
||||
exit 1
|
||||
}
|
||||
|
||||
if ($null -ne $previousGhTokenForAuth) {
|
||||
$env:GH_TOKEN = $previousGhTokenForAuth
|
||||
}
|
||||
else {
|
||||
Remove-Item Env:GH_TOKEN -ErrorAction SilentlyContinue
|
||||
}
|
||||
|
||||
Write-Log -Level "OK" -Message " GitHub CLI authenticated."
|
||||
|
||||
# 13. Create or update GitHub release
|
||||
Write-Log -Level "STEP" -Message "Creating GitHub release..."
|
||||
|
||||
# gh release subcommands do not support custom auth headers.
|
||||
# Scope GH_TOKEN to this block so commands authenticate with the configured token.
|
||||
$previousGhToken = $env:GH_TOKEN
|
||||
$env:GH_TOKEN = $githubToken
|
||||
|
||||
try {
|
||||
# Check if release already exists
|
||||
$releaseViewArgs = @(
|
||||
"release", "view", $tag,
|
||||
"--repo", $repo
|
||||
)
|
||||
& gh @releaseViewArgs 2>$null
|
||||
|
||||
if ($LASTEXITCODE -eq 0) {
|
||||
Write-Log -Level "WARN" -Message " Release $tag already exists. Deleting..."
|
||||
$releaseDeleteArgs = @("release", "delete", $tag, "--repo", $repo, "--yes")
|
||||
& gh @releaseDeleteArgs
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
Write-Error "Failed to delete existing release $tag."
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
|
||||
# Create release using the existing tag
|
||||
# Write release notes to a temp file to avoid shell interpretation issues with special characters
|
||||
$notesFilePath = Join-Path $releaseDir "release-notes-temp.md"
|
||||
[System.IO.File]::WriteAllText($notesFilePath, $releaseNotes, [System.Text.UTF8Encoding]::new($false))
|
||||
|
||||
$releaseAssets = @($packageFile.FullName)
|
||||
if ($symbolsPackageFile) {
|
||||
$releaseAssets += $symbolsPackageFile.FullName
|
||||
}
|
||||
|
||||
$createReleaseArgs = @("release", "create", $tag) + $releaseAssets + @(
|
||||
"--repo", $repo
|
||||
"--title", $releaseName
|
||||
"--notes-file", $notesFilePath
|
||||
)
|
||||
& gh @createReleaseArgs
|
||||
|
||||
$ghExitCode = $LASTEXITCODE
|
||||
|
||||
# Cleanup temp notes file
|
||||
if (Test-Path $notesFilePath) {
|
||||
Remove-Item $notesFilePath -Force
|
||||
}
|
||||
|
||||
if ($ghExitCode -ne 0) {
|
||||
Write-Error "Failed to create GitHub release for tag $tag."
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
finally {
|
||||
if ($null -ne $previousGhToken) {
|
||||
$env:GH_TOKEN = $previousGhToken
|
||||
}
|
||||
else {
|
||||
Remove-Item Env:GH_TOKEN -ErrorAction SilentlyContinue
|
||||
}
|
||||
}
|
||||
|
||||
Write-Log -Level "OK" -Message " GitHub release created successfully."
|
||||
}
|
||||
else {
|
||||
Write-Log -Level "WARN" -Message "Skipping GitHub release (disabled)."
|
||||
}
|
||||
|
||||
|
||||
# Release to NuGet
|
||||
|
||||
if ($nugetReleseEnabled) {
|
||||
Write-Log -Level "STEP" -Message "Pushing to NuGet.org..."
|
||||
dotnet nuget push $packageFile.FullName -k $nugetApiKey -s $nugetSource --skip-duplicate
|
||||
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
Write-Error "Failed to push the package to NuGet."
|
||||
exit 1
|
||||
}
|
||||
|
||||
Write-Log -Level "OK" -Message " NuGet push completed."
|
||||
}
|
||||
else {
|
||||
Write-Log -Level "WARN" -Message "Skipping NuGet publish (disabled)."
|
||||
}
|
||||
|
||||
}
|
||||
else {
|
||||
Write-Log -Level "WARN" -Message "Skipping remote tag verification and GitHub release (dev branch)."
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Cleanup
|
||||
if (Test-Path $testResultsDir) {
|
||||
Remove-Item $testResultsDir -Recurse -Force
|
||||
Write-Log -Level "INFO" -Message " Cleaned up test results directory."
|
||||
}
|
||||
|
||||
Get-ChildItem -Path $releaseDir -File |
|
||||
Where-Object { $_.Name -like "*$version*.nupkg" -or $_.Name -like "*$version*.snupkg" } |
|
||||
Remove-Item -Force -ErrorAction SilentlyContinue
|
||||
#endregion
|
||||
|
||||
#region Summary
|
||||
Write-Log -Level "OK" -Message "=================================================="
|
||||
if ($isDevBranch) {
|
||||
Write-Log -Level "OK" -Message "DEV BUILD COMPLETE"
|
||||
}
|
||||
else {
|
||||
Write-Log -Level "OK" -Message "RELEASE COMPLETE"
|
||||
}
|
||||
Write-Log -Level "OK" -Message "=================================================="
|
||||
|
||||
if (-not $isDevBranch) {
|
||||
Write-Log -Level "STEP" -Message "Release URL: https://github.com/$repo/releases/tag/$tag"
|
||||
}
|
||||
|
||||
Write-Log -Level "INFO" -Message "Artifacts location: $releaseDir"
|
||||
|
||||
if ($isDevBranch) {
|
||||
Write-Log -Level "WARN" -Message "To publish to GitHub, switch to '$releaseBranch', merge dev, tag, and run this script again:"
|
||||
Write-Log -Level "WARN" -Message " git checkout $releaseBranch"
|
||||
Write-Log -Level "WARN" -Message " git merge dev"
|
||||
Write-Log -Level "WARN" -Message " git tag v$version"
|
||||
Write-Log -Level "WARN" -Message " .\Release-NuGetPackage.ps1"
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#endregion
|
||||
65
utils/Release-NuGetPackage/scriptsettings.json
Normal file
65
utils/Release-NuGetPackage/scriptsettings.json
Normal file
@ -0,0 +1,65 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft-07/schema",
|
||||
"title": "Release NuGet Package Script Settings",
|
||||
"description": "Configuration file for Release-NuGetPackage.ps1 script.",
|
||||
|
||||
"github": {
|
||||
"enabled": true,
|
||||
"githubToken": "GITHUB_MAKS_IT_COM",
|
||||
"repository": "https://github.com/MAKS-IT-COM/maksit-dapr"
|
||||
},
|
||||
|
||||
"nuget": {
|
||||
"enabled": true,
|
||||
"nugetApiKey": "NUGET_MAKS_IT",
|
||||
"source": "https://api.nuget.org/v3/index.json"
|
||||
},
|
||||
|
||||
"branches": {
|
||||
"release": "main",
|
||||
"dev": "dev"
|
||||
},
|
||||
|
||||
"paths": {
|
||||
"csprojPaths": [
|
||||
"..\\..\\src\\MaksIT.Dapr\\MaksIT.Dapr.csproj"
|
||||
],
|
||||
"testResultsDir": "..\\..\\testResults",
|
||||
"releaseDir": "..\\..\\release",
|
||||
"changelogPath": "..\\..\\CHANGELOG.md",
|
||||
"testProject": "..\\..\\src\\MaksIT.Dapr.Tests"
|
||||
},
|
||||
|
||||
"release": {
|
||||
"zipNamePattern": "maksit.dapr-{version}.zip",
|
||||
"releaseTitlePattern": "Release {version}"
|
||||
},
|
||||
|
||||
"_comments": {
|
||||
"github": {
|
||||
"enabled": "Enable/disable GitHub release creation.",
|
||||
"githubToken": "Environment variable name containing GitHub token used by gh CLI.",
|
||||
"repository": "GitHub repository override used for releases (supports owner/repo or full GitHub URL)."
|
||||
},
|
||||
"nuget": {
|
||||
"enabled": "Enable/disable NuGet publish step.",
|
||||
"nugetApiKey": "Environment variable name containing NuGet API key.",
|
||||
"source": "NuGet feed URL passed to dotnet nuget push."
|
||||
},
|
||||
"branches": {
|
||||
"release": "Branch that requires tag and allows full publish flow.",
|
||||
"dev": "Branch for local/dev build flow (no tag required)."
|
||||
},
|
||||
"paths": {
|
||||
"csprojPaths": "List of project files used for version discovery and publish output.",
|
||||
"testResultsDir": "Directory where test artifacts are written.",
|
||||
"releaseDir": "Output directory for release archives and artifacts.",
|
||||
"changelogPath": "Path to CHANGELOG.md used for version and release notes extraction.",
|
||||
"testProject": "Test project path used by TestRunner."
|
||||
},
|
||||
"release": {
|
||||
"zipNamePattern": "Archive name pattern. Supports {version} placeholder.",
|
||||
"releaseTitlePattern": "GitHub release title pattern. Supports {version} placeholder."
|
||||
}
|
||||
}
|
||||
}
|
||||
32
utils/ScriptConfig.psm1
Normal file
32
utils/ScriptConfig.psm1
Normal file
@ -0,0 +1,32 @@
|
||||
function Get-ScriptSettings {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$ScriptDir,
|
||||
|
||||
[Parameter(Mandatory = $false)]
|
||||
[string]$SettingsFileName = "scriptsettings.json"
|
||||
)
|
||||
|
||||
$settingsPath = Join-Path $ScriptDir $SettingsFileName
|
||||
|
||||
if (-not (Test-Path $settingsPath -PathType Leaf)) {
|
||||
Write-Error "Settings file not found: $settingsPath"
|
||||
exit 1
|
||||
}
|
||||
|
||||
return Get-Content $settingsPath -Raw | ConvertFrom-Json
|
||||
}
|
||||
|
||||
function Assert-Command {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$Command
|
||||
)
|
||||
|
||||
if (-not (Get-Command $Command -ErrorAction SilentlyContinue)) {
|
||||
Write-Error "Required command '$Command' is missing. Aborting."
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
|
||||
Export-ModuleMember -Function Get-ScriptSettings, Assert-Command
|
||||
199
utils/TestRunner.psm1
Normal file
199
utils/TestRunner.psm1
Normal file
@ -0,0 +1,199 @@
|
||||
<#
|
||||
.SYNOPSIS
|
||||
PowerShell module for running tests with code coverage.
|
||||
|
||||
.DESCRIPTION
|
||||
Provides the Invoke-TestsWithCoverage function for running .NET tests
|
||||
with Coverlet code coverage collection and parsing results.
|
||||
|
||||
.NOTES
|
||||
Author: MaksIT
|
||||
Usage: Import-Module .\TestRunner.psm1
|
||||
#>
|
||||
|
||||
function Import-LoggingModuleInternal {
|
||||
if (Get-Command Write-Log -ErrorAction SilentlyContinue) {
|
||||
return
|
||||
}
|
||||
|
||||
$modulePath = Join-Path $PSScriptRoot "Logging.psm1"
|
||||
if (Test-Path $modulePath) {
|
||||
Import-Module $modulePath -Force
|
||||
}
|
||||
}
|
||||
|
||||
function Write-TestRunnerLogInternal {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$Message,
|
||||
|
||||
[Parameter(Mandatory = $false)]
|
||||
[ValidateSet("INFO", "OK", "WARN", "ERROR", "STEP", "DEBUG")]
|
||||
[string]$Level = "INFO"
|
||||
)
|
||||
|
||||
Import-LoggingModuleInternal
|
||||
|
||||
if (Get-Command Write-Log -ErrorAction SilentlyContinue) {
|
||||
Write-Log -Level $Level -Message $Message
|
||||
return
|
||||
}
|
||||
|
||||
Write-Host $Message -ForegroundColor Gray
|
||||
}
|
||||
|
||||
function Invoke-TestsWithCoverage {
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Runs unit tests with code coverage and returns coverage metrics.
|
||||
|
||||
.PARAMETER TestProjectPath
|
||||
Path to the test project directory.
|
||||
|
||||
.PARAMETER Silent
|
||||
Suppress console output (for JSON consumption).
|
||||
|
||||
.PARAMETER ResultsDirectory
|
||||
Optional fixed directory where test result files are written.
|
||||
|
||||
.PARAMETER KeepResults
|
||||
Keep the TestResults folder after execution.
|
||||
|
||||
.OUTPUTS
|
||||
PSCustomObject with properties:
|
||||
- Success: bool
|
||||
- Error: string (if failed)
|
||||
- LineRate: double
|
||||
- BranchRate: double
|
||||
- MethodRate: double
|
||||
- TotalMethods: int
|
||||
- CoveredMethods: int
|
||||
- CoverageFile: string
|
||||
|
||||
.EXAMPLE
|
||||
$result = Invoke-TestsWithCoverage -TestProjectPath ".\Tests"
|
||||
if ($result.Success) { Write-TestRunnerLogInternal -Level "INFO" -Message "Line coverage: $($result.LineRate)%" }
|
||||
#>
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$TestProjectPath,
|
||||
|
||||
[switch]$Silent,
|
||||
|
||||
[string]$ResultsDirectory,
|
||||
|
||||
[switch]$KeepResults
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
# Resolve path
|
||||
$TestProjectDir = Resolve-Path $TestProjectPath -ErrorAction SilentlyContinue
|
||||
if (-not $TestProjectDir) {
|
||||
return [PSCustomObject]@{
|
||||
Success = $false
|
||||
Error = "Test project not found at: $TestProjectPath"
|
||||
}
|
||||
}
|
||||
|
||||
if ([string]::IsNullOrWhiteSpace($ResultsDirectory)) {
|
||||
$ResultsDir = Join-Path $TestProjectDir "TestResults"
|
||||
}
|
||||
else {
|
||||
$ResultsDir = [System.IO.Path]::GetFullPath($ResultsDirectory)
|
||||
}
|
||||
|
||||
# Clean previous results
|
||||
if (Test-Path $ResultsDir) {
|
||||
Remove-Item -Recurse -Force $ResultsDir
|
||||
}
|
||||
|
||||
if (-not $Silent) {
|
||||
Write-TestRunnerLogInternal -Level "STEP" -Message "Running tests with code coverage..."
|
||||
Write-TestRunnerLogInternal -Level "INFO" -Message "Test Project: $TestProjectDir"
|
||||
}
|
||||
|
||||
# Run tests with coverage collection
|
||||
Push-Location $TestProjectDir
|
||||
try {
|
||||
$dotnetArgs = @(
|
||||
"test"
|
||||
"--collect:XPlat Code Coverage"
|
||||
"--results-directory", $ResultsDir
|
||||
"--verbosity", $(if ($Silent) { "quiet" } else { "normal" })
|
||||
)
|
||||
|
||||
if ($Silent) {
|
||||
$null = & dotnet @dotnetArgs 2>&1
|
||||
} else {
|
||||
& dotnet @dotnetArgs
|
||||
}
|
||||
|
||||
$testExitCode = $LASTEXITCODE
|
||||
if ($testExitCode -ne 0) {
|
||||
return [PSCustomObject]@{
|
||||
Success = $false
|
||||
Error = "Tests failed with exit code $testExitCode"
|
||||
}
|
||||
}
|
||||
}
|
||||
finally {
|
||||
Pop-Location
|
||||
}
|
||||
|
||||
# Find the coverage file
|
||||
$CoverageFile = Get-ChildItem -Path $ResultsDir -Filter "coverage.cobertura.xml" -Recurse | Select-Object -First 1
|
||||
|
||||
if (-not $CoverageFile) {
|
||||
return [PSCustomObject]@{
|
||||
Success = $false
|
||||
Error = "Coverage file not found"
|
||||
}
|
||||
}
|
||||
|
||||
if (-not $Silent) {
|
||||
Write-TestRunnerLogInternal -Level "OK" -Message "Coverage file found: $($CoverageFile.FullName)"
|
||||
Write-TestRunnerLogInternal -Level "STEP" -Message "Parsing coverage data..."
|
||||
}
|
||||
|
||||
# Parse coverage data from Cobertura XML
|
||||
[xml]$coverageXml = Get-Content $CoverageFile.FullName
|
||||
|
||||
$lineRate = [math]::Round([double]$coverageXml.coverage.'line-rate' * 100, 1)
|
||||
$branchRate = [math]::Round([double]$coverageXml.coverage.'branch-rate' * 100, 1)
|
||||
|
||||
# Calculate method coverage from packages
|
||||
$totalMethods = 0
|
||||
$coveredMethods = 0
|
||||
foreach ($package in $coverageXml.coverage.packages.package) {
|
||||
foreach ($class in $package.classes.class) {
|
||||
foreach ($method in $class.methods.method) {
|
||||
$totalMethods++
|
||||
if ([double]$method.'line-rate' -gt 0) {
|
||||
$coveredMethods++
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
$methodRate = if ($totalMethods -gt 0) { [math]::Round(($coveredMethods / $totalMethods) * 100, 1) } else { 0 }
|
||||
|
||||
# Cleanup unless KeepResults is specified
|
||||
if (-not $KeepResults) {
|
||||
if (Test-Path $ResultsDir) {
|
||||
Remove-Item -Recurse -Force $ResultsDir
|
||||
}
|
||||
}
|
||||
|
||||
# Return results
|
||||
return [PSCustomObject]@{
|
||||
Success = $true
|
||||
LineRate = $lineRate
|
||||
BranchRate = $branchRate
|
||||
MethodRate = $methodRate
|
||||
TotalMethods = $totalMethods
|
||||
CoveredMethods = $coveredMethods
|
||||
CoverageFile = $CoverageFile.FullName
|
||||
}
|
||||
}
|
||||
|
||||
Export-ModuleMember -Function Invoke-TestsWithCoverage
|
||||
Loading…
Reference in New Issue
Block a user