From 6820ec90afa34d96c28cefa236f23f3aead767e1 Mon Sep 17 00:00:00 2001 From: Maksym Sadovnychyy Date: Sun, 1 Mar 2026 12:12:11 +0100 Subject: [PATCH] (feature): update repo utils --- CONTRIBUTING.md | 2 +- README.md | 112 ++- ....UScheduler.ScheduleManager_HjRiCd1jnn.png | Bin 0 -> 144434 bytes ....UScheduler.ScheduleManager_M7ZQAkaymD.png | Bin 0 -> 44111 bytes ....UScheduler.ScheduleManager_MiY7biadQg.png | Bin 0 -> 119847 bytes ....UScheduler.ScheduleManager_aYFXXtK8V2.png | Bin 0 -> 19999 bytes .../badges}/coverage-branches.svg | 2 +- {badges => assets/badges}/coverage-lines.svg | 2 +- .../badges}/coverage-methods.svg | 2 +- assets/explorer_6Ai8GBZ7xg.png | Bin 0 -> 9447 bytes .../Configuration/PSScriptGatewayOptions.cs | 8 + .../Controllers/PSScriptController.cs | 65 ++ .../MaksIT.PSScriptGateway.csproj | 18 + .../MaksIT.PSScriptGateway.http | 6 + .../Models/ScriptExecutionRequest.cs | 12 + .../Models/ScriptExecutionResponse.cs | 7 + src/MaksIT.PSScriptGateway/Program.cs | 15 + .../Properties/launchSettings.json | 14 + .../Services/IPSScriptGatewayService.cs | 8 + .../Services/PSScriptGatewayService.cs | 192 +++++ .../Services/ResultMapper.cs | 108 +++ .../appsettings.Development.json | 11 + src/MaksIT.PSScriptGateway/appsettings.json | 12 + src/MaksIT.UScheduler.sln | 42 -- src/MaksIT.UScheduler.slnx | 7 + .../MaksIT.UScheduler.csproj | 2 +- .../Force-AmendTaggedCommit.bat | 8 +- .../Force-AmendTaggedCommit.ps1 | 375 +++++----- .../scriptsettings.json | 8 + .../Generate-CoverageBadges.bat | 10 +- .../Generate-CoverageBadges.ps1 | 160 ++-- .../scriptsettings.json | 12 +- utils/GitTools.psm1 | 268 +++++++ utils/Logging.psm1 | 70 ++ .../CorePlugins/CleanupArtifacts.psm1 | 121 +++ .../CorePlugins/CreateArchive.psm1 | 93 +++ .../CorePlugins/DotNetPack.psm1 | 99 +++ .../CorePlugins/DotNetPublish.psm1 | 71 ++ .../CorePlugins/DotNetTest.psm1 | 72 ++ utils/Release-Package/CorePlugins/GitHub.psm1 | 232 ++++++ utils/Release-Package/CorePlugins/NuGet.psm1 | 67 ++ .../CorePlugins/QualityGate.psm1 | 119 +++ utils/Release-Package/CustomPlugins/.gitkeep | 1 + .../CustomPlugins/BundleCustomization.psm1 | 314 ++++++++ .../Release-Package/DotNetProjectSupport.psm1 | 110 +++ utils/Release-Package/EngineSupport.psm1 | 165 +++++ utils/Release-Package/PluginSupport.psm1 | 368 ++++++++++ utils/Release-Package/Release-Package.bat | 3 + utils/Release-Package/Release-Package.ps1 | 183 +++++ utils/Release-Package/scriptsettings.json | 104 +++ utils/Release-ToGitHub/Release-ToGitHub.bat | 9 - utils/Release-ToGitHub/Release-ToGitHub.ps1 | 695 ------------------ utils/Release-ToGitHub/scriptsettings.json | 57 -- utils/ScriptConfig.psm1 | 35 + utils/TestRunner.psm1 | 58 +- utils/Update-RepoUtils/Update-RepoUtils.bat | 3 + utils/Update-RepoUtils/Update-RepoUtils.ps1 | 325 ++++++++ utils/Update-RepoUtils/scriptsettings.json | 15 + 58 files changed, 3792 insertions(+), 1085 deletions(-) create mode 100644 assets/MaksIT.UScheduler.ScheduleManager_HjRiCd1jnn.png create mode 100644 assets/MaksIT.UScheduler.ScheduleManager_M7ZQAkaymD.png create mode 100644 assets/MaksIT.UScheduler.ScheduleManager_MiY7biadQg.png create mode 100644 assets/MaksIT.UScheduler.ScheduleManager_aYFXXtK8V2.png rename {badges => assets/badges}/coverage-branches.svg (89%) rename {badges => assets/badges}/coverage-lines.svg (89%) rename {badges => assets/badges}/coverage-methods.svg (89%) create mode 100644 assets/explorer_6Ai8GBZ7xg.png create mode 100644 src/MaksIT.PSScriptGateway/Configuration/PSScriptGatewayOptions.cs create mode 100644 src/MaksIT.PSScriptGateway/Controllers/PSScriptController.cs create mode 100644 src/MaksIT.PSScriptGateway/MaksIT.PSScriptGateway.csproj create mode 100644 src/MaksIT.PSScriptGateway/MaksIT.PSScriptGateway.http create mode 100644 src/MaksIT.PSScriptGateway/Models/ScriptExecutionRequest.cs create mode 100644 src/MaksIT.PSScriptGateway/Models/ScriptExecutionResponse.cs create mode 100644 src/MaksIT.PSScriptGateway/Program.cs create mode 100644 src/MaksIT.PSScriptGateway/Properties/launchSettings.json create mode 100644 src/MaksIT.PSScriptGateway/Services/IPSScriptGatewayService.cs create mode 100644 src/MaksIT.PSScriptGateway/Services/PSScriptGatewayService.cs create mode 100644 src/MaksIT.PSScriptGateway/Services/ResultMapper.cs create mode 100644 src/MaksIT.PSScriptGateway/appsettings.Development.json create mode 100644 src/MaksIT.PSScriptGateway/appsettings.json delete mode 100644 src/MaksIT.UScheduler.sln create mode 100644 src/MaksIT.UScheduler.slnx create mode 100644 utils/GitTools.psm1 create mode 100644 utils/Logging.psm1 create mode 100644 utils/Release-Package/CorePlugins/CleanupArtifacts.psm1 create mode 100644 utils/Release-Package/CorePlugins/CreateArchive.psm1 create mode 100644 utils/Release-Package/CorePlugins/DotNetPack.psm1 create mode 100644 utils/Release-Package/CorePlugins/DotNetPublish.psm1 create mode 100644 utils/Release-Package/CorePlugins/DotNetTest.psm1 create mode 100644 utils/Release-Package/CorePlugins/GitHub.psm1 create mode 100644 utils/Release-Package/CorePlugins/NuGet.psm1 create mode 100644 utils/Release-Package/CorePlugins/QualityGate.psm1 create mode 100644 utils/Release-Package/CustomPlugins/.gitkeep create mode 100644 utils/Release-Package/CustomPlugins/BundleCustomization.psm1 create mode 100644 utils/Release-Package/DotNetProjectSupport.psm1 create mode 100644 utils/Release-Package/EngineSupport.psm1 create mode 100644 utils/Release-Package/PluginSupport.psm1 create mode 100644 utils/Release-Package/Release-Package.bat create mode 100644 utils/Release-Package/Release-Package.ps1 create mode 100644 utils/Release-Package/scriptsettings.json delete mode 100644 utils/Release-ToGitHub/Release-ToGitHub.bat delete mode 100644 utils/Release-ToGitHub/Release-ToGitHub.ps1 delete mode 100644 utils/Release-ToGitHub/scriptsettings.json create mode 100644 utils/ScriptConfig.psm1 create mode 100644 utils/Update-RepoUtils/Update-RepoUtils.bat create mode 100644 utils/Update-RepoUtils/Update-RepoUtils.ps1 create mode 100644 utils/Update-RepoUtils/scriptsettings.json diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 19fac68..aff6157 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -32,7 +32,7 @@ Thank you for your interest in contributing to MaksIT.UScheduler! 2. Open the solution in Visual Studio or your preferred IDE: ``` - src/MaksIT.UScheduler/MaksIT.UScheduler.sln + src/MaksIT.UScheduler.slnx ``` 3. Build the project: diff --git a/README.md b/README.md index 9813a7f..94feba6 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,12 @@ # MaksIT Unified Scheduler Service -![Line Coverage](badges/coverage-lines.svg) ![Branch Coverage](badges/coverage-branches.svg) ![Method Coverage](badges/coverage-methods.svg) +![Line Coverage](assets/badges/coverage-lines.svg) ![Branch Coverage](assets/badges/coverage-branches.svg) ![Method Coverage](assets/badges/coverage-methods.svg) A modern, fully rewritten Windows service built on **.NET 10** for scheduling and running PowerShell scripts and console applications. Designed for system administrators — and also for those who *feel like* system administrators — who need a predictable, resilient, and secure background execution environment. +> **Tip:** A graphical [Schedule Manager UI](#schedule-manager-ui) is included for easy service registration, script scheduling, and log viewing — no command-line required. + --- ## Table of Contents @@ -16,6 +18,12 @@ Designed for system administrators — and also for those who *feel like* system - [Installation](#installation) - [Using CLI Commands](#using-cli-commands) - [Using sc.exe](#using-scexe) + - [Schedule Manager UI](#schedule-manager-ui) + - [Getting Started](#getting-started) + - [Settings View](#settings-view) + - [Main View — Schedule Management](#main-view--schedule-management) + - [Service Logs View](#service-logs-view) + - [Script Logs View](#script-logs-view) - [Configuration (`appsettings.json`)](#configuration-appsettingsjson) - [Path Resolution](#path-resolution) - [Log Levels](#log-levels) @@ -52,6 +60,7 @@ Designed for system administrators — and also for those who *feel like* system ## Features at a Glance * **.NET 10 Worker Service** – clean, robust, stable. +* **Fully portable** – relocate between machines without reconfiguration. * **Windows only** – designed specifically for Windows services. * **Strongly typed configuration** via `appsettings.json`. * **Parallel execution** – PowerShell scripts & executables run concurrently using RunspacePool and Task.WhenAll. @@ -120,6 +129,96 @@ sc.exe delete "MaksIT.UScheduler" --- +## Schedule Manager UI + +The Schedule Manager is a WPF application that provides a graphical interface for managing the UScheduler service and its scheduled scripts. + +### Getting Started + +When you download and unpack the release bundle, launch `Start-ScheduleManager.bat` as administrator. + +![Manager launcher](./assets/explorer_6Ai8GBZ7xg.png) + +> **Note:** Administrator privileges are required only for service management operations (register, start, stop, unregister). Regular schedule editing can be done without elevation. + +### Settings View + +The Settings view is your starting point for configuring the Schedule Manager. + +![Settings view](./assets/MaksIT.UScheduler.ScheduleManager_aYFXXtK8V2.png) + +| Feature | Description | +|---------|-------------| +| **Service Bin Path** | Path to the UScheduler installation folder containing `MaksIT.UScheduler.exe` | +| **Service Status** | Real-time status indicator (Running, Stopped, Starting, Stopping, Paused, Not Installed) | +| **Register/Unregister** | Install or remove the Windows service (requires admin) | +| **Start/Stop** | Control the service state (requires admin) | +| **Refresh** | Update the current service status display | +| **Reload Settings** | Refresh service configuration from `appsettings.json` | + +### Main View — Schedule Management + +The Main view allows you to manage script schedules and execution settings. + +![Main view](./assets/MaksIT.UScheduler.ScheduleManager_M7ZQAkaymD.png) + +**Script List Panel:** +- Lists all PowerShell scripts configured in `appsettings.json` +- Select a script to view and edit its schedule + +**Script Configuration:** + +| Setting | Description | +|---------|-------------| +| **Name** | Display name for the script | +| **Is Signed** | Require script to be digitally signed (AllSigned policy) | +| **Disabled** | Skip this script during scheduled execution | + +**Schedule Configuration:** + +| Setting | Description | +|---------|-------------| +| **Run Month** | Select specific months to run (empty = every month) | +| **Run Weekday** | Select specific days of the week (empty = every day) | +| **Run Time** | Add/remove specific execution times (HH:mm format) | +| **Min Interval** | Minimum minutes between executions (prevents duplicate runs) | + +**Actions:** +- **Save** — Persist schedule changes to `scriptsettings.json` +- **Revert** — Discard unsaved changes +- **Launch** — Execute the script immediately via its `.bat` file + +**Script Status:** +- View lock file status (indicates if script is currently running) +- View last execution timestamp +- Remove stale lock files from crashed scripts + +### Service Logs View + +Monitor the UScheduler service activity and troubleshoot issues. + +![Logs view](./assets/MaksIT.UScheduler.ScheduleManager_MiY7biadQg.png) + +Features: +- Browse service log files sorted by date +- View log content directly in the application +- Open log files in Windows Explorer +- Refresh logs to see latest entries + +### Script Logs View + +View execution logs for individual scheduled scripts. + +![Script logs view](./assets/MaksIT.UScheduler.ScheduleManager_HjRiCd1jnn.png) + +Features: +- Browse log folders organized by script name +- Select and view individual log files +- Track script execution history and errors +- Open logs in Explorer for external tools + + + ## Configuration (`appsettings.json`) ```json @@ -142,12 +241,12 @@ sc.exe delete "MaksIT.UScheduler" "LogDir": "C:\\Logs", "Powershell": [ - { "Path": "../Scripts/MyScript.ps1", "IsSigned": true, "Disabled": false }, + { "Path": "..\\Scripts\\MyScript.ps1", "IsSigned": true, "Disabled": false }, { "Path": "C:\\Scripts\\AnotherScript.ps1", "IsSigned": false, "Disabled": true } ], "Processes": [ - { "Path": "../Tools/MyApp.exe", "Args": ["--option"], "RestartOnFailure": true, "Disabled": false } + { "Path": "..\\Tools\\MyApp.exe", "Args": ["--option"], "RestartOnFailure": true, "Disabled": false } ] } } @@ -373,16 +472,13 @@ dotnet tool install --global dotnet-reportgenerator-globaltool ## Contact -Maksym Sadovnychyy – MAKS-IT, 2025 +**Maksym Sadovnychyy** — [MAKS-IT](https://github.com/MAKS-IT-COM) Email: maksym.sadovnychyy@gmail.com --- ## License -MIT License -Copyright (c) 2025 -Maksym Sadovnychyy – MAKS-IT -maksym.sadovnychyy@gmail.com +This project is licensed under the MIT License. See [LICENSE.md](LICENSE.md) for details. --- diff --git a/assets/MaksIT.UScheduler.ScheduleManager_HjRiCd1jnn.png b/assets/MaksIT.UScheduler.ScheduleManager_HjRiCd1jnn.png new file mode 100644 index 0000000000000000000000000000000000000000..63551f5e3b91fa268779741a8f482fcb447bd741 GIT binary patch literal 144434 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 Y5#t;yft8<~0w3`=t#g{0>Xrfj2S|6kb^rhX literal 0 HcmV?d00001 diff --git a/assets/MaksIT.UScheduler.ScheduleManager_aYFXXtK8V2.png b/assets/MaksIT.UScheduler.ScheduleManager_aYFXXtK8V2.png new file mode 100644 index 0000000000000000000000000000000000000000..c0b86e2add6e25b8aa7201e299248534ac4c0353 GIT binary patch literal 19999 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 TzHXabYP2;B)Qhg&e)PWpUJhD2 literal 0 HcmV?d00001 diff --git a/badges/coverage-branches.svg b/assets/badges/coverage-branches.svg similarity index 89% rename from badges/coverage-branches.svg rename to assets/badges/coverage-branches.svg index 4a0fa25..0516e60 100644 --- a/badges/coverage-branches.svg +++ b/assets/badges/coverage-branches.svg @@ -1,4 +1,4 @@ - + Branch Coverage: 7% diff --git a/badges/coverage-lines.svg b/assets/badges/coverage-lines.svg similarity index 89% rename from badges/coverage-lines.svg rename to assets/badges/coverage-lines.svg index f5a3014..9adecb2 100644 --- a/badges/coverage-lines.svg +++ b/assets/badges/coverage-lines.svg @@ -1,4 +1,4 @@ - + Line Coverage: 15.2% diff --git a/badges/coverage-methods.svg b/assets/badges/coverage-methods.svg similarity index 89% rename from badges/coverage-methods.svg rename to assets/badges/coverage-methods.svg index 7e83000..c0678d5 100644 --- a/badges/coverage-methods.svg +++ b/assets/badges/coverage-methods.svg @@ -1,4 +1,4 @@ - + Method Coverage: 38.8% diff --git a/assets/explorer_6Ai8GBZ7xg.png b/assets/explorer_6Ai8GBZ7xg.png new file mode 100644 index 0000000000000000000000000000000000000000..9bb6ee9d444c91bc6f0c90bdf83bdbc2bbfecfeb GIT binary patch literal 9447 zcmc(lXH-+`y6=OCpfp_;Km=42QK`}-l!z#ZG!>Mr_rp8i{{KDyXXeQb8*}k} zNB02$0C9_JS8f3SyR!MuO(J{vzmXi(9R5uJcFX)SpnB-o3cs-1*TmWc0H{qBWqSzm z%X>qvIl%w`iQb({pfC8fHvn)d#^Q>}?FV4WoJ-Q(J6}jYd7^qIhs3*e@9p2){kfJ#|R=diT`gyV@&eWoOD-wl0}QSSn>@^Ke-|zvP3^ zrG+b{>UZX~wJG9$1)3j(QR$5sa_ew;MSVGt6l1`61dPUXOfL9u)YCDOGw-qO97?bh zzk3?@c%h?LrB>!(!xIVOvaxpzcx)+^cuFQm#6`{{zD<*Wx=UPRZ;~u`IM(xYPEu82X@Y zS?{Sik=pRLNF`ZP1YD_*Cz9=`1MT zsF_oS?AbY!?*gBG{7FeKl<6$vz=&&Uw{p;0=yL@S2er>${Koc)$t zwv`|&SgbO=VA=tTEe4-)9eTW{GI&t$GB5fS^ZghpxH;n%>$$?SXrgoV?Rk6=gL!t; zcCq3o+vj1O2>Lx~y`s}Ly{wk^;a%(A?0Fjpgx0yw>ZA|5D*XrbdOp&pP;akXZF12n zM_8yrQf`iGE@Wf!f8%IZA05WX42I>ge&}oyGaW%DFr|E@^0F&)vd^?0-%3t6bl2*d z>SWgFiX$Z2E~7r*hl|IbE_@4U@DsH?s7n z#rt$k48dS>oU266vl%4V_OH9pnf3$2?37!3~2vhn$XM2>zjj;?) z7w7IUFX$z%bR*LEEpHMFKcY>Mz2+TBo3;G-` zfg#%WlCB|&B(u{vftArf9SEqTCVv20BlG?oPgdPGIj?MJud~h^WALsbyZoV&!8M!F z7FQcyl>=D=I)kk`gri6$)Wc2ZzQ+$Qlcw5^1MG_!X3{JhXT^g!W{@P4`=QAKvAx|J z8PYCeBih>NeJoLtq+@snYPZOsgjK=NyjwT0o}MT)`-Z*|scvVh zF~zdG#orq(18VUSXyf zuZJUk@NU78n#q9%PT8zr!^(L6;eBPjbD({72&vQsu#)_>+HiXqQd8z14zbfu+QIgX zd;7HsIfwHh-hT-O!2a%EQh~!FKM_mnYD9C7?O@bMAIq`qtN!^e|6Wf0PI7Y!th>j9eA0e@0o7|B`;~&4Lyb zzTc@U=6>Hqrv6A8~T>0dFDBzdfG&k4|$r2oA)JuaTyx@VA;du_fsR! zStQ%%5I)KMsPFd{75BXIMT4PHqC&fC)GB%`E#J}{sU+8G;mphn`>|)Gix9@d?Zs!PoIEO$b_32vL20u?(I4RfS1Pm)` z*pl=6nKSl~Tp|cb+a9)HRcCC2504Y1`8y$4%MEU@M4F0^G@D1Z46p;@>}-ud*PN|l zL*ujVnAaImVNPX@hEB$k5y=7?q6TTe+|Vap_n><2Gb{A*H_ZO|Y6TT_g3j)5wVDON z32KXI$Suq&PstjtS0#AkXynNFD8$m1b2(}($$3G==;y(eAJ$j9*wqaW;v0U^PP&87 zQ`*d;*wnFwn?Uh0}trOLl+|;FSfyFDJ$1f5zaH5Ck377phH=Hn{47&c4eEIXyN-|s_50T zZO398r4m#R=JlkN)VP-5MK9lJmuSvAUd+JLkZpHvoG%SfEW~5#nE_4WpY)E|EdnDN znl{E=_Q;`Fp*a>Jm&?6s(9w76(w0Lj<~JPdbT>c6)h8RxWL38jDN2M1%JOUuWKR!5 zptSdUyw4F0)G0aU!?5qHv9acoc;X)tV2JQQ-?P{jdi|JJep)z0$%v`Bl4cpt+yrEm zmnG773d|X?wvIg0ffXv&! z-QY%}8HX6x<9r&{pC44?EMiRmC@>j|Cn1j@eHV^m_5{naw z3gc2aKs}jIk0A&NO$7-$%G!(6qs>iYJ>-YdZXC%unL2r(iEvE2chh7)4dg;w7EsoF z?Dj;?u6X_$hkJjV)IfhuK3BCQo5~> zDbEMZQgo)zZmk?8bz(33GELq`P6OmDWJQU=Y>l+VGkdx&zF}OAQ__%U-+XI=6Q_o3 zn>qzNHMBPmaFRYm2I#pmKEobqRY?CxJv$mylPm?ku-*^=Hopn!lQ2!+A9}0LUA-BZ z^60}K4!+(wFNFpj+sZk@I}=fwrKBf7?SeCuee|vKIuaRY{pe??ew%LS>utu$l$iXj zk;<*lZN|-o(VPFW*mQ~ z^Z4VRc^4juX~FXX^ltjTUyxFN=WWN>7Fjv<=+(I96poXhWB#NUNDZ7Tr+#ew`ckxP z$cw@Z6u|p_Gok(xbZ`=&p7TrP$uB3CxS_8ZsAtGW*&fjFEoKo573m$SuQ6gtqM}Rr zyIvM$(SUJEHO3QmI322ng!!4kxwAO#p`nM9dqXwZM}p^YPZVjlNB!aYUJl*Nu zmPO$Ys5$)W>I{N>b=B3dl>?{&i*q5u;<`_P06?H@uEn!Nzld99ti$Rq-|AqTWdY_Gt)@c+HyUj zF+BcLj`V#)60BxKvt0Q&T%rg{mzY1Lim(Mfh!6Yh4@B}l6m*KqE;kfh9UNxj1d`%y zf-Nj9TaXb-j@Uvx{uX8t30^HJpT0{P^>iZ?0#7T2SNoz1iwh z9jhH2!{-ziHD5Ci%8-~>Rz;XQvrEH1N3C=@6mwu>oo`Ay9E4~Gv>$5f;_o!uAybSp zea9(i+`zEXMcOzluC!x5mXFP>d>pW9sed!_t-nMplEXz#(6%ac*8&w;2OH8hys$(& z%<7r8lul;%W~iI#C3#svrqA~aNXC@@6TgZ!Ke^bkkMX4`T<54&sj+mQ@lBtVC;8VB zgU{_+jwpFATl^j)B|vrdBkGJT=A*ZRUTnJ_80UK{rN?sTM3t@xDTXc!1PKkQKqnM! zO+ubcerY5nXJ?OgYFwFN5pD*LKnVq$iVX9fk2hG0)uOm>f8!G^r%T>1x>D@ma>CjA zu}$l{Ig+3xf26>Wl#{A3l+`TQ1lV^w->37i$+Y_wT%xzX)`@m$~< zM0yNPZ=3(t+BzQ-O~qY$tKJnRjTjl&wVYaP3i?9=0dZzFh5e8n>n4+984nI<(2@30 zYg%LLxMFjVl2NGN4G0jLy<~r5AnvZykego4X`JW$T%6MZKjBL`J>^i)aUGRr`L98* zlU^h@=vmdkw$-B(F%k#9bn3044{CD|gXCjAs0wE8$i2ksR%2i7#+eVI7GC|wOyvM% zWYZ3s7BF8TsjSn%y(?HKB1oSuH+8#>q5H)cfcM%mp^ivCi(TnRY#B~+2P;x^@Za*! z?^<3OD^ftqGk$L%n~wEvNUmD$8B}ppgAO@o_(vT#bC>tt*KceUex7Eb#XS0Xnx6c=fRSvT3sJOddO3(Q|mi33Ma()O->}a3@ zpbGgT&Tk3>&Yjj>S3f^$Y?Y#{*O)Vda*s+fhGuMdWFHs^$Zn63q>4^NXTrPK(8C&y z)Q&OJ4T)90q^V>q6}_^`6{KvMf7zp|H_+RH{K4{gBXPGD=!xq&?m*DrLDeEM67Ez1 zNS{+^YhMvoZ}*<_n~otqTl_xd0sZZ=&CZ%4KR-(i>1$37uCFVQ@z-nQb#3w2P#gbE z$zA@fn!uqe^NxtPr5H88xNSDukDIbI0>N1FDdY-5SuiNILyX!qx z=PG(te7z4sZ=Q?E5{3mtelUDhmDr?r*v;CjZxwn^{m96zrTG0)GF4U-bo1PNqN9ez zqco_|cISiHtj!_qz&{sip9A+qxA0)zTZS!x9mxi8c>}$mmXKA$$!g*e4TPDzb}A_3 z%^z=Sm^{kO0iAl_v)bCC6ghX zFwncq{rBiOSmd#ily30)%b#Vn@i3`!{PYLOLXXU0O#1&ziiKP$offEkd4fwY5lsG5 z0&r=TKXyV2XkG{ivJ~h^{-&|A3WJpzGZl$ zZe)piOFqS+M4VZ9FL!3;-F7;yXWMS;SFkO?g&1mcL!RHCbiRngc%|v}v5c=Nc43}< z<$RA;&i7upEp`*Wk)-Fl8b<{m3=rJ$YFe8Gk)D11z_iB$_$~G}zAlzA*Q6E3UOFu|M7hoCOR@xoD_j*Gcn5?Iv+lD85n_m?rqaV@uCeGckaliJUa;Z$W_Rt8- zsmsu=FXB}Z7WANHYh0a=HFOHw3K*8&uw}IJS$ymvX5|-0HlBF_CR)K$g$_Z8-r?Y9 z&g1L=Z@Jh>c$B5H^Cw!HRyR{TH!mYfj!wO0oBNe&)eH%QydkjQr)F`h;^;Ft12S`U z=uuxrh1O>|d_g$E$*C>%f*fsNyL;1hA@~VLUiQ7Fb6sclN(DqN=WPAJqROxf_KgfG zx2Sx*6b`tqD4ESOY_(ug!&huwNyQxw-&b?Nbd}{ypRW5<;I<>w!==bo_zNASjhj|-@{m>u?yJnfdQLm@k4n3TSl@g1A>b^ zaZVm)mni@zwxO??1r-(95h-hb^+~HhrK@z4)q{QJ89kvdZPZ2X`~=QL#r?5tOJGs# zO{y~Mk{{V%hgR#Sb(M)BR>|bK(V0%WV3{yB1sAAvLga79zuznyS00XqSWkaEFFK^V z4fSnZM>glUIyC152P0)f=t7wie9Pg~#ZC+RE<9GaVQERskU*LOA0Xs36x4SuFAQH# zMVQSXvbMhW6`QMxui_>OF%mJ1I*nLn_qb&C^rx6dbJ|49Iii0rn){P*;^X+u$3^#K)lVg# zhKa2kWHav1^kp#hxz8+altTXvD#EK<9S z-$y+)0nlssO0iHy_o{rAVgN=Tny?gqd==HSIw*LW`G`aYa0fGbBEfRTA&I_<}Y$)tSKN3BQphU99?5j#7EZ}+?mLDylON6 zbVoMq%ba0=R|+G9X$+r1jZzuEV-_O==2weM@Ik5NF!#s*onEflBgj}JfuZb$ACI&f zX=u9NQ-O6Gd^A`i2M{dT7ys?VK3C)e{1S&OVOvq&^0=GY0wBNlGW}BwS5?9{G-O;!u}m@DCmk zT61$;005ok51whZgTV*of6FL7JuAACdi;CwUnLm-w*>r8DoIC%<6=F`kT z&fn64{K0X4Onswcc^yTF(yhRz^d^3vz^S98DHz zq)G#>lS=Y{{ZV`-6_c+up(kY3Y%>(beN1zkvVySZ78M*|964GAg?-^Yu#Fg}>jh7d z!hmY9BrwJ`v_FNAA|>TXpHzg%0vDw(7G=d?XH2R71bi zTgr<{yOgmn)JCplf;A3 zygdyv<-I)D)h$oSwQicQq!J0ng<+)pv<&R28cB@?J_9X{%>J$W82|i@k2;M3NKkWs zFphUj%PQ+8nxCRMBt+0gLDEEDDYhwhO z%jtGA`DnFg&~x{YxYg2YSN-~2+XHR&^Ggl)c@Zj+(u{l=^_xx5ite+3NkgXR3K2TYaMyakr|zOm9BT z4}*FG37@0Nfzb=$3vej*!-3o&_^u|R&CQ5DvmD z(*h-a1}>F@F;3E#p2gaw#QE&~D2cc_-SXIz6_ZmJMDMUbDR35_aMlR9>ce|9a2!1o z^;vT)_qKt(BP^vnGN&-5pPX1@TZK?&>4;YJu0>ReO*&t^^QzS&8do3j=c;VjMY+Ro zdR6(zp0ZvjZ?3hD#(rv|3Zta811IBsGMOF1b^)t%m9Skmbas6=8vo#kh@mNm=v-fW zWzNbR9Jbeh4c+OiUelr@>?3li3xCerm!UhF_PavT$e-@45xPc}6`mo~s{B=_J}^t( z>n~r^e|GS8yl%(#V6AYV?LoXkEmD=B!+EYb(2{RhB|JEm5y-RT(e#>ESiBVw8At5A zNkEazj5fb}neG|9ta)-d>Ez7A9+|J#g(Np%VJ}oiOL^-)C57T;+`26N^IKP&{3?Po zTR9i58oUICPXe=3{4x=8d}kYyLNWE0t!#W3ugU1z4Eo0Hoke{X{>vf{ z*2H+_WEd7_CYpI3owXCXjy2}NN>{qQjo4Kych9`AzBW?H{IFWI8PiIdF!tg7TI3vd zBMHS;*ybYD%p~fECtv%2le>j(hJ^p2n%W2?KJj|8=GCzNnHrkGLVVqW|KXggR2uBb zfmEM2f5|aU$u-9gct`cemDYy++Upk4ORvkvv`5*nB#$|QM^w!FJq6zHeYkX}mN6|U z&n0zN=sFzJR-Dy-Tvy6WD4#tG*?OFXte&x)(DcTZH2DEb3%4go7@YZw-VtI0(b1(m z;`=R}_=eefV$CY7GG(f+CIwg^=3xBmSQ@h^A1bpmJf*`TZ1{W4+ueN-(DKt*LMGzdwiUXL#|flCw9gw3TuYn7-nj=h`Tt|s)&w{6BK2`O zEbrE=P77M%MDVn__!G#^WVA@GW~r$Ajl^u#b}(m}v1Q2+wu2jjf^H1val1`m{21gY zcPj)u#1za^c~l&i$kRVL?ZXdAD#WRzzUb%N4? zr6=f(E5Fu4rr$~m4yl8Iqkubm9)H|SLMb)}k1WJbV~WZ!zsK$6EQUBOD6{4}lR*Y}0DIMx{^HsU(?4EHCCoS_8=H>YvpzFKF|qiS!`J}O7m zC@koBK=k_k-+yFzxzBf#mQ?7dlt#TOE0ov)ZCP+c|ECLowuD-n;2&)Nx=#ygQ*_J| z%f34Gn(Ku6d~@)X53Uk4=HxnZxmN_;mP-V~a=5MEZ#VF!^L4`)q6c_FsEb~g;2%@&WG2Ikd8tAyj5P7;lK34SGw|gnrvja zU3eMA6J9)nn{>fH5BhlRMQVf0x^GLa-Y&AC>uPH*N3Mc99{&;IkW_t|kg7+2n2Y0v zd|1(AWpWnj#x8`*R0Y9E#P>u&Uj3b5wwxh7Duk>q*cdrnSC*&a?7~UA0dKD*-|Nye zU}->G96qcZ#1hMrS8}FWR0%DJoZPmx8ThbMNT7AijvL2@+&v=g&=Wu=Bx+{7QtJzo zT}Dx~J}R&Bzo!O%GdZZdq{YQCcxk%;u@;v{-ibKjrx|lPMOgrVmFEA?B=~>G8~i6x o(|_#)@V^tE{rfF_>l?&08hH%^8f$< literal 0 HcmV?d00001 diff --git a/src/MaksIT.PSScriptGateway/Configuration/PSScriptGatewayOptions.cs b/src/MaksIT.PSScriptGateway/Configuration/PSScriptGatewayOptions.cs new file mode 100644 index 0000000..ec919a3 --- /dev/null +++ b/src/MaksIT.PSScriptGateway/Configuration/PSScriptGatewayOptions.cs @@ -0,0 +1,8 @@ +namespace MaksIT.PSScriptGateway.Configuration; + +public sealed class PSScriptGatewayOptions +{ + public const string SectionName = "PSScriptGateway"; + + public string ScriptsRoot { get; set; } = @"..\..\..\..\Scripts"; +} diff --git a/src/MaksIT.PSScriptGateway/Controllers/PSScriptController.cs b/src/MaksIT.PSScriptGateway/Controllers/PSScriptController.cs new file mode 100644 index 0000000..52fa619 --- /dev/null +++ b/src/MaksIT.PSScriptGateway/Controllers/PSScriptController.cs @@ -0,0 +1,65 @@ +using MaksIT.PSScriptGateway.Models; +using MaksIT.PSScriptGateway.Services; +using Microsoft.AspNetCore.Mvc; + +namespace MaksIT.PSScriptGateway.Controllers; + +[ApiController] +[Route("api/scripts")] +public sealed class PSScriptController : ControllerBase +{ + private readonly IPSScriptGatewayService _scriptGatewayService; + + public PSScriptController(IPSScriptGatewayService scriptGatewayService) + { + _scriptGatewayService = scriptGatewayService; + } + + [AcceptVerbs("GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS")] + [Route("{**scriptName}")] + public async Task Execute(string scriptName, CancellationToken cancellationToken) + { + var request = await BuildRequestAsync(scriptName, cancellationToken); + var response = await _scriptGatewayService.ExecuteAsync(scriptName, request, cancellationToken); + return ResultMapper.ToActionResult(response.StatusCode, response.Value, response.Messages); + } + + private async Task BuildRequestAsync(string scriptName, CancellationToken cancellationToken) + { + var body = await ReadBodyAsync(cancellationToken); + + return new ScriptExecutionRequest( + Request.Method, + Request.Path.Value ?? string.Empty, + scriptName, + Request.ContentType, + body, + Request.Query.ToDictionary( + pair => pair.Key, + pair => pair.Value.Select(static value => value ?? string.Empty).ToArray(), + StringComparer.OrdinalIgnoreCase), + Request.Headers.ToDictionary( + pair => pair.Key, + pair => pair.Value.Select(static value => value ?? string.Empty).ToArray(), + StringComparer.OrdinalIgnoreCase), + RouteData.Values.ToDictionary( + pair => pair.Key, + pair => pair.Value, + StringComparer.OrdinalIgnoreCase)); + } + + private async Task ReadBodyAsync(CancellationToken cancellationToken) + { + if (Request.ContentLength is null or 0) + return null; + + Request.EnableBuffering(); + Request.Body.Position = 0; + + using var reader = new StreamReader(Request.Body, leaveOpen: true); + var body = await reader.ReadToEndAsync(cancellationToken); + Request.Body.Position = 0; + + return string.IsNullOrWhiteSpace(body) ? null : body; + } +} diff --git a/src/MaksIT.PSScriptGateway/MaksIT.PSScriptGateway.csproj b/src/MaksIT.PSScriptGateway/MaksIT.PSScriptGateway.csproj new file mode 100644 index 0000000..9ddeefe --- /dev/null +++ b/src/MaksIT.PSScriptGateway/MaksIT.PSScriptGateway.csproj @@ -0,0 +1,18 @@ + + + + net10.0 + enable + enable + + + + + + + + + + + + diff --git a/src/MaksIT.PSScriptGateway/MaksIT.PSScriptGateway.http b/src/MaksIT.PSScriptGateway/MaksIT.PSScriptGateway.http new file mode 100644 index 0000000..f6eaba9 --- /dev/null +++ b/src/MaksIT.PSScriptGateway/MaksIT.PSScriptGateway.http @@ -0,0 +1,6 @@ +@MaksIT.PSScriptGateway_HostAddress = http://localhost:5078 + +GET {{MaksIT.PSScriptGateway_HostAddress}}/weatherforecast/ +Accept: application/json + +### diff --git a/src/MaksIT.PSScriptGateway/Models/ScriptExecutionRequest.cs b/src/MaksIT.PSScriptGateway/Models/ScriptExecutionRequest.cs new file mode 100644 index 0000000..396a6ce --- /dev/null +++ b/src/MaksIT.PSScriptGateway/Models/ScriptExecutionRequest.cs @@ -0,0 +1,12 @@ +namespace MaksIT.PSScriptGateway.Models; + +public sealed record ScriptExecutionRequest( + string HttpMethod, + string RequestPath, + string ScriptPath, + string? ContentType, + string? Body, + IReadOnlyDictionary Query, + IReadOnlyDictionary Headers, + IReadOnlyDictionary RouteValues +); diff --git a/src/MaksIT.PSScriptGateway/Models/ScriptExecutionResponse.cs b/src/MaksIT.PSScriptGateway/Models/ScriptExecutionResponse.cs new file mode 100644 index 0000000..dd3c67a --- /dev/null +++ b/src/MaksIT.PSScriptGateway/Models/ScriptExecutionResponse.cs @@ -0,0 +1,7 @@ +namespace MaksIT.PSScriptGateway.Models; + +public sealed record ScriptExecutionResponse( + int StatusCode, + object? Value, + IReadOnlyList Messages +); diff --git a/src/MaksIT.PSScriptGateway/Program.cs b/src/MaksIT.PSScriptGateway/Program.cs new file mode 100644 index 0000000..92e22b7 --- /dev/null +++ b/src/MaksIT.PSScriptGateway/Program.cs @@ -0,0 +1,15 @@ +using MaksIT.PSScriptGateway.Configuration; +using MaksIT.PSScriptGateway.Services; + +var builder = WebApplication.CreateBuilder(args); + +builder.Services.Configure( + builder.Configuration.GetSection(PSScriptGatewayOptions.SectionName)); +builder.Services.AddSingleton(); +builder.Services.AddControllers(); + +var app = builder.Build(); + +app.MapControllers(); + +app.Run(); diff --git a/src/MaksIT.PSScriptGateway/Properties/launchSettings.json b/src/MaksIT.PSScriptGateway/Properties/launchSettings.json new file mode 100644 index 0000000..73449ff --- /dev/null +++ b/src/MaksIT.PSScriptGateway/Properties/launchSettings.json @@ -0,0 +1,14 @@ +{ + "$schema": "https://json.schemastore.org/launchsettings.json", + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": false, + "applicationUrl": "http://localhost:5078", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/src/MaksIT.PSScriptGateway/Services/IPSScriptGatewayService.cs b/src/MaksIT.PSScriptGateway/Services/IPSScriptGatewayService.cs new file mode 100644 index 0000000..09a60cc --- /dev/null +++ b/src/MaksIT.PSScriptGateway/Services/IPSScriptGatewayService.cs @@ -0,0 +1,8 @@ +using MaksIT.PSScriptGateway.Models; + +namespace MaksIT.PSScriptGateway.Services; + +public interface IPSScriptGatewayService +{ + Task ExecuteAsync(string scriptName, ScriptExecutionRequest request, CancellationToken cancellationToken); +} diff --git a/src/MaksIT.PSScriptGateway/Services/PSScriptGatewayService.cs b/src/MaksIT.PSScriptGateway/Services/PSScriptGatewayService.cs new file mode 100644 index 0000000..0096f5c --- /dev/null +++ b/src/MaksIT.PSScriptGateway/Services/PSScriptGatewayService.cs @@ -0,0 +1,192 @@ +using System.Collections; +using System.Collections.ObjectModel; +using System.Management.Automation; +using MaksIT.PSScriptGateway.Configuration; +using MaksIT.PSScriptGateway.Models; +using MaksIT.UScheduler.Shared.Helpers; +using Microsoft.Extensions.Options; + +namespace MaksIT.PSScriptGateway.Services; + +public sealed class PSScriptGatewayService : IPSScriptGatewayService +{ + private readonly ILogger _logger; + private readonly string _scriptsRoot; + + public PSScriptGatewayService( + ILogger logger, + IOptions options) + { + _logger = logger; + _scriptsRoot = ResolveScriptsRoot(options.Value.ScriptsRoot); + } + + public async Task ExecuteAsync( + string scriptName, + ScriptExecutionRequest request, + CancellationToken cancellationToken) + { + var scriptPath = ResolveScriptPath(scriptName); + if (scriptPath is null) + return new ScriptExecutionResponse(StatusCodes.Status404NotFound, null, ["Script not found."]); + + using var powerShell = PowerShell.Create(); + using var registration = cancellationToken.Register(() => { + try { + powerShell.Stop(); + } + catch (ObjectDisposedException) { + } + catch (InvalidOperationException) { + } + }); + + powerShell.AddCommand(scriptPath) + .AddParameter("Request", request) + .AddParameter("HttpMethod", request.HttpMethod) + .AddParameter("RequestPath", request.RequestPath) + .AddParameter("ScriptPath", request.ScriptPath) + .AddParameter("ContentType", request.ContentType) + .AddParameter("Body", request.Body) + .AddParameter("Query", request.Query) + .AddParameter("Headers", request.Headers) + .AddParameter("RouteValues", request.RouteValues); + + Collection output; + + try { + output = await Task.Run(() => powerShell.Invoke(), cancellationToken); + } + catch (OperationCanceledException) { + return new ScriptExecutionResponse(StatusCodes.Status499ClientClosedRequest, null, ["The request was canceled."]); + } + catch (RuntimeException exception) { + _logger.LogError(exception, "PowerShell runtime error while executing {ScriptPath}", scriptPath); + return new ScriptExecutionResponse(StatusCodes.Status500InternalServerError, null, [exception.Message]); + } + catch (Exception exception) { + _logger.LogError(exception, "Unhandled error while executing {ScriptPath}", scriptPath); + return new ScriptExecutionResponse(StatusCodes.Status500InternalServerError, null, ["Unhandled script execution error."]); + } + + if (TryParseScriptResponse(output, out var response)) + return response; + + if (powerShell.HadErrors) { + var errors = powerShell.Streams.Error + .Select(error => error.ToString()) + .Where(message => !string.IsNullOrWhiteSpace(message)) + .ToArray(); + + return new ScriptExecutionResponse( + StatusCodes.Status500InternalServerError, + null, + errors.Length == 0 ? ["Script execution failed."] : errors); + } + + return new ScriptExecutionResponse(StatusCodes.Status204NoContent, null, ["No content."]); + } + + private string ResolveScriptsRoot(string scriptsRoot) + { + var resolvedRoot = PathHelper.ResolvePath(scriptsRoot); + return Path.GetFullPath(resolvedRoot); + } + + private string? ResolveScriptPath(string scriptName) + { + var relativePath = scriptName + .Replace('/', Path.DirectorySeparatorChar) + .TrimStart(Path.DirectorySeparatorChar); + + if (string.IsNullOrWhiteSpace(relativePath)) + return null; + + if (!relativePath.EndsWith(".ps1", StringComparison.OrdinalIgnoreCase)) + relativePath += ".ps1"; + + var combinedPath = Path.GetFullPath(Path.Combine(_scriptsRoot, relativePath)); + if (!combinedPath.StartsWith(_scriptsRoot, StringComparison.OrdinalIgnoreCase)) + return null; + + return File.Exists(combinedPath) ? combinedPath : null; + } + + private static bool TryParseScriptResponse(Collection output, out ScriptExecutionResponse response) + { + response = default!; + + if (output.Count == 0) + return false; + + if (TryParseExplicitResponse(output[0], out response)) + return true; + + var values = output.Select(UnwrapValue).ToArray(); + var payload = values.Length == 1 ? values[0] : values; + response = new ScriptExecutionResponse(StatusCodes.Status200OK, payload, ["OK"]); + return true; + } + + private static bool TryParseExplicitResponse(PSObject psObject, out ScriptExecutionResponse response) + { + response = default!; + + if (!TryReadIntProperty(psObject, "StatusCode", out var statusCode)) + return false; + + var messages = ReadMessages(psObject); + var value = ReadProperty(psObject, "Value") + ?? ReadProperty(psObject, "Body") + ?? ReadProperty(psObject, "Data") + ?? ReadProperty(psObject, "Result"); + + response = new ScriptExecutionResponse(statusCode, value, messages); + return true; + } + + private static object? ReadProperty(PSObject psObject, string propertyName) + { + var property = psObject.Properties[propertyName]; + return property is null ? null : UnwrapValue(property.Value); + } + + private static IReadOnlyList ReadMessages(PSObject psObject) + { + var property = psObject.Properties["Messages"]; + if (property?.Value is null) + return []; + + if (property.Value is string message) + return [message]; + + if (property.Value is IEnumerable enumerable) { + return enumerable + .Cast() + .Select(item => item?.ToString()) + .Where(item => !string.IsNullOrWhiteSpace(item)) + .Cast() + .ToArray(); + } + + return [property.Value.ToString() ?? "Script response."]; + } + + private static bool TryReadIntProperty(PSObject psObject, string propertyName, out int value) + { + value = default; + var property = psObject.Properties[propertyName]; + if (property?.Value is null) + return false; + + return int.TryParse(property.Value.ToString(), out value); + } + + private static object? UnwrapValue(object? value) + { + if (value is PSObject psObject) + return psObject.BaseObject; + + return value; + } +} diff --git a/src/MaksIT.PSScriptGateway/Services/ResultMapper.cs b/src/MaksIT.PSScriptGateway/Services/ResultMapper.cs new file mode 100644 index 0000000..dea5206 --- /dev/null +++ b/src/MaksIT.PSScriptGateway/Services/ResultMapper.cs @@ -0,0 +1,108 @@ +using System.Net; +using System.Reflection; +using MaksIT.Results; +using Microsoft.AspNetCore.Mvc; + +namespace MaksIT.PSScriptGateway.Services; + +internal static class ResultMapper +{ + private static readonly Type GenericResultType = typeof(Result); + private static readonly Type NonGenericResultType = typeof(Result); + private static readonly IReadOnlyDictionary StatusMethodNames = Enum + .GetValues() + .Distinct() + .ToDictionary(code => (int)code, code => code.ToString()); + + public static IActionResult ToActionResult(int statusCode, object? value, IReadOnlyList messages) + { + var normalizedStatusCode = NormalizeStatusCode(statusCode); + var resolvedMessages = messages.Count == 0 + ? [GetDefaultMessage(normalizedStatusCode)] + : messages.ToArray(); + + if (TryBuildGenericResult(normalizedStatusCode, value, resolvedMessages, out var genericResult)) + return genericResult.ToActionResult(); + + if (value is null && TryBuildResult(normalizedStatusCode, resolvedMessages, out var result)) + return result.ToActionResult(); + + if (value is null) + return new StatusCodeResult(normalizedStatusCode); + + return new ObjectResult(value) { StatusCode = normalizedStatusCode }; + } + + private static bool TryBuildGenericResult(int statusCode, object? value, string[] messages, out Result result) + { + result = null!; + + if (!StatusMethodNames.TryGetValue(statusCode, out var methodName)) + return false; + + var method = GenericResultType + .GetMethods(BindingFlags.Public | BindingFlags.Static) + .FirstOrDefault(current => + current.Name == methodName && + Matches(current.GetParameters(), typeof(object), typeof(string[]))); + + if (method is null) + return false; + + if (method.Invoke(null, [value, messages]) is not Result invoked) + return false; + + result = invoked; + return true; + } + + private static bool TryBuildResult(int statusCode, string[] messages, out Result result) + { + result = null!; + + if (!StatusMethodNames.TryGetValue(statusCode, out var methodName)) + return false; + + var method = NonGenericResultType + .GetMethods(BindingFlags.Public | BindingFlags.Static) + .FirstOrDefault(current => + current.Name == methodName && + Matches(current.GetParameters(), typeof(string[]))); + + if (method is null) + return false; + + if (method.Invoke(null, [messages]) is not Result invoked) + return false; + + result = invoked; + return true; + } + + private static bool Matches(ParameterInfo[] parameters, params Type[] parameterTypes) + { + if (parameters.Length != parameterTypes.Length) + return false; + + for (var index = 0; index < parameters.Length; index++) { + if (parameters[index].ParameterType != parameterTypes[index]) + return false; + } + + return true; + } + + private static int NormalizeStatusCode(int statusCode) + { + return statusCode is >= 100 and <= 599 + ? statusCode + : StatusCodes.Status500InternalServerError; + } + + private static string GetDefaultMessage(int statusCode) + { + return StatusMethodNames.TryGetValue(statusCode, out var methodName) + ? methodName + : "Request completed."; + } +} diff --git a/src/MaksIT.PSScriptGateway/appsettings.Development.json b/src/MaksIT.PSScriptGateway/appsettings.Development.json new file mode 100644 index 0000000..e41c864 --- /dev/null +++ b/src/MaksIT.PSScriptGateway/appsettings.Development.json @@ -0,0 +1,11 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "PSScriptGateway": { + "ScriptsRoot": "..\\..\\..\\..\\Scripts" + } +} diff --git a/src/MaksIT.PSScriptGateway/appsettings.json b/src/MaksIT.PSScriptGateway/appsettings.json new file mode 100644 index 0000000..7e31da6 --- /dev/null +++ b/src/MaksIT.PSScriptGateway/appsettings.json @@ -0,0 +1,12 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "PSScriptGateway": { + "ScriptsRoot": "..\\..\\..\\..\\Scripts" + }, + "AllowedHosts": "*" +} diff --git a/src/MaksIT.UScheduler.sln b/src/MaksIT.UScheduler.sln deleted file mode 100644 index c8eddd7..0000000 --- a/src/MaksIT.UScheduler.sln +++ /dev/null @@ -1,42 +0,0 @@ -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 18 -VisualStudioVersion = 18.0.11222.15 -MinimumVisualStudioVersion = 10.0.40219.1 -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MaksIT.UScheduler", "MaksIT.UScheduler\MaksIT.UScheduler.csproj", "{DE1F347C-D201-42E2-8D22-924508FD30AA}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MaksIT.UScheduler.Tests", "MaksIT.UScheduler.Tests\MaksIT.UScheduler.Tests.csproj", "{DC193ABC-89F8-131B-060F-6C3A3CE2652A}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MaksIT.UScheduler.ScheduleManager", "MaksIT.UScheduler.ScheduleManager\MaksIT.UScheduler.ScheduleManager.csproj", "{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MaksIT.UScheduler.Shared", "MaksIT.UScheduler.Shared\MaksIT.UScheduler.Shared.csproj", "{B2C3D4E5-F6A7-8901-BCDE-F12345678901}" -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Release|Any CPU = Release|Any CPU - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {DE1F347C-D201-42E2-8D22-924508FD30AA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {DE1F347C-D201-42E2-8D22-924508FD30AA}.Debug|Any CPU.Build.0 = Debug|Any CPU - {DE1F347C-D201-42E2-8D22-924508FD30AA}.Release|Any CPU.ActiveCfg = Release|Any CPU - {DE1F347C-D201-42E2-8D22-924508FD30AA}.Release|Any CPU.Build.0 = Release|Any CPU - {DC193ABC-89F8-131B-060F-6C3A3CE2652A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {DC193ABC-89F8-131B-060F-6C3A3CE2652A}.Debug|Any CPU.Build.0 = Debug|Any CPU - {DC193ABC-89F8-131B-060F-6C3A3CE2652A}.Release|Any CPU.ActiveCfg = Release|Any CPU - {DC193ABC-89F8-131B-060F-6C3A3CE2652A}.Release|Any CPU.Build.0 = Release|Any CPU - {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|Any CPU.Build.0 = Debug|Any CPU - {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|Any CPU.ActiveCfg = Release|Any CPU - {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|Any CPU.Build.0 = Release|Any CPU - {B2C3D4E5-F6A7-8901-BCDE-F12345678901}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {B2C3D4E5-F6A7-8901-BCDE-F12345678901}.Debug|Any CPU.Build.0 = Debug|Any CPU - {B2C3D4E5-F6A7-8901-BCDE-F12345678901}.Release|Any CPU.ActiveCfg = Release|Any CPU - {B2C3D4E5-F6A7-8901-BCDE-F12345678901}.Release|Any CPU.Build.0 = Release|Any CPU - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection - GlobalSection(ExtensibilityGlobals) = postSolution - SolutionGuid = {3C81929E-84E5-4648-9FC6-C73902D7E58C} - EndGlobalSection -EndGlobal diff --git a/src/MaksIT.UScheduler.slnx b/src/MaksIT.UScheduler.slnx new file mode 100644 index 0000000..32dbf5d --- /dev/null +++ b/src/MaksIT.UScheduler.slnx @@ -0,0 +1,7 @@ + + + + + + + diff --git a/src/MaksIT.UScheduler/MaksIT.UScheduler.csproj b/src/MaksIT.UScheduler/MaksIT.UScheduler.csproj index a465ae0..c25ba7d 100644 --- a/src/MaksIT.UScheduler/MaksIT.UScheduler.csproj +++ b/src/MaksIT.UScheduler/MaksIT.UScheduler.csproj @@ -38,7 +38,7 @@ %(Filename)%(Extension) PreserveNewest - + PreserveNewest diff --git a/utils/Force-AmendTaggedCommit/Force-AmendTaggedCommit.bat b/utils/Force-AmendTaggedCommit/Force-AmendTaggedCommit.bat index 9ccdcaf..20029f8 100644 --- a/utils/Force-AmendTaggedCommit/Force-AmendTaggedCommit.bat +++ b/utils/Force-AmendTaggedCommit/Force-AmendTaggedCommit.bat @@ -1,9 +1,3 @@ @echo off - -REM Change directory to the location of the script -cd /d %~dp0 - -REM Run Force Amend Tagged Commit script -powershell -ExecutionPolicy Bypass -File "%~dp0Force-AmendTaggedCommit.ps1" - +pwsh -NoProfile -ExecutionPolicy Bypass -File "%~dp0Force-AmendTaggedCommit.ps1" pause diff --git a/utils/Force-AmendTaggedCommit/Force-AmendTaggedCommit.ps1 b/utils/Force-AmendTaggedCommit/Force-AmendTaggedCommit.ps1 index e57c399..d338a36 100644 --- a/utils/Force-AmendTaggedCommit/Force-AmendTaggedCommit.ps1 +++ b/utils/Force-AmendTaggedCommit/Force-AmendTaggedCommit.ps1 @@ -1,3 +1,6 @@ +#requires -Version 7.0 +#requires -PSEdition Core + <# .SYNOPSIS Amends the latest commit, recreates its associated tag, and force pushes both to remote. @@ -16,10 +19,10 @@ If specified, shows what would be done without making changes. .EXAMPLE - .\Force-AmendTaggedCommit.ps1 + pwsh -File .\Force-AmendTaggedCommit.ps1 .EXAMPLE - .\Force-AmendTaggedCommit.ps1 -DryRun + pwsh -File .\Force-AmendTaggedCommit.ps1 -DryRun .NOTES CONFIGURATION (scriptsettings.json): @@ -34,209 +37,213 @@ param( [switch]$DryRun ) -$ErrorActionPreference = "Stop" - -# ============================================================================== -# PATH CONFIGURATION -# ============================================================================== - +# 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 -# ============================================================================== -# LOAD SETTINGS -# ============================================================================== +#region Import Modules -$settingsPath = Join-Path $scriptDir "scriptsettings.json" -if (-not (Test-Path $settingsPath)) { - Write-Error "Settings file not found: $settingsPath" +# 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 } -$settings = Get-Content $settingsPath -Raw | ConvertFrom-Json +# 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 +} -# Extract settings with defaults +$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 } -# ============================================================================== -# HELPER FUNCTIONS -# ============================================================================== +#endregion -function Write-Step { - param([string]$Text) - Write-Host "`n>> $Text" -ForegroundColor Cyan +#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 ***" } -function Write-Success { - param([string]$Text) - Write-Host " $Text" -ForegroundColor Green +#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 } -function Write-Info { - param([string]$Text) - Write-Host " $Text" -ForegroundColor Gray +# 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" -function Write-Warn { - param([string]$Text) - Write-Host " $Text" -ForegroundColor Yellow +# 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 " $_" } } - -function Invoke-Git { - 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) { - throw "$ErrorMessage (exit code: $exitCode)" - } - return $output - } else { - & git @Arguments - $exitCode = $LASTEXITCODE - if ($exitCode -ne 0) { - throw "$ErrorMessage (exit code: $exitCode)" - } - } -} - -# ============================================================================== -# MAIN EXECUTION -# ============================================================================== - -try { - Write-Host "`n========================================" -ForegroundColor Magenta - Write-Host " Force Amend Tagged Commit Script" -ForegroundColor Magenta - Write-Host "========================================`n" -ForegroundColor Magenta - - if ($DryRun) { - Write-Warn "*** DRY RUN MODE - No changes will be made ***`n" - } - - # Get current branch - Write-Step "Getting current branch..." - $Branch = Invoke-Git -Arguments @("rev-parse", "--abbrev-ref", "HEAD") -CaptureOutput -ErrorMessage "Failed to get current branch" - Write-Info "Branch: $Branch" - - # Get last commit info - Write-Step "Getting last commit..." - $null = Invoke-Git -Arguments @("rev-parse", "HEAD") -CaptureOutput -ErrorMessage "Failed to get HEAD commit" - $CommitMessage = Invoke-Git -Arguments @("log", "-1", "--format=%s") -CaptureOutput - $CommitHash = Invoke-Git -Arguments @("log", "-1", "--format=%h") -CaptureOutput - Write-Info "Commit: $CommitHash - $CommitMessage" - - # Find tag pointing to HEAD - Write-Step "Finding tag on last commit..." - $Tags = & git tag --points-at HEAD 2>&1 - - if (-not $Tags -or [string]::IsNullOrWhiteSpace("$Tags")) { - throw "No tag found on the last commit ($CommitHash). This script requires the last commit to have an associated tag." - } - - # If multiple tags, use the first one - $TagName = ("$Tags" -split "`n")[0].Trim() - Write-Success "Found tag: $TagName" - - # Show current status - Write-Step "Checking pending changes..." - $Status = & git status --short 2>&1 - if ($Status -and -not [string]::IsNullOrWhiteSpace("$Status")) { - Write-Info "Pending changes:" - "$Status" -split "`n" | ForEach-Object { Write-Info " $_" } - } else { - Write-Warn "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-Host "`nAborted by user" -ForegroundColor Yellow - exit 0 - } - } - } - - # Confirm operation - Write-Host "`n----------------------------------------" -ForegroundColor White - Write-Host " Summary of operations:" -ForegroundColor White - Write-Host "----------------------------------------" -ForegroundColor White - Write-Host " Branch: $Branch" -ForegroundColor White - Write-Host " Commit: $CommitHash" -ForegroundColor White - Write-Host " Tag: $TagName" -ForegroundColor White - Write-Host " Remote: $Remote" -ForegroundColor White - Write-Host "----------------------------------------`n" -ForegroundColor White - - if ($ConfirmBeforeAmend -and -not $DryRun) { - $confirm = Read-Host " Proceed with amend and force push? (y/N)" +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-Host "`nAborted by user" -ForegroundColor Yellow + Write-Log -Level "WARN" -Message "Aborted by user" exit 0 } } - - # Stage all changes - Write-Step "Staging all changes..." - if (-not $DryRun) { - Invoke-Git -Arguments @("add", "-A") -ErrorMessage "Failed to stage changes" - } - Write-Success "All changes staged" - - # Amend commit - Write-Step "Amending commit..." - if (-not $DryRun) { - Invoke-Git -Arguments @("commit", "--amend", "--no-edit") -ErrorMessage "Failed to amend commit" - } - Write-Success "Commit amended" - - # Delete local tag - Write-Step "Deleting local tag '$TagName'..." - if (-not $DryRun) { - Invoke-Git -Arguments @("tag", "-d", $TagName) -ErrorMessage "Failed to delete local tag" - } - Write-Success "Local tag deleted" - - # Recreate tag on new commit - Write-Step "Recreating tag '$TagName' on amended commit..." - if (-not $DryRun) { - Invoke-Git -Arguments @("tag", $TagName) -ErrorMessage "Failed to create tag" - } - Write-Success "Tag recreated" - - # Force push branch - Write-Step "Force pushing branch '$Branch' to $Remote..." - if (-not $DryRun) { - Invoke-Git -Arguments @("push", "--force", $Remote, $Branch) -ErrorMessage "Failed to force push branch" - } - Write-Success "Branch force pushed" - - # Force push tag - Write-Step "Force pushing tag '$TagName' to $Remote..." - if (-not $DryRun) { - Invoke-Git -Arguments @("push", "--force", $Remote, $TagName) -ErrorMessage "Failed to force push tag" - } - Write-Success "Tag force pushed" - - Write-Host "`n========================================" -ForegroundColor Green - Write-Host " Operation completed successfully!" -ForegroundColor Green - Write-Host "========================================`n" -ForegroundColor Green - - # Show final state - Write-Host "Final state:" -ForegroundColor White - & git log -1 --oneline - Write-Host "" - -} catch { - Write-Host "`n========================================" -ForegroundColor Red - Write-Host " ERROR: $($_.Exception.Message)" -ForegroundColor Red - Write-Host "========================================`n" -ForegroundColor Red - exit 1 } + +# 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 diff --git a/utils/Force-AmendTaggedCommit/scriptsettings.json b/utils/Force-AmendTaggedCommit/scriptsettings.json index fad2135..df73911 100644 --- a/utils/Force-AmendTaggedCommit/scriptsettings.json +++ b/utils/Force-AmendTaggedCommit/scriptsettings.json @@ -6,5 +6,13 @@ "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" + } } } diff --git a/utils/Generate-CoverageBadges/Generate-CoverageBadges.bat b/utils/Generate-CoverageBadges/Generate-CoverageBadges.bat index 46ae950..2790074 100644 --- a/utils/Generate-CoverageBadges/Generate-CoverageBadges.bat +++ b/utils/Generate-CoverageBadges/Generate-CoverageBadges.bat @@ -1,9 +1,3 @@ @echo off -REM Generate-CoverageBadges.bat - Wrapper for Generate-CoverageBadges.ps1 -REM Runs tests and generates SVG coverage badges for README - -pushd "%~dp0" -powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0Generate-CoverageBadges.ps1" %* -set EXITCODE=%ERRORLEVEL% -popd -exit /b %EXITCODE% +pwsh -NoProfile -ExecutionPolicy Bypass -File "%~dp0Generate-CoverageBadges.ps1" +pause diff --git a/utils/Generate-CoverageBadges/Generate-CoverageBadges.ps1 b/utils/Generate-CoverageBadges/Generate-CoverageBadges.ps1 index 21b31e9..24f7d09 100644 --- a/utils/Generate-CoverageBadges/Generate-CoverageBadges.ps1 +++ b/utils/Generate-CoverageBadges/Generate-CoverageBadges.ps1 @@ -1,3 +1,6 @@ +#requires -Version 7.0 +#requires -PSEdition Core + <# .SYNOPSIS Generates SVG coverage badges for README. @@ -7,6 +10,7 @@ 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) @@ -15,18 +19,12 @@ Badge colors based on coverage: - brightgreen (>=80%), green (>=60%), yellowgreen (>=40%) - yellow (>=20%), orange (>=10%), red (<10%) - -.PARAMETER OpenReport - Generate and open a full HTML coverage report in the default browser. - Requires ReportGenerator: dotnet tool install -g dotnet-reportgenerator-globaltool + If openReport is true, ReportGenerator is required: + dotnet tool install -g dotnet-reportgenerator-globaltool .EXAMPLE - .\Generate-CoverageBadges.ps1 - Runs tests and generates coverage badges. - -.EXAMPLE - .\Generate-CoverageBadges.ps1 -OpenReport - Runs tests, generates badges, and opens HTML report in browser. + 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. @@ -35,32 +33,55 @@ Author: MaksIT Requires: .NET SDK, Coverlet (included in test project) #> -param( - [switch]$OpenReport -) $ErrorActionPreference = "Stop" +# Get the directory of the current script (for loading settings and relative paths) $ScriptDir = $PSScriptRoot +$UtilsDir = Split-Path $ScriptDir -Parent -# Import TestRunner module -$ModulePath = Join-Path (Split-Path $ScriptDir -Parent) "TestRunner.psm1" -if (-not (Test-Path $ModulePath)) { - Write-Host "TestRunner module not found at: $ModulePath" -ForegroundColor Red +#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 $ModulePath -Force +Import-Module $testRunnerModulePath -Force -# Load settings -$SettingsFile = Join-Path $ScriptDir "scriptsettings.json" -if (-not (Test-Path $SettingsFile)) { - Write-Host "Settings file not found: $SettingsFile" -ForegroundColor Red +# 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 -$Settings = Get-Content $SettingsFile | ConvertFrom-Json +# 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 -# Resolve paths from settings +#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)) @@ -69,29 +90,14 @@ if (-not (Test-Path $BadgesDir)) { New-Item -ItemType Directory -Path $BadgesDir | Out-Null } -# Run tests with coverage -$coverage = Invoke-TestsWithCoverage -TestProjectPath $TestProjectPath -KeepResults:$OpenReport +#endregion -if (-not $coverage.Success) { - Write-Host "Tests failed: $($coverage.Error)" -ForegroundColor Red - exit 1 -} +#region Helpers -Write-Host "Tests passed!" -ForegroundColor Green - -# Store metrics in a hashtable for easy lookup -$metrics = @{ - "line" = $coverage.LineRate - "branch" = $coverage.BranchRate - "method" = $coverage.MethodRate -} - -# Function to get badge color based on coverage percentage and thresholds from settings +# Maps a coverage percentage to a shields.io color using configured thresholds. function Get-BadgeColor { param([double]$percentage) - - $thresholds = $Settings.colorThresholds - + if ($percentage -ge $thresholds.brightgreen) { return "brightgreen" } if ($percentage -ge $thresholds.green) { return "green" } if ($percentage -ge $thresholds.yellowgreen) { return "yellowgreen" } @@ -100,7 +106,7 @@ function Get-BadgeColor { return "red" } -# Function to create shields.io style SVG badge +# Builds a shields.io-like SVG badge string for one metric. function New-Badge { param( [string]$label, @@ -151,33 +157,59 @@ function New-Badge { "@ } -# Generate badges from settings -Write-Host "Generating coverage badges..." -ForegroundColor Cyan +#endregion + +#region Main + +#region Test And Coverage + +$coverage = Invoke-TestsWithCoverage -TestProjectPath $TestProjectPath -KeepResults:$OpenReport +if (-not $coverage.Success) { + Write-Error "Tests failed: $($coverage.Error)" + exit 1 +} + +Write-Log -Level "OK" -Message "Tests passed!" + +$metrics = @{ + "line" = $coverage.LineRate + "branch" = $coverage.BranchRate + "method" = $coverage.MethodRate +} + +#endregion + +#region Generate Badges + +Write-LogStep -Message "Generating coverage badges..." foreach ($badge in $Settings.badges) { $metricValue = $metrics[$badge.metric] $color = Get-BadgeColor $metricValue $svg = New-Badge -label $badge.label -value "$metricValue%" -color $color $path = Join-Path $BadgesDir $badge.name - $svg | Out-File -FilePath $path -Encoding utf8 - Write-Host " $($badge.name): $($badge.label) = $metricValue%" -ForegroundColor Green + $svg | Out-File -FilePath $path -Encoding utf8NoBOM + Write-Log -Level "OK" -Message "$($badge.name): $($badge.label) = $metricValue%" } -# Display summary -Write-Host "" -Write-Host "=== Coverage Summary ===" -ForegroundColor Yellow -Write-Host " Line Coverage: $($coverage.LineRate)%" -Write-Host " Branch Coverage: $($coverage.BranchRate)%" -Write-Host " Method Coverage: $($coverage.MethodRate)% ($($coverage.CoveredMethods) of $($coverage.TotalMethods) methods)" -Write-Host "========================" -ForegroundColor Yellow -Write-Host "" -Write-Host "Badges generated in: $BadgesDir" -ForegroundColor Green -Write-Host "Commit the badges/ folder to update README." -ForegroundColor Cyan +#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 -# Optionally generate full HTML report if ($OpenReport -and $coverage.CoverageFile) { - Write-Host "" - Write-Host "Generating HTML report..." -ForegroundColor Cyan + Write-LogStep -Message "Generating HTML report..." + Assert-Command reportgenerator $ResultsDir = Split-Path (Split-Path $coverage.CoverageFile -Parent) -Parent $ReportDir = Join-Path $ResultsDir "report" @@ -194,5 +226,9 @@ if ($OpenReport -and $coverage.CoverageFile) { Start-Process $IndexFile } - Write-Host "TestResults kept for HTML report viewing." -ForegroundColor Gray + Write-Log -Level "INFO" -Message "TestResults kept for HTML report viewing." } + +#endregion + +#endregion diff --git a/utils/Generate-CoverageBadges/scriptsettings.json b/utils/Generate-CoverageBadges/scriptsettings.json index 6e883cc..2000db7 100644 --- a/utils/Generate-CoverageBadges/scriptsettings.json +++ b/utils/Generate-CoverageBadges/scriptsettings.json @@ -2,9 +2,10 @@ "$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.UScheduler.Tests", - "badgesDir": "..\\..\\badges" + "badgesDir": "..\\..\\assets\\badges" }, "badges": [ { @@ -30,5 +31,14 @@ "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." } } diff --git a/utils/GitTools.psm1 b/utils/GitTools.psm1 new file mode 100644 index 0000000..405f408 --- /dev/null +++ b/utils/GitTools.psm1 @@ -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 diff --git a/utils/Logging.psm1 b/utils/Logging.psm1 new file mode 100644 index 0000000..a0cbb3d --- /dev/null +++ b/utils/Logging.psm1 @@ -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 diff --git a/utils/Release-Package/CorePlugins/CleanupArtifacts.psm1 b/utils/Release-Package/CorePlugins/CleanupArtifacts.psm1 new file mode 100644 index 0000000..43dc044 --- /dev/null +++ b/utils/Release-Package/CorePlugins/CleanupArtifacts.psm1 @@ -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 diff --git a/utils/Release-Package/CorePlugins/CreateArchive.psm1 b/utils/Release-Package/CorePlugins/CreateArchive.psm1 new file mode 100644 index 0000000..54cce44 --- /dev/null +++ b/utils/Release-Package/CorePlugins/CreateArchive.psm1 @@ -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 diff --git a/utils/Release-Package/CorePlugins/DotNetPack.psm1 b/utils/Release-Package/CorePlugins/DotNetPack.psm1 new file mode 100644 index 0000000..8353217 --- /dev/null +++ b/utils/Release-Package/CorePlugins/DotNetPack.psm1 @@ -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 diff --git a/utils/Release-Package/CorePlugins/DotNetPublish.psm1 b/utils/Release-Package/CorePlugins/DotNetPublish.psm1 new file mode 100644 index 0000000..8acb8bc --- /dev/null +++ b/utils/Release-Package/CorePlugins/DotNetPublish.psm1 @@ -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 diff --git a/utils/Release-Package/CorePlugins/DotNetTest.psm1 b/utils/Release-Package/CorePlugins/DotNetTest.psm1 new file mode 100644 index 0000000..7759fc0 --- /dev/null +++ b/utils/Release-Package/CorePlugins/DotNetTest.psm1 @@ -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 diff --git a/utils/Release-Package/CorePlugins/GitHub.psm1 b/utils/Release-Package/CorePlugins/GitHub.psm1 new file mode 100644 index 0000000..38a9386 --- /dev/null +++ b/utils/Release-Package/CorePlugins/GitHub.psm1 @@ -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[:/](?[^/]+)/(?[^/.]+)(\.git)?$") { + return "$($matches['owner'])/$($matches['repo'])" + } + + if ($repoSource -match "^(?[^/]+)/(?[^/]+)$") { + 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 diff --git a/utils/Release-Package/CorePlugins/NuGet.psm1 b/utils/Release-Package/CorePlugins/NuGet.psm1 new file mode 100644 index 0000000..4dafc54 --- /dev/null +++ b/utils/Release-Package/CorePlugins/NuGet.psm1 @@ -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 diff --git a/utils/Release-Package/CorePlugins/QualityGate.psm1 b/utils/Release-Package/CorePlugins/QualityGate.psm1 new file mode 100644 index 0000000..450a468 --- /dev/null +++ b/utils/Release-Package/CorePlugins/QualityGate.psm1 @@ -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 diff --git a/utils/Release-Package/CustomPlugins/.gitkeep b/utils/Release-Package/CustomPlugins/.gitkeep new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/utils/Release-Package/CustomPlugins/.gitkeep @@ -0,0 +1 @@ + diff --git a/utils/Release-Package/CustomPlugins/BundleCustomization.psm1 b/utils/Release-Package/CustomPlugins/BundleCustomization.psm1 new file mode 100644 index 0000000..e05906b --- /dev/null +++ b/utils/Release-Package/CustomPlugins/BundleCustomization.psm1 @@ -0,0 +1,314 @@ +#requires -Version 7.0 +#requires -PSEdition Core + +<# +.SYNOPSIS + Customizes the staged release bundle after build artifacts are produced. + +.DESCRIPTION + Recreates the legacy release bundle layout by: + - publishing all configured .NET projects into bundle/bin/ + - copying the Scripts folder into bundle/Scripts + - rewriting appsettings.json for the bundled runtime layout + - optionally creating a launcher batch file + + The plugin then updates the shared release context so CreateArchive zips the + fully prepared bundle directory instead of the raw publish output. +#> + +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 Resolve-PluginPath { + param( + [Parameter(Mandatory = $true)] + [string]$Path, + + [Parameter(Mandatory = $true)] + [string]$BasePath + ) + + return [System.IO.Path]::GetFullPath((Join-Path $BasePath $Path)) +} + +function Get-ProjectPropertyValueInternal { + param( + [Parameter(Mandatory = $true)] + [xml]$Csproj, + + [Parameter(Mandatory = $true)] + [string]$PropertyName + ) + + $propNode = $Csproj.Project.PropertyGroup | + Where-Object { $_.$PropertyName } | + Select-Object -First 1 + + if ($propNode) { + return $propNode.$PropertyName + } + + return $null +} + +function Resolve-ProjectExeNameInternal { + param( + [Parameter(Mandatory = $true)] + [string]$ProjectPath + ) + + [xml]$csproj = Get-Content $ProjectPath + $assemblyName = Get-ProjectPropertyValueInternal -Csproj $csproj -PropertyName "AssemblyName" + + if (-not [string]::IsNullOrWhiteSpace([string]$assemblyName)) { + return [string]$assemblyName + } + + return [System.IO.Path]::GetFileNameWithoutExtension($ProjectPath) +} + +function Find-ProjectBySuffixInternal { + param( + [Parameter(Mandatory = $true)] + [object[]]$PublishedProjects, + + [Parameter(Mandatory = $false)] + [string]$Suffix + ) + + if ([string]::IsNullOrWhiteSpace($Suffix)) { + return $null + } + + return $PublishedProjects | + Where-Object { $_.ProjectPath -like "*$Suffix" } | + Select-Object -First 1 +} + +function Set-JsonFileContentInternal { + param( + [Parameter(Mandatory = $true)] + [string]$Path, + + [Parameter(Mandatory = $true)] + [object]$Value + ) + + $jsonOutput = $Value | ConvertTo-Json -Depth 20 + Set-Content -Path $Path -Value $jsonOutput -Encoding UTF8 +} + +function Ensure-NotePropertyInternal { + param( + [Parameter(Mandatory = $true)] + [object]$Target, + + [Parameter(Mandatory = $true)] + [string]$PropertyName, + + [Parameter(Mandatory = $true)] + [object]$PropertyValue + ) + + if ($Target.PSObject.Properties[$PropertyName]) { + $Target.$PropertyName = $PropertyValue + return + } + + $Target | Add-Member -MemberType NoteProperty -Name $PropertyName -Value $PropertyValue +} + +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 + + if (-not $sharedSettings.PSObject.Properties['ProjectFiles'] -or $sharedSettings.ProjectFiles.Count -eq 0) { + throw "BundleCustomization plugin requires project files in the shared context." + } + + $scriptDir = $sharedSettings.ScriptDir + $projectFiles = @($sharedSettings.ProjectFiles) + $bundleDirectory = if ($pluginSettings.PSObject.Properties['bundleDir'] -and -not [string]::IsNullOrWhiteSpace([string]$pluginSettings.bundleDir)) { + Resolve-PluginPath -Path ([string]$pluginSettings.bundleDir) -BasePath $scriptDir + } + else { + Join-Path $sharedSettings.ArtifactsDirectory "bundle" + } + + if (-not $pluginSettings.PSObject.Properties['scriptsPath'] -or [string]::IsNullOrWhiteSpace([string]$pluginSettings.scriptsPath)) { + throw "BundleCustomization plugin requires a scriptsPath setting." + } + + $scriptsSourcePath = Resolve-PluginPath -Path ([string]$pluginSettings.scriptsPath) -BasePath $scriptDir + if (-not (Test-Path $scriptsSourcePath -PathType Container)) { + throw "Scripts folder not found: $scriptsSourcePath" + } + + Assert-Command dotnet + + Write-Log -Level "STEP" -Message "Preparing customized release bundle..." + + if (Test-Path $bundleDirectory) { + Remove-Item -Path $bundleDirectory -Recurse -Force + } + + New-Item -ItemType Directory -Path $bundleDirectory | Out-Null + + $binDirectory = Join-Path $bundleDirectory "bin" + New-Item -ItemType Directory -Path $binDirectory | Out-Null + + $publishedProjects = @() + + foreach ($projectPath in $projectFiles) { + if (-not (Test-Path $projectPath -PathType Leaf)) { + throw "Project file not found: $projectPath" + } + + $projectName = [System.IO.Path]::GetFileNameWithoutExtension($projectPath) + $projectBinDirectory = Join-Path $binDirectory $projectName + + Write-Log -Level "STEP" -Message " Publishing $projectName into bundle..." + dotnet publish $projectPath -c Release -o $projectBinDirectory --nologo + if ($LASTEXITCODE -ne 0) { + throw "dotnet publish failed for $projectPath." + } + + $exeBaseName = Resolve-ProjectExeNameInternal -ProjectPath $projectPath + $publishedProjects += [pscustomobject]@{ + ProjectPath = $projectPath + ProjectName = $projectName + BinDirectory = $projectBinDirectory + ExeBaseName = $exeBaseName + } + + Write-Log -Level "OK" -Message " Published: $projectBinDirectory" + } + + $scriptsDestination = Join-Path $bundleDirectory "Scripts" + Copy-Item -Path $scriptsSourcePath -Destination $scriptsDestination -Recurse + Write-Log -Level "OK" -Message " Scripts copied: $scriptsDestination" + + if ($pluginSettings.PSObject.Properties['projects'] -and $null -ne $pluginSettings.projects) { + $projectConfig = $pluginSettings.projects + + $scheduleManagerProject = Find-ProjectBySuffixInternal -PublishedProjects $publishedProjects -Suffix ([string]$projectConfig.scheduleManagerCsprojEndsWith) + if ($null -ne $scheduleManagerProject) { + $scheduleManagerAppSettingsFile = [string]$projectConfig.scheduleManagerAppSettingsFile + $scheduleManagerAppSettingsPath = Join-Path $scheduleManagerProject.BinDirectory $scheduleManagerAppSettingsFile + + if (Test-Path $scheduleManagerAppSettingsPath -PathType Leaf) { + $scheduleManagerAppSettings = Get-Content $scheduleManagerAppSettingsPath -Raw | ConvertFrom-Json + if ($scheduleManagerAppSettings.PSObject.Properties['USchedulerSettings'] -and $null -ne $scheduleManagerAppSettings.USchedulerSettings) { + Ensure-NotePropertyInternal -Target $scheduleManagerAppSettings.USchedulerSettings -PropertyName "ServiceBinPath" -PropertyValue ([string]$projectConfig.scheduleManagerServiceBinPath) + Set-JsonFileContentInternal -Path $scheduleManagerAppSettingsPath -Value $scheduleManagerAppSettings + Write-Log -Level "OK" -Message " Updated ScheduleManager appsettings." + } + else { + Write-Log -Level "WARN" -Message " ScheduleManager appsettings has no USchedulerSettings section." + } + } + else { + Write-Log -Level "WARN" -Message " ScheduleManager appsettings not found: $scheduleManagerAppSettingsPath" + } + } + else { + Write-Log -Level "WARN" -Message " ScheduleManager project not found in configured project files." + } + + $uSchedulerProject = Find-ProjectBySuffixInternal -PublishedProjects $publishedProjects -Suffix ([string]$projectConfig.uschedulerCsprojEndsWith) + if ($null -ne $uSchedulerProject) { + $uSchedulerAppSettingsFile = [string]$projectConfig.uschedulerAppSettingsFile + $uSchedulerAppSettingsPath = Join-Path $uSchedulerProject.BinDirectory $uSchedulerAppSettingsFile + + if (Test-Path $uSchedulerAppSettingsPath -PathType Leaf) { + $uSchedulerAppSettings = Get-Content $uSchedulerAppSettingsPath -Raw | ConvertFrom-Json + + if (-not $uSchedulerAppSettings.PSObject.Properties['Configuration'] -or $null -eq $uSchedulerAppSettings.Configuration) { + Ensure-NotePropertyInternal -Target $uSchedulerAppSettings -PropertyName "Configuration" -PropertyValue ([pscustomobject]@{}) + } + + Ensure-NotePropertyInternal -Target $uSchedulerAppSettings.Configuration -PropertyName "LogDir" -PropertyValue ([string]$projectConfig.uschedulerLogDir) + + $powerShellScripts = @( + Get-ChildItem -Path $scriptsDestination -Filter "*.ps1" -Recurse -File | + Where-Object { $_.Directory.Name -ne "Utilities" } | + ForEach-Object { + $relativePath = $_.FullName.Substring($scriptsDestination.Length + 1).Replace('/', '\') + $scriptPath = "{0}\{1}" -f ([string]$projectConfig.scriptsRelativeToExe), $relativePath + + [pscustomobject]@{ + Path = $scriptPath + IsSigned = $false + Disabled = $true + } + } + ) + + Ensure-NotePropertyInternal -Target $uSchedulerAppSettings.Configuration -PropertyName "Powershell" -PropertyValue $powerShellScripts + Set-JsonFileContentInternal -Path $uSchedulerAppSettingsPath -Value $uSchedulerAppSettings + + Write-Log -Level "OK" -Message " Updated UScheduler appsettings." + if ($powerShellScripts.Count -gt 0) { + Write-Log -Level "INFO" -Message " Added $($powerShellScripts.Count) bundled script entries." + } + } + else { + Write-Log -Level "WARN" -Message " UScheduler appsettings not found: $uSchedulerAppSettingsPath" + } + } + else { + Write-Log -Level "WARN" -Message " UScheduler project not found in configured project files." + } + } + + if ($pluginSettings.PSObject.Properties['launcher'] -and $null -ne $pluginSettings.launcher -and $pluginSettings.launcher.enabled) { + $launcherSettings = $pluginSettings.launcher + $launcherTarget = $null + + switch ([string]$launcherSettings.targetProject) { + "scheduleManager" { + $launcherTarget = Find-ProjectBySuffixInternal -PublishedProjects $publishedProjects -Suffix ([string]$pluginSettings.projects.scheduleManagerCsprojEndsWith) + } + "uscheduler" { + $launcherTarget = Find-ProjectBySuffixInternal -PublishedProjects $publishedProjects -Suffix ([string]$pluginSettings.projects.uschedulerCsprojEndsWith) + } + default { + Write-Log -Level "WARN" -Message " Unknown launcher targetProject '$([string]$launcherSettings.targetProject)'." + } + } + + if ($null -ne $launcherTarget) { + $launcherPath = Join-Path $bundleDirectory ([string]$launcherSettings.fileName) + $launcherExePath = "%~dp0bin\$($launcherTarget.ProjectName)\$($launcherTarget.ExeBaseName).exe" + $launcherContent = @" +@echo off +start "" "$launcherExePath" +"@ + + Set-Content -Path $launcherPath -Value $launcherContent -Encoding ASCII + Write-Log -Level "OK" -Message " Created launcher: $launcherPath" + } + else { + Write-Log -Level "WARN" -Message " Launcher target project could not be resolved." + } + } + + $sharedSettings | Add-Member -NotePropertyName BundleDirectory -NotePropertyValue $bundleDirectory -Force + $sharedSettings | Add-Member -NotePropertyName ReleaseArchiveInputs -NotePropertyValue @($bundleDirectory) -Force + + Write-Log -Level "OK" -Message "Customized release bundle ready: $bundleDirectory" +} + +Export-ModuleMember -Function Invoke-Plugin diff --git a/utils/Release-Package/DotNetProjectSupport.psm1 b/utils/Release-Package/DotNetProjectSupport.psm1 new file mode 100644 index 0000000..a510eb5 --- /dev/null +++ b/utils/Release-Package/DotNetProjectSupport.psm1 @@ -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 diff --git a/utils/Release-Package/EngineSupport.psm1 b/utils/Release-Package/EngineSupport.psm1 new file mode 100644 index 0000000..c3d29f2 --- /dev/null +++ b/utils/Release-Package/EngineSupport.psm1 @@ -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 diff --git a/utils/Release-Package/PluginSupport.psm1 b/utils/Release-Package/PluginSupport.psm1 new file mode 100644 index 0000000..326a16c --- /dev/null +++ b/utils/Release-Package/PluginSupport.psm1 @@ -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 "INFO" -Message "Skipping plugin '$($Plugin.Name)' because no publish branches are configured." + } + return $false + } + + if (-not ($allowedBranches -contains $SharedSettings.CurrentBranch)) { + if ($WriteLogs) { + Write-Log -Level "INFO" -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 diff --git a/utils/Release-Package/Release-Package.bat b/utils/Release-Package/Release-Package.bat new file mode 100644 index 0000000..6a4aba8 --- /dev/null +++ b/utils/Release-Package/Release-Package.bat @@ -0,0 +1,3 @@ +@echo off +pwsh -NoProfile -ExecutionPolicy Bypass -File "%~dp0Release-Package.ps1" +pause diff --git a/utils/Release-Package/Release-Package.ps1 b/utils/Release-Package/Release-Package.ps1 new file mode 100644 index 0000000..8cef2be --- /dev/null +++ b/utils/Release-Package/Release-Package.ps1 @@ -0,0 +1,183 @@ +#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) { + $noReleasePluginsLogLevel = if ($engineContext.IsNonReleaseBranch) { "INFO" } else { "WARN" } + Write-Log -Level $noReleasePluginsLogLevel -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 "INFO" -Message "To execute release-stage plugins, rerun from an allowed release branch such as '$preferredReleaseBranch'." +} + +#endregion + +#endregion diff --git a/utils/Release-Package/scriptsettings.json b/utils/Release-Package/scriptsettings.json new file mode 100644 index 0000000..96fbd64 --- /dev/null +++ b/utils/Release-Package/scriptsettings.json @@ -0,0 +1,104 @@ +{ + "$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.UScheduler.Tests", + "resultsDir": "..\\..\\testResults" + }, + { + "Name": "QualityGate", + "Stage": "QualityGate", + "Enabled": true, + "coverageThreshold": 0, + "failOnVulnerabilities": true + }, + { + "Name": "DotNetPublish", + "Stage": "Build", + "Enabled": true, + "projectFiles": [ + "..\\..\\src\\MaksIT.UScheduler\\MaksIT.UScheduler.csproj", + "..\\..\\src\\MaksIT.UScheduler.ScheduleManager\\MaksIT.UScheduler.ScheduleManager.csproj" + ], + "artifactsDir": "..\\..\\release", + "bundleDir": "..\\..\\release\\bundle" + }, + { + "Name": "BundleCustomization", + "Stage": "Build", + "Enabled": true, + "bundleDir": "..\\..\\release\\bundle", + "scriptsPath": "..\\..\\src\\Scripts", + "launcher": { + "enabled": true, + "fileName": "Start-ScheduleManager.bat", + "targetProject": "scheduleManager" + }, + "projects": { + "scheduleManagerCsprojEndsWith": "MaksIT.UScheduler.ScheduleManager.csproj", + "uschedulerCsprojEndsWith": "MaksIT.UScheduler.csproj", + "scheduleManagerAppSettingsFile": "appsettings.json", + "uschedulerAppSettingsFile": "appsettings.json", + "scheduleManagerServiceBinPath": "..\\MaksIT.UScheduler\\", + "uschedulerLogDir": "..\\..\\Logs", + "scriptsRelativeToExe": "..\\..\\Scripts" + } + }, + { + "Name": "CreateArchive", + "Stage": "Build", + "Enabled": true, + "zipNamePattern": "maksit.uscheduler-{version}.zip" + }, + { + "Name": "GitHub", + "Stage": "Release", + "branches": [ + "main" + ], + "githubToken": "GITHUB_MAKS_IT_COM", + "repository": "https://github.com/MAKS-IT-COM/uscheduler", + "releaseNotesFile": "..\\..\\CHANGELOG.md", + "releaseTitlePattern": "Release {version}" + }, + { + "Name": "CleanupArtifacts", + "Stage": "Release", + "Enabled": true, + "includePatterns": [ + "*" + ], + "excludePatterns": [ + "*.zip" + ] + } + ], + "_comments": { + "Plugins": { + "Name": "Plugin module file name in CorePlugins or CustomPlugins (for example, DotNetPublish -> CorePlugins/DotNetPublish.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": "Plugin-specific allowed branches. Omit to allow any branch.", + "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": "DotNetPublish or another producer plugin can define the project files used for version discovery and artifact creation.", + "artifactsDir": "DotNetPublish or another producer plugin can define the artifacts output directory, relative to the script folder.", + "bundleDir": "DotNetPublish and BundleCustomization plugins can define the staged bundle 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": "GitHub plugin only. Archive name pattern for packaged release assets. Supports {version} placeholder.", + "scriptsPath": "BundleCustomization plugin only. Scripts folder copied into the staged bundle, relative to the script folder.", + "launcher": "BundleCustomization plugin only. Optional launcher batch file settings.", + "projects": "BundleCustomization plugin only. Project-specific appsettings rewrite settings." + } + } +} diff --git a/utils/Release-ToGitHub/Release-ToGitHub.bat b/utils/Release-ToGitHub/Release-ToGitHub.bat deleted file mode 100644 index 454936c..0000000 --- a/utils/Release-ToGitHub/Release-ToGitHub.bat +++ /dev/null @@ -1,9 +0,0 @@ -@echo off - -REM Change directory to the location of the script -cd /d %~dp0 - -REM Run GitHub release script -powershell -ExecutionPolicy Bypass -File "%~dp0Release-ToGitHub.ps1" - -pause diff --git a/utils/Release-ToGitHub/Release-ToGitHub.ps1 b/utils/Release-ToGitHub/Release-ToGitHub.ps1 deleted file mode 100644 index 69a23b6..0000000 --- a/utils/Release-ToGitHub/Release-ToGitHub.ps1 +++ /dev/null @@ -1,695 +0,0 @@ -<# -.SYNOPSIS - Automated GitHub release script for MaksIT.UScheduler. - -.DESCRIPTION - Creates a GitHub release by performing the following steps: - - Pre-flight checks: - - Detects current branch (main or dev) - - On main: requires clean working directory; on dev: uncommitted changes allowed - - Reads version from .csproj (source of truth) - - On main: requires matching tag (vX.Y.Z format) - - Ensures version consistency with CHANGELOG.md - - Confirms GitHub CLI authentication via GH_TOKEN (main branch only) - - Test execution: - - Runs all unit tests via Run-Tests.ps1 - - Aborts release if any tests fail - - Displays coverage summary (line, branch, method) - - Build and release: - - Publishes the .NET project in Release configuration - - Copies Scripts folder into the release - - Creates a versioned ZIP archive - - Extracts release notes from CHANGELOG.md - - Pushes tag to remote if not already present (main branch only) - - Creates (or recreates) the GitHub release with assets (main branch only) - - Branch-based behavior (configurable in scriptsettings.json): - - On dev branch: Local build only, no tag required, uncommitted changes allowed - - On release branch: Full GitHub release, tag required, clean working directory required - - On other branches: Blocked - -.NOTES - File: Release-ToGitHub.ps1 - Author: Maksym Sadovnychyy (MAKS-IT) - Requires: dotnet, git, gh (GitHub CLI - required on main branch only) - - Configuration is loaded from scriptsettings.json in the same directory. - Set the GitHub token in an environment variable specified by github.tokenEnvVar. - -.EXAMPLE - .\Release-ToGitHub.ps1 - - Runs the release process using settings from scriptsettings.json. - On dev branch: creates local build (no tag needed). - On main branch: publishes to GitHub (tag required). - -.EXAMPLE - # Recommended workflow: - # 1. On dev branch: Update version in .csproj and CHANGELOG.md - # 2. Commit changes - # 3. Run: .\Release-ToGitHub.ps1 - # (creates local build for testing - no tag needed) - # 4. Test the build - # 5. Merge to main: git checkout main && git merge dev - # 6. Create tag: git tag v1.0.1 - # 7. Run: .\Release-ToGitHub.ps1 - # (publishes to GitHub) -#> - -# No parameters - behavior is controlled by current branch (configured in scriptsettings.json): -# - dev branch -> Local build only (no tag required, uncommitted changes allowed) -# - release branch -> Full release to GitHub (tag required, clean working directory) - -# Load settings from scriptsettings.json -$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path -$settingsPath = Join-Path $scriptDir "scriptsettings.json" - -if (-not (Test-Path $settingsPath)) { - Write-Error "Settings file not found: $settingsPath" - exit 1 -} - -$settings = Get-Content $settingsPath -Raw | ConvertFrom-Json - -# Import TestRunner module -$modulePath = Join-Path (Split-Path $scriptDir -Parent) "TestRunner.psm1" -if (-not (Test-Path $modulePath)) { - Write-Error "TestRunner module not found at: $modulePath" - exit 1 -} -Import-Module $modulePath -Force - -# Set GH_TOKEN from custom environment variable for GitHub CLI authentication -$tokenEnvVar = $settings.github.tokenEnvVar -$env:GH_TOKEN = [System.Environment]::GetEnvironmentVariable($tokenEnvVar) - -# Paths from settings (resolve relative to script directory) -$csprojPaths = @() -if ($settings.paths.csprojPath -is [System.Collections.IEnumerable] -and -not ($settings.paths.csprojPath -is [string])) { - foreach ($path in $settings.paths.csprojPath) { - $csprojPaths += [System.IO.Path]::GetFullPath((Join-Path $scriptDir $path)) - } -} -else { - $csprojPaths += [System.IO.Path]::GetFullPath((Join-Path $scriptDir $settings.paths.csprojPath)) -} - -$stagingDir = [System.IO.Path]::GetFullPath((Join-Path $scriptDir $settings.paths.stagingDir)) -$releaseDir = [System.IO.Path]::GetFullPath((Join-Path $scriptDir $settings.paths.releaseDir)) -$changelogPath = [System.IO.Path]::GetFullPath((Join-Path $scriptDir $settings.paths.changelogPath)) -$scriptsPath = [System.IO.Path]::GetFullPath((Join-Path $scriptDir $settings.paths.scriptsPath)) -$testProjectPath = [System.IO.Path]::GetFullPath((Join-Path $scriptDir $settings.paths.testProject)) - -# Release naming patterns -$zipNamePattern = $settings.release.zipNamePattern -$releaseTitlePattern = $settings.release.releaseTitlePattern - -# Branch configuration -$releaseBranch = $settings.branches.release -$devBranch = $settings.branches.dev - -# Project configuration (avoid hardcoding project names) -$projectsSettings = $settings.projects -$scheduleManagerCsprojEndsWith = $projectsSettings.scheduleManagerCsprojEndsWith -$uschedulerCsprojEndsWith = $projectsSettings.uschedulerCsprojEndsWith -$scheduleManagerAppSettingsFile = $projectsSettings.scheduleManagerAppSettingsFile -$uschedulerAppSettingsFile = $projectsSettings.uschedulerAppSettingsFile -$scheduleManagerServiceBinPath = $projectsSettings.scheduleManagerServiceBinPath -$uschedulerLogDir = $projectsSettings.uschedulerLogDir -$scriptsRelativeToExe = $projectsSettings.scriptsRelativeToExe - -# Helper: ensure required commands exist -function Assert-Command { - param([string]$cmd) - - if (-not (Get-Command $cmd -ErrorAction SilentlyContinue)) { - Write-Error "Required command '$cmd' is missing. Aborting." - exit 1 - } -} - -# Helper: extract a csproj property (first match) -function Get-CsprojPropertyValue { - param( - [Parameter(Mandatory=$true)][xml]$csproj, - [Parameter(Mandatory=$true)][string]$propertyName - ) - - $propNode = $csproj.Project.PropertyGroup | - Where-Object { $_.$propertyName } | - Select-Object -First 1 - - if ($propNode) { - return $propNode.$propertyName - } - - return $null -} - -# Helper: resolve output assembly name for published exe -function Resolve-ProjectExeName { - param( - [Parameter(Mandatory=$true)][string]$projPath - ) - - [xml]$csproj = Get-Content $projPath - $assemblyName = Get-CsprojPropertyValue -csproj $csproj -propertyName "AssemblyName" - if ($assemblyName) { - return $assemblyName - } - - return [System.IO.Path]::GetFileNameWithoutExtension($projPath) -} - -# Helper: find csproj by configured suffix -function Find-CsprojByEndsWith { - param( - [Parameter(Mandatory=$true)][string[]]$paths, - [Parameter(Mandatory=$true)][string]$endsWith - ) - - if (-not $endsWith) { - return $null - } - - return $paths | Where-Object { $_ -like "*$endsWith" } | Select-Object -First 1 -} - -Assert-Command dotnet -Assert-Command git -# gh command check deferred until after branch detection (only needed on main branch) - -Write-Host "" -Write-Host "==================================================" -ForegroundColor Cyan -Write-Host "RELEASE BUILD" -ForegroundColor Cyan -Write-Host "==================================================" -ForegroundColor Cyan -Write-Host "" - -# ============================================================================== -# PRE-FLIGHT CHECKS -# ============================================================================== - -# 1. Detect current branch and determine release mode -Write-Host "Detecting current branch..." -ForegroundColor Gray -$currentBranch = git rev-parse --abbrev-ref HEAD 2>$null -if ($LASTEXITCODE -ne 0 -or -not $currentBranch) { - Write-Error "Could not determine current branch." - exit 1 -} - -$currentBranch = $currentBranch.Trim() -Write-Host " Branch: $currentBranch" -ForegroundColor Green - -$isDevBranch = $currentBranch -eq $devBranch -$isReleaseBranch = $currentBranch -eq $releaseBranch - -if (-not $isDevBranch -and -not $isReleaseBranch) { - Write-Error "Releases can only be created from '$releaseBranch' or '$devBranch' branches. Current branch: $currentBranch" - exit 1 -} - -if ($isDevBranch) { - Write-Host " Dev branch ($devBranch) - local build only (no GitHub release)." -ForegroundColor Yellow -} -else { - Write-Host " Release branch ($releaseBranch) - will publish to GitHub." -ForegroundColor Cyan - Assert-Command gh -} - -# 2. Check for uncommitted changes (required on main, allowed on dev) -$gitStatus = git status --porcelain 2>$null -if ($gitStatus) { - if ($isReleaseBranch) { - Write-Error "Working directory has uncommitted changes. Commit or stash them before releasing." - Write-Host "" - Write-Host "Uncommitted files:" -ForegroundColor Yellow - $gitStatus | ForEach-Object { Write-Host " $_" -ForegroundColor Yellow } - exit 1 - } - else { - Write-Host " Uncommitted changes detected (allowed on dev branch)." -ForegroundColor Yellow - } -} else { - Write-Host " Working directory is clean." -ForegroundColor Green -} - -# 3. Get version from csproj (source of truth) - -Write-Host "Reading version(s) from csproj(s)..." -ForegroundColor Gray -$projectVersions = @{} -foreach ($projPath in $csprojPaths) { - if (-not (Test-Path $projPath)) { - Write-Error "Csproj not found at: $projPath" - exit 1 - } - - [xml]$csproj = Get-Content $projPath - $version = Get-CsprojPropertyValue -csproj $csproj -propertyName "Version" - - if (-not $version) { - Write-Error "Version not found in $projPath" - exit 1 - } - - $projectVersions[$projPath] = $version - Write-Host " $([System.IO.Path]::GetFileName($projPath)): $version" -ForegroundColor Green -} - -# Use the first project's version as the main version for tag/release -$version = $projectVersions[$csprojPaths[0]] - -# 4. Handle tag based on branch -if ($isReleaseBranch) { - # Main branch: tag is required and must match version - Write-Host "Checking for tag on current commit..." -ForegroundColor Gray - $tag = git describe --tags --exact-match HEAD 2>$null - if ($LASTEXITCODE -ne 0 -or -not $tag) { - Write-Error "No tag found on current commit. Create a tag: git tag v$version" - exit 1 - } - - $tag = $tag.Trim() - - if ($tag -notmatch '^v(\d+\.\d+\.\d+)$') { - Write-Error "Tag '$tag' does not match expected format 'vX.Y.Z' (e.g., v$version)." - exit 1 - } - - $tagVersion = $Matches[1] - - if ($tagVersion -ne $version) { - Write-Error "Tag version ($tagVersion) does not match csproj version ($version)." - Write-Host " Either update the tag or the csproj version." -ForegroundColor Yellow - exit 1 - } - - Write-Host " Tag found: $tag (matches csproj)" -ForegroundColor Green -} -else { - # Dev branch: no tag required, use version from csproj - $tag = "v$version" - Write-Host " Using version from csproj (no tag required on dev)." -ForegroundColor Gray -} - -# 5. Verify CHANGELOG.md has matching version entry -Write-Host "Verifying CHANGELOG.md..." -ForegroundColor Gray -if (-not (Test-Path $changelogPath)) { - Write-Error "CHANGELOG.md not found at: $changelogPath" - exit 1 -} - -$changelog = Get-Content $changelogPath -Raw - -if ($changelog -notmatch '##\s+v(\d+\.\d+\.\d+)') { - Write-Error "No version entry found in CHANGELOG.md" - exit 1 -} - -$changelogVersion = $Matches[1] - -if ($changelogVersion -ne $version) { - Write-Error "Csproj version ($version) does not match latest CHANGELOG.md version ($changelogVersion)." - Write-Host " Update CHANGELOG.md or the csproj version." -ForegroundColor Yellow - exit 1 -} - -Write-Host " CHANGELOG.md version matches: v$changelogVersion" -ForegroundColor Green - -# 6. Check GitHub authentication (skip for local-only builds) -if (-not $isDevBranch) { - Write-Host "Checking GitHub authentication..." -ForegroundColor Gray - if (-not $env:GH_TOKEN) { - Write-Error "GH_TOKEN environment variable is not set. Set $tokenEnvVar and rerun." - exit 1 - } - - $authTest = gh api user 2>$null - if ($LASTEXITCODE -ne 0 -or -not $authTest) { - Write-Error "GitHub CLI authentication failed. GH_TOKEN may be invalid or missing repo scope." - exit 1 - } - Write-Host " GitHub CLI authenticated." -ForegroundColor Green -} -else { - Write-Host "Skipping GitHub authentication (local-only mode)." -ForegroundColor Gray -} - -Write-Host "" -Write-Host "All pre-flight checks passed!" -ForegroundColor Green -Write-Host "" - -# ============================================================================== -# RUN TESTS -# ============================================================================== - -Write-Host "Running tests..." -ForegroundColor Cyan - -# Run tests using TestRunner module -$testResult = Invoke-TestsWithCoverage -TestProjectPath $testProjectPath -Silent - -if (-not $testResult.Success) { - Write-Error "Tests failed. Release aborted." - Write-Host " Error: $($testResult.Error)" -ForegroundColor Red - exit 1 -} - -Write-Host " All tests passed!" -ForegroundColor Green -Write-Host " Line Coverage: $($testResult.LineRate)%" -ForegroundColor Gray -Write-Host " Branch Coverage: $($testResult.BranchRate)%" -ForegroundColor Gray -Write-Host " Method Coverage: $($testResult.MethodRate)%" -ForegroundColor Gray -Write-Host "" - -# ============================================================================== -# BUILD AND RELEASE -# ============================================================================== - -# 7. Prepare staging directory -Write-Host "Preparing staging directory..." -ForegroundColor Cyan -if (Test-Path $stagingDir) { - Remove-Item $stagingDir -Recurse -Force -} - -New-Item -ItemType Directory -Path $stagingDir | Out-Null - -$binDir = Join-Path $stagingDir "bin" - -# 8. Publish the project to staging/bin - -Write-Host "Publishing projects to bin folder..." -ForegroundColor Cyan -$publishSuccess = $true -$publishedProjects = @() - -foreach ($projPath in $csprojPaths) { - $projName = [System.IO.Path]::GetFileNameWithoutExtension($projPath) - $projBinDir = Join-Path $binDir $projName - - dotnet publish $projPath -c Release -o $projBinDir - if ($LASTEXITCODE -ne 0) { - Write-Error "dotnet publish failed for $projName." - $publishSuccess = $false - } - else { - $exeBaseName = Resolve-ProjectExeName -projPath $projPath - $publishedProjects += [PSCustomObject]@{ - ProjPath = $projPath - ProjName = $projName - BinDir = $projBinDir - ExeBaseName = $exeBaseName - } - - Write-Host " Published $projName successfully to: $projBinDir" -ForegroundColor Green - } -} - -if (-not $publishSuccess) { - exit 1 -} - -# 9. Copy Scripts folder to staging -Write-Host "Copying Scripts folder..." -ForegroundColor Cyan -if (-not (Test-Path $scriptsPath)) { - Write-Error "Scripts folder not found at: $scriptsPath" - exit 1 -} - -$scriptsDestination = Join-Path $stagingDir "Scripts" -Copy-Item -Path $scriptsPath -Destination $scriptsDestination -Recurse -Write-Host " Scripts copied to: $scriptsDestination" -ForegroundColor Green - -Write-Host "Updating ScheduleManager appsettings with UScheduler path..." -ForegroundColor Cyan - -# 10. Update appsettings.json with scripts in disabled state -# Dynamically locate ScheduleManager appsettings based on settings.projects.scheduleManagerCsprojEndsWith -$scheduleManagerCsprojPath = Find-CsprojByEndsWith -paths $csprojPaths -endsWith $scheduleManagerCsprojEndsWith -if ($scheduleManagerCsprojPath) { - $scheduleManagerProjName = [System.IO.Path]::GetFileNameWithoutExtension($scheduleManagerCsprojPath) - $scheduleManagerBinDir = Join-Path $binDir $scheduleManagerProjName - $scheduleManagerAppSettingsPath = Join-Path $scheduleManagerBinDir $scheduleManagerAppSettingsFile - - if (Test-Path $scheduleManagerAppSettingsPath) { - $smAppSettings = Get-Content $scheduleManagerAppSettingsPath -Raw | ConvertFrom-Json - if ($smAppSettings.USchedulerSettings) { - $smAppSettings.USchedulerSettings.ServiceBinPath = $scheduleManagerServiceBinPath - $jsonOutput = $smAppSettings | ConvertTo-Json -Depth 10 - Set-Content -Path $scheduleManagerAppSettingsPath -Value $jsonOutput -Encoding UTF8 - Write-Host " Updated ServiceBinPath in ScheduleManager appsettings" -ForegroundColor Green - } - else { - Write-Host " Warning: USchedulerSettings section not found in ScheduleManager appsettings" -ForegroundColor Yellow - } - } - else { - Write-Host " Warning: $scheduleManagerAppSettingsFile not found in $scheduleManagerProjName bin folder" -ForegroundColor Yellow - } -} -else { - Write-Host " Warning: ScheduleManager csproj not found in csprojPaths array" -ForegroundColor Yellow -} - -Write-Host "Updating UScheduler appsettings with new LogDir bundled scripts paths..." -ForegroundColor Cyan - -# Resolve UScheduler csproj by configured suffix (avoid hardcoded ScheduleManager exclusion) -$uschedulerCsprojPath = Find-CsprojByEndsWith -paths $csprojPaths -endsWith $uschedulerCsprojEndsWith -if ($uschedulerCsprojPath) { - $uschedulerProjName = [System.IO.Path]::GetFileNameWithoutExtension($uschedulerCsprojPath) - $uschedulerBinDir = Join-Path $binDir $uschedulerProjName - $appSettingsPath = Join-Path $uschedulerBinDir $uschedulerAppSettingsFile - - if (Test-Path $appSettingsPath) { - $appSettings = Get-Content $appSettingsPath -Raw | ConvertFrom-Json - - # Update LogDir for release - if ($appSettings.Configuration) { - $appSettings.Configuration.LogDir = $uschedulerLogDir - Write-Host " Updated LogDir in UScheduler appsettings" -ForegroundColor Green - } - else { - Write-Host " Warning: Configuration section not found in UScheduler appsettings" -ForegroundColor Yellow - } - - # Find all .ps1 files in Scripts folder (exclude utility scripts in subfolders named "Utilities") - $psScripts = Get-ChildItem -Path $scriptsDestination -Filter "*.ps1" -Recurse | - Where-Object { $_.Directory.Name -ne "Utilities" } | - ForEach-Object { - $relativePath = $_.FullName.Substring($scriptsDestination.Length + 1).Replace('/', '\') - $scriptPath = "$scriptsRelativeToExe\$relativePath" - [PSCustomObject]@{ - Path = $scriptPath - IsSigned = $false - Disabled = $true - } - } - - # Add scripts to Powershell configuration - if ($psScripts) { - if (-not $appSettings.Configuration) { - $appSettings | Add-Member -MemberType NoteProperty -Name "Configuration" -Value ([PSCustomObject]@{}) - } - - $appSettings.Configuration.Powershell = @($psScripts) - $jsonOutput = $appSettings | ConvertTo-Json -Depth 10 - Set-Content -Path $appSettingsPath -Value $jsonOutput -Encoding UTF8 - Write-Host " Added $($psScripts.Count) PowerShell script(s) to appsettings (disabled)" -ForegroundColor Green - $psScripts | ForEach-Object { Write-Host " - $($_.Path)" -ForegroundColor Gray } - } - } - else { - Write-Host " Warning: $uschedulerAppSettingsFile not found in $uschedulerProjName bin folder" -ForegroundColor Yellow - } -} -else { - Write-Host " Warning: UScheduler csproj not found in csprojPaths array" -ForegroundColor Yellow -} - -# 11. Create launcher batch file (if enabled) -if ($settings.launcher -and $settings.launcher.enabled) { - Write-Host "Creating launcher batch file..." -ForegroundColor Cyan - - $launcherFileName = $settings.launcher.fileName - $targetProject = $settings.launcher.targetProject - - # Determine which project to launch - $targetCsprojPath = $null - $targetExeName = $null - $targetProjName = $null - - if ($targetProject -eq "scheduleManager") { - $targetCsprojPath = Find-CsprojByEndsWith -paths $csprojPaths -endsWith $scheduleManagerCsprojEndsWith - } - elseif ($targetProject -eq "uscheduler") { - $targetCsprojPath = Find-CsprojByEndsWith -paths $csprojPaths -endsWith $uschedulerCsprojEndsWith - } - else { - Write-Host " Warning: Unknown targetProject '$targetProject' in launcher settings" -ForegroundColor Yellow - } - - if ($targetCsprojPath) { - $targetProjName = [System.IO.Path]::GetFileNameWithoutExtension($targetCsprojPath) - $targetExeName = Resolve-ProjectExeName -projPath $targetCsprojPath - - $batPath = Join-Path $stagingDir $launcherFileName - $exePath = "%~dp0bin\$targetProjName\$targetExeName.exe" - - $batContent = @" -@echo off -start "" "$exePath" -"@ - - Set-Content -Path $batPath -Value $batContent -Encoding ASCII - Write-Host " Created launcher: $launcherFileName -> $exePath" -ForegroundColor Green - } - else { - Write-Host " Warning: Could not find target project for launcher" -ForegroundColor Yellow - } -} -else { - Write-Host "Skipping launcher batch file creation (disabled in settings)." -ForegroundColor Gray -} - -Write-Host "" - -# 12. Prepare release directory -if (!(Test-Path $releaseDir)) { - New-Item -ItemType Directory -Path $releaseDir | Out-Null -} - -# 13. Create zip file -$zipName = $zipNamePattern -replace '\{version\}', $version -$zipPath = Join-Path $releaseDir $zipName - -if (Test-Path $zipPath) { - Remove-Item $zipPath -Force -} - -Write-Host "Creating archive $zipName..." -ForegroundColor Cyan -Compress-Archive -Path "$stagingDir\*" -DestinationPath $zipPath -Force - -if (-not (Test-Path $zipPath)) { - Write-Error "Failed to create archive $zipPath" - exit 1 -} - -Write-Host " Archive created: $zipPath" -ForegroundColor Green - - -# 14. Extract release notes from CHANGELOG.md -Write-Host "Extracting release notes..." -ForegroundColor Cyan -$pattern = "(?ms)^##\s+v$([regex]::Escape($version))\b.*?(?=^##\s+v\d+\.\d+\.\d+|\Z)" -$match = [regex]::Match($changelog, $pattern) - -if (-not $match.Success) { - Write-Error "Changelog entry for version $version not found." - exit 1 -} - -$releaseNotes = $match.Value.Trim() -Write-Host " Release notes extracted." -ForegroundColor Green - -# 15. Get repository info -$remoteUrl = git config --get remote.origin.url -if ($LASTEXITCODE -ne 0 -or -not $remoteUrl) { - Write-Error "Could not determine git remote origin URL." - exit 1 -} - -if ($remoteUrl -match "[:/](?[^/]+)/(?[^/.]+)(\.git)?$") { - $owner = $matches['owner'] - $repoName = $matches['repo'] - $repo = "$owner/$repoName" -} else { - Write-Error "Could not parse GitHub repo from remote URL: $remoteUrl" - exit 1 -} - -$releaseName = $releaseTitlePattern -replace '\{version\}', $version - -Write-Host "" -Write-Host "Release Summary:" -ForegroundColor Cyan -Write-Host " Repository: $repo" -ForegroundColor White -Write-Host " Tag: $tag" -ForegroundColor White -Write-Host " Title: $releaseName" -ForegroundColor White -Write-Host "" - -# 16. Check if tag is pushed to remote (skip on dev branch) -if (-not $isDevBranch) { - Write-Host "Verifying tag is pushed to remote..." -ForegroundColor Cyan - $remoteTag = git ls-remote --tags origin $tag 2>$null - if (-not $remoteTag) { - Write-Host " Tag $tag not found on remote. Pushing..." -ForegroundColor Yellow - git push origin $tag - if ($LASTEXITCODE -ne 0) { - Write-Error "Failed to push tag $tag to remote." - exit 1 - } - Write-Host " Tag pushed successfully." -ForegroundColor Green - } - else { - Write-Host " Tag exists on remote." -ForegroundColor Green - } - - # 17. Create or update GitHub release - Write-Host "Creating GitHub release..." -ForegroundColor Cyan - - # Check if release already exists - gh release view $tag --repo $repo 2>$null - if ($LASTEXITCODE -eq 0) { - Write-Host " Release $tag already exists. Deleting..." -ForegroundColor Yellow - gh release delete $tag --repo $repo --yes - if ($LASTEXITCODE -ne 0) { - Write-Error "Failed to delete existing release $tag." - exit 1 - } - } - - # Create new release using existing tag - $ghArgs = @( - "release", "create", $tag, $zipPath - "--repo", $repo - "--title", $releaseName - "--notes", $releaseNotes - ) - & gh @ghArgs - - if ($LASTEXITCODE -ne 0) { - Write-Error "Failed to create GitHub release for tag $tag." - exit 1 - } - - Write-Host " GitHub release created successfully." -ForegroundColor Green -} -else { - Write-Host "Skipping GitHub release (dev branch)." -ForegroundColor Yellow -} - -# 18. Cleanup -if (Test-Path $stagingDir) { - Remove-Item $stagingDir -Recurse -Force - Write-Host " Cleaned up staging directory." -ForegroundColor Gray -} - -Write-Host "" -Write-Host "==================================================" -ForegroundColor Green -if ($isDevBranch) { - Write-Host "DEV BUILD COMPLETE" -ForegroundColor Green -} -else { - Write-Host "RELEASE COMPLETE" -ForegroundColor Green -} -Write-Host "==================================================" -ForegroundColor Green -Write-Host "" -if (-not $isDevBranch) { - Write-Host "Release URL: https://github.com/$repo/releases/tag/$tag" -ForegroundColor Cyan -} -Write-Host "Artifacts location: $releaseDir" -ForegroundColor Gray -if ($isDevBranch) { - Write-Host "" - Write-Host "To publish to GitHub, merge to main, tag, and run the script again:" -ForegroundColor Yellow - Write-Host " git checkout main" -ForegroundColor Yellow - Write-Host " git merge dev" -ForegroundColor Yellow - Write-Host " git tag v$version" -ForegroundColor Yellow - Write-Host " .\Release-ToGitHub.ps1" -ForegroundColor Yellow -} -Write-Host "" diff --git a/utils/Release-ToGitHub/scriptsettings.json b/utils/Release-ToGitHub/scriptsettings.json deleted file mode 100644 index 34f7a5f..0000000 --- a/utils/Release-ToGitHub/scriptsettings.json +++ /dev/null @@ -1,57 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft-07/schema", - "title": "Release to GitHub Script Settings", - "description": "Configuration file for Release-ToGitHub.ps1 script (automated GitHub release creation)", - "github": { - "tokenEnvVar": "GITHUB_MAKS_IT_COM" - }, - "branches": { - "release": "main", - "dev": "dev" - }, - "paths": { - "csprojPath": [ - "..\\..\\src\\MaksIT.UScheduler\\MaksIT.UScheduler.csproj", - "..\\..\\src\\MaksIT.UScheduler.ScheduleManager\\MaksIT.UScheduler.ScheduleManager.csproj" - ], - "stagingDir": "..\\..\\staging", - "releaseDir": "..\\..\\release", - "changelogPath": "..\\..\\CHANGELOG.md", - "scriptsPath": "..\\..\\src\\Scripts", - "testProject": "..\\..\\src\\MaksIT.UScheduler.Tests" - }, - "release": { - "zipNamePattern": "maksit.uscheduler-{version}.zip", - "releaseTitlePattern": "Release {version}" - }, - "launcher": { - "enabled": true, - "fileName": "Start-ScheduleManager.bat", - "targetProject": "scheduleManager" - }, - "projects": { - "scheduleManagerCsprojEndsWith": "MaksIT.UScheduler.ScheduleManager.csproj", - "uschedulerCsprojEndsWith": "MaksIT.UScheduler.csproj", - "scheduleManagerAppSettingsFile": "appsettings.json", - "uschedulerAppSettingsFile": "appsettings.json", - "scheduleManagerServiceBinPath": "..\\MaksIT.UScheduler\\", - "uschedulerLogDir": "..\\..\\Logs", - "scriptsRelativeToExe": "..\\..\\Scripts" - }, - "_comments": { - "projects": { - "scheduleManagerCsprojEndsWith": "Used to detect ScheduleManager csproj from csprojPath list", - "uschedulerCsprojEndsWith": "Used to detect UScheduler csproj from csprojPath list", - "scheduleManagerAppSettingsFile": "Config file name inside published output for ScheduleManager", - "uschedulerAppSettingsFile": "Config file name inside published output for UScheduler", - "scheduleManagerServiceBinPath": "Value written into USchedulerSettings.ServiceBinPath in ScheduleManager config", - "uschedulerLogDir": "Value written into Configuration.LogDir in UScheduler config", - "scriptsRelativeToExe": "Scripts base path relative to executable folder (used for appsettings script list)" - }, - "shortcut": { - "enabled": "If true, creates a .lnk in staging root", - "projectRole": "Which project to point the shortcut to (ScheduleManager or UScheduler)", - "fileName": "Shortcut file name in staging root" - } - } -} diff --git a/utils/ScriptConfig.psm1 b/utils/ScriptConfig.psm1 new file mode 100644 index 0000000..738cd5c --- /dev/null +++ b/utils/ScriptConfig.psm1 @@ -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 diff --git a/utils/TestRunner.psm1 b/utils/TestRunner.psm1 index 864fcb5..f382b24 100644 --- a/utils/TestRunner.psm1 +++ b/utils/TestRunner.psm1 @@ -1,3 +1,6 @@ +#requires -Version 7.0 +#requires -PSEdition Core + <# .SYNOPSIS PowerShell module for running tests with code coverage. @@ -8,9 +11,40 @@ .NOTES Author: MaksIT - Usage: Import-Module .\TestRunner.psm1 + 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 @@ -22,6 +56,9 @@ function Invoke-TestsWithCoverage { .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. @@ -38,7 +75,7 @@ function Invoke-TestsWithCoverage { .EXAMPLE $result = Invoke-TestsWithCoverage -TestProjectPath ".\Tests" - if ($result.Success) { Write-Host "Line coverage: $($result.LineRate)%" } + if ($result.Success) { Write-TestRunnerLogInternal -Level "INFO" -Message "Line coverage: $($result.LineRate)%" } #> param( [Parameter(Mandatory = $true)] @@ -46,6 +83,8 @@ function Invoke-TestsWithCoverage { [switch]$Silent, + [string]$ResultsDirectory, + [switch]$KeepResults ) @@ -60,7 +99,12 @@ function Invoke-TestsWithCoverage { } } - $ResultsDir = Join-Path $TestProjectDir "TestResults" + if ([string]::IsNullOrWhiteSpace($ResultsDirectory)) { + $ResultsDir = Join-Path $TestProjectDir "TestResults" + } + else { + $ResultsDir = [System.IO.Path]::GetFullPath($ResultsDirectory) + } # Clean previous results if (Test-Path $ResultsDir) { @@ -68,8 +112,8 @@ function Invoke-TestsWithCoverage { } if (-not $Silent) { - Write-Host "Running tests with code coverage..." -ForegroundColor Cyan - Write-Host " Test Project: $TestProjectDir" -ForegroundColor Gray + Write-TestRunnerLogInternal -Level "STEP" -Message "Running tests with code coverage..." + Write-TestRunnerLogInternal -Level "INFO" -Message "Test Project: $TestProjectDir" } # Run tests with coverage collection @@ -111,8 +155,8 @@ function Invoke-TestsWithCoverage { } if (-not $Silent) { - Write-Host "Coverage file found: $($CoverageFile.FullName)" -ForegroundColor Green - Write-Host "Parsing coverage data..." -ForegroundColor Cyan + Write-TestRunnerLogInternal -Level "OK" -Message "Coverage file found: $($CoverageFile.FullName)" + Write-TestRunnerLogInternal -Level "STEP" -Message "Parsing coverage data..." } # Parse coverage data from Cobertura XML diff --git a/utils/Update-RepoUtils/Update-RepoUtils.bat b/utils/Update-RepoUtils/Update-RepoUtils.bat new file mode 100644 index 0000000..8ff94ac --- /dev/null +++ b/utils/Update-RepoUtils/Update-RepoUtils.bat @@ -0,0 +1,3 @@ +@echo off +pwsh -NoProfile -ExecutionPolicy Bypass -File "%~dp0Update-RepoUtils.ps1" +pause diff --git a/utils/Update-RepoUtils/Update-RepoUtils.ps1 b/utils/Update-RepoUtils/Update-RepoUtils.ps1 new file mode 100644 index 0000000..0e8a20d --- /dev/null +++ b/utils/Update-RepoUtils/Update-RepoUtils.ps1 @@ -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 diff --git a/utils/Update-RepoUtils/scriptsettings.json b/utils/Update-RepoUtils/scriptsettings.json new file mode 100644 index 0000000..9d55393 --- /dev/null +++ b/utils/Update-RepoUtils/scriptsettings.json @@ -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" + ] + } +}