Compare commits
2 Commits
3f29c71995
...
1816f76736
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1816f76736 | ||
|
|
e390b42dce |
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
|
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
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
|||||||
57
README.md
57
README.md
@ -1,26 +1,27 @@
|
|||||||
# MaksIT.Dapr
|
# 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.
|
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
|
## Table of Contents
|
||||||
|
|
||||||
- [Overview](#overview)
|
- [MaksIT.Dapr](#maksitdapr)
|
||||||
- [Features](#features)
|
- [Table of Contents](#table-of-contents)
|
||||||
- [Getting Started](#getting-started)
|
- [Overview](#overview)
|
||||||
- [Prerequisites](#prerequisites)
|
- [Features](#features)
|
||||||
|
- [Getting Started](#getting-started)
|
||||||
- [Installation](#installation)
|
- [Installation](#installation)
|
||||||
- [Configuration](#configuration)
|
- [Usage](#usage)
|
||||||
- [Usage](#usage)
|
- [Registering Dapr Services](#registering-dapr-services)
|
||||||
- [Running the Project](#running-the-project)
|
- [Injecting and Using Dapr Services](#injecting-and-using-dapr-services)
|
||||||
- [Environment Variables](#environment-variables)
|
- [Contributing](#contributing)
|
||||||
- [Testing](#testing)
|
- [Contact](#contact)
|
||||||
- [Deployment](#deployment)
|
- [License](#license)
|
||||||
- [Contributing](#contributing)
|
|
||||||
- [License](#license)
|
|
||||||
|
|
||||||
## Overview
|
## 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
|
## Features
|
||||||
|
|
||||||
@ -176,32 +177,4 @@ If you have any questions or need further assistance, feel free to reach out:
|
|||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
This project is licensed under the MIT License. See the full license text below.
|
See `LICENSE.md`.
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 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.
|
|
||||||
```
|
|
||||||
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>
|
||||||
|
After Width: | Height: | Size: 1.0 KiB |
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>
|
||||||
|
After Width: | Height: | Size: 1.0 KiB |
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>
|
||||||
|
After Width: | Height: | Size: 1.0 KiB |
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">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net8.0</TargetFramework>
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
|
|
||||||
<!-- NuGet package metadata -->
|
<!-- NuGet package metadata -->
|
||||||
<PackageId>MaksIT.Dapr</PackageId>
|
<PackageId>MaksIT.Dapr</PackageId>
|
||||||
<Version>1.0.8</Version>
|
<Version>2.0.0</Version>
|
||||||
<Authors>Maksym Sadovnychyy</Authors>
|
<Authors>Maksym Sadovnychyy</Authors>
|
||||||
<Company>MAKS-IT</Company>
|
<Company>MAKS-IT</Company>
|
||||||
<Product>MaksIT.Dapr</Product>
|
<Product>MaksIT.Dapr</Product>
|
||||||
@ -21,16 +21,21 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<None Include="../../README.md" Pack="true" PackagePath="" />
|
<PackageReference Include="Dapr.Actors.AspNetCore" Version="1.16.1" />
|
||||||
<None Include="../../LICENSE.md" Pack="true" PackagePath="" />
|
<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>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Dapr.Actors.AspNetCore" Version="1.16.0" />
|
<None Include="..\..\LICENSE.md" Pack="true" PackagePath="\" Link="LICENSE.md" />
|
||||||
<PackageReference Include="Dapr.AspNetCore" Version="1.16.0" />
|
<None Include="..\..\README.md" Pack="true" PackagePath="\" Link="README.md" />
|
||||||
<PackageReference Include="Dapr.Workflow" Version="1.16.0" />
|
<None Include="..\..\CHANGELOG.md" Pack="true" PackagePath="\" Link="CHANGELOG.md" />
|
||||||
<PackageReference Include="MaksIT.Core" Version="1.5.1" />
|
|
||||||
<PackageReference Include="MaksIT.Results" Version="1.1.0" />
|
<None Include="..\..\assets\badges\**\*" Link="assets\badges\%(RecursiveDir)%(Filename)%(Extension)">
|
||||||
|
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||||
|
</None>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</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
|
||||||
|
pwsh -NoProfile -ExecutionPolicy Bypass -File "%~dp0Force-AmendTaggedCommit.ps1"
|
||||||
|
pause
|
||||||
249
utils/Force-AmendTaggedCommit/Force-AmendTaggedCommit.ps1
Normal file
249
utils/Force-AmendTaggedCommit/Force-AmendTaggedCommit.ps1
Normal file
@ -0,0 +1,249 @@
|
|||||||
|
#requires -Version 7.0
|
||||||
|
#requires -PSEdition Core
|
||||||
|
|
||||||
|
<#
|
||||||
|
.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
|
||||||
|
pwsh -File .\Force-AmendTaggedCommit.ps1
|
||||||
|
|
||||||
|
.EXAMPLE
|
||||||
|
pwsh -File .\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
|
||||||
|
pwsh -NoProfile -ExecutionPolicy Bypass -File "%~dp0Generate-CoverageBadges.ps1"
|
||||||
|
pause
|
||||||
234
utils/Generate-CoverageBadges/Generate-CoverageBadges.ps1
Normal file
234
utils/Generate-CoverageBadges/Generate-CoverageBadges.ps1
Normal file
@ -0,0 +1,234 @@
|
|||||||
|
#requires -Version 7.0
|
||||||
|
#requires -PSEdition Core
|
||||||
|
|
||||||
|
<#
|
||||||
|
.SYNOPSIS
|
||||||
|
Generates SVG coverage 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.
|
||||||
|
|
||||||
|
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
|
||||||
|
pwsh -File .\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 utf8NoBOM
|
||||||
|
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."
|
||||||
|
}
|
||||||
|
}
|
||||||
268
utils/GitTools.psm1
Normal file
268
utils/GitTools.psm1
Normal file
@ -0,0 +1,268 @@
|
|||||||
|
#requires -Version 7.0
|
||||||
|
#requires -PSEdition Core
|
||||||
|
|
||||||
|
#
|
||||||
|
# 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
|
||||||
70
utils/Logging.psm1
Normal file
70
utils/Logging.psm1
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
#requires -Version 7.0
|
||||||
|
#requires -PSEdition Core
|
||||||
|
|
||||||
|
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
|
||||||
121
utils/Release-Package/CorePlugins/CleanupArtifacts.psm1
Normal file
121
utils/Release-Package/CorePlugins/CleanupArtifacts.psm1
Normal file
@ -0,0 +1,121 @@
|
|||||||
|
#requires -Version 7.0
|
||||||
|
#requires -PSEdition Core
|
||||||
|
|
||||||
|
<#
|
||||||
|
.SYNOPSIS
|
||||||
|
Cleanup plugin for removing generated artifacts after pipeline completion.
|
||||||
|
|
||||||
|
.DESCRIPTION
|
||||||
|
This plugin removes files from the configured artifacts directory using
|
||||||
|
glob patterns. It is typically placed at the end of the Release stage so
|
||||||
|
cleanup becomes explicit and opt-in per repository.
|
||||||
|
#>
|
||||||
|
|
||||||
|
if (-not (Get-Command Import-PluginDependency -ErrorAction SilentlyContinue)) {
|
||||||
|
$pluginSupportModulePath = Join-Path (Split-Path $PSScriptRoot -Parent) "PluginSupport.psm1"
|
||||||
|
if (Test-Path $pluginSupportModulePath -PathType Leaf) {
|
||||||
|
Import-Module $pluginSupportModulePath -Force -Global -ErrorAction Stop
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-CleanupPatternsInternal {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory = $false)]
|
||||||
|
$ConfiguredPatterns
|
||||||
|
)
|
||||||
|
|
||||||
|
if ($null -eq $ConfiguredPatterns) {
|
||||||
|
return @('*.nupkg', '*.snupkg')
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($ConfiguredPatterns -is [System.Collections.IEnumerable] -and -not ($ConfiguredPatterns -is [string])) {
|
||||||
|
return @($ConfiguredPatterns | Where-Object { -not [string]::IsNullOrWhiteSpace([string]$_) })
|
||||||
|
}
|
||||||
|
|
||||||
|
if ([string]::IsNullOrWhiteSpace([string]$ConfiguredPatterns)) {
|
||||||
|
return @('*.nupkg', '*.snupkg')
|
||||||
|
}
|
||||||
|
|
||||||
|
return @([string]$ConfiguredPatterns)
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-ExcludePatternsInternal {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory = $false)]
|
||||||
|
$ConfiguredPatterns
|
||||||
|
)
|
||||||
|
|
||||||
|
if ($null -eq $ConfiguredPatterns) {
|
||||||
|
return @()
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($ConfiguredPatterns -is [System.Collections.IEnumerable] -and -not ($ConfiguredPatterns -is [string])) {
|
||||||
|
return @($ConfiguredPatterns | Where-Object { -not [string]::IsNullOrWhiteSpace([string]$_) })
|
||||||
|
}
|
||||||
|
|
||||||
|
if ([string]::IsNullOrWhiteSpace([string]$ConfiguredPatterns)) {
|
||||||
|
return @()
|
||||||
|
}
|
||||||
|
|
||||||
|
return @([string]$ConfiguredPatterns)
|
||||||
|
}
|
||||||
|
|
||||||
|
function Invoke-Plugin {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
$Settings
|
||||||
|
)
|
||||||
|
|
||||||
|
Import-PluginDependency -ModuleName "Logging" -RequiredCommand "Write-Log"
|
||||||
|
|
||||||
|
$pluginSettings = $Settings
|
||||||
|
$sharedSettings = $Settings.Context
|
||||||
|
$artifactsDirectory = $sharedSettings.ArtifactsDirectory
|
||||||
|
$patterns = Get-CleanupPatternsInternal -ConfiguredPatterns $pluginSettings.includePatterns
|
||||||
|
$excludePatterns = Get-ExcludePatternsInternal -ConfiguredPatterns $pluginSettings.excludePatterns
|
||||||
|
|
||||||
|
if ([string]::IsNullOrWhiteSpace($artifactsDirectory)) {
|
||||||
|
throw "CleanupArtifacts plugin requires an artifacts directory in the shared context."
|
||||||
|
}
|
||||||
|
|
||||||
|
if (-not (Test-Path $artifactsDirectory -PathType Container)) {
|
||||||
|
Write-Log -Level "WARN" -Message " Artifacts directory not found: $artifactsDirectory"
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Log -Level "STEP" -Message "Cleaning generated artifacts..."
|
||||||
|
|
||||||
|
$itemsToRemove = @()
|
||||||
|
foreach ($pattern in $patterns) {
|
||||||
|
$matchedItems = @(
|
||||||
|
Get-ChildItem -Path $artifactsDirectory -Force -ErrorAction SilentlyContinue |
|
||||||
|
Where-Object { $_.Name -like $pattern }
|
||||||
|
)
|
||||||
|
|
||||||
|
if ($excludePatterns.Count -gt 0) {
|
||||||
|
$matchedItems = @(
|
||||||
|
$matchedItems |
|
||||||
|
Where-Object {
|
||||||
|
$item = $_
|
||||||
|
-not ($excludePatterns | Where-Object { $item.Name -like $_ } | Select-Object -First 1)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
$itemsToRemove += @($matchedItems)
|
||||||
|
}
|
||||||
|
|
||||||
|
$itemsToRemove = @($itemsToRemove | Sort-Object FullName -Unique)
|
||||||
|
|
||||||
|
if ($itemsToRemove.Count -eq 0) {
|
||||||
|
Write-Log -Level "INFO" -Message " No artifacts matched cleanup rules."
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($item in $itemsToRemove) {
|
||||||
|
Remove-Item -Path $item.FullName -Recurse -Force -ErrorAction SilentlyContinue
|
||||||
|
Write-Log -Level "OK" -Message " Removed: $($item.Name)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Export-ModuleMember -Function Invoke-Plugin
|
||||||
93
utils/Release-Package/CorePlugins/CreateArchive.psm1
Normal file
93
utils/Release-Package/CorePlugins/CreateArchive.psm1
Normal file
@ -0,0 +1,93 @@
|
|||||||
|
#requires -Version 7.0
|
||||||
|
#requires -PSEdition Core
|
||||||
|
|
||||||
|
<#
|
||||||
|
.SYNOPSIS
|
||||||
|
Creates a release zip from prepared build artifacts.
|
||||||
|
|
||||||
|
.DESCRIPTION
|
||||||
|
This plugin compresses the release artifact inputs prepared by an earlier
|
||||||
|
producer plugin (for example DotNetPack or DotNetPublish) into a zip file
|
||||||
|
and exposes the resulting release assets for later publisher plugins.
|
||||||
|
#>
|
||||||
|
|
||||||
|
if (-not (Get-Command Import-PluginDependency -ErrorAction SilentlyContinue)) {
|
||||||
|
$pluginSupportModulePath = Join-Path (Split-Path $PSScriptRoot -Parent) "PluginSupport.psm1"
|
||||||
|
if (Test-Path $pluginSupportModulePath -PathType Leaf) {
|
||||||
|
Import-Module $pluginSupportModulePath -Force -Global -ErrorAction Stop
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Invoke-Plugin {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
$Settings
|
||||||
|
)
|
||||||
|
|
||||||
|
Import-PluginDependency -ModuleName "Logging" -RequiredCommand "Write-Log"
|
||||||
|
|
||||||
|
$pluginSettings = $Settings
|
||||||
|
$sharedSettings = $Settings.Context
|
||||||
|
$artifactsDirectory = $sharedSettings.ArtifactsDirectory
|
||||||
|
$version = $sharedSettings.Version
|
||||||
|
$archiveInputs = @()
|
||||||
|
|
||||||
|
if ($sharedSettings.PSObject.Properties['ReleaseArchiveInputs'] -and $sharedSettings.ReleaseArchiveInputs) {
|
||||||
|
$archiveInputs = @($sharedSettings.ReleaseArchiveInputs)
|
||||||
|
}
|
||||||
|
elseif ($sharedSettings.PSObject.Properties['PackageFile'] -and $sharedSettings.PackageFile) {
|
||||||
|
$archiveInputs = @($sharedSettings.PackageFile.FullName)
|
||||||
|
if ($sharedSettings.PSObject.Properties['SymbolsPackageFile'] -and $sharedSettings.SymbolsPackageFile) {
|
||||||
|
$archiveInputs += $sharedSettings.SymbolsPackageFile.FullName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($archiveInputs.Count -eq 0) {
|
||||||
|
throw "CreateArchive plugin requires prepared artifacts. Run a producer plugin (for example DotNetPack or DotNetPublish) first."
|
||||||
|
}
|
||||||
|
|
||||||
|
if ([string]::IsNullOrWhiteSpace($artifactsDirectory)) {
|
||||||
|
throw "CreateArchive plugin requires an artifacts directory in the shared context."
|
||||||
|
}
|
||||||
|
|
||||||
|
if (-not (Test-Path $artifactsDirectory -PathType Container)) {
|
||||||
|
New-Item -ItemType Directory -Path $artifactsDirectory | Out-Null
|
||||||
|
}
|
||||||
|
|
||||||
|
$zipNamePattern = if ($pluginSettings.PSObject.Properties['zipNamePattern'] -and -not [string]::IsNullOrWhiteSpace([string]$pluginSettings.zipNamePattern)) {
|
||||||
|
[string]$pluginSettings.zipNamePattern
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
"release-{version}.zip"
|
||||||
|
}
|
||||||
|
|
||||||
|
$zipFileName = $zipNamePattern -replace '\{version\}', $version
|
||||||
|
$zipPath = Join-Path $artifactsDirectory $zipFileName
|
||||||
|
|
||||||
|
if (Test-Path $zipPath) {
|
||||||
|
Remove-Item -Path $zipPath -Force
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Log -Level "STEP" -Message "Creating release archive..."
|
||||||
|
Compress-Archive -Path $archiveInputs -DestinationPath $zipPath -CompressionLevel Optimal -Force
|
||||||
|
|
||||||
|
if (-not (Test-Path $zipPath -PathType Leaf)) {
|
||||||
|
throw "Failed to create release archive at: $zipPath"
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Log -Level "OK" -Message " Release archive ready: $zipPath"
|
||||||
|
|
||||||
|
$releaseAssetPaths = @($zipPath)
|
||||||
|
if ($sharedSettings.PSObject.Properties['PackageFile'] -and $sharedSettings.PackageFile) {
|
||||||
|
$releaseAssetPaths += $sharedSettings.PackageFile.FullName
|
||||||
|
}
|
||||||
|
if ($sharedSettings.PSObject.Properties['SymbolsPackageFile'] -and $sharedSettings.SymbolsPackageFile) {
|
||||||
|
$releaseAssetPaths += $sharedSettings.SymbolsPackageFile.FullName
|
||||||
|
}
|
||||||
|
|
||||||
|
$sharedSettings | Add-Member -NotePropertyName ReleaseDir -NotePropertyValue $artifactsDirectory -Force
|
||||||
|
$sharedSettings | Add-Member -NotePropertyName ReleaseArchivePath -NotePropertyValue $zipPath -Force
|
||||||
|
$sharedSettings | Add-Member -NotePropertyName ReleaseAssetPaths -NotePropertyValue $releaseAssetPaths -Force
|
||||||
|
}
|
||||||
|
|
||||||
|
Export-ModuleMember -Function Invoke-Plugin
|
||||||
99
utils/Release-Package/CorePlugins/DotNetPack.psm1
Normal file
99
utils/Release-Package/CorePlugins/DotNetPack.psm1
Normal file
@ -0,0 +1,99 @@
|
|||||||
|
#requires -Version 7.0
|
||||||
|
#requires -PSEdition Core
|
||||||
|
|
||||||
|
<#
|
||||||
|
.SYNOPSIS
|
||||||
|
.NET pack plugin for producing package artifacts.
|
||||||
|
|
||||||
|
.DESCRIPTION
|
||||||
|
This plugin creates package output for the release pipeline.
|
||||||
|
It packs the configured .NET project, resolves the generated
|
||||||
|
package artifacts, and publishes them into shared runtime context
|
||||||
|
for later plugins.
|
||||||
|
#>
|
||||||
|
|
||||||
|
if (-not (Get-Command Import-PluginDependency -ErrorAction SilentlyContinue)) {
|
||||||
|
$pluginSupportModulePath = Join-Path (Split-Path $PSScriptRoot -Parent) "PluginSupport.psm1"
|
||||||
|
if (Test-Path $pluginSupportModulePath -PathType Leaf) {
|
||||||
|
# Load this globally only as a fallback. Re-importing PluginSupport in its own execution path
|
||||||
|
# can invalidate commands already resolved by the release engine.
|
||||||
|
Import-Module $pluginSupportModulePath -Force -Global -ErrorAction Stop
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Invoke-Plugin {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
$Settings
|
||||||
|
)
|
||||||
|
|
||||||
|
Import-PluginDependency -ModuleName "Logging" -RequiredCommand "Write-Log"
|
||||||
|
Import-PluginDependency -ModuleName "ScriptConfig" -RequiredCommand "Assert-Command"
|
||||||
|
|
||||||
|
$sharedSettings = $Settings.Context
|
||||||
|
$projectFiles = $sharedSettings.ProjectFiles
|
||||||
|
$artifactsDirectory = $sharedSettings.ArtifactsDirectory
|
||||||
|
$version = $sharedSettings.Version
|
||||||
|
$packageProjectPath = $null
|
||||||
|
$releaseArchiveInputs = @()
|
||||||
|
|
||||||
|
Assert-Command dotnet
|
||||||
|
|
||||||
|
if (-not $sharedSettings.PSObject.Properties['ProjectFiles'] -or $projectFiles.Count -eq 0) {
|
||||||
|
throw "DotNetPack plugin requires project files in the shared context."
|
||||||
|
}
|
||||||
|
|
||||||
|
$outputDir = $artifactsDirectory
|
||||||
|
|
||||||
|
if (!(Test-Path $outputDir)) {
|
||||||
|
New-Item -ItemType Directory -Path $outputDir | Out-Null
|
||||||
|
}
|
||||||
|
|
||||||
|
# The release context guarantees ProjectFiles is an array, so index 0 is the first project path,
|
||||||
|
# not the first character of a string.
|
||||||
|
$packageProjectPath = $projectFiles[0]
|
||||||
|
Write-Log -Level "STEP" -Message "Packing NuGet package..."
|
||||||
|
dotnet pack $packageProjectPath -c Release -o $outputDir --nologo `
|
||||||
|
-p:IncludeSymbols=true `
|
||||||
|
-p:SymbolPackageFormat=snupkg
|
||||||
|
if ($LASTEXITCODE -ne 0) {
|
||||||
|
throw "dotnet pack failed for $packageProjectPath."
|
||||||
|
}
|
||||||
|
|
||||||
|
# dotnet pack can leave older packages in the artifacts directory.
|
||||||
|
# Pick the newest file matching the current version rather than assuming a clean folder.
|
||||||
|
$packageFile = Get-ChildItem -Path $outputDir -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) {
|
||||||
|
throw "Could not locate generated NuGet package for version $version in: $outputDir"
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Log -Level "OK" -Message " Package ready: $($packageFile.FullName)"
|
||||||
|
$releaseArchiveInputs = @($packageFile.FullName)
|
||||||
|
|
||||||
|
$symbolsPackageFile = Get-ChildItem -Path $outputDir -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)"
|
||||||
|
$releaseArchiveInputs += $symbolsPackageFile.FullName
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Write-Log -Level "WARN" -Message " Symbols package (.snupkg) not found for version $version."
|
||||||
|
}
|
||||||
|
|
||||||
|
$sharedSettings | Add-Member -NotePropertyName PackageFile -NotePropertyValue $packageFile -Force
|
||||||
|
$sharedSettings | Add-Member -NotePropertyName SymbolsPackageFile -NotePropertyValue $symbolsPackageFile -Force
|
||||||
|
$sharedSettings | Add-Member -NotePropertyName ReleaseArchiveInputs -NotePropertyValue $releaseArchiveInputs -Force
|
||||||
|
}
|
||||||
|
|
||||||
|
Export-ModuleMember -Function Invoke-Plugin
|
||||||
71
utils/Release-Package/CorePlugins/DotNetPublish.psm1
Normal file
71
utils/Release-Package/CorePlugins/DotNetPublish.psm1
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
#requires -Version 7.0
|
||||||
|
#requires -PSEdition Core
|
||||||
|
|
||||||
|
<#
|
||||||
|
.SYNOPSIS
|
||||||
|
.NET publish plugin for producing application release artifacts.
|
||||||
|
|
||||||
|
.DESCRIPTION
|
||||||
|
This plugin publishes the configured .NET project into a release output
|
||||||
|
directory and exposes that published directory to the shared release
|
||||||
|
context so later release-stage plugins can archive and publish it.
|
||||||
|
#>
|
||||||
|
|
||||||
|
if (-not (Get-Command Import-PluginDependency -ErrorAction SilentlyContinue)) {
|
||||||
|
$pluginSupportModulePath = Join-Path (Split-Path $PSScriptRoot -Parent) "PluginSupport.psm1"
|
||||||
|
if (Test-Path $pluginSupportModulePath -PathType Leaf) {
|
||||||
|
Import-Module $pluginSupportModulePath -Force -Global -ErrorAction Stop
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Invoke-Plugin {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
$Settings
|
||||||
|
)
|
||||||
|
|
||||||
|
Import-PluginDependency -ModuleName "Logging" -RequiredCommand "Write-Log"
|
||||||
|
Import-PluginDependency -ModuleName "ScriptConfig" -RequiredCommand "Assert-Command"
|
||||||
|
|
||||||
|
$sharedSettings = $Settings.Context
|
||||||
|
$projectFiles = $sharedSettings.ProjectFiles
|
||||||
|
$artifactsDirectory = $sharedSettings.ArtifactsDirectory
|
||||||
|
$publishProjectPath = $null
|
||||||
|
|
||||||
|
Assert-Command dotnet
|
||||||
|
|
||||||
|
if (-not $sharedSettings.PSObject.Properties['ProjectFiles'] -or $projectFiles.Count -eq 0) {
|
||||||
|
throw "DotNetPublish plugin requires project files in the shared context."
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!(Test-Path $artifactsDirectory)) {
|
||||||
|
New-Item -ItemType Directory -Path $artifactsDirectory | Out-Null
|
||||||
|
}
|
||||||
|
|
||||||
|
# The first configured project remains the canonical release artifact source.
|
||||||
|
$publishProjectPath = $projectFiles[0]
|
||||||
|
$publishDir = Join-Path $artifactsDirectory ([System.IO.Path]::GetFileNameWithoutExtension($publishProjectPath))
|
||||||
|
|
||||||
|
if (Test-Path $publishDir) {
|
||||||
|
Remove-Item -Path $publishDir -Recurse -Force
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Log -Level "STEP" -Message "Publishing release artifact..."
|
||||||
|
dotnet publish $publishProjectPath -c Release -o $publishDir --nologo
|
||||||
|
if ($LASTEXITCODE -ne 0) {
|
||||||
|
throw "dotnet publish failed for $publishProjectPath."
|
||||||
|
}
|
||||||
|
|
||||||
|
$publishedItems = @(Get-ChildItem -Path $publishDir -Force -ErrorAction SilentlyContinue)
|
||||||
|
if ($publishedItems.Count -eq 0) {
|
||||||
|
throw "dotnet publish completed, but no files were produced in: $publishDir"
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Log -Level "OK" -Message " Published artifact ready: $publishDir"
|
||||||
|
|
||||||
|
$sharedSettings | Add-Member -NotePropertyName PackageFile -NotePropertyValue $null -Force
|
||||||
|
$sharedSettings | Add-Member -NotePropertyName SymbolsPackageFile -NotePropertyValue $null -Force
|
||||||
|
$sharedSettings | Add-Member -NotePropertyName ReleaseArchiveInputs -NotePropertyValue @($publishDir) -Force
|
||||||
|
}
|
||||||
|
|
||||||
|
Export-ModuleMember -Function Invoke-Plugin
|
||||||
72
utils/Release-Package/CorePlugins/DotNetTest.psm1
Normal file
72
utils/Release-Package/CorePlugins/DotNetTest.psm1
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
#requires -Version 7.0
|
||||||
|
#requires -PSEdition Core
|
||||||
|
|
||||||
|
<#
|
||||||
|
.SYNOPSIS
|
||||||
|
.NET test plugin for executing automated tests.
|
||||||
|
|
||||||
|
.DESCRIPTION
|
||||||
|
This plugin resolves the configured .NET test project and optional
|
||||||
|
results directory, runs tests through TestRunner, and stores
|
||||||
|
the resulting test metrics in shared runtime context.
|
||||||
|
#>
|
||||||
|
|
||||||
|
if (-not (Get-Command Import-PluginDependency -ErrorAction SilentlyContinue)) {
|
||||||
|
$pluginSupportModulePath = Join-Path (Split-Path $PSScriptRoot -Parent) "PluginSupport.psm1"
|
||||||
|
if (Test-Path $pluginSupportModulePath -PathType Leaf) {
|
||||||
|
# Same fallback pattern as the other plugins: use the existing shared module if it is already loaded.
|
||||||
|
Import-Module $pluginSupportModulePath -Force -Global -ErrorAction Stop
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Invoke-Plugin {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
$Settings
|
||||||
|
)
|
||||||
|
|
||||||
|
Import-PluginDependency -ModuleName "Logging" -RequiredCommand "Write-Log"
|
||||||
|
Import-PluginDependency -ModuleName "TestRunner" -RequiredCommand "Invoke-TestsWithCoverage"
|
||||||
|
|
||||||
|
$pluginSettings = $Settings
|
||||||
|
$sharedSettings = $Settings.Context
|
||||||
|
$testProjectSetting = $pluginSettings.project
|
||||||
|
$testResultsDirSetting = $pluginSettings.resultsDir
|
||||||
|
$scriptDir = $sharedSettings.ScriptDir
|
||||||
|
|
||||||
|
if ([string]::IsNullOrWhiteSpace($testProjectSetting)) {
|
||||||
|
throw "DotNetTest plugin requires 'project' in scriptsettings.json."
|
||||||
|
}
|
||||||
|
|
||||||
|
$testProjectPath = [System.IO.Path]::GetFullPath((Join-Path $scriptDir $testProjectSetting))
|
||||||
|
$testResultsDir = $null
|
||||||
|
if (-not [string]::IsNullOrWhiteSpace($testResultsDirSetting)) {
|
||||||
|
$testResultsDir = [System.IO.Path]::GetFullPath((Join-Path $scriptDir $testResultsDirSetting))
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Log -Level "STEP" -Message "Running tests..."
|
||||||
|
|
||||||
|
# Build a splatted hashtable so optional arguments can be added without duplicating the call site.
|
||||||
|
$invokeTestParams = @{
|
||||||
|
TestProjectPath = $testProjectPath
|
||||||
|
Silent = $true
|
||||||
|
}
|
||||||
|
if ($testResultsDir) {
|
||||||
|
$invokeTestParams.ResultsDirectory = $testResultsDir
|
||||||
|
}
|
||||||
|
|
||||||
|
$testResult = Invoke-TestsWithCoverage @invokeTestParams
|
||||||
|
|
||||||
|
if (-not $testResult.Success) {
|
||||||
|
throw "Tests failed. $($testResult.Error)"
|
||||||
|
}
|
||||||
|
|
||||||
|
$sharedSettings | Add-Member -NotePropertyName TestResult -NotePropertyValue $testResult -Force
|
||||||
|
|
||||||
|
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)%"
|
||||||
|
}
|
||||||
|
|
||||||
|
Export-ModuleMember -Function Invoke-Plugin
|
||||||
232
utils/Release-Package/CorePlugins/GitHub.psm1
Normal file
232
utils/Release-Package/CorePlugins/GitHub.psm1
Normal file
@ -0,0 +1,232 @@
|
|||||||
|
#requires -Version 7.0
|
||||||
|
#requires -PSEdition Core
|
||||||
|
|
||||||
|
<#
|
||||||
|
.SYNOPSIS
|
||||||
|
GitHub release plugin.
|
||||||
|
|
||||||
|
.DESCRIPTION
|
||||||
|
This plugin validates GitHub CLI access, resolves the target
|
||||||
|
repository, and creates the configured GitHub release using the
|
||||||
|
shared release artifacts and extracted release notes.
|
||||||
|
#>
|
||||||
|
|
||||||
|
if (-not (Get-Command Import-PluginDependency -ErrorAction SilentlyContinue)) {
|
||||||
|
$pluginSupportModulePath = Join-Path (Split-Path $PSScriptRoot -Parent) "PluginSupport.psm1"
|
||||||
|
if (Test-Path $pluginSupportModulePath -PathType Leaf) {
|
||||||
|
Import-Module $pluginSupportModulePath -Force -Global -ErrorAction Stop
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-GitHubRepositoryInternal {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory = $false)]
|
||||||
|
[string]$ConfiguredRepository
|
||||||
|
)
|
||||||
|
|
||||||
|
$repoSource = $ConfiguredRepository
|
||||||
|
|
||||||
|
if ([string]::IsNullOrWhiteSpace($repoSource)) {
|
||||||
|
$repoSource = git config --get remote.origin.url
|
||||||
|
if ($LASTEXITCODE -ne 0 -or [string]::IsNullOrWhiteSpace($repoSource)) {
|
||||||
|
throw "Could not determine git remote origin URL."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$repoSource = $repoSource.Trim()
|
||||||
|
|
||||||
|
if ($repoSource -match "(?i)github\.com[:/](?<owner>[^/]+)/(?<repo>[^/.]+)(\.git)?$") {
|
||||||
|
return "$($matches['owner'])/$($matches['repo'])"
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($repoSource -match "^(?<owner>[^/]+)/(?<repo>[^/]+)$") {
|
||||||
|
return "$($matches['owner'])/$($matches['repo'])"
|
||||||
|
}
|
||||||
|
|
||||||
|
throw "Could not parse GitHub repo from source: $repoSource. Configure Plugins[].repository with 'owner/repo' or a GitHub URL."
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-ReleaseNotesInternal {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[string]$ReleaseNotesFile,
|
||||||
|
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[string]$Version
|
||||||
|
)
|
||||||
|
|
||||||
|
Write-Log -Level "INFO" -Message "Verifying release notes source..."
|
||||||
|
if (-not (Test-Path $ReleaseNotesFile -PathType Leaf)) {
|
||||||
|
throw "Release notes source file not found at: $ReleaseNotesFile"
|
||||||
|
}
|
||||||
|
|
||||||
|
$releaseNotesContent = Get-Content $ReleaseNotesFile -Raw
|
||||||
|
if ($releaseNotesContent -notmatch '##\s+v(\d+\.\d+\.\d+)') {
|
||||||
|
throw "No version entry found in the configured release notes source."
|
||||||
|
}
|
||||||
|
|
||||||
|
$releaseNotesVersion = $Matches[1]
|
||||||
|
if ($releaseNotesVersion -ne $Version) {
|
||||||
|
throw "Project version ($Version) does not match the latest release notes version ($releaseNotesVersion)."
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Log -Level "OK" -Message " Release notes version matches: v$releaseNotesVersion"
|
||||||
|
|
||||||
|
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($releaseNotesContent, $pattern)
|
||||||
|
|
||||||
|
if (-not $match.Success) {
|
||||||
|
throw "Release notes entry for version $Version not found."
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Log -Level "OK" -Message " Release notes extracted."
|
||||||
|
return $match.Value.Trim()
|
||||||
|
}
|
||||||
|
|
||||||
|
function Invoke-Plugin {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
$Settings
|
||||||
|
)
|
||||||
|
|
||||||
|
Import-PluginDependency -ModuleName "Logging" -RequiredCommand "Write-Log"
|
||||||
|
Import-PluginDependency -ModuleName "ScriptConfig" -RequiredCommand "Assert-Command"
|
||||||
|
|
||||||
|
$pluginSettings = $Settings
|
||||||
|
$sharedSettings = $Settings.Context
|
||||||
|
$githubTokenEnvVar = $pluginSettings.githubToken
|
||||||
|
$configuredRepository = $pluginSettings.repository
|
||||||
|
$releaseNotesFileSetting = $pluginSettings.releaseNotesFile
|
||||||
|
$releaseTitlePatternSetting = $pluginSettings.releaseTitlePattern
|
||||||
|
$scriptDir = $sharedSettings.ScriptDir
|
||||||
|
$version = $sharedSettings.Version
|
||||||
|
$tag = $sharedSettings.Tag
|
||||||
|
$releaseDir = $sharedSettings.ReleaseDir
|
||||||
|
$releaseAssetPaths = @()
|
||||||
|
|
||||||
|
Assert-Command gh
|
||||||
|
|
||||||
|
if ([string]::IsNullOrWhiteSpace($githubTokenEnvVar)) {
|
||||||
|
throw "GitHub plugin requires 'githubToken' in scriptsettings.json."
|
||||||
|
}
|
||||||
|
|
||||||
|
$githubToken = [System.Environment]::GetEnvironmentVariable($githubTokenEnvVar)
|
||||||
|
if ([string]::IsNullOrWhiteSpace($githubToken)) {
|
||||||
|
throw "GitHub token is not set. Set '$githubTokenEnvVar' and rerun."
|
||||||
|
}
|
||||||
|
|
||||||
|
if ([string]::IsNullOrWhiteSpace($releaseNotesFileSetting)) {
|
||||||
|
throw "GitHub plugin requires 'releaseNotesFile' in scriptsettings.json."
|
||||||
|
}
|
||||||
|
|
||||||
|
$releaseNotesFile = [System.IO.Path]::GetFullPath((Join-Path $scriptDir $releaseNotesFileSetting))
|
||||||
|
$releaseNotes = Get-ReleaseNotesInternal -ReleaseNotesFile $releaseNotesFile -Version $version
|
||||||
|
|
||||||
|
if ($sharedSettings.PSObject.Properties['ReleaseAssetPaths'] -and $sharedSettings.ReleaseAssetPaths) {
|
||||||
|
$releaseAssetPaths = @($sharedSettings.ReleaseAssetPaths)
|
||||||
|
}
|
||||||
|
elseif ($sharedSettings.PSObject.Properties['PackageFile'] -and $sharedSettings.PackageFile) {
|
||||||
|
$releaseAssetPaths = @($sharedSettings.PackageFile.FullName)
|
||||||
|
if ($sharedSettings.PSObject.Properties['SymbolsPackageFile'] -and $sharedSettings.SymbolsPackageFile) {
|
||||||
|
$releaseAssetPaths += $sharedSettings.SymbolsPackageFile.FullName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($releaseAssetPaths.Count -eq 0) {
|
||||||
|
throw "GitHub release requires at least one prepared release asset."
|
||||||
|
}
|
||||||
|
|
||||||
|
$repo = Get-GitHubRepositoryInternal -ConfiguredRepository $configuredRepository
|
||||||
|
$releaseTitlePattern = if ([string]::IsNullOrWhiteSpace($releaseTitlePatternSetting)) {
|
||||||
|
"Release {version}"
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$releaseTitlePatternSetting
|
||||||
|
}
|
||||||
|
$releaseName = $releaseTitlePattern -replace '\{version\}', $version
|
||||||
|
|
||||||
|
Write-Log -Level "INFO" -Message " GitHub repository: $repo"
|
||||||
|
Write-Log -Level "INFO" -Message " GitHub tag: $tag"
|
||||||
|
Write-Log -Level "INFO" -Message " GitHub title: $releaseName"
|
||||||
|
|
||||||
|
$previousGhToken = $env:GH_TOKEN
|
||||||
|
$env:GH_TOKEN = $githubToken
|
||||||
|
|
||||||
|
try {
|
||||||
|
$ghVersion = & gh --version 2>&1
|
||||||
|
if ($ghVersion) {
|
||||||
|
Write-Log -Level "INFO" -Message " gh version: $($ghVersion[0])"
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Log -Level "INFO" -Message " Auth env var: $githubTokenEnvVar (set)"
|
||||||
|
|
||||||
|
$authArgs = @("api", "repos/$repo", "--jq", ".full_name")
|
||||||
|
$authOutput = & gh @authArgs 2>&1
|
||||||
|
$authExitCode = $LASTEXITCODE
|
||||||
|
|
||||||
|
if ($authExitCode -ne 0 -or [string]::IsNullOrWhiteSpace(($authOutput | Out-String))) {
|
||||||
|
Write-Log -Level "WARN" -Message " gh auth check failed (exit code: $authExitCode)."
|
||||||
|
if ($authOutput) {
|
||||||
|
$authOutput | ForEach-Object { Write-Log -Level "WARN" -Message " $_" }
|
||||||
|
}
|
||||||
|
|
||||||
|
$authStatus = & gh auth status --hostname github.com 2>&1
|
||||||
|
if ($authStatus) {
|
||||||
|
$authStatus | ForEach-Object { Write-Log -Level "WARN" -Message " $_" }
|
||||||
|
}
|
||||||
|
|
||||||
|
throw "GitHub CLI authentication failed for repository '$repo'. Ensure '$githubTokenEnvVar' is valid and has access to this repository."
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Log -Level "OK" -Message " GitHub token validated for repository: $($authOutput | Select-Object -First 1)"
|
||||||
|
Write-Log -Level "STEP" -Message "Creating GitHub release..."
|
||||||
|
|
||||||
|
$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) {
|
||||||
|
throw "Failed to delete existing release $tag."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$notesFilePath = Join-Path $releaseDir ("release-notes-{0}.md" -f $version)
|
||||||
|
|
||||||
|
try {
|
||||||
|
[System.IO.File]::WriteAllText($notesFilePath, $releaseNotes, [System.Text.UTF8Encoding]::new($false))
|
||||||
|
|
||||||
|
$createReleaseArgs = @("release", "create", $tag) + $releaseAssetPaths + @(
|
||||||
|
"--repo", $repo,
|
||||||
|
"--title", $releaseName,
|
||||||
|
"--notes-file", $notesFilePath
|
||||||
|
)
|
||||||
|
& gh @createReleaseArgs
|
||||||
|
|
||||||
|
if ($LASTEXITCODE -ne 0) {
|
||||||
|
throw "Failed to create GitHub release for tag $tag."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
if (Test-Path $notesFilePath) {
|
||||||
|
Remove-Item $notesFilePath -Force
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Log -Level "OK" -Message " GitHub release created successfully."
|
||||||
|
$sharedSettings | Add-Member -NotePropertyName PublishCompleted -NotePropertyValue $true -Force
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
if ($null -ne $previousGhToken) {
|
||||||
|
$env:GH_TOKEN = $previousGhToken
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Remove-Item Env:GH_TOKEN -ErrorAction SilentlyContinue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Export-ModuleMember -Function Invoke-Plugin
|
||||||
67
utils/Release-Package/CorePlugins/NuGet.psm1
Normal file
67
utils/Release-Package/CorePlugins/NuGet.psm1
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
#requires -Version 7.0
|
||||||
|
#requires -PSEdition Core
|
||||||
|
|
||||||
|
<#
|
||||||
|
.SYNOPSIS
|
||||||
|
NuGet publish plugin.
|
||||||
|
|
||||||
|
.DESCRIPTION
|
||||||
|
This plugin publishes the package artifact from shared runtime
|
||||||
|
context to the configured NuGet feed using the configured API key.
|
||||||
|
#>
|
||||||
|
|
||||||
|
if (-not (Get-Command Import-PluginDependency -ErrorAction SilentlyContinue)) {
|
||||||
|
$pluginSupportModulePath = Join-Path (Split-Path $PSScriptRoot -Parent) "PluginSupport.psm1"
|
||||||
|
if (Test-Path $pluginSupportModulePath -PathType Leaf) {
|
||||||
|
Import-Module $pluginSupportModulePath -Force -Global -ErrorAction Stop
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Invoke-Plugin {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
$Settings
|
||||||
|
)
|
||||||
|
|
||||||
|
Import-PluginDependency -ModuleName "Logging" -RequiredCommand "Write-Log"
|
||||||
|
Import-PluginDependency -ModuleName "ScriptConfig" -RequiredCommand "Assert-Command"
|
||||||
|
|
||||||
|
$pluginSettings = $Settings
|
||||||
|
$sharedSettings = $Settings.Context
|
||||||
|
$nugetApiKeyEnvVar = $pluginSettings.nugetApiKey
|
||||||
|
$packageFile = $sharedSettings.PackageFile
|
||||||
|
|
||||||
|
Assert-Command dotnet
|
||||||
|
|
||||||
|
if (-not $packageFile) {
|
||||||
|
throw "NuGet plugin requires a NuGet package artifact. Ensure DotNetPack produced a .nupkg before running NuGet."
|
||||||
|
}
|
||||||
|
|
||||||
|
if ([string]::IsNullOrWhiteSpace($nugetApiKeyEnvVar)) {
|
||||||
|
throw "NuGet plugin requires 'nugetApiKey' in scriptsettings.json."
|
||||||
|
}
|
||||||
|
|
||||||
|
$nugetApiKey = [System.Environment]::GetEnvironmentVariable($nugetApiKeyEnvVar)
|
||||||
|
if ([string]::IsNullOrWhiteSpace($nugetApiKey)) {
|
||||||
|
throw "NuGet API key is not set. Set '$nugetApiKeyEnvVar' and rerun."
|
||||||
|
}
|
||||||
|
|
||||||
|
$nugetSource = if ([string]::IsNullOrWhiteSpace($pluginSettings.source)) {
|
||||||
|
"https://api.nuget.org/v3/index.json"
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$pluginSettings.source
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Log -Level "STEP" -Message "Pushing to NuGet.org..."
|
||||||
|
dotnet nuget push $packageFile.FullName -k $nugetApiKey -s $nugetSource --skip-duplicate
|
||||||
|
|
||||||
|
if ($LASTEXITCODE -ne 0) {
|
||||||
|
throw "Failed to push the package to NuGet."
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Log -Level "OK" -Message " NuGet push completed."
|
||||||
|
$sharedSettings | Add-Member -NotePropertyName PublishCompleted -NotePropertyValue $true -Force
|
||||||
|
}
|
||||||
|
|
||||||
|
Export-ModuleMember -Function Invoke-Plugin
|
||||||
119
utils/Release-Package/CorePlugins/QualityGate.psm1
Normal file
119
utils/Release-Package/CorePlugins/QualityGate.psm1
Normal file
@ -0,0 +1,119 @@
|
|||||||
|
#requires -Version 7.0
|
||||||
|
#requires -PSEdition Core
|
||||||
|
|
||||||
|
<#
|
||||||
|
.SYNOPSIS
|
||||||
|
Quality gate plugin for validating release readiness.
|
||||||
|
|
||||||
|
.DESCRIPTION
|
||||||
|
This plugin evaluates quality constraints using shared test
|
||||||
|
results and project files. It enforces coverage thresholds
|
||||||
|
and checks for vulnerable packages before release plugins run.
|
||||||
|
#>
|
||||||
|
|
||||||
|
if (-not (Get-Command Import-PluginDependency -ErrorAction SilentlyContinue)) {
|
||||||
|
$pluginSupportModulePath = Join-Path (Split-Path $PSScriptRoot -Parent) "PluginSupport.psm1"
|
||||||
|
if (Test-Path $pluginSupportModulePath -PathType Leaf) {
|
||||||
|
Import-Module $pluginSupportModulePath -Force -Global -ErrorAction Stop
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Test-VulnerablePackagesInternal {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[string[]]$ProjectFiles
|
||||||
|
)
|
||||||
|
|
||||||
|
$findings = @()
|
||||||
|
|
||||||
|
foreach ($projectPath in $ProjectFiles) {
|
||||||
|
Write-Log -Level "STEP" -Message "Checking vulnerable packages: $([System.IO.Path]::GetFileName($projectPath))"
|
||||||
|
|
||||||
|
$output = & dotnet list $projectPath package --vulnerable --include-transitive 2>&1
|
||||||
|
if ($LASTEXITCODE -ne 0) {
|
||||||
|
throw "dotnet list package --vulnerable failed for $projectPath."
|
||||||
|
}
|
||||||
|
|
||||||
|
$outputText = ($output | Out-String)
|
||||||
|
if ($outputText -match "(?im)\bhas the following vulnerable packages\b" -or $outputText -match "(?im)^\s*>\s+[A-Za-z0-9_.-]+\s") {
|
||||||
|
$findings += [pscustomobject]@{
|
||||||
|
Project = $projectPath
|
||||||
|
Output = $outputText.Trim()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $findings
|
||||||
|
}
|
||||||
|
|
||||||
|
function Invoke-Plugin {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
$Settings
|
||||||
|
)
|
||||||
|
|
||||||
|
Import-PluginDependency -ModuleName "Logging" -RequiredCommand "Write-Log"
|
||||||
|
Import-PluginDependency -ModuleName "ScriptConfig" -RequiredCommand "Assert-Command"
|
||||||
|
|
||||||
|
$pluginSettings = $Settings
|
||||||
|
$sharedSettings = $Settings.Context
|
||||||
|
$coverageThresholdSetting = $pluginSettings.coverageThreshold
|
||||||
|
$failOnVulnerabilitiesSetting = $pluginSettings.failOnVulnerabilities
|
||||||
|
$projectFiles = $sharedSettings.ProjectFiles
|
||||||
|
$testResult = $null
|
||||||
|
if ($sharedSettings.PSObject.Properties['TestResult']) {
|
||||||
|
$testResult = $sharedSettings.TestResult
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($null -eq $testResult) {
|
||||||
|
throw "QualityGate plugin requires test results. Run the DotNetTest plugin first."
|
||||||
|
}
|
||||||
|
|
||||||
|
$coverageThreshold = 0
|
||||||
|
if ($null -ne $coverageThresholdSetting) {
|
||||||
|
$coverageThreshold = [double]$coverageThresholdSetting
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($coverageThreshold -gt 0) {
|
||||||
|
Write-Log -Level "STEP" -Message "Checking coverage threshold..."
|
||||||
|
if ([double]$testResult.LineRate -lt $coverageThreshold) {
|
||||||
|
throw "Line coverage $($testResult.LineRate)% is below the configured threshold of $coverageThreshold%."
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Log -Level "OK" -Message " Coverage threshold met: $($testResult.LineRate)% >= $coverageThreshold%"
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Write-Log -Level "WARN" -Message "Skipping coverage threshold check (disabled)."
|
||||||
|
}
|
||||||
|
|
||||||
|
Assert-Command dotnet
|
||||||
|
|
||||||
|
$failOnVulnerabilities = $true
|
||||||
|
if ($null -ne $failOnVulnerabilitiesSetting) {
|
||||||
|
$failOnVulnerabilities = [bool]$failOnVulnerabilitiesSetting
|
||||||
|
}
|
||||||
|
|
||||||
|
$vulnerabilities = Test-VulnerablePackagesInternal -ProjectFiles $projectFiles
|
||||||
|
|
||||||
|
if ($vulnerabilities.Count -eq 0) {
|
||||||
|
Write-Log -Level "OK" -Message " No vulnerable packages detected."
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($finding in $vulnerabilities) {
|
||||||
|
Write-Log -Level "WARN" -Message " Vulnerable packages detected in $([System.IO.Path]::GetFileName($finding.Project))"
|
||||||
|
$finding.Output -split "`r?`n" | ForEach-Object {
|
||||||
|
if (-not [string]::IsNullOrWhiteSpace($_)) {
|
||||||
|
Write-Log -Level "WARN" -Message " $_"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($failOnVulnerabilities) {
|
||||||
|
throw "Vulnerable packages were detected and failOnVulnerabilities is enabled."
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Log -Level "WARN" -Message "Vulnerable packages detected, but failOnVulnerabilities is disabled."
|
||||||
|
}
|
||||||
|
|
||||||
|
Export-ModuleMember -Function Invoke-Plugin
|
||||||
1
utils/Release-Package/CustomPlugins/.gitkeep
Normal file
1
utils/Release-Package/CustomPlugins/.gitkeep
Normal file
@ -0,0 +1 @@
|
|||||||
|
|
||||||
110
utils/Release-Package/DotNetProjectSupport.psm1
Normal file
110
utils/Release-Package/DotNetProjectSupport.psm1
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
#requires -Version 7.0
|
||||||
|
#requires -PSEdition Core
|
||||||
|
|
||||||
|
if (-not (Get-Command Write-Log -ErrorAction SilentlyContinue)) {
|
||||||
|
$loggingModulePath = Join-Path (Split-Path $PSScriptRoot -Parent) "Logging.psm1"
|
||||||
|
if (Test-Path $loggingModulePath -PathType Leaf) {
|
||||||
|
Import-Module $loggingModulePath -Force
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (-not (Get-Command Get-PluginPathListSetting -ErrorAction SilentlyContinue)) {
|
||||||
|
$pluginSupportModulePath = Join-Path $PSScriptRoot "PluginSupport.psm1"
|
||||||
|
if (Test-Path $pluginSupportModulePath -PathType Leaf) {
|
||||||
|
Import-Module $pluginSupportModulePath -Force
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-DotNetProjectPropertyValue {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[xml]$Csproj,
|
||||||
|
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[string]$PropertyName
|
||||||
|
)
|
||||||
|
|
||||||
|
# SDK-style .csproj files can have multiple PropertyGroup nodes.
|
||||||
|
# Use the first group that defines the requested property.
|
||||||
|
$propNode = $Csproj.Project.PropertyGroup |
|
||||||
|
Where-Object { $_.$PropertyName } |
|
||||||
|
Select-Object -First 1
|
||||||
|
|
||||||
|
if ($propNode) {
|
||||||
|
return $propNode.$PropertyName
|
||||||
|
}
|
||||||
|
|
||||||
|
return $null
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-DotNetProjectVersions {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[string[]]$ProjectFiles
|
||||||
|
)
|
||||||
|
|
||||||
|
Write-Log -Level "INFO" -Message "Reading version(s) from .NET project files..."
|
||||||
|
$projectVersions = @{}
|
||||||
|
|
||||||
|
foreach ($projectPath in $ProjectFiles) {
|
||||||
|
if (-not (Test-Path $projectPath -PathType Leaf)) {
|
||||||
|
Write-Error "Project file not found at: $projectPath"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
if ([System.IO.Path]::GetExtension($projectPath) -ne ".csproj") {
|
||||||
|
Write-Error "Configured project file is not a .csproj file: $projectPath"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
[xml]$csproj = Get-Content $projectPath
|
||||||
|
$version = Get-DotNetProjectPropertyValue -Csproj $csproj -PropertyName "Version"
|
||||||
|
|
||||||
|
if (-not $version) {
|
||||||
|
Write-Error "Version not found in $projectPath"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
$projectVersions[$projectPath] = $version
|
||||||
|
Write-Log -Level "OK" -Message " $([System.IO.Path]::GetFileName($projectPath)): $version"
|
||||||
|
}
|
||||||
|
|
||||||
|
return $projectVersions
|
||||||
|
}
|
||||||
|
|
||||||
|
function New-DotNetReleaseContext {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[object[]]$Plugins,
|
||||||
|
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[string]$ScriptDir
|
||||||
|
)
|
||||||
|
|
||||||
|
# The array wrapper is intentional: without it, one configured project can collapse to a string,
|
||||||
|
# and later indexing [0] would return only the first character of the path.
|
||||||
|
$projectFiles = @(Get-PluginPathListSetting -Plugins $Plugins -PropertyName "projectFiles" -BasePath $ScriptDir)
|
||||||
|
$artifactsDirectory = Get-PluginPathSetting -Plugins $Plugins -PropertyName "artifactsDir" -BasePath $ScriptDir
|
||||||
|
|
||||||
|
if ($projectFiles.Count -eq 0) {
|
||||||
|
Write-Error "No .NET project files configured in plugin settings. Add 'projectFiles' to a relevant plugin."
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
if ([string]::IsNullOrWhiteSpace($artifactsDirectory)) {
|
||||||
|
Write-Error "No artifacts directory configured in plugin settings. Add 'artifactsDir' to a relevant plugin."
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
$projectVersions = Get-DotNetProjectVersions -ProjectFiles $projectFiles
|
||||||
|
# The first configured project is treated as the canonical version source for the release.
|
||||||
|
$version = $projectVersions[$projectFiles[0]]
|
||||||
|
|
||||||
|
return [pscustomobject]@{
|
||||||
|
ProjectFiles = $projectFiles
|
||||||
|
ArtifactsDirectory = $artifactsDirectory
|
||||||
|
Version = $version
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Export-ModuleMember -Function Get-DotNetProjectPropertyValue, Get-DotNetProjectVersions, New-DotNetReleaseContext
|
||||||
165
utils/Release-Package/EngineSupport.psm1
Normal file
165
utils/Release-Package/EngineSupport.psm1
Normal file
@ -0,0 +1,165 @@
|
|||||||
|
#requires -Version 7.0
|
||||||
|
#requires -PSEdition Core
|
||||||
|
|
||||||
|
if (-not (Get-Command Write-Log -ErrorAction SilentlyContinue)) {
|
||||||
|
$loggingModulePath = Join-Path (Split-Path $PSScriptRoot -Parent) "Logging.psm1"
|
||||||
|
if (Test-Path $loggingModulePath -PathType Leaf) {
|
||||||
|
Import-Module $loggingModulePath -Force
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (-not (Get-Command Get-CurrentBranch -ErrorAction SilentlyContinue)) {
|
||||||
|
$gitToolsModulePath = Join-Path (Split-Path $PSScriptRoot -Parent) "GitTools.psm1"
|
||||||
|
if (Test-Path $gitToolsModulePath -PathType Leaf) {
|
||||||
|
Import-Module $gitToolsModulePath -Force
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (-not (Get-Command Get-PluginStage -ErrorAction SilentlyContinue) -or -not (Get-Command Test-IsPublishPlugin -ErrorAction SilentlyContinue)) {
|
||||||
|
$pluginSupportModulePath = Join-Path $PSScriptRoot "PluginSupport.psm1"
|
||||||
|
if (Test-Path $pluginSupportModulePath -PathType Leaf) {
|
||||||
|
Import-Module $pluginSupportModulePath -Force
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (-not (Get-Command New-DotNetReleaseContext -ErrorAction SilentlyContinue)) {
|
||||||
|
$dotNetProjectSupportModulePath = Join-Path $PSScriptRoot "DotNetProjectSupport.psm1"
|
||||||
|
if (Test-Path $dotNetProjectSupportModulePath -PathType Leaf) {
|
||||||
|
Import-Module $dotNetProjectSupportModulePath -Force
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Log -Level "WARN" -Message " Uncommitted changes detected (allowed on dev branch)."
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Log -Level "OK" -Message " Working directory is clean."
|
||||||
|
}
|
||||||
|
|
||||||
|
function Initialize-ReleaseStageContext {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[object[]]$RemainingPlugins,
|
||||||
|
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[psobject]$SharedSettings,
|
||||||
|
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[string]$ArtifactsDirectory,
|
||||||
|
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[string]$Version
|
||||||
|
)
|
||||||
|
|
||||||
|
Write-Log -Level "STEP" -Message "Verifying tag is pushed to remote..."
|
||||||
|
$remoteTagExists = Test-RemoteTagExists -Tag $SharedSettings.Tag -Remote "origin"
|
||||||
|
if (-not $remoteTagExists) {
|
||||||
|
Write-Log -Level "WARN" -Message " Tag $($SharedSettings.Tag) not found on remote. Pushing..."
|
||||||
|
Push-TagToRemote -Tag $SharedSettings.Tag -Remote "origin"
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Write-Log -Level "OK" -Message " Tag exists on remote."
|
||||||
|
}
|
||||||
|
|
||||||
|
if (-not $SharedSettings.PSObject.Properties['ReleaseDir'] -or [string]::IsNullOrWhiteSpace([string]$SharedSettings.ReleaseDir)) {
|
||||||
|
$SharedSettings | Add-Member -NotePropertyName ReleaseDir -NotePropertyValue $ArtifactsDirectory -Force
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function New-EngineContext {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[object[]]$Plugins,
|
||||||
|
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[string]$ScriptDir,
|
||||||
|
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[string]$UtilsDir
|
||||||
|
)
|
||||||
|
|
||||||
|
$dotNetContext = New-DotNetReleaseContext -Plugins $Plugins -ScriptDir $ScriptDir
|
||||||
|
|
||||||
|
$currentBranch = Get-CurrentBranch
|
||||||
|
$releaseBranches = @(
|
||||||
|
$Plugins |
|
||||||
|
Where-Object { Test-IsPublishPlugin -Plugin $_ } |
|
||||||
|
ForEach-Object { Get-PluginBranches -Plugin $_ } |
|
||||||
|
Where-Object { -not [string]::IsNullOrWhiteSpace($_) } |
|
||||||
|
Select-Object -Unique
|
||||||
|
)
|
||||||
|
|
||||||
|
$isReleaseBranch = $releaseBranches -contains $currentBranch
|
||||||
|
$isNonReleaseBranch = -not $isReleaseBranch
|
||||||
|
|
||||||
|
Assert-WorkingTreeClean -IsReleaseBranch:$isReleaseBranch
|
||||||
|
|
||||||
|
$version = $dotNetContext.Version
|
||||||
|
|
||||||
|
if ($isReleaseBranch) {
|
||||||
|
$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 the project version ($version)."
|
||||||
|
Write-Log -Level "WARN" -Message " Either update the tag or the project version."
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Log -Level "OK" -Message " Tag found: $tag (matches project version)"
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$tag = "v$version"
|
||||||
|
Write-Log -Level "INFO" -Message " Using version from the package project (no tag required on non-release branches)."
|
||||||
|
}
|
||||||
|
|
||||||
|
return [pscustomobject]@{
|
||||||
|
ScriptDir = $ScriptDir
|
||||||
|
UtilsDir = $UtilsDir
|
||||||
|
CurrentBranch = $currentBranch
|
||||||
|
Version = $version
|
||||||
|
Tag = $tag
|
||||||
|
ProjectFiles = $dotNetContext.ProjectFiles
|
||||||
|
ArtifactsDirectory = $dotNetContext.ArtifactsDirectory
|
||||||
|
IsReleaseBranch = $isReleaseBranch
|
||||||
|
IsNonReleaseBranch = $isNonReleaseBranch
|
||||||
|
ReleaseBranches = $releaseBranches
|
||||||
|
NonReleaseBranches = @()
|
||||||
|
PublishCompleted = $false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-PreferredReleaseBranch {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[psobject]$EngineContext
|
||||||
|
)
|
||||||
|
|
||||||
|
if ($EngineContext.ReleaseBranches.Count -gt 0) {
|
||||||
|
return $EngineContext.ReleaseBranches[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
return "main"
|
||||||
|
}
|
||||||
|
|
||||||
|
Export-ModuleMember -Function Assert-WorkingTreeClean, Initialize-ReleaseStageContext, New-EngineContext, Get-PreferredReleaseBranch
|
||||||
368
utils/Release-Package/PluginSupport.psm1
Normal file
368
utils/Release-Package/PluginSupport.psm1
Normal file
@ -0,0 +1,368 @@
|
|||||||
|
#requires -Version 7.0
|
||||||
|
#requires -PSEdition Core
|
||||||
|
|
||||||
|
if (-not (Get-Command Write-Log -ErrorAction SilentlyContinue)) {
|
||||||
|
$loggingModulePath = Join-Path (Split-Path $PSScriptRoot -Parent) "Logging.psm1"
|
||||||
|
if (Test-Path $loggingModulePath -PathType Leaf) {
|
||||||
|
Import-Module $loggingModulePath -Force
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Import-PluginDependency {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[string]$ModuleName,
|
||||||
|
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[string]$RequiredCommand
|
||||||
|
)
|
||||||
|
|
||||||
|
if (Get-Command $RequiredCommand -ErrorAction SilentlyContinue) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
$moduleRoot = Split-Path $PSScriptRoot -Parent
|
||||||
|
$modulePath = Join-Path $moduleRoot "$ModuleName.psm1"
|
||||||
|
if (Test-Path $modulePath -PathType Leaf) {
|
||||||
|
# Import into the global session so the calling plugin can see the exported commands.
|
||||||
|
# Importing only into this module's scope would make the dependency invisible to the plugin.
|
||||||
|
Import-Module $modulePath -Force -Global -ErrorAction Stop
|
||||||
|
}
|
||||||
|
|
||||||
|
if (-not (Get-Command $RequiredCommand -ErrorAction SilentlyContinue)) {
|
||||||
|
throw "Required command '$RequiredCommand' is still unavailable after importing module '$ModuleName'."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-ConfiguredPlugins {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[psobject]$Settings
|
||||||
|
)
|
||||||
|
|
||||||
|
if (-not $Settings.PSObject.Properties['Plugins'] -or $null -eq $Settings.Plugins) {
|
||||||
|
return @()
|
||||||
|
}
|
||||||
|
|
||||||
|
# JSON can deserialize a single plugin as one object or multiple plugins as an array.
|
||||||
|
# Always return an array so the engine can loop without special-case logic.
|
||||||
|
if ($Settings.Plugins -is [System.Collections.IEnumerable] -and -not ($Settings.Plugins -is [string])) {
|
||||||
|
return @($Settings.Plugins)
|
||||||
|
}
|
||||||
|
|
||||||
|
return @($Settings.Plugins)
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-PluginStage {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
$Plugin
|
||||||
|
)
|
||||||
|
|
||||||
|
if (-not $Plugin.PSObject.Properties['Stage'] -or [string]::IsNullOrWhiteSpace([string]$Plugin.Stage)) {
|
||||||
|
return "Release"
|
||||||
|
}
|
||||||
|
|
||||||
|
return [string]$Plugin.Stage
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-PluginBranches {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
$Plugin
|
||||||
|
)
|
||||||
|
|
||||||
|
if (-not $Plugin.PSObject.Properties['branches'] -or $null -eq $Plugin.branches) {
|
||||||
|
return @()
|
||||||
|
}
|
||||||
|
|
||||||
|
# Strings are also IEnumerable in PowerShell, so exclude them or we would split into characters.
|
||||||
|
if ($Plugin.branches -is [System.Collections.IEnumerable] -and -not ($Plugin.branches -is [string])) {
|
||||||
|
return @($Plugin.branches | Where-Object { -not [string]::IsNullOrWhiteSpace($_) })
|
||||||
|
}
|
||||||
|
|
||||||
|
if ([string]::IsNullOrWhiteSpace([string]$Plugin.branches)) {
|
||||||
|
return @()
|
||||||
|
}
|
||||||
|
|
||||||
|
return @([string]$Plugin.branches)
|
||||||
|
}
|
||||||
|
|
||||||
|
function Test-IsPublishPlugin {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
$Plugin
|
||||||
|
)
|
||||||
|
|
||||||
|
if ($null -eq $Plugin -or [string]::IsNullOrWhiteSpace([string]$Plugin.Name)) {
|
||||||
|
return $false
|
||||||
|
}
|
||||||
|
|
||||||
|
return @('GitHub', 'NuGet') -contains ([string]$Plugin.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-PluginSettingValue {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[object[]]$Plugins,
|
||||||
|
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[string]$PropertyName
|
||||||
|
)
|
||||||
|
|
||||||
|
foreach ($plugin in $Plugins) {
|
||||||
|
if ($null -eq $plugin -or [string]::IsNullOrWhiteSpace($plugin.Name)) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if (-not $plugin.PSObject.Properties[$PropertyName]) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
$value = $plugin.$PropertyName
|
||||||
|
if ($null -eq $value) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($value -is [string] -and [string]::IsNullOrWhiteSpace($value)) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
return $value
|
||||||
|
}
|
||||||
|
|
||||||
|
return $null
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-PluginPathListSetting {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[object[]]$Plugins,
|
||||||
|
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[string]$PropertyName,
|
||||||
|
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[string]$BasePath
|
||||||
|
)
|
||||||
|
|
||||||
|
$rawPaths = @()
|
||||||
|
$value = Get-PluginSettingValue -Plugins $Plugins -PropertyName $PropertyName
|
||||||
|
|
||||||
|
if ($null -eq $value) {
|
||||||
|
return @()
|
||||||
|
}
|
||||||
|
|
||||||
|
# Same rule as above: treat a string as one path, not a char-by-char sequence.
|
||||||
|
if ($value -is [System.Collections.IEnumerable] -and -not ($value -is [string])) {
|
||||||
|
$rawPaths += $value
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$rawPaths += $value
|
||||||
|
}
|
||||||
|
|
||||||
|
$resolvedPaths = @()
|
||||||
|
foreach ($path in $rawPaths) {
|
||||||
|
if ([string]::IsNullOrWhiteSpace([string]$path)) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
$resolvedPaths += [System.IO.Path]::GetFullPath((Join-Path $BasePath ([string]$path)))
|
||||||
|
}
|
||||||
|
|
||||||
|
# Wrap again to stop PowerShell from unrolling a single-item array into a bare string.
|
||||||
|
return @($resolvedPaths)
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-PluginPathSetting {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[object[]]$Plugins,
|
||||||
|
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[string]$PropertyName,
|
||||||
|
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[string]$BasePath
|
||||||
|
)
|
||||||
|
|
||||||
|
$value = Get-PluginSettingValue -Plugins $Plugins -PropertyName $PropertyName
|
||||||
|
if ($null -eq $value -or [string]::IsNullOrWhiteSpace([string]$value)) {
|
||||||
|
return $null
|
||||||
|
}
|
||||||
|
|
||||||
|
return [System.IO.Path]::GetFullPath((Join-Path $BasePath ([string]$value)))
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-ArchiveNamePattern {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[object[]]$Plugins,
|
||||||
|
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[string]$CurrentBranch
|
||||||
|
)
|
||||||
|
|
||||||
|
foreach ($plugin in $Plugins) {
|
||||||
|
if ($null -eq $plugin -or [string]::IsNullOrWhiteSpace($plugin.Name)) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if (-not $plugin.Enabled) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
$allowedBranches = Get-PluginBranches -Plugin $plugin
|
||||||
|
if ($allowedBranches.Count -gt 0 -and -not ($allowedBranches -contains $CurrentBranch)) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($plugin.PSObject.Properties['zipNamePattern'] -and -not [string]::IsNullOrWhiteSpace([string]$plugin.zipNamePattern)) {
|
||||||
|
return [string]$plugin.zipNamePattern
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return "release-{version}.zip"
|
||||||
|
}
|
||||||
|
|
||||||
|
function Resolve-PluginModulePath {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
$Plugin,
|
||||||
|
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[string]$PluginsDirectory
|
||||||
|
)
|
||||||
|
|
||||||
|
$pluginFileName = "{0}.psm1" -f $Plugin.Name
|
||||||
|
$candidatePaths = @(
|
||||||
|
(Join-Path $PluginsDirectory $pluginFileName),
|
||||||
|
(Join-Path (Join-Path (Split-Path $PluginsDirectory -Parent) "CustomPlugins") $pluginFileName)
|
||||||
|
)
|
||||||
|
|
||||||
|
foreach ($candidatePath in $candidatePaths) {
|
||||||
|
if (Test-Path $candidatePath -PathType Leaf) {
|
||||||
|
return $candidatePath
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $candidatePaths[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
function Test-PluginRunnable {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
$Plugin,
|
||||||
|
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[psobject]$SharedSettings,
|
||||||
|
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[string]$PluginsDirectory,
|
||||||
|
|
||||||
|
[Parameter(Mandatory = $false)]
|
||||||
|
[bool]$WriteLogs = $true
|
||||||
|
)
|
||||||
|
|
||||||
|
if ($null -eq $Plugin -or [string]::IsNullOrWhiteSpace($Plugin.Name)) {
|
||||||
|
if ($WriteLogs) {
|
||||||
|
Write-Log -Level "WARN" -Message "Skipping plugin entry with no Name."
|
||||||
|
}
|
||||||
|
return $false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (-not $Plugin.Enabled) {
|
||||||
|
if ($WriteLogs) {
|
||||||
|
Write-Log -Level "WARN" -Message "Skipping plugin '$($Plugin.Name)' (disabled)."
|
||||||
|
}
|
||||||
|
return $false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Test-IsPublishPlugin -Plugin $Plugin) {
|
||||||
|
$allowedBranches = Get-PluginBranches -Plugin $Plugin
|
||||||
|
if ($allowedBranches.Count -eq 0) {
|
||||||
|
if ($WriteLogs) {
|
||||||
|
Write-Log -Level "WARN" -Message "Skipping plugin '$($Plugin.Name)' because no publish branches are configured."
|
||||||
|
}
|
||||||
|
return $false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (-not ($allowedBranches -contains $SharedSettings.CurrentBranch)) {
|
||||||
|
if ($WriteLogs) {
|
||||||
|
Write-Log -Level "WARN" -Message "Skipping plugin '$($Plugin.Name)' on branch '$($SharedSettings.CurrentBranch)'."
|
||||||
|
}
|
||||||
|
return $false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$pluginModulePath = Resolve-PluginModulePath -Plugin $Plugin -PluginsDirectory $PluginsDirectory
|
||||||
|
if (-not (Test-Path $pluginModulePath -PathType Leaf)) {
|
||||||
|
if ($WriteLogs) {
|
||||||
|
Write-Log -Level "ERROR" -Message "Plugin module not found: $pluginModulePath"
|
||||||
|
}
|
||||||
|
return $false
|
||||||
|
}
|
||||||
|
|
||||||
|
return $true
|
||||||
|
}
|
||||||
|
|
||||||
|
function New-PluginInvocationSettings {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
$Plugin,
|
||||||
|
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[psobject]$SharedSettings
|
||||||
|
)
|
||||||
|
|
||||||
|
$properties = @{}
|
||||||
|
foreach ($property in $Plugin.PSObject.Properties) {
|
||||||
|
$properties[$property.Name] = $property.Value
|
||||||
|
}
|
||||||
|
|
||||||
|
# Plugins receive their own config plus a shared Context object that carries runtime artifacts.
|
||||||
|
$properties['Context'] = $SharedSettings
|
||||||
|
return [pscustomobject]$properties
|
||||||
|
}
|
||||||
|
|
||||||
|
function Invoke-ConfiguredPlugin {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
$Plugin,
|
||||||
|
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[psobject]$SharedSettings,
|
||||||
|
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[string]$PluginsDirectory,
|
||||||
|
|
||||||
|
[Parameter(Mandatory = $false)]
|
||||||
|
[bool]$ContinueOnError = $true
|
||||||
|
)
|
||||||
|
|
||||||
|
if (-not (Test-PluginRunnable -Plugin $Plugin -SharedSettings $SharedSettings -PluginsDirectory $PluginsDirectory -WriteLogs:$true)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
$pluginModulePath = Resolve-PluginModulePath -Plugin $Plugin -PluginsDirectory $PluginsDirectory
|
||||||
|
Write-Log -Level "STEP" -Message "Running plugin '$($Plugin.Name)'..."
|
||||||
|
|
||||||
|
try {
|
||||||
|
$moduleInfo = Import-Module $pluginModulePath -Force -PassThru -ErrorAction Stop
|
||||||
|
# Resolve Invoke-Plugin from the imported module explicitly so we call the plugin we just loaded,
|
||||||
|
# not some command with the same name from another module already in session.
|
||||||
|
$invokeCommand = Get-Command -Name "Invoke-Plugin" -Module $moduleInfo.Name -ErrorAction Stop
|
||||||
|
$pluginSettings = New-PluginInvocationSettings -Plugin $Plugin -SharedSettings $SharedSettings
|
||||||
|
|
||||||
|
& $invokeCommand -Settings $pluginSettings
|
||||||
|
Write-Log -Level "OK" -Message " Plugin '$($Plugin.Name)' completed."
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
Write-Log -Level "ERROR" -Message " Plugin '$($Plugin.Name)' failed: $($_.Exception.Message)"
|
||||||
|
if (-not $ContinueOnError) {
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Export-ModuleMember -Function Import-PluginDependency, Get-ConfiguredPlugins, Get-PluginStage, Get-PluginBranches, Test-IsPublishPlugin, Get-PluginSettingValue, Get-PluginPathListSetting, Get-PluginPathSetting, Get-ArchiveNamePattern, Resolve-PluginModulePath, Test-PluginRunnable, New-PluginInvocationSettings, Invoke-ConfiguredPlugin
|
||||||
3
utils/Release-Package/Release-Package.bat
Normal file
3
utils/Release-Package/Release-Package.bat
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
@echo off
|
||||||
|
pwsh -NoProfile -ExecutionPolicy Bypass -File "%~dp0Release-Package.ps1"
|
||||||
|
pause
|
||||||
182
utils/Release-Package/Release-Package.ps1
Normal file
182
utils/Release-Package/Release-Package.ps1
Normal file
@ -0,0 +1,182 @@
|
|||||||
|
#requires -Version 7.0
|
||||||
|
#requires -PSEdition Core
|
||||||
|
|
||||||
|
<#
|
||||||
|
.SYNOPSIS
|
||||||
|
Plugin-driven release engine.
|
||||||
|
|
||||||
|
.DESCRIPTION
|
||||||
|
This script is the orchestration layer for release automation.
|
||||||
|
It loads scriptsettings.json, evaluates the configured plugins in order,
|
||||||
|
builds shared execution context, and invokes each plugin's Invoke-Plugin
|
||||||
|
entrypoint with that plugin's own settings object plus runtime context.
|
||||||
|
|
||||||
|
The engine is intentionally generic:
|
||||||
|
- It does not embed release-provider-specific logic
|
||||||
|
- It preserves plugin execution order from scriptsettings.json
|
||||||
|
- It isolates plugin failures according to the stage/runtime policy
|
||||||
|
- It keeps shared orchestration helpers in dedicated support modules
|
||||||
|
|
||||||
|
.REQUIREMENTS
|
||||||
|
Tools (Required):
|
||||||
|
- Shared support modules required by the engine
|
||||||
|
- Any commands required by configured plugins or support helpers
|
||||||
|
|
||||||
|
.WORKFLOW
|
||||||
|
1. Load and normalize plugin configuration
|
||||||
|
2. Determine branch mode from configured plugin metadata
|
||||||
|
3. Validate repository state and resolve the release version
|
||||||
|
4. Build shared execution context
|
||||||
|
5. Execute plugins one by one in configured order
|
||||||
|
6. Initialize release-stage shared artifacts only when needed
|
||||||
|
7. Report completion summary
|
||||||
|
|
||||||
|
.USAGE
|
||||||
|
Configure plugin order and plugin settings in scriptsettings.json, then run:
|
||||||
|
pwsh -File .\Release-Package.ps1
|
||||||
|
|
||||||
|
.CONFIGURATION
|
||||||
|
All settings are stored in scriptsettings.json:
|
||||||
|
- Plugins: Ordered plugin definitions and plugin-specific settings
|
||||||
|
|
||||||
|
.NOTES
|
||||||
|
Plugin-specific behavior belongs in the plugin modules, not in this engine.
|
||||||
|
#>
|
||||||
|
|
||||||
|
# No parameters - behavior is controlled by configured plugin metadata:
|
||||||
|
# - non-release branches -> Run only the plugins allowed for those branches
|
||||||
|
# - release branches -> Require a matching tag and allow release-stage plugins
|
||||||
|
|
||||||
|
# Get the directory of the current script (for loading settings and relative paths)
|
||||||
|
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
|
||||||
|
|
||||||
|
#region Import Modules
|
||||||
|
|
||||||
|
$utilsDir = Split-Path $scriptDir -Parent
|
||||||
|
|
||||||
|
# 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 PluginSupport module
|
||||||
|
$pluginSupportModulePath = Join-Path $scriptDir "PluginSupport.psm1"
|
||||||
|
if (-not (Test-Path $pluginSupportModulePath)) {
|
||||||
|
Write-Error "PluginSupport module not found at: $pluginSupportModulePath"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
Import-Module $pluginSupportModulePath -Force
|
||||||
|
|
||||||
|
# Import DotNetProjectSupport module
|
||||||
|
$dotNetProjectSupportModulePath = Join-Path $scriptDir "DotNetProjectSupport.psm1"
|
||||||
|
if (-not (Test-Path $dotNetProjectSupportModulePath)) {
|
||||||
|
Write-Error "DotNetProjectSupport module not found at: $dotNetProjectSupportModulePath"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
Import-Module $dotNetProjectSupportModulePath -Force
|
||||||
|
|
||||||
|
# Import EngineSupport module
|
||||||
|
$engineSupportModulePath = Join-Path $scriptDir "EngineSupport.psm1"
|
||||||
|
if (-not (Test-Path $engineSupportModulePath)) {
|
||||||
|
Write-Error "EngineSupport module not found at: $engineSupportModulePath"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
Import-Module $engineSupportModulePath -Force
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Load Settings
|
||||||
|
$settings = Get-ScriptSettings -ScriptDir $scriptDir
|
||||||
|
$configuredPlugins = Get-ConfiguredPlugins -Settings $settings
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Configuration
|
||||||
|
|
||||||
|
$pluginsDir = Join-Path $scriptDir "CorePlugins"
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Main
|
||||||
|
|
||||||
|
Write-Log -Level "STEP" -Message "=================================================="
|
||||||
|
Write-Log -Level "STEP" -Message "RELEASE ENGINE"
|
||||||
|
Write-Log -Level "STEP" -Message "=================================================="
|
||||||
|
|
||||||
|
#region Preflight
|
||||||
|
|
||||||
|
$plugins = $configuredPlugins
|
||||||
|
$engineContext = New-EngineContext -Plugins $plugins -ScriptDir $scriptDir -UtilsDir $utilsDir
|
||||||
|
Write-Log -Level "OK" -Message "All pre-flight checks passed!"
|
||||||
|
$sharedPluginSettings = $engineContext
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Plugin Execution
|
||||||
|
|
||||||
|
$releaseStageInitialized = $false
|
||||||
|
|
||||||
|
if ($plugins.Count -eq 0) {
|
||||||
|
Write-Log -Level "WARN" -Message "No plugins configured in scriptsettings.json."
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
for ($pluginIndex = 0; $pluginIndex -lt $plugins.Count; $pluginIndex++) {
|
||||||
|
$plugin = $plugins[$pluginIndex]
|
||||||
|
$pluginStage = Get-PluginStage -Plugin $plugin
|
||||||
|
|
||||||
|
if ((Test-IsPublishPlugin -Plugin $plugin) -and -not $releaseStageInitialized) {
|
||||||
|
if (Test-PluginRunnable -Plugin $plugin -SharedSettings $sharedPluginSettings -PluginsDirectory $pluginsDir -WriteLogs:$false) {
|
||||||
|
$remainingPlugins = @($plugins[$pluginIndex..($plugins.Count - 1)])
|
||||||
|
Initialize-ReleaseStageContext -RemainingPlugins $remainingPlugins -SharedSettings $sharedPluginSettings -ArtifactsDirectory $engineContext.ArtifactsDirectory -Version $engineContext.Version
|
||||||
|
$releaseStageInitialized = $true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$continueOnError = $pluginStage -eq "Release"
|
||||||
|
Invoke-ConfiguredPlugin -Plugin $plugin -SharedSettings $sharedPluginSettings -PluginsDirectory $pluginsDir -ContinueOnError:$continueOnError
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (-not $releaseStageInitialized) {
|
||||||
|
Write-Log -Level "WARN" -Message "No release plugins executed for branch '$($engineContext.CurrentBranch)'."
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Summary
|
||||||
|
Write-Log -Level "OK" -Message "=================================================="
|
||||||
|
if ($engineContext.IsNonReleaseBranch) {
|
||||||
|
Write-Log -Level "OK" -Message "NON-RELEASE RUN COMPLETE"
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Write-Log -Level "OK" -Message "RELEASE COMPLETE"
|
||||||
|
}
|
||||||
|
Write-Log -Level "OK" -Message "=================================================="
|
||||||
|
|
||||||
|
Write-Log -Level "INFO" -Message "Artifacts location: $($engineContext.ArtifactsDirectory)"
|
||||||
|
|
||||||
|
if ($engineContext.IsNonReleaseBranch) {
|
||||||
|
$preferredReleaseBranch = Get-PreferredReleaseBranch -EngineContext $engineContext
|
||||||
|
Write-Log -Level "WARN" -Message "To execute release-stage plugins, rerun from an allowed release branch such as '$preferredReleaseBranch'."
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#endregion
|
||||||
92
utils/Release-Package/scriptsettings.json
Normal file
92
utils/Release-Package/scriptsettings.json
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://json-schema.org/draft-07/schema",
|
||||||
|
"title": "Release Package Script Settings",
|
||||||
|
"description": "Configuration file for Release-Package.ps1 script.",
|
||||||
|
"Plugins": [
|
||||||
|
{
|
||||||
|
"Name": "DotNetTest",
|
||||||
|
"Stage": "Test",
|
||||||
|
"Enabled": true,
|
||||||
|
"project": "..\\..\\src\\MaksIT.Dapr.Tests",
|
||||||
|
"resultsDir": "..\\..\\testResults"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "QualityGate",
|
||||||
|
"Stage": "QualityGate",
|
||||||
|
"Enabled": true,
|
||||||
|
"coverageThreshold": 0,
|
||||||
|
"failOnVulnerabilities": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "DotNetPack",
|
||||||
|
"Stage": "Build",
|
||||||
|
"Enabled": true,
|
||||||
|
"projectFiles": [
|
||||||
|
"..\\..\\src\\MaksIT.Dapr\\MaksIT.Dapr.csproj"
|
||||||
|
],
|
||||||
|
"artifactsDir": "..\\..\\release"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "CreateArchive",
|
||||||
|
"Stage": "Build",
|
||||||
|
"Enabled": true,
|
||||||
|
"zipNamePattern": "maksit.dapr-{version}.zip"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "GitHub",
|
||||||
|
"Stage": "Release",
|
||||||
|
"Enabled": true,
|
||||||
|
"branches": [
|
||||||
|
"main"
|
||||||
|
],
|
||||||
|
"githubToken": "GITHUB_MAKS_IT_COM",
|
||||||
|
"repository": "https://github.com/MAKS-IT-COM/maksit-dapr",
|
||||||
|
"releaseNotesFile": "..\\..\\CHANGELOG.md",
|
||||||
|
"releaseTitlePattern": "Release {version}"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "NuGet",
|
||||||
|
"Stage": "Release",
|
||||||
|
"Enabled": true,
|
||||||
|
"branches": [
|
||||||
|
"main"
|
||||||
|
],
|
||||||
|
"nugetApiKey": "NUGET_MAKS_IT",
|
||||||
|
"source": "https://api.nuget.org/v3/index.json"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "CleanupArtifacts",
|
||||||
|
"Stage": "Release",
|
||||||
|
"Enabled": true,
|
||||||
|
"includePatterns": [
|
||||||
|
"*"
|
||||||
|
],
|
||||||
|
"excludePatterns": [
|
||||||
|
"*.zip"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"_comments": {
|
||||||
|
"Plugins": {
|
||||||
|
"Name": "Plugin module file name in CorePlugins (for example, DotNetPack -> CorePlugins/DotNetPack.psm1).",
|
||||||
|
"Stage": "Execution phase. Supported values are Test, QualityGate, Build, and Release.",
|
||||||
|
"Enabled": "If true, the plugin is imported and Invoke-Plugin is called in the configured order.",
|
||||||
|
"branches": "Used only by publish plugins such as GitHub and NuGet. When the current branch is not listed, publishing is skipped.",
|
||||||
|
"project": "DotNetTest plugin only. Path to the test project directory, relative to the script folder.",
|
||||||
|
"resultsDir": "DotNetTest plugin only. Optional results directory path, relative to the script folder.",
|
||||||
|
"projectFiles": "DotNetPack, DotNetPublish, or another producer plugin can define the project files used for version discovery and artifact creation.",
|
||||||
|
"artifactsDir": "DotNetPack, DotNetPublish, or another producer plugin can define the artifacts output directory, relative to the script folder.",
|
||||||
|
"coverageThreshold": "QualityGate plugin only. Coverage threshold percent (0 disables threshold check).",
|
||||||
|
"failOnVulnerabilities": "QualityGate plugin only. If true, fail when vulnerable packages are detected.",
|
||||||
|
"githubToken": "GitHub plugin only. Environment variable name containing the GitHub token used by gh CLI.",
|
||||||
|
"repository": "GitHub plugin only. Optional owner/repo or GitHub remote URL. Leave empty to use remote.origin.url.",
|
||||||
|
"releaseNotesFile": "GitHub plugin (or another notes consumer plugin) can define the release notes source file, relative to the script folder.",
|
||||||
|
"releaseTitlePattern": "GitHub plugin only. Release title pattern. Supports {version} placeholder.",
|
||||||
|
"zipNamePattern": "CreateArchive plugin only. Archive name pattern for packaged release assets. Supports {version} placeholder.",
|
||||||
|
"nugetApiKey": "NuGet plugin only. Environment variable name containing the NuGet API key.",
|
||||||
|
"source": "NuGet plugin only. Feed URL passed to dotnet nuget push.",
|
||||||
|
"includePatterns": "CleanupArtifacts plugin only. File patterns to remove from artifactsDir (for example ['*.nupkg','*.snupkg']).",
|
||||||
|
"excludePatterns": "CleanupArtifacts plugin only. File patterns to keep even when includePatterns match (for example ['*.zip'])."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
35
utils/ScriptConfig.psm1
Normal file
35
utils/ScriptConfig.psm1
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
#requires -Version 7.0
|
||||||
|
#requires -PSEdition Core
|
||||||
|
|
||||||
|
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
|
||||||
202
utils/TestRunner.psm1
Normal file
202
utils/TestRunner.psm1
Normal file
@ -0,0 +1,202 @@
|
|||||||
|
#requires -Version 7.0
|
||||||
|
#requires -PSEdition Core
|
||||||
|
|
||||||
|
<#
|
||||||
|
.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: pwsh -Command "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
|
||||||
3
utils/Update-RepoUtils/Update-RepoUtils.bat
Normal file
3
utils/Update-RepoUtils/Update-RepoUtils.bat
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
@echo off
|
||||||
|
pwsh -NoProfile -ExecutionPolicy Bypass -File "%~dp0Update-RepoUtils.ps1"
|
||||||
|
pause
|
||||||
325
utils/Update-RepoUtils/Update-RepoUtils.ps1
Normal file
325
utils/Update-RepoUtils/Update-RepoUtils.ps1
Normal file
@ -0,0 +1,325 @@
|
|||||||
|
#requires -Version 7.0
|
||||||
|
#requires -PSEdition Core
|
||||||
|
|
||||||
|
<#
|
||||||
|
.SYNOPSIS
|
||||||
|
Refreshes a local maksit-repoutils copy from GitHub.
|
||||||
|
|
||||||
|
.DESCRIPTION
|
||||||
|
This script clones the configured repository into a temporary directory,
|
||||||
|
refreshes the parent directory of this script, preserves existing
|
||||||
|
scriptsettings.json files in subfolders, and copies the cloned source
|
||||||
|
contents into that parent directory.
|
||||||
|
|
||||||
|
All configuration is stored in scriptsettings.json.
|
||||||
|
|
||||||
|
.EXAMPLE
|
||||||
|
pwsh -File .\Update-RepoUtils.ps1
|
||||||
|
|
||||||
|
.NOTES
|
||||||
|
CONFIGURATION (scriptsettings.json):
|
||||||
|
- dryRun: If true, logs the planned update without modifying files
|
||||||
|
- repository.url: Git repository to clone
|
||||||
|
- repository.sourceSubdirectory: Folder copied into the target directory
|
||||||
|
- repository.preserveFileName: Existing file name to preserve in subfolders
|
||||||
|
- repository.cloneDepth: Depth used for git clone
|
||||||
|
- repository.skippedRelativeDirectories: Relative directories to exclude from phase-two refresh
|
||||||
|
#>
|
||||||
|
|
||||||
|
[CmdletBinding()]
|
||||||
|
param(
|
||||||
|
[switch]$ContinueAfterSelfUpdate,
|
||||||
|
[string]$TargetDirectoryOverride,
|
||||||
|
[string]$ClonedSourceDirectoryOverride,
|
||||||
|
[string]$TemporaryRootOverride
|
||||||
|
)
|
||||||
|
|
||||||
|
Set-StrictMode -Version Latest
|
||||||
|
$ErrorActionPreference = 'Stop'
|
||||||
|
|
||||||
|
# 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
|
||||||
|
|
||||||
|
# Refresh the parent directory that contains the shared modules and sibling tools.
|
||||||
|
$targetDirectory = if ([string]::IsNullOrWhiteSpace($TargetDirectoryOverride)) {
|
||||||
|
Split-Path $scriptDir -Parent
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
[System.IO.Path]::GetFullPath($TargetDirectoryOverride)
|
||||||
|
}
|
||||||
|
$currentScriptPath = [System.IO.Path]::GetFullPath($MyInvocation.MyCommand.Path)
|
||||||
|
$selfUpdateDirectory = 'Update-RepoUtils'
|
||||||
|
|
||||||
|
#region Import Modules
|
||||||
|
|
||||||
|
$scriptConfigModulePath = Join-Path $utilsDir "ScriptConfig.psm1"
|
||||||
|
if (-not (Test-Path $scriptConfigModulePath)) {
|
||||||
|
Write-Error "ScriptConfig module not found at: $scriptConfigModulePath"
|
||||||
|
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
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Load Settings
|
||||||
|
|
||||||
|
$settings = Get-ScriptSettings -ScriptDir $scriptDir
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Configuration
|
||||||
|
|
||||||
|
$repositoryUrl = $settings.repository.url
|
||||||
|
$dryRun = if ($null -ne $settings.dryRun) { [bool]$settings.dryRun } else { $false }
|
||||||
|
$sourceSubdirectory = if ($settings.repository.sourceSubdirectory) { $settings.repository.sourceSubdirectory } else { 'src' }
|
||||||
|
$preserveFileName = if ($settings.repository.preserveFileName) { $settings.repository.preserveFileName } else { 'scriptsettings.json' }
|
||||||
|
$cloneDepth = if ($settings.repository.cloneDepth) { [int]$settings.repository.cloneDepth } else { 1 }
|
||||||
|
$skippedRelativeDirectories = if ($settings.repository.skippedRelativeDirectories) {
|
||||||
|
@(
|
||||||
|
$settings.repository.skippedRelativeDirectories |
|
||||||
|
ForEach-Object {
|
||||||
|
([string]$_).Replace('/', [System.IO.Path]::DirectorySeparatorChar).Replace('\', [System.IO.Path]::DirectorySeparatorChar)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
@([System.IO.Path]::Combine('Release-Package', 'CustomPlugins'))
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Validate CLI Dependencies
|
||||||
|
|
||||||
|
Assert-Command git
|
||||||
|
Assert-Command pwsh
|
||||||
|
|
||||||
|
if ([string]::IsNullOrWhiteSpace($repositoryUrl)) {
|
||||||
|
Write-Error "repository.url is required in scriptsettings.json."
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Main
|
||||||
|
|
||||||
|
Write-Log -Level "INFO" -Message "========================================"
|
||||||
|
Write-Log -Level "INFO" -Message "Update RepoUtils Script"
|
||||||
|
Write-Log -Level "INFO" -Message "========================================"
|
||||||
|
Write-Log -Level "INFO" -Message "Target directory: $targetDirectory"
|
||||||
|
Write-Log -Level "INFO" -Message "Dry run: $dryRun"
|
||||||
|
|
||||||
|
$ownsTemporaryRoot = [string]::IsNullOrWhiteSpace($TemporaryRootOverride)
|
||||||
|
$temporaryRoot = if ($ownsTemporaryRoot) {
|
||||||
|
Join-Path ([System.IO.Path]::GetTempPath()) ("maksit-repoutils-update-" + [System.Guid]::NewGuid().ToString('N'))
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
[System.IO.Path]::GetFullPath($TemporaryRootOverride)
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$clonedSourceDirectory = if ([string]::IsNullOrWhiteSpace($ClonedSourceDirectoryOverride)) {
|
||||||
|
Write-LogStep "Cloning latest repository snapshot..."
|
||||||
|
& git clone --depth $cloneDepth $repositoryUrl $temporaryRoot
|
||||||
|
if ($LASTEXITCODE -ne 0) {
|
||||||
|
throw "git clone failed with exit code $LASTEXITCODE."
|
||||||
|
}
|
||||||
|
Write-Log -Level "OK" -Message "Repository cloned"
|
||||||
|
|
||||||
|
Join-Path $temporaryRoot $sourceSubdirectory
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
[System.IO.Path]::GetFullPath($ClonedSourceDirectoryOverride)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (-not (Test-Path -Path $clonedSourceDirectory -PathType Container)) {
|
||||||
|
throw "The cloned repository does not contain the expected source directory: $clonedSourceDirectory"
|
||||||
|
}
|
||||||
|
|
||||||
|
if (-not $ContinueAfterSelfUpdate) {
|
||||||
|
if ($dryRun) {
|
||||||
|
Write-LogStep "Dry run self-update summary"
|
||||||
|
Write-Log -Level "INFO" -Message "Would refresh shared modules and $selfUpdateDirectory before relaunching the updater"
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Write-LogStep "Refreshing updater files..."
|
||||||
|
$selfUpdateFiles = Get-ChildItem -Path $clonedSourceDirectory -Recurse -Force -File |
|
||||||
|
Where-Object {
|
||||||
|
$relativePath = [System.IO.Path]::GetRelativePath($clonedSourceDirectory, $_.FullName)
|
||||||
|
$isRootFile = -not $relativePath.Contains([System.IO.Path]::DirectorySeparatorChar)
|
||||||
|
$isUpdaterFile = $relativePath.StartsWith($selfUpdateDirectory + [System.IO.Path]::DirectorySeparatorChar, [System.StringComparison]::OrdinalIgnoreCase)
|
||||||
|
|
||||||
|
$_.Name -ne $preserveFileName -and
|
||||||
|
($isRootFile -or $isUpdaterFile)
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($sourceFile in $selfUpdateFiles) {
|
||||||
|
$relativePath = [System.IO.Path]::GetRelativePath($clonedSourceDirectory, $sourceFile.FullName)
|
||||||
|
$destinationPath = Join-Path $targetDirectory $relativePath
|
||||||
|
$destinationDirectory = Split-Path -Parent $destinationPath
|
||||||
|
if (-not (Test-Path -Path $destinationDirectory -PathType Container)) {
|
||||||
|
New-Item -ItemType Directory -Path $destinationDirectory -Force | Out-Null
|
||||||
|
}
|
||||||
|
|
||||||
|
Copy-Item -Path $sourceFile.FullName -Destination $destinationPath -Force
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Log -Level "OK" -Message "Updater files refreshed"
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($dryRun) {
|
||||||
|
Write-LogStep "Dry run bootstrap completed"
|
||||||
|
Write-Log -Level "INFO" -Message "Continuing with phase two in the current process because no files were changed"
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Write-LogStep "Relaunching the updated updater..."
|
||||||
|
& pwsh -File $currentScriptPath `
|
||||||
|
-ContinueAfterSelfUpdate `
|
||||||
|
-TargetDirectoryOverride $targetDirectory `
|
||||||
|
-ClonedSourceDirectoryOverride $clonedSourceDirectory `
|
||||||
|
-TemporaryRootOverride $temporaryRoot
|
||||||
|
if ($LASTEXITCODE -ne 0) {
|
||||||
|
throw "Relaunched updater failed with exit code $LASTEXITCODE."
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Log -Level "OK" -Message "Bootstrap phase completed"
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$preservedFiles = @()
|
||||||
|
$updatePhaseSkippedDirectories = $skippedRelativeDirectories + $selfUpdateDirectory
|
||||||
|
$existingPreservedFiles = Get-ChildItem -Path $targetDirectory -Recurse -File -Filter $preserveFileName -ErrorAction SilentlyContinue
|
||||||
|
if ($existingPreservedFiles) {
|
||||||
|
foreach ($file in $existingPreservedFiles) {
|
||||||
|
$relativePath = [System.IO.Path]::GetRelativePath($targetDirectory, $file.FullName)
|
||||||
|
$backupPath = Join-Path $temporaryRoot ("preserved-" + ($relativePath -replace '[\\/:*?""<>|]', '_'))
|
||||||
|
$preservedFiles += [pscustomobject]@{
|
||||||
|
RelativePath = $relativePath
|
||||||
|
BackupPath = $backupPath
|
||||||
|
}
|
||||||
|
|
||||||
|
if (-not $dryRun) {
|
||||||
|
Copy-Item -Path $file.FullName -Destination $backupPath -Force
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Write-Log -Level "OK" -Message "Preserved $($preservedFiles.Count) existing $preserveFileName file(s)"
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Write-Log -Level "WARN" -Message "No existing $preserveFileName files found in subfolders"
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($dryRun) {
|
||||||
|
Write-LogStep "Dry run summary"
|
||||||
|
Write-Log -Level "INFO" -Message "Would remove all files under target except preserved $preserveFileName files"
|
||||||
|
Write-Log -Level "INFO" -Message "Would skip phase-two refresh for: $($updatePhaseSkippedDirectories -join ', ')"
|
||||||
|
Write-Log -Level "INFO" -Message "Would copy refreshed files from: $clonedSourceDirectory"
|
||||||
|
if ($preservedFiles.Count -gt 0) {
|
||||||
|
$preservedList = ($preservedFiles | ForEach-Object { $_.RelativePath }) -join ", "
|
||||||
|
Write-Log -Level "INFO" -Message "Would restore preserved files: $preservedList"
|
||||||
|
}
|
||||||
|
Write-Log -Level "OK" -Message "Dry run completed. No files were modified."
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-LogStep "Cleaning target directory..."
|
||||||
|
$filesToRemove = Get-ChildItem -Path $targetDirectory -Recurse -Force -File |
|
||||||
|
Where-Object {
|
||||||
|
$relativePath = [System.IO.Path]::GetRelativePath($targetDirectory, $_.FullName)
|
||||||
|
$isInSkippedDirectory = $false
|
||||||
|
foreach ($skippedDirectory in $updatePhaseSkippedDirectories) {
|
||||||
|
if ($relativePath.StartsWith($skippedDirectory + [System.IO.Path]::DirectorySeparatorChar, [System.StringComparison]::OrdinalIgnoreCase)) {
|
||||||
|
$isInSkippedDirectory = $true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$_.Name -ne $preserveFileName -and
|
||||||
|
-not $isInSkippedDirectory
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($file in $filesToRemove) {
|
||||||
|
Remove-Item -Path $file.FullName -Force
|
||||||
|
}
|
||||||
|
|
||||||
|
$directoriesToRemove = Get-ChildItem -Path $targetDirectory -Recurse -Force -Directory |
|
||||||
|
Sort-Object { $_.FullName.Length } -Descending
|
||||||
|
|
||||||
|
foreach ($directory in $directoriesToRemove) {
|
||||||
|
$remainingItems = Get-ChildItem -Path $directory.FullName -Force -ErrorAction SilentlyContinue
|
||||||
|
if (-not $remainingItems) {
|
||||||
|
Remove-Item -Path $directory.FullName -Force
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Write-Log -Level "OK" -Message "Target directory cleaned"
|
||||||
|
|
||||||
|
Write-LogStep "Copying refreshed source files..."
|
||||||
|
$sourceFilesToCopy = Get-ChildItem -Path $clonedSourceDirectory -Recurse -Force -File |
|
||||||
|
Where-Object {
|
||||||
|
$relativePath = [System.IO.Path]::GetRelativePath($clonedSourceDirectory, $_.FullName)
|
||||||
|
$isInSkippedDirectory = $false
|
||||||
|
foreach ($skippedDirectory in $updatePhaseSkippedDirectories) {
|
||||||
|
if ($relativePath.StartsWith($skippedDirectory + [System.IO.Path]::DirectorySeparatorChar, [System.StringComparison]::OrdinalIgnoreCase)) {
|
||||||
|
$isInSkippedDirectory = $true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
-not $isInSkippedDirectory
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($sourceFile in $sourceFilesToCopy) {
|
||||||
|
$relativePath = [System.IO.Path]::GetRelativePath($clonedSourceDirectory, $sourceFile.FullName)
|
||||||
|
$destinationPath = Join-Path $targetDirectory $relativePath
|
||||||
|
$destinationDirectory = Split-Path -Parent $destinationPath
|
||||||
|
if (-not (Test-Path -Path $destinationDirectory -PathType Container)) {
|
||||||
|
New-Item -ItemType Directory -Path $destinationDirectory -Force | Out-Null
|
||||||
|
}
|
||||||
|
|
||||||
|
Copy-Item -Path $sourceFile.FullName -Destination $destinationPath -Force
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($skippedDirectory in $updatePhaseSkippedDirectories) {
|
||||||
|
$skippedSourcePath = Join-Path $clonedSourceDirectory $skippedDirectory
|
||||||
|
if (Test-Path -Path $skippedSourcePath) {
|
||||||
|
Write-Log -Level "INFO" -Message "Skipped refresh for $skippedDirectory"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Write-Log -Level "OK" -Message "Source files copied"
|
||||||
|
|
||||||
|
if ($preservedFiles.Count -gt 0) {
|
||||||
|
foreach ($preservedFile in $preservedFiles) {
|
||||||
|
if (-not (Test-Path -Path $preservedFile.BackupPath -PathType Leaf)) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
$restorePath = Join-Path $targetDirectory $preservedFile.RelativePath
|
||||||
|
$restoreDirectory = Split-Path -Parent $restorePath
|
||||||
|
if (-not (Test-Path -Path $restoreDirectory -PathType Container)) {
|
||||||
|
New-Item -ItemType Directory -Path $restoreDirectory -Force | Out-Null
|
||||||
|
}
|
||||||
|
|
||||||
|
Copy-Item -Path $preservedFile.BackupPath -Destination $restorePath -Force
|
||||||
|
}
|
||||||
|
Write-Log -Level "OK" -Message "$preserveFileName files restored"
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Log -Level "OK" -Message "========================================"
|
||||||
|
Write-Log -Level "OK" -Message "Update completed successfully!"
|
||||||
|
Write-Log -Level "OK" -Message "========================================"
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
if ($ownsTemporaryRoot -and (Test-Path -Path $temporaryRoot)) {
|
||||||
|
Remove-Item -Path $temporaryRoot -Recurse -Force -ErrorAction SilentlyContinue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
15
utils/Update-RepoUtils/scriptsettings.json
Normal file
15
utils/Update-RepoUtils/scriptsettings.json
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://json-schema.org/draft-07/schema",
|
||||||
|
"title": "Update RepoUtils Script Settings",
|
||||||
|
"description": "Configuration for the Update-RepoUtils utility.",
|
||||||
|
"dryRun": true,
|
||||||
|
"repository": {
|
||||||
|
"url": "https://github.com/MAKS-IT-COM/maksit-repoutils.git",
|
||||||
|
"sourceSubdirectory": "src",
|
||||||
|
"preserveFileName": "scriptsettings.json",
|
||||||
|
"cloneDepth": 1,
|
||||||
|
"skippedRelativeDirectories": [
|
||||||
|
"Release-Package/CustomPlugins"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user