diff --git a/.github/scripts/System.psm1 b/.github/scripts/System.psm1 new file mode 100644 index 0000000..c9cafd5 --- /dev/null +++ b/.github/scripts/System.psm1 @@ -0,0 +1,1500 @@ +#----------------------------------------------------------------------- +# Add-Prefix [-String []] +# +# Example: .\Add-Prefix -String ello -Add H +# Result: Hello +#----------------------------------------------------------------------- +function Add-Prefix +{ + param ( + [Parameter(Mandatory=$true,ValueFromPipelineByPropertyName=$true)] + [string]$String = $(throw '-String is a required parameter.'), + [Parameter(Mandatory=$true,ValueFromPipelineByPropertyName=$true)] + [string]$Add = $(throw '-Add is a required parameter.') + ) + Write-Verbose "Add-Prefix -String $String -Add $Add" + [string]$ReturnValue = $String + if (-not (Compare-IsFirst -String $String -BeginsWith $Add)) + { + $ReturnValue = ($Add + $ReturnValue) + Write-Verbose "[Success] 1 items affected. $(Get-CurrentFile) at $(Get-CurrentLine)." + } + else + { + Write-Verbose "[OK] 0 items affected. $(Get-CurrentFile) at $(Get-CurrentLine). -String $String already has prefix of -Add $Add" + } + return $ReturnValue +} +export-modulemember -function Add-Prefix + +#----------------------------------------------------------------------- +# Add-Suffix [-String []] +# +# Example: .\Add-Suffix -String Hell -Add o +# Result: Hello +#----------------------------------------------------------------------- +function Add-Suffix +{ + param ( + [Parameter(Mandatory=$true,ValueFromPipelineByPropertyName=$true)] + [string]$String = $(throw '-String is a required parameter.'), + [Parameter(Mandatory=$true,ValueFromPipelineByPropertyName=$true)] + [string]$Add = $(throw '-Add is a required parameter.') + ) + Write-Verbose "Add-Suffix -String $String -Add $Add" + [string]$ReturnValue = $String + if (-not (Compare-IsLast -String $String -EndsWith $Add)) + { + $ReturnValue = ($String + $Add) + Write-Verbose "[Success] 1 items affected. $(Get-CurrentFile) at $(Get-CurrentLine)." + } + else + { + Write-Verbose "[OK] 0 items affected. $(Get-CurrentFile) at $(Get-CurrentLine). -String $String already has suffix of -Add $Add" + } + + return $ReturnValue +} +export-modulemember -function Add-Suffix + +#----------------------------------------------------------------------- +# Compare-IsFirst [-String []] +# +# Example: .\Compare-IsFirst -String Hell -EndsWith H +# Result: false +# Example: .\Compare-IsFirst -String Hello -Add H +# Result: Hello +#----------------------------------------------------------------------- +function Compare-IsFirst +{ + param ( + [Parameter(Mandatory=$true,ValueFromPipelineByPropertyName=$true)] + [string]$String = $(throw '-String is a required parameter.'), + [Parameter(Mandatory=$true,ValueFromPipelineByPropertyName=$true)] + [string]$BeginsWith = $(throw '-BeginsWith is a required parameter.') + ) + + Write-Verbose "Compare-IsFirst -String $String -EndsWith $EndsWith" + [Boolean]$ReturnValue = $false + if($BeginsWith.Length -lt $String.Length) + { + $StringBeginning = $String.SubString(0, $BeginsWith.Length).ToLower() + if ($StringBeginning.ToLower().Equals($BeginsWith.ToLower())) + { + $ReturnValue = $true + } + Write-Verbose "[Success] 1 items affected. $(Get-CurrentFile) at $(Get-CurrentLine)." + } + else + { + Write-Verbose "[OK] 0 items affected. $(Get-CurrentFile) at $(Get-CurrentLine)." + } + return $ReturnValue +} +export-modulemember -function Compare-IsFirst + +#----------------------------------------------------------------------- +# Compare-IsLast [-String []] +# +# Example: .\Compare-IsLast -String Hell -EndsWith H +# Result: false +# Example: .\Compare-IsLast -String Hello -Add H +# Result: Hello +#----------------------------------------------------------------------- +function Compare-IsLast +{ + param ( + [Parameter(Mandatory=$true,ValueFromPipelineByPropertyName=$true)] + [string]$String = $(throw '-String is a required parameter.'), + [Parameter(Mandatory=$true,ValueFromPipelineByPropertyName=$true)] + [string]$EndsWith = $(throw '-EndsWith is a required parameter.') + ) + Write-Verbose "Compare-IsLast -String $String -EndsWith $EndsWith" + [Boolean]$ReturnValue = $false + if($EndsWith.Length -lt $String.Length) + { + $StringEnding = $String.SubString(($String.Length - $EndsWith.Length), $EndsWith.Length) + if ($StringEnding.ToLower().Equals($EndsWith.ToLower())) + { + $ReturnValue = $true + } + Write-Verbose "[Success] 1 items affected. $(Get-CurrentFile) at $(Get-CurrentLine)." + } + else + { + Write-Verbose "[OK] 0 items affected. $(Get-CurrentFile) at $(Get-CurrentLine)." + } + + return $ReturnValue +} +export-modulemember -function Compare-IsLast + +#----------------------------------------------------------------------- +# Compress-Path [-Path []] [-File []] +# +# Example: .\Compress-Path \\source\path \\destination\path\file.zip +#----------------------------------------------------------------------- +function Compress-Path +{ + param ( + [Parameter(Mandatory=$true,ValueFromPipelineByPropertyName=$true)] + [string]$Path=$(throw '-Path is a required parameter.'), + [Parameter(Mandatory=$true,ValueFromPipelineByPropertyName=$true)] + [string]$File=$(throw '-File is a required parameter.') + ) + Write-Verbose "Compress-Path -Path $Path -File $File" + New-Path -Path $Path + Remove-File $File + [Reflection.Assembly]::LoadWithPartialName("System.IO.Compression.FileSystem") + [System.IO.Compression.ZipFile]::CreateFromDirectory($Path, $File) +} +export-modulemember -function Compress-Path + +#----------------------------------------------------------------------- +# Convert-PathSafe [-Path []] +# +# Example: .\Convert-PathSafe -Path \\source\path +#----------------------------------------------------------------------- +function Convert-PathSafe +{ + param ( + [Parameter(Mandatory=$true,ValueFromPipelineByPropertyName=$true)] + [string]$Path = $(throw '-Path is a required parameter.') + ) + Write-Verbose "Convert-PathSafe -Path $Path" + $Path = $Path.Trim() + $ReturnValue = $Path + $Path = Set-Unc -Path $Path + if(Test-Path -Path $Path) + { + $ReturnValue = Convert-Path -Path $Path + if (-not ($ReturnValue)) + { + $ReturnValue = $Path + Write-Verbose "[Warning] 0 items affected. $(Get-CurrentFile) at $(Get-CurrentLine). $(Get-CurrentFile) at $(Get-CurrentLine). -Path $Path didnt convert." + } + } + else + { + Write-Verbose "[Error] 0 items affected. $(Get-CurrentFile) at $(Get-CurrentLine). $(Get-CurrentFile) at $(Get-CurrentLine). -Path $Path does not exist." + } + return $ReturnValue +} +export-modulemember -function Convert-PathSafe + +#----------------------------------------------------------------------- +# Copy-Backup [-Path []] [-Destination []] +# +# Example: .\Copy-Backup -Path \\source\path -Destination \\destination\path +#----------------------------------------------------------------------- +function Copy-Backup +{ + param ( + [Parameter(Mandatory=$true,ValueFromPipelineByPropertyName=$true)] + [string]$Path = $(throw '-Path is a required parameter.'), + [Parameter(Mandatory=$true,ValueFromPipelineByPropertyName=$true)] + [string]$Destination = $(throw '-Destination is a required parameter.') + ) + Write-Verbose "Copy-Backup -Path $Path -Destination $Destination" + $Path = Remove-Suffix -String $Path -Remove "\" + New-Path -Path $Destination + [String]$BackupPath=[string]::Format("{0}\{1}", $Destination, (Get-Date).ToString("yyyy-MM-dd")) + if($Path) + { + if(-not (Test-Path -Path $BackupPath -PathType Container)){ + New-Path -Path $BackupPath + } + Copy-Recurse -Path $Path -Destination $BackupPath + Write-Verbose "[Success] 1 items affected. $(Get-CurrentFile) at $(Get-CurrentLine). -Path $Path to -Destination $BackupPath" + } + else + { + Write-Verbose "[Error] 0 items affected. $(Get-CurrentFile) at $(Get-CurrentLine). $(Get-CurrentFile) at $(Get-CurrentLine). -Path $Path does not exist." + } +} +export-modulemember -function Copy-Backup + +#----------------------------------------------------------------------- +# Copy-File [-Path []] [-Destination []] +# +# Example: .\Copy-File -Path \\source\path\File.name -Destination \\destination\path +#----------------------------------------------------------------------- +function Copy-File +{ + param ( + [Parameter(Mandatory = $True)] + [string]$Path = $(throw '-Path is a required parameter.'), + [Parameter(Mandatory = $True)] + [string]$Destination = $(throw '-Destination is a required parameter.'), + [string[]]$Include = "*.*", + [string[]]$Exclude = "", + [bool]$Overwrite = $true + ) + Write-Verbose "Copy-File -Path $Path -Destination $Destination -Overwrite $Overwrite" + $Destination = Set-Unc -Path $Destination + if(Test-File -Path $Path) + { + New-Path -Path $Destination + $DestinationAbsolute = $Destination + if(Test-Folder -Path $Destination) + { + $DestinationAbsolute = Convert-PathSafe -Path $Destination + } + $DestinationPathFile = $DestinationAbsolute + $FolderArray = $Path.Split('\') + if($FolderArray.Count -gt 0) + { + $DestinationPathFile = Join-Path $DestinationAbsolute $FolderArray[$FolderArray.Count-1] + } + if((-not (Test-Path $DestinationPathFile -PathType Leaf)) -or ($Overwrite -eq $true)) + { + try{ + Copy-Item -Path $Path -Destination $DestinationAbsolute -Include $Include -Exclude $Exclude -Force + } + catch{ + Write-Verbose "[Error] 0 items affected. $(Get-CurrentFile) at $(Get-CurrentLine). -Path $Path does not exist." + } + } + Write-Verbose "[Success] 1 items affected. $(Get-CurrentFile) at $(Get-CurrentLine). -Path $Path to -Destination $DestinationAbsolute" + } + else + { + Write-Verbose "[Error] 0 items affected. $(Get-CurrentFile) at $(Get-CurrentLine). -Path $Path does not exist." + } +} +export-modulemember -function Copy-File + +#----------------------------------------------------------------------- +# Copy-Recurse [-Source []] [-Destination []] +# [-Include [] [-Exclude []] +# +# Example: .\Copy-Recurse \\source\path \\destination\path +#----------------------------------------------------------------------- +function Copy-Recurse +{ + param ( + [Parameter(Mandatory=$true,ValueFromPipelineByPropertyName=$true)] + [string]$Path = $(throw '-Source is a required parameter.'), + [Parameter(Mandatory=$true,ValueFromPipelineByPropertyName=$true)] + [string]$Destination = $(throw '-Destination is a required parameter.'), + [string[]]$Include = "*.*", + [string[]]$Exclude = "", + [Int32]$First = 1000, + [bool]$Overwrite=$True, + [bool]$Clean = $False + ) + Write-Verbose "Copy-Recurse -Path $Path -Destination $Destination -Include $Include -Exclude $Exclude -First $ -Overwrite $Overwrite -Clean $Clean" + $Affected = 0 + $Path = Set-Unc -Path $Path + if (Test-Path $Path) + { + $PathAbsolute = Convert-PathSafe -Path $Path + # Optionally Clean + if($Clean -eq $True) { Remove-Path -Path $Destination } + New-Path -Path $Destination + $DestinationAbsolute = $Destination + if(Test-Path $Destination) { $DestinationAbsolute=Convert-PathSafe -Path $Destination } + $Items = Get-ChildItem -Path $PathAbsolute -Recurse -Include $Include -Exclude $Exclude | where { ! $_.PSIsContainer } + ForEach ($Item in $Items) { + $PathArray = $PathAbsolute.Split('\') + $Folder = $DestinationAbsolute + for ($count=1; $count -lt $PathArray.length-1; $count++) { + $Subfolder = $PathArray[$count] + $Folder = Join-Path $Folder $Subfolder + if (($Folder.Length > 0) -and (-not (Test-Path $Folder))) { + Write-Verbose "New-Item -ItemType directory -Force -Path $Folder" + New-Item -ItemType directory -Force -Path $Folder + } + } + $DirName = $Item.DirectoryName + $Position = $DirName.IndexOf($PathAbsolute) + $PathSegment = $DirName.SubString($Position + $PathAbsolute.Length) + $NewPath = Join-Path $DestinationAbsolute $PathSegment + Copy-File -Path $Item.FullName -Destination $NewPath -Overwrite $Overwrite + $Affected = $Affected + 1 + } + Write-Verbose "[Success] $Affected items affected. $(Get-CurrentFile) at $(Get-CurrentLine)." + } + else + { + Write-Verbose "[Error] 0 items affected. $(Get-CurrentFile) at $(Get-CurrentLine). -Path $Path does not exist." + } +} +export-modulemember -function Copy-Recurse + +#----------------------------------------------------------------------- +# Expand-File [-Path []] [-File []] +# +# Example: .\Expand-File \\source\path\file.zip \\destination\path +#----------------------------------------------------------------------- +function Expand-File +{ + param ( + [Parameter(Mandatory=$true,ValueFromPipelineByPropertyName=$true)] + [string]$Path=$(throw '-Path is a required parameter.'), + [Parameter(Mandatory=$true,ValueFromPipelineByPropertyName=$true)] + [string]$File=$(throw '-File is a required parameter.') + ) + Write-Verbose "Expand-Zip -Path $Path -File $File" + New-Path -Path $Path + [Reflection.Assembly]::LoadWithPartialName("System.IO.Compression.FileSystem") + [System.IO.Compression.ZipFile]::ExtractToDirectory($File, $Path) +} +export-modulemember -function Expand-File + +#----------------------------------------------------------------------- +# Find-File [-Path []] [-File []] +# +# Example: .\Find-File \\source\path\file.zip \\destination\path +#----------------------------------------------------------------------- +function Find-File +{ + param ( + [Parameter(Mandatory=$true,ValueFromPipelineByPropertyName=$true)] + [string]$Path=$(throw '-Path is a required parameter.'), + [Parameter(Mandatory=$true,ValueFromPipelineByPropertyName=$true)] + [string]$File=$(throw '-File is a required parameter.'), + [Int32]$First=1 + ) + Write-Verbose "Find-Zip -Path $Path -File $File" + Get-Childitem -Path $Path -Include $File -Recurse| select -First $First +} +export-modulemember -function Find-File + +#----------------------------------------------------------------------- +# Get-AssemblyStrongName +# +# Example: Get-AssemblyStrongName +#----------------------------------------------------------------------- +function Get-AssemblyStrongName($assemblyPath) +{ + [System.Reflection.AssemblyName]::GetAssemblyName($assemblyPath).FullName +} +export-modulemember -function Get-AssemblyStrongName + +#----------------------------------------------------------------------- +# Get-CurrentLine +# +# Example: Get-CurrentLine +#----------------------------------------------------------------------- +function Get-CurrentLine { + $MyInvocation.ScriptLineNumber +} +export-modulemember -function Get-CurrentLine + +#----------------------------------------------------------------------- +# Get-CurrentFile +# +# Example: Get-CurrentFile +#----------------------------------------------------------------------- +function Get-CurrentFile { + $MyInvocation.ScriptName +} +export-modulemember -function Get-CurrentFile + +#----------------------------------------------------------------------- +# Get-FilesByString +# +# Example: Get-FilesByString +#----------------------------------------------------------------------- +function Get-FilesByString { + param ( + [Parameter(Mandatory=$true,ValueFromPipelineByPropertyName=$true)] + [string]$Path = $(throw '-Path is a required parameter.'), + [string]$String = $(throw '-String is a required parameter.'), + [string[]]$Include = ("*.*"), + [string[]]$Exclude = "" + ) + Write-Verbose "Get-FilesByString -Path $Path -String $String -Include $Include -Exclude $Exclude" + $Path = Set-Unc -Path $Path + + $ReturnData = Get-Childitem -Path $Path -Include $Include -Exclude $Exclude -Recurse | Select-String -pattern $String | group path | select name + + return $ReturnData +} +export-modulemember -function Get-FilesByString + +#----------------------------------------------------------------------- +# Get-SystemFolders +# +# Example: Get-SystemFolder +#----------------------------------------------------------------------- +function Get-SystemFolders +{ + param ( + ) + Write-Verbose "Get-SystemFolders" + $SpecialFolders = @{} + $names = [Environment+SpecialFolder]::GetNames([Environment+SpecialFolder]) + foreach($name in $names) + { + if($path = [Environment]::GetFolderPath($name)){ + $SpecialFolders[$name] = $path + } + else + { + Write-Verbose "[Warning] 0 items affected. $(Get-CurrentFile) at $(Get-CurrentLine)." + } + } + return $SpecialFolders +} +export-modulemember -function Get-SystemFolders + +#----------------------------------------------------------------------- +# Get-SystemFolder [-Name []] +# Keys: Desktop,Programs,MyDocuments,Personal,Favorites,Startup,Recent,SendTo,StartMenu,MyMusic,MyVideos,DesktopDirectory,NetworkShortcuts,Fonts +# Templates,CommonStartMenu,CommonPrograms,CommonStartup,CommonDesktopDirectory,ApplicationData,PrinterShortcuts,LocalApplicationData,InternetCache +# Cookies,History,CommonApplicationData,Windows,System,ProgramFiles,MyPictures,UserProfile,SystemX86,ProgramFilesX86 +# CommonProgramFiles,CommonProgramFilesX86,CommonTemplates,CommonDocuments,CommonAdminTools,AdminTools,CommonMusic,CommonPictures,CommonVideos,ResourcesCDBurning +# Example: Get-SystemFolder -Name 'UserProfile' +#----------------------------------------------------------------------- +function Get-SystemFolder +{ + param ( + [Parameter(Mandatory=$true,ValueFromPipelineByPropertyName=$true)] + [String]$Name=$(throw '-Folder is a required parameter.') + ) + Write-Verbose "Get-SystemFolder -Folder $Folder" + if($path = [Environment]::GetFolderPath($name)){ + $Folder = $path + } + else + { + Write-Verbose "[Warning] 0 items affected. $(Get-CurrentFile) at $(Get-CurrentLine)." + } + return $Folder +} +export-modulemember -function Get-SystemFolder + +#----------------------------------------------------------------------- +# Move-Path [-Path []] [-Destination []] +# [-Exclude []] +# +# Example: .\Move-Path -Path \\source\path +#----------------------------------------------------------------------- +function Move-Path +{ + param ( + [Parameter(Mandatory=$true,ValueFromPipelineByPropertyName=$true)] + [string]$Path = $(throw '-Path is a required parameter.'), + [Parameter(Mandatory=$true,ValueFromPipelineByPropertyName=$true)] + [string]$Destination = $(throw '-Destination is a required parameter.'), + [string]$Exclude = "" + ) + Write-Verbose "Move-Path -Path $Path -Destination $Destination" + $Path = Remove-Suffix -String $Path -Remove "\" + if (test-folder -Path $Path) + { + Remove-Path -Destination $Destination + New-Path -Destination $Destination + Copy-Recurse -Path $Path -Destination $Destination -Exclude $Exclude + Remove-Path -Destination $Path + Write-Verbose "[Success] 1 items affected. $(Get-CurrentFile) at $(Get-CurrentLine). -Path $Path moved to -Destination $Destination" + } + else + { + Write-Verbose "[Error] 0 items affected. $(Get-CurrentFile) at $(Get-CurrentLine). -Path $Path does not exist." + } +} +export-modulemember -function Move-Path + +#----------------------------------------------------------------------- +# New-Path [-Path []] +# +# Example: .\New-Path \\source\path +#----------------------------------------------------------------------- +function New-Path +{ + param ( + [Parameter(Mandatory=$true,ValueFromPipelineByPropertyName=$true)] + [string]$Path = $(throw '-Path is a required parameter.'), + [bool]$Clean=$false + ) + Write-Verbose "New-Path -Path $Path" + [String]$Folder = "" + $Path = Remove-Suffix -String $Path -Remove "\" + if ($Clean) {Remove-Path -Path $Path} + if (-not (test-path $Path)) { + if (Test-Unc $Path) + { + $PathArray = $Path.Split('\') + foreach($item in $PathArray) + { + if($item.Length -gt 0) + { + if($Folder.Length -lt 1) + { + $Folder = "\\$item" + } + else + { + $Folder = "$Folder\$item" + if (-not (Test-Path $Folder)) { + New-Item -ItemType directory -Path $Folder -Force + } + } + } + } + } + else + { + New-Item -ItemType directory -Path $Path -Force + } + } +} +export-modulemember -function New-Path + +#----------------------------------------------------------------------- +# Redo-Path [-Path []] [-Destination []] +# [-Exclude []] +# +# Example: .\Redo-Path -Path \\source\path +#----------------------------------------------------------------------- +function Redo-Path +{ + param ( + [Parameter(Mandatory=$true,ValueFromPipelineByPropertyName=$true)] + [string]$Path = $(throw '-Path is a required parameter.'), + [Parameter(Mandatory=$true,ValueFromPipelineByPropertyName=$true)] + [string]$Destination = $(throw '-Destination is a required parameter.'), + [string]$Exclude = "" + ) + Write-Verbose "Redo-Path -Path $Path -Destination $Destination" + $Path = Remove-Suffix -String $Path -Remove "\" + Remove-Path -Destination $Path + New-Path -Destination $Path + Copy-Recurse -Path $Path -Destination $Destination -Exclude $Exclude + Write-Verbose "[Success] 1 items affected. $(Get-CurrentFile) at $(Get-CurrentLine). -Path $Path to -Destination $Destination" +} +export-modulemember -function Redo-Path + +#----------------------------------------------------------------------- +# Remove-File [-File []] +# +# Example: .\Remove-File \\source\path\file.txt +#----------------------------------------------------------------------- +function Remove-File +{ + param ( + [Parameter(Mandatory=$true,ValueFromPipelineByPropertyName=$true)] + [string]$Path=$(throw '-Path is a required parameter.') + ) + Write-Verbose "Remove-File -Path $Path" + if (Test-File -Path $Path) { + Remove-Item -Path $Path -Force + Write-Verbose "[Success] 1 items affected. $(Get-CurrentFile) at $(Get-CurrentLine). -Path $Path removed." + } + else + { + Write-Verbose "[Error] 0 items affected. $(Get-CurrentFile) at $(Get-CurrentLine). -Path $Path does not exist." + } +} +export-modulemember -function Remove-File + +#----------------------------------------------------------------------- +# Remove-Path [-Path []] +# [-Include [] [-Exclude []] +# +# Example: .\Remove-Path -Path \\source\path +#----------------------------------------------------------------------- +function Remove-Path +{ + param ( + [Parameter(Mandatory=$true,ValueFromPipelineByPropertyName=$true)] + [string]$Path = $(throw '-Path is a required parameter.'), + [string[]]$Include = "*.*", + [string[]]$Exclude = "", + [Int16]$Retention = 1, + [Int32]$First = 1000 + ) + Write-Verbose "Remove-Path -Path $Path -Include $Include -Exclude $Exclude -Retention $Retention -First $First" + $Path = Remove-Suffix -String $Path -Remove "\" + if (test-folder -Path $Path) { + $ErrorActionPreferenceBackup = $ErrorActionPreference + $ErrorActionPreference = 'SilentlyContinue' + Get-ChildItem -Path $Path -Include $Include -Exclude $Exclude -Recurse | Where-Object {($_.PSIsContainer) -and ($_.lastwritetime -le (get-date).addDays(($Retention*-1)))} | select -First $First | Remove-Item -Force -Recurse + Remove-Item $Path -Recurse -Force + $ErrorActionPreference = $ErrorActionPreferenceBackup + Write-Verbose "[Success] 1 items affected. $(Get-CurrentFile) at $(Get-CurrentLine). -Path $Path removed." + } + else + { + Write-Verbose "[Warning] 0 items affected. $(Get-CurrentFile) at $(Get-CurrentLine)." + } +} +export-modulemember -function Remove-Path + +#----------------------------------------------------------------------- +# Remove-Subfolders [-Path []] +# [-Include [] [-Exclude []] +# +# Example: .\Remove-Subfolders -Path \\source\path +#----------------------------------------------------------------------- +function Remove-Subfolders +{ + param ( + [Parameter(Mandatory=$true,ValueFromPipelineByPropertyName=$true)] + [string]$Path = $(throw '-Path is a required parameter.'), + [Parameter(Mandatory=$true,ValueFromPipelineByPropertyName=$true)] + [string]$Subfolder = $(throw '-Subfolder is a required parameter.'), + [Int32]$First = 1000 + ) + Write-Verbose "Remove-Subfolders -Path $Path -Subfolder $Subfolder -First $First" + $Path = Set-Unc -Path $Path + + if (test-folder -Path $Path) { + $Affected = 0 + if(Test-Unc -Path $Path) + { + $Folders=Get-ChildItem $Path -Recurse | Where-Object {($_.Name -EQ $Subfolder) -and ($_.PSIsContainer)} | select -First $First + foreach ($Folder in $Folders) { + if ($Folder.FullName) + { + [String]$FolderToRemove=Add-Suffix -String $Folder.FullName -Add "\" + Remove-Path -Path $FolderToRemove + $Affected = $Affected + 1 + } + } + } + else + { + $Remove = Add-Suffix -String $Path -Add '\' + $Remove = Add-Suffix -String $Remove -Add $Subfolder + Remove-Path -Path $Remove + $Affected = 1 + } + Write-Verbose "[Success] $Affected items affected. $(Get-CurrentFile) at $(Get-CurrentLine). -Path $Path -Subfolder $Subfolder removed." + } + else + { + Write-Verbose "[Warning] 0 items affected. $(Get-CurrentFile) at $(Get-CurrentLine). -Path $Path does not exist." + } +} +export-modulemember -function Remove-Subfolders + +#----------------------------------------------------------------------- +# Remove-Recurse [-Source []] [-Destination []] +# [-Include [] [-Exclude []] +# +# Example: .\Remove-Recurse \\source\path \\destination\path +#----------------------------------------------------------------------- +function Remove-Recurse +{ + param ( + [Parameter(Mandatory=$true,ValueFromPipelineByPropertyName=$true)] + [string]$Path = $(throw '-Source is a required parameter.'), + [string[]]$Include = "*.*", + [string[]]$Exclude = "", + [Int32]$First = 1000 + ) + Write-Verbose "Remove-Recurse -Path $Path -Include $Include -Exclude $Exclude" + $Path = Remove-Suffix -String $Path -Remove "\" + if (Test-Path $Path) + { + $PathAbsolute = Convert-PathSafe -Path $Path + $Items = Get-ChildItem -Path $PathAbsolute -Recurse -Include $Include -Exclude $Exclude | where { ! $_.PSIsContainer } + $Affected = 0 + ForEach ($Item in $Items) { + Remove-File -Path $Item + $Affected = $Affected + 1 + } + Write-Verbose "[Success] $Affected items affected. $(Get-CurrentFile) at $(Get-CurrentLine). -Path $Path." + } + else + { + Write-Verbose "[Warning] 0 items affected. $(Get-CurrentFile) at $(Get-CurrentLine). -Path $Path does not exist." + } +} +export-modulemember -function Remove-Recurse + +#----------------------------------------------------------------------- +# Remove-Prefix [-String []] +# +# Example: .\Remove-Prefix -String Hell -Remove o +# Result: Hello +#----------------------------------------------------------------------- +function Remove-Prefix +{ + param ( + [Parameter(Mandatory=$true,ValueFromPipelineByPropertyName=$true)] + [string]$String = $(throw '-String is a required parameter.'), + [Parameter(Mandatory=$true,ValueFromPipelineByPropertyName=$true)] + [string]$Remove = $(throw '-Remove is a required parameter.') + ) + Write-Verbose "Remove-Prefix -String $String -Remove $Remove" + [string]$ReturnValue = $String + if (Compare-IsFirst -String $String -BeginsWith $Remove) + { + $ReturnValue = $String.Substring($Remove.Length, $String.Length - $Remove.Length) + Write-Verbose "[Success] 1 items affected. $(Get-CurrentFile) at $(Get-CurrentLine)." + } + else + { + Write-Verbose "[OK] 0 items affected. $(Get-CurrentFile) at $(Get-CurrentLine). -String $String already has prefix of -Remove $Remove" + } + + return $ReturnValue +} +export-modulemember -function Remove-Prefix + +#----------------------------------------------------------------------- +# Remove-Suffix [-String []] +# +# Example: .\Remove-Suffix -String Hell -Remove o +# Result: Hello +#----------------------------------------------------------------------- +function Remove-Suffix +{ + param ( + [Parameter(Mandatory=$true,ValueFromPipelineByPropertyName=$true)] + [string]$String = $(throw '-String is a required parameter.'), + [Parameter(Mandatory=$true,ValueFromPipelineByPropertyName=$true)] + [string]$Remove = $(throw '-Remove is a required parameter.') + ) + Write-Verbose "Remove-Suffix -String $String -Remove $Remove" + [string]$ReturnValue = $String + if($String) + { + if (Compare-IsLast -String $String -EndsWith $Remove) + { + $ReturnValue = $ReturnValue.Substring(0, $String.Length - $Remove.Length) + } + Write-Verbose "[Success] 1 items affected. $(Get-CurrentFile) at $(Get-CurrentLine)." + } + else + { + Write-Verbose "[OK] 0 items affected. $(Get-CurrentFile) at $(Get-CurrentLine). -String $String already has suffix of -Remove $Remove" + } + + return $ReturnValue +} +export-modulemember -function Remove-Suffix + +#----------------------------------------------------------------------- +# Remove-Element [-Path []] +# +# Example: .\Remove-Element -Value "" +# -XPath "//msb:None/msb:Generator" +# -Namespace @{msb = "http://schemas.microsoft.com/developer/msbuild/2003"} +# +# Called: $XMLValue = [xml](Get-Content $path) +# $Namespace = @{msb = 'http://schemas.microsoft.com/developer/msbuild/2003'} +# Remove-Element $XMLValue -XPath '//msb:None/msb:Generator' -Namespace $Namespace +# $proj.Save($path) +#----------------------------------------------------------------------- +function Remove-Element +{ + param ( + [Parameter(Mandatory=$true,ValueFromPipelineByPropertyName=$true)] + [xml]$Value=$(throw '-Value is a required parameter.'), + [Parameter(Mandatory=$true,ValueFromPipelineByPropertyName=$true)] + [String]$XPath=$(throw '-Value is a required parameter.'), + [Parameter(Mandatory=$true,ValueFromPipelineByPropertyName=$true)] + [String]$Namespace=$(throw '-Value is a required parameter.') + ) + Write-Verbose ".\Remove-Element -Value $Value -XPath $XPath -Namespace $Namespace -SingleNode" + $nodes = @(Select-Xml $XPath $Value -Namespace $Namespace | Foreach {$_.Node}) + if (!$nodes) { Write-Verbose "RemoveElement: XPath $XPath not found" } + if ($singleNode -and ($nodes.Count -gt 1)) { + throw "XPath $XPath found multiple nodes" + } + $Count = 0 + foreach ($node in $nodes) + { + $parentNode = $node.ParentNode + [void]$parentNode.RemoveChild($node) + $Count = $Count + 1 + } + Write-Verbose "[Success] $Count items affected. $(Get-CurrentFile) at $(Get-CurrentLine)." +} +export-modulemember -function Remove-Element + +#----------------------------------------------------------------------- +# Remove-ContentsByTagContains [-Path []] +# [-Open [] [-Close []] +# +# Example: .\Remove-ContentsByTagContains \\source\path \\destination\path +# GlobalSection(TeamFoundationVersionControl) = preSolution +# EndGlobalSection +#----------------------------------------------------------------------- +function Remove-ContentsByTag +{ + param ( + [Parameter(Mandatory=$true,ValueFromPipelineByPropertyName=$true)] + [string]$Path = $(throw '-Path is a required parameter.'), + [Parameter(Mandatory=$true,ValueFromPipelineByPropertyName=$true)] + [string]$Open = $(throw '-Open is a required parameter.'), + [Parameter(Mandatory=$true,ValueFromPipelineByPropertyName=$true)] + [string]$Close = $(throw '-Close is a required parameter.'), + [string[]]$Include = "*.*", + [string[]]$Exclude = "", + [Int32]$First = 100 + ) + Write-Verbose "Remove-ContentsByTag -Path $Path -Open $Open -Close $Close -Include $Include -Exclude $Exclude -First $First" + $Path = Remove-Suffix -String $Path -Remove "\" + if (Test-Path $Path) + { + $Open = $Open.Trim() + $Close = $Close.Trim() + $Files = Get-Childitem -Path $Path -Include $Include -Exclude $Exclude -Recurse -Force | select -First $First + ForEach ($File in $Files) + { + [Int32]$OpenIndex = -1 + [Int32]$CloseIndex = -1 + $Content=Get-Content $File.PSPath + # Search for matches + For([Int32]$Count = 0; $Count -lt $Content.Length; $Count++) + { + $CurrentLine = $Content[$Count].Trim() + If(($OpenIndex -eq -1) -and ($CurrentLine -eq $Open)) + { + $OpenIndex = $Count + } + ElseIf(($OpenIndex -gt -1) -and ($CurrentLine -eq $Close)) + { + $CloseIndex = $Count + Break + } + } + # Evaluate search + If(($OpenIndex -gt -1) -and ($OpenIndex -lt $CloseIndex)) + { + # Match Found Remove block. + $NewContent = ($Content | Select -First $OpenIndex) + ($Content | select -Last ($Content.Length - $CloseIndex - 1)) + } + else + { + # No Match Found + $NewContent = $Content + } + Set-Content $File.PSPath -Value $NewContent + } + Write-Verbose "[Success] 1 items affected. $(Get-CurrentFile) at $(Get-CurrentLine)." + } + else + { + Write-Verbose "[OK] 0 items affected. $(Get-CurrentFile) at $(Get-CurrentLine)." + } +} +export-modulemember -function Remove-ContentsByTag + +#----------------------------------------------------------------------- +# Remove-ContentsByTagContains [-Path []] +# [-Open [] [-Close []] +# +# Example: .\Remove-ContentsByTagContains \\source\path \\destination\path +# GlobalSection(TeamFoundationVersionControl) = preSolution +# EndGlobalSection +#----------------------------------------------------------------------- +function Remove-ContentsByTagContains +{ + param ( + [Parameter(Mandatory=$true,ValueFromPipelineByPropertyName=$true)] + [string]$Path = $(throw '-Path is a required parameter.'), + [Parameter(Mandatory=$true,ValueFromPipelineByPropertyName=$true)] + [string]$Open = $(throw '-Open is a required parameter.'), + [Parameter(Mandatory=$true,ValueFromPipelineByPropertyName=$true)] + [string]$Close = $(throw '-Close is a required parameter.'), + [Parameter(Mandatory=$true,ValueFromPipelineByPropertyName=$true)] + [string]$Contains = $(throw '-Contains is a required parameter.'), + [string[]]$Include = "*.*", + [string[]]$Exclude = "", + [Int32]$First = 100 + ) + Write-Verbose "Remove-ContentsByTagContains -Path $Path -Open $Open -Close $Close -Contains $Contains -Include $Include -Exclude $Exclude -First $First" + $Path = Remove-Suffix -String $Path -Remove "\" + if (Test-Path $Path) + { + $Open = $Open.Trim() + $Close = $Close.Trim() + $Contains = $Contains.Trim() + + $Files = Get-Childitem -Path $Path -Include $Include -Exclude $Exclude -Recurse -Force | select -First $First + ForEach ($File in $Files) + { + [Int32]$OpenIndex = -1 + [Int32]$ContainsIndex = -1 + [Int32]$CloseIndex = -1 + $Content=Get-Content $File.PSPath + # Search for matches + For([Int32]$Count = 0; $Count -lt $Content.Length; $Count++) + { + $CurrentLine = $Content[$Count].Trim() + If ($CurrentLine -like "*$Open*") + { + If($OpenIndex -gt -1) + { + # Fail: Block did not contain -Content and/or -Open was found before -Close. Reset for next open tag match. + $ContainsIndex = -1 + $CloseIndex = -1 + } + $OpenIndex = $Count + }ElseIf($OpenIndex -gt -1) + { + If($CurrentLine -like "*$Contains*") + { + $ContainsIndex = $Count + }ElseIf(($ContainsIndex -gt -1) -and ($CurrentLine -like "*$Close*")) + { + # Success, block starts with -Open, ends with -Close and includes -Contains + $CloseIndex = $Count + Break + } + } + } + # Any matches? + If(($OpenIndex -gt -1) -and ($ContainsIndex -gt $OpenIndex) -and ($CloseIndex -gt $ContainsIndex)) + { + If($CloseIndex -eq ($OpenIndex + 2)) + { + # Match Found with single element. Remove Block. + $NewContent = ($Content | Select -First $OpenIndex) + ($Content | select -Last ($Content.Length - $CloseIndex - 1)) + } + Else + { + # Match Found with multiple elements. Remove Line Only. + $NewContent = ($Content | Select -First $ContainsIndex) + ($Content | select -Last ($Content.Length - $ContainsIndex -1)) + } + } + else + { + # No Match Found + $NewContent = $Content + } + Set-Content $File.PSPath -Value $NewContent + } + Write-Verbose "[Success] 1 items affected. $(Get-CurrentFile) at $(Get-CurrentLine)." + } + else + { + Write-Verbose "[OK] 0 items affected. $(Get-CurrentFile) at $(Get-CurrentLine)." + } +} +export-modulemember -function Remove-ContentsByTagContains + +#----------------------------------------------------------------------- +# Rename-File [-File []] +# +# Example: .\Rename-File -Path ($StagingZipPath + 'root.vstbak') -NewName root.vstemplate -Force +#----------------------------------------------------------------------- +function Rename-File +{ + param ( + [Parameter(Mandatory=$true,ValueFromPipelineByPropertyName=$true)] + [string]$Path=$(throw '-Path is a required parameter.'), + [string]$NewName=$(throw '-NewName is a required parameter.') + ) + Write-Verbose "Rename-File -Path $Path -NewName $NewName" + if (Test-File -Path $Path) { + Rename-Item -Path $Path -NewName $NewName -Force + Write-Verbose "[Success] 1 items affected. $(Get-CurrentFile) at $(Get-CurrentLine). -Path $Path removed." + } + else + { + Write-Verbose "[Error] 0 items affected. $(Get-CurrentFile) at $(Get-CurrentLine). -Path $Path does not exist." + } +} +export-modulemember -function Rename-File + +#----------------------------------------------------------------------- +# Set-ReadOnly [-Path []] [-ReadOnly []] +# +# Example: .\Set-ReadOnly -Path \\source\path\File.name -ReadOnly $False +#----------------------------------------------------------------------- +function Set-ReadOnly +{ + param ( + [Parameter(Mandatory = $True)] + [string]$Path = $(throw '-Path is a required parameter.'), + [bool]$ReadOnly = $True, + [string]$ErrorPreference = 'SilentlyContinue' + ) + Write-Verbose "Set-ReadOnly -Path $Path -ReadOnly $ReadOnly -ErrorPreference $ErrorPreference" + $Path = Remove-Suffix -String $Path -Remove "\" + if(test-path $Path) + { + $PathAbsolute = Convert-PathSafe -Path $Path + if (Test-Path $PathAbsolute -PathType Leaf) + { + $ErrorActionPreferenceBackup = $ErrorActionPreference + $ErrorActionPreference = $ErrorPreference + Set-ItemProperty $PathAbsolute -name IsReadOnly -value $ReadOnly -Force + $ErrorActionPreference = $ErrorActionPreferenceBackup + } + Write-Verbose "[Success] 1 items affected. $(Get-CurrentFile) at $(Get-CurrentLine). -Path $Path set." + } + else + { + Write-Verbose "[Error] 0 items affected. $(Get-CurrentFile) at $(Get-CurrentLine). -Path $Path does not exist." + } +} +export-modulemember -function Set-ReadOnly + +#----------------------------------------------------------------------- +# Set-SystemFolderDrives +# +# Example: Set-SystemFolderDrives +#----------------------------------------------------------------------- +function Set-SystemFolderDrives +{ + param ( + ) + Write-Verbose "Set-SystemFolderDrives" + + $SpecialFolders = @{} + $names = [Environment+SpecialFolder]::GetNames([Environment+SpecialFolder]) + foreach($name in $names) + { + if($path = [Environment]::GetFolderPath($name)){ + $SpecialFolders[$name] = $path + New-PSDrive -Name $name -PSProvider FileSystem -Root $path + } + } + # #TBD: Find the 10 Largest Files in the Documents Folder + # gci Personal: -Recurse -Force -ea SilentlyContinue | + # Sort-Object -Property Length -Descending | + # Select-Object -First 10 | + # Format-Table -AutoSize -Wrap -Property ` + # Length,LastWriteTime,FullName + return $SpecialFolders +} +export-modulemember -function Set-SystemFolderDrives + +#----------------------------------------------------------------------- +# Test-File [-Path []] +# +# Example: .\Test-File -Path \\source\path +#----------------------------------------------------------------------- +function Test-File +{ + param ( + [Parameter(Mandatory=$true,ValueFromPipelineByPropertyName=$true)] + [string]$Path = $(throw '-Path is a required parameter.') + ) + Write-Verbose "Test-File -Path $Path" + [bool]$ReturnValue = $false + if(Test-Path -Path $Path -PathType Leaf) + { + $ReturnValue = $true + } + else + { + Write-Verbose "[Warning] 0 items affected. $(Get-CurrentFile) at $(Get-CurrentLine). -Path $Path does not exist or is not a File." + } + return $ReturnValue +} +export-modulemember -function Test-File + +#----------------------------------------------------------------------- +# Test-Folder [-Path []] +# +# +# Example: .\Test-Folder -Path \\source\path +#----------------------------------------------------------------------- +function Test-Folder +{ + param ( + [Parameter(Mandatory=$true,ValueFromPipelineByPropertyName=$true)] + [string]$Path = $(throw '-Path is a required parameter.') + ) + Write-Verbose "Test-Folder -Path $Path" + [bool]$ReturnValue = $false + if(test-path -Path $Path -PathType Container) + { + $ReturnValue = $true + } + else + { + Write-Verbose "[Warning] 0 items affected. $(Get-CurrentFile) at $(Get-CurrentLine). -Path $Path does not exist or is not a Folder." + } + return $ReturnValue +} +export-modulemember -function Test-Folder + +#----------------------------------------------------------------------- +# Test-PathEmpty [-Path []] +# +# Example: .\Test-PathEmpty -Path \\source\path +#----------------------------------------------------------------------- +function Test-PathEmpty +{ + param ( + [Parameter(Mandatory=$true,ValueFromPipelineByPropertyName=$true)] + [string]$Path = $(throw '-Path is a required parameter.') + ) + Write-Verbose "Test-PathEmpty -Path $Path" + [bool]$ReturnValue = $false + if((Get-ChildItem $Path -force | Select-Object -First 1 | Measure-Object).Count -eq 0) + { + $ReturnValue = $true + } + else + { + Write-Verbose "[Error] 0 items affected. $(Get-CurrentFile) at $(Get-CurrentLine). -Path $Path does not exist." + } + return $ReturnValue +} +export-modulemember -function Test-PathEmpty + +#----------------------------------------------------------------------- +# Set-Unc [-Path []] +# +# Example: .\Set-Unc -Path \\source\path +#----------------------------------------------------------------------- +function Set-Unc +{ + param ( + [Parameter(Mandatory=$true,ValueFromPipelineByPropertyName=$true)] + [string]$Path = $(throw '-Path is a required parameter.') + ) + Write-Verbose "Set-Unc -Path $Path" + $Path = $Path.Trim() + $Path = Remove-Suffix -String $Path -Remove '\' + if(-not ($Path.Contains(':\') -or $Path.Contains('.\') -or (Compare-IsFirst -String $Path -BeginsWith '\'))) + { + $ReturnValue = Add-Prefix -String $Path -Add '\\' + } + else + { + $ReturnValue = $Path + Write-Verbose "[OK] 0 items affected. $(Get-CurrentFile) at $(Get-CurrentLine). -Path $Path already a UNC, drive letter, absolute or relative path." + } + return $ReturnValue +} +export-modulemember -function Set-Unc + +#----------------------------------------------------------------------- +# Test-Unc [-Path []] +# +# +# Example: .\Test-Unc -Path \\source\path +#----------------------------------------------------------------------- +function Test-Unc +{ + param ( + [Parameter(Mandatory=$true,ValueFromPipelineByPropertyName=$true)] + [string]$Path = $(throw '-Path is a required parameter.') + ) + Write-Verbose "Test-Unc -Path $Path" + [bool]$ReturnValue = $false + if(($Path.Contains('\\')) -and (-not ($Path.Contains(':\')))) + { + $ReturnValue = $true + } + else + { + Write-Verbose "[OK] 0 items affected. $(Get-CurrentFile) at $(Get-CurrentLine)." + } + return $ReturnValue +} +export-modulemember -function Test-Unc + +#----------------------------------------------------------------------- +# Update-LineByContains [-Path []] +# [-Contains [] [-Close []] +# +# Example: .\Update-LineByContains -Path \\source\path -Include AssemblyInfo.cs -Contains 'AssemblyVersion(' -Line '[assembly: AssemblyVersion("5.20.07")]' +#----------------------------------------------------------------------- +function Update-LineByContains +{ + param ( + [Parameter(Mandatory=$true,ValueFromPipelineByPropertyName=$true)] + [string]$Path = $(throw '-Path is a required parameter.'), + [Parameter(Mandatory=$true,ValueFromPipelineByPropertyName=$true)] + [string]$Contains = $(throw '-Contains is a required parameter.'), + [Parameter(Mandatory=$true,ValueFromPipelineByPropertyName=$true)] + [string]$Line = $(throw '-Line is a required parameter.'), + [string[]]$Include = "*.*", + [string[]]$Exclude = "", + [Int32]$First = 100 + ) + Write-Verbose "Update-LineByContains -Path $Path -Contains $Contains -Line $Line -Include $Include -Exclude $Exclude -First $First" + $Path = Remove-Suffix -String $Path -Remove "\" + if (Test-Path $Path) + { + $Contains = $Contains.Trim() + $Count = 0 + $Files = Get-Childitem -Path $Path -Include $Include -Exclude $Exclude -Recurse -Force | select -First $First + ForEach ($File in $Files) + { + [Int32]$ContainsIndex = -1 + $Affected = 0 + $Content=Get-Content $File.PSPath + # Search for matches + For([Int32]$Count = 0; $Count -lt $Content.Length; $Count++) + { + $CurrentLine = $Content[$Count].Trim() + If(($ContainsIndex -eq -1) -and ($CurrentLine -eq $Contains)) + { + $ContainsIndex = $Count + Break + } + } + # Evaluate search + If($ContainsIndex -gt -1) + { + # Select before line, add -Line, select after line + $NewContent = (($Content | Select -First $ContainsIndex) + ($Line + [Environment]::NewLine) + ($Content | select -Last ($Content.Length - $ContainsIndex -1))) + } + else + { + # No Match Found + $NewContent = $Content + } + Set-Content $File.PSPath -Value $NewContent + $Affected = $Count + } + Write-Verbose "[Success] $Count items affected. $(Get-CurrentFile) at $(Get-CurrentLine)." + } + else + { + Write-Verbose "[OK] 0 items affected. $(Get-CurrentFile) at $(Get-CurrentLine)." + } +} +export-modulemember -function Update-LineByContains + +#----------------------------------------------------------------------- +# Update-ContentsByTag [-Path []] +# [-Open [] [-Close []] +# +# Example: .\Update-ContentsByTag -Path $Path -Include *.sln -Open "GlobalSection(TeamFoundationVersionControl) = preSolution" -Close "EndGlobalSection" +#----------------------------------------------------------------------- +function Update-ContentsByTag +{ + param ( + [Parameter(Mandatory=$true,ValueFromPipelineByPropertyName=$true)] + [string]$Path = $(throw '-Path is a required parameter.'), + [Parameter(Mandatory=$true,ValueFromPipelineByPropertyName=$true)] + [string]$Open = $(throw '-Open is a required parameter.'), + [Parameter(Mandatory=$true,ValueFromPipelineByPropertyName=$true)] + [string]$Close = $(throw '-Close is a required parameter.'), + [Parameter(Mandatory=$true,ValueFromPipelineByPropertyName=$true)] + [string]$Value = $(throw '-Value is a required parameter.'), + [string[]]$Include = "*.*", + [string[]]$Exclude = "", + [Int32]$First = 100 + ) + Write-Verbose "Update-ContentsByTag -Path $Path -Open $Open -Close $Close -Include $Include -Exclude $Exclude -First $First" + $Path = Remove-Suffix -String $Path -Remove "\" + [String]$PaddingLeft = ' ' + if (Test-Path $Path) + { + $Open = $Open.Trim() + $Close = $Close.Trim() + $Affected = 0 + $Files = Get-Childitem -Path $Path -Include $Include -Exclude $Exclude -Recurse -Force | select -First $First + ForEach ($File in $Files) + { + [Int32]$OpenIndex = -1 + [Int32]$CloseIndex = -1 + [String]$NewValue = "" + $Content=Get-Content $File.PSPath + # Search for matches + For([Int32]$Count = 0; $Count -lt $Content.Length; $Count++) + { + $CurrentLine = $Content[$Count].Trim() + If(($OpenIndex -eq -1) -and ($CurrentLine -like "*$Open*")) + { + $OpenIndex = $Count + } + If(($OpenIndex -gt -1) -and ($CurrentLine -like "*$Close*")) + { + $CloseIndex = $Count + Break + } + } + # Evaluate search + If(($OpenIndex -gt -1) -and ($OpenIndex -le $CloseIndex)) + { + if($OpenIndex -eq $CloseIndex) + { + # Open/Close on same line, rebuild the line with new contents + $NewValue = ($Open + $Value + $Close) + } + else + { + $NewValue = $Value + } + # Update content + $NewContent = ($Content | Select -First ($OpenIndex)) + ($PaddingLeft + $NewValue) + ($Content | select -Last ($Content.Length - $CloseIndex - 1)) + $Affected = 1 + } + else + { + # No Match Found + $NewContent = $Content + } + Set-Content $File.PSPath -Value $NewContent + } + Write-Verbose "[Success] $Affected items affected. $(Get-CurrentFile) at $(Get-CurrentLine)." + } + else + { + Write-Verbose "[OK] 0 items affected. $(Get-CurrentFile) at $(Get-CurrentLine)." + } +} +export-modulemember -function Update-ContentsByTag + +#----------------------------------------------------------------------- +# Update-Text [-Path []] +# [-Include [] [-Exclude []] +# +# Example: .\Update-Text -Path \\source\path -Include *.cs -Old "Use gotos" -New "Point at people who use gotos" +#----------------------------------------------------------------------- +function Update-Text +{ + param ( + [Parameter(Mandatory=$true,ValueFromPipelineByPropertyName=$true)] + [string]$Path = $(throw '-Path is a required parameter.'), + [Parameter(Mandatory=$true,ValueFromPipelineByPropertyName=$true)] + [string]$Old = $(throw '-Old is a required parameter.'), + [Parameter(Mandatory=$true,ValueFromPipelineByPropertyName=$true)] + [string]$New = $(throw '-New is a required parameter.'), + [Parameter(Mandatory=$true,ValueFromPipelineByPropertyName=$true)] + [string[]]$Include = $(throw '-Include is a required parameter.'), + [string[]]$Exclude = "", + [Int32]$First = 100 + ) + Write-Verbose "Update-Text -Path $Path -Old $Old -New $New -Include $Include -Exclude $Exclude -First $First" + $Path = Remove-Suffix -String $Path -Remove "\" + $Count = 0 + if (Test-Path $Path) + { + $ConfigFiles=Get-Childitem $Path -Include $Include -Exclude $Exclude -Recurse -Force | select -First $First + foreach ($Item in $ConfigFiles) + { + Set-ReadOnly -Path $Item.PSPath -ReadOnly $false + (Get-Content $Item.PSPath) | + Foreach-Object {$_.Replace($Old, $New) + } | + Set-Content $Item.PSPath -force + $Count = $Count + 1 + } + Write-Verbose "[Success] $Count items affected. $(Get-CurrentFile) at $(Get-CurrentLine)." + } + else + { + Write-Verbose "[OK] 0 items affected. $(Get-CurrentFile) at $(Get-CurrentLine)." + } +} +export-modulemember -function Update-Text + +#----------------------------------------------------------------------- +# Update-TextByContains [-Path []] +# [-Contains [] [-Close []] +# +# Example: .\Update-TextByContains -Path \\source\path -Include AssemblyInfo.cs -Contains 'AssemblyVersion(' -Line '[assembly: AssemblyVersion("5.20.07")]' +#----------------------------------------------------------------------- +function Update-TextByContains +{ + param ( + [string]$Path = $(throw '-Path is a required parameter.'), + [string]$Contains = $(throw '-Contains is a required parameter.'), + [string]$Old = $(throw '-Old is a required parameter.'), + [string]$New = $(throw '-New is a required parameter.'), + [string[]]$Include = "*.*", + [string[]]$Exclude = "", + [Int32]$First = 100 + ) + Write-Verbose "Update-TextByContains -Path $Path -Contains $Contains -Old $Old -New $New -Include $Include -Exclude $Exclude -First $First" + $Path = Remove-Suffix -String $Path -Remove "\" + if (Test-Path $Path) + { + $Contains = $Contains.Trim() + $Count = 0 + $Files = Get-Childitem -Path $Path -Include $Include -Exclude $Exclude -Recurse -Force | select -First $First + ForEach ($File in $Files) + { + [Int32]$FoundIndex = -1 + [String]$FoundLine = '' + $Affected = 0 + $Content=Get-Content $File.PSPath + # Search for matches + For([Int32]$Count = 0; $Count -lt $Content.Length; $Count++) + { + $CurrentLine = $Content[$Count].Trim() + If(($FoundIndex -eq -1) -and ($CurrentLine.ToLowerInvariant().Contains($Contains.ToLowerInvariant()))) + { + $FoundIndex = $Count + $FoundLine = $CurrentLine + Break + } + } + # Evaluate search + If($FoundIndex -gt -1) + { + # Replace text inside of line + $NewLine = $FoundLine.Replace($Old, $New) + # Select before line, add $NewLine, select after line + $NewContent = (($Content | Select -First $FoundIndex) + ($NewLine + [Environment]::NewLine) + ($Content | select -Last ($Content.Length - $FoundIndex -1))) + } + else + { + # No Match Found + $NewContent = $Content + } + Set-Content $File.PSPath -Value $NewContent + $Affected = $Count + } + Write-Verbose "[Success] $Count items affected. $(Get-CurrentFile) at $(Get-CurrentLine)." + } + else + { + Write-Verbose "[OK] 0 items affected. $(Get-CurrentFile) at $(Get-CurrentLine)." + } +} +export-modulemember -function Update-TextByContains + +#----------------------------------------------------------------------- +# Update-TextByTable [-Path []] [-Replace []] +# [-Include [] [-Exclude []] +# +# Example: .\Update-TextByTable -Path \\source\path -Include *.cs +# -Replace @{'Old1' = 'New1' +# 'Old2' = 'New2' +# 'Old3' = 'New3'} +#----------------------------------------------------------------------- +function Update-TextByTable +{ + param ( + [Parameter(Mandatory=$true,ValueFromPipelineByPropertyName=$true)] + [string]$Path = $(throw '-Path is a required parameter.'), + [Parameter(Mandatory=$true,ValueFromPipelineByPropertyName=$true)] + [hashtable]$Replace = $(throw '-Replace is a required parameter.'), + [Parameter(Mandatory=$true,ValueFromPipelineByPropertyName=$true)] + [string[]]$Include = $(throw '-Include is a required parameter.'), + [string[]]$Exclude = "", + [Int32]$First = 100 + ) + Write-Verbose "Update-Text -Path $Path -Old $Old -New $New -Include $Include -Exclude $Exclude -First $First" + $Path = Remove-Suffix -String $Path -Remove "\" + if (Test-Path $Path) + { + Write-Verbose "Get-Childitem $Path -Include $Include -Exclude $Exclude -Recurse -Force | select -First $First" + $ConfigFiles=Get-Childitem $Path -Include $Include -Exclude $Exclude -Recurse -Force | select -First $First + Write-Verbose "ConfigFiles: $ConfigFiles" + $Count = 0 + foreach ($Item in $ConfigFiles) + { + Write-Verbose "Get-Content $Item.PSPath" + $fileLines = Get-Content $Item.PSPath + Write-Verbose "fileLines: $fileLines" + if($fileLines) + { + foreach($replaceItem in $Replace.GetEnumerator()) { + Write-Verbose "$fileLines.Replace($replaceItem.Key, $replaceItem.Value)" + $fileLines = $fileLines.Replace($replaceItem.Key, $replaceItem.Value) + } + Write-Verbose "Set-Content -Path $Item.PSPath -Value $fileLines -force" + Set-Content -Path $Item.PSPath -Value $fileLines -force + } + $Count = $Count + 1 + } + Write-Verbose "[Success] $Count items affected. $(Get-CurrentFile) at $(Get-CurrentLine)." + } + else + { + Write-Verbose "[OK] 0 items affected. $(Get-CurrentFile) at $(Get-CurrentLine)." + } +} +export-modulemember -function Update-TextByTable \ No newline at end of file diff --git a/.github/scripts/cd/Add-ManagedIdentityRole.ps1 b/.github/scripts/cd/Add-ManagedIdentityRole.ps1 new file mode 100644 index 0000000..0042cf8 --- /dev/null +++ b/.github/scripts/cd/Add-ManagedIdentityRole.ps1 @@ -0,0 +1,25 @@ +#----------------------------------------------------------------------- +# Add-RoleToManagedIdentity [ObjectId []] [ApplicationId []] [Permission []] +# +# Example: .\Add-RoleToManagedIdentity -ObjectId 00000000-0000-0000-0000-000000000000 -ApplicationId 00000000-0000-0000-0000-000000000000 +# -Permission User.Read.All +# +# The app IDs of the Microsoft APIs are the same in all tenants: +# Microsoft Graph: 00000003-0000-0000-c000-000000000000 +# SharePoint Online: 00000003-0000-0ff1-ce00-000000000000 +#----------------------------------------------------------------------- + +# *** +# *** Parameters +# *** +[Cmdletbinding()] +Param( + [Parameter(Mandatory = $true)][string]$ObjectId=$(throw 'ObjectId is a required parameter. (Your Managed Identity Object Id to get new roles)'), + [Parameter(Mandatory = $true)][string]$ApplicationId=$(throw 'ApplicationId is a required parameter. (The Application Id of the resource to access)'), + [Parameter(Mandatory = $true)][string]$Permission=$(throw 'Permission is a required parameter. (I.e. "User.Read.All", "User.Invite.All", "GroupMember.ReadWrite.All")') +) + +Connect-AzureAD +$app = Get-AzureADServicePrincipal -Filter "AppId eq '$ApplicationId'" +$role = $app.AppRoles | where Value -Like $Permission | Select-Object -First 1 +New-AzureADServiceAppRoleAssignment -Id $role.Id -ObjectId $ObjectId -PrincipalId $ObjectId -ResourceId $app.ObjectId diff --git a/.github/scripts/cd/Get-ManagedIdentityRole.ps1 b/.github/scripts/cd/Get-ManagedIdentityRole.ps1 new file mode 100644 index 0000000..6961d11 --- /dev/null +++ b/.github/scripts/cd/Get-ManagedIdentityRole.ps1 @@ -0,0 +1,29 @@ +#----------------------------------------------------------------------- +# Get-ManagedIdentityRole [ObjectId []] [ApplicationId []] +# +# Example: .\Get-ManagedIdentityRole -ObjectId 00000000-0000-0000-0000-000000000000 -ApplicationId 00000000-0000-0000-0000-000000000000 +# +# The app IDs of the Microsoft APIs are the same in all tenants: +# Microsoft Graph: 00000003-0000-0000-c000-000000000000 +# SharePoint Online: 00000003-0000-0ff1-ce00-000000000000 +#----------------------------------------------------------------------- + +# *** +# *** Parameters +# *** +[Cmdletbinding()] +Param( + [Parameter(Mandatory = $true)][string]$ObjectId=$(throw 'ObjectId is a required parameter. (Your Managed Identity Object Id to get new roles)'), + [Parameter(Mandatory = $true)][string]$ApplicationId=$(throw 'ApplicationId is a required parameter. (The Application Id of the resource to access)') +) + +Connect-AzureAD + +$app = Get-AzureADServicePrincipal -Filter "AppId eq '$ApplicationId'" + +$appRoles = Get-AzureADServiceAppRoleAssignment -ObjectId $app.ObjectId | where PrincipalId -eq $ObjectId + +foreach ($appRole in $appRoles) { + $role = $app.AppRoles | where Id -eq $appRole.Id | Select-Object -First 1 + write-host $role.Value +} diff --git a/.github/scripts/cd/Remove-ManagedIdentityRole.ps1 b/.github/scripts/cd/Remove-ManagedIdentityRole.ps1 new file mode 100644 index 0000000..c71329b --- /dev/null +++ b/.github/scripts/cd/Remove-ManagedIdentityRole.ps1 @@ -0,0 +1,30 @@ +#----------------------------------------------------------------------- +# Remove-ManagedIdentityRole [ObjectId []] [ApplicationId []] [Permission []] +# +# Example: .\Remove-ManagedIdentityRole -ObjectId 00000000-0000-0000-0000-000000000000 -ApplicationId 00000000-0000-0000-0000-000000000000 +# -Permission User.Read.All +# +# The app IDs of the Microsoft APIs are the same in all tenants: +# Microsoft Graph: 00000003-0000-0000-c000-000000000000 +# SharePoint Online: 00000003-0000-0ff1-ce00-000000000000 +#----------------------------------------------------------------------- + +# *** +# *** Parameters +# *** +[Cmdletbinding()] +Param( + [Parameter(Mandatory = $true)][string]$ObjectId=$(throw 'ObjectId is a required parameter. (Your Managed Identity Object Id to get new roles)'), + [Parameter(Mandatory = $true)][string]$ApplicationId=$(throw 'ApplicationId is a required parameter. (The Application Id of the resource to access)'), + [Parameter(Mandatory = $true)][string]$Permission=$(throw 'Permission is a required parameter. (I.e. "User.Read.All", "User.Invite.All", "GroupMember.ReadWrite.All")') +) + +Connect-AzureAD +$app = Get-AzureADServicePrincipal -Filter "AppId eq '$ApplicationId'" +$appRoles = Get-AzureADServiceAppRoleAssignment -ObjectId $app.ObjectId | where PrincipalId -eq $ObjectId +foreach ($appRole in $appRoles) { + $role = $app.AppRoles | where Id -eq $appRole.Id | Select-Object -First 1 + if ($Permission.Contains($role.Value)) { + Remove-AzureADServiceAppRoleAssignment -ObjectId $app.ObjectId -AppRoleAssignmentId $appRole.ObjectId + } +} diff --git a/.github/scripts/ci/Set-Version.ps1 b/.github/scripts/ci/Set-Version.ps1 new file mode 100644 index 0000000..e779d53 --- /dev/null +++ b/.github/scripts/ci/Set-Version.ps1 @@ -0,0 +1,86 @@ +#----------------------------------------------------------------------- +# Set-Version [-Path []] [-VersionToReplace []] [-Type []] +# +# Example: .\Set-Version -Path \\source\path -Major 1 +#----------------------------------------------------------------------- + +# *** +# *** Parameters +# *** +param +( + [Parameter(Mandatory=$true,ValueFromPipelineByPropertyName=$true)] + [string] $Path=$(throw '-Path is a required parameter. i.e. $(Build.SourcesDirectory)'), + [Version] $VersionToReplace='1.0.0', + [String] $Major='-1', + [String] $Minor='-1', + [String] $Revision='-1', + [String] $Build='-1', + [String] $Patch='-1', + [String] $PreRelease='-1', + [String] $CommitHash='-1' +) + +# *** +# *** Initialize +# *** +if ($IsWindows) { Set-ExecutionPolicy Unrestricted -Scope Process -Force } +$VerbosePreference = 'SilentlyContinue' #'Continue' +if ($MyInvocation.MyCommand -and $MyInvocation.MyCommand.Path) { + [String]$ThisScript = $MyInvocation.MyCommand.Path + [String]$ThisDir = Split-Path $ThisScript + [DateTime]$Now = Get-Date + Write-Debug "*****************************" + Write-Debug "*** Starting: $ThisScript on $Now" + Write-Debug "*****************************" + # Imports + Import-Module "$ThisDir/../System.psm1" +} else { + Write-Verbose "No script file context detected. Skipping module import." +} + +# *** +# *** Validate and cleanse +# *** +If($IsWindows){ + $Path = Set-Unc -Path $Path +} + +# *** +# *** Locals +# *** + +# *** +# *** Execute +# *** +$Major = $Major.Replace('-1', $VersionToReplace.ToString().Substring(0,1)) # Static 1, 2, 3 +$Minor = $Minor.Replace('-1', (Get-Date -UFormat '%Y').ToString().Substring(2,2)) # Year YYYY 2023 +$Revision = $Revision.Replace('-1', (Get-Date -UFormat '%j').ToString()) # DayOfYear D[DD]1-365 +$Build = $Build.Replace('-1', (Get-Date -UFormat '%H%M').ToString()) # HrMin 1937 +$Patch = $Patch.Replace('-1', (Get-Date -UFormat '%m').ToString()) # Month mm +$PreRelease = $PreRelease.Replace('-1', '') # -alpha +$CommitHash = $CommitHash.Replace('-1', '') # +204ff0a + + +# Version Formats +$FileVersion = "$Major.$Minor.$Revision.$Build" # Ref: https://learn.microsoft.com/en-us/dotnet/standard/library-guidance/versioning +$AssemblyVersion = "$Major.$Minor.0.0" +$InformationalVersion = "$Major.$Minor.$Revision$PreRelease$CommitHash" +$SemanticVersion = "$Major.$Minor.$Patch$PreRelease" +Write-Debug "FileVersion: $FileVersion SemanticVersion: $SemanticVersion AssemblyVersion: $AssemblyVersion InformationalVersion: $InformationalVersion" + +# Package.json version +Update-LineByContains -Path $Path -Contains 'version' -Line """version"": ""$FileVersion""," -Include package.json +# OpenApiConfigurationOptions.cs version +Update-LineByContains -Path $Path -Contains 'Version' -Line "Version = ""$AssemblyVersion""," -Include OpenApiConfigurationOptions.cs +# *.csproj C# Project files +Update-ContentsByTag -Path $Path -Value $FileVersion -Open '' -Close '' -Include *.csproj +# *.nuspec NuGet packages +Update-ContentsByTag -Path $Path -Value $SemanticVersion -Open '' -Close '' -Include *.nuspec +# Assembly.cs C# assembly manifest +Update-LineByContains -Path $Path -Contains "FileVersion(" -Line "[assembly: FileVersion(""$FileVersion"")]" -Include AssemblyInfo.cs +Update-LineByContains -Path $Path -Contains "AssemblyVersion(" -Line "[assembly: AssemblyVersion(""$AssemblyVersion"")]" -Include AssemblyInfo.cs +# *.vsixmanifest VSIX Visual Studio Templates +Update-TextByContains -Path $Path -Contains "]] [ClientSecret []] [TenantId []] +# [Folder []] [Files []] +# +# Example: .\Copy-CustomPolicy -ClientId 00000000-0000-0000-0000-000000000000 -ClientSecret xxxxx -TenantId 00000000-0000-0000-0000-000000000000 +# -Folder $(System.DefaultWorkingDirectory)/policy/B2CAssets/ +# -Files "TrustFrameworkBase.xml,TrustFrameworkLocalization.xml,TrustFrameworkExtensions.xml,SignUpOrSignin.xml,ProfileEdit.xml,PasswordReset.xml" +#----------------------------------------------------------------------- + +# *** +# *** Parameters +# *** +[Cmdletbinding()] +Param( + [Parameter(Mandatory = $true)][string]$ClientId, + [Parameter(Mandatory = $true)][string]$ClientSecret, + [Parameter(Mandatory = $true)][string]$TenantId, + [Parameter(Mandatory = $true)][string]$Folder, + [Parameter(Mandatory = $true)][string]$Files +) + +try { + $body = @{grant_type = "client_credentials"; scope = "https://graph.microsoft.com/.default"; client_id = $ClientId; client_secret = $ClientSecret } + + $response = Invoke-RestMethod -Uri https://login.microsoftonline.com/$TenantId/oauth2/v2.0/token -Method Post -Body $body + $token = $response.access_token + + $headers = New-Object "System.Collections.Generic.Dictionary[[String],[String]]" + $headers.Add("Content-Type", 'application/xml') + $headers.Add("Authorization", 'Bearer ' + $token) + + # Get the list of files to upload + $filesArray = $Files.Split(",") + + Foreach ($file in $filesArray) { + + $filePath = $Folder + $file.Trim() + + # Check if file exists + $FileExists = Test-Path -Path $filePath -PathType Leaf + + if ($FileExists) { + $policycontent = Get-Content $filePath + + # Optional: Change the content of the policy. For example, replace the tenant-name with your tenant name. + # $policycontent = $policycontent.Replace("your-tenant.onmicrosoft.com", "contoso.onmicrosoft.com") + + + # Get the policy name from the XML document + $match = Select-String -InputObject $policycontent -Pattern '(?<=\bPolicyId=")[^"]*' + + If ($match.matches.groups.count -ge 1) { + $PolicyId = $match.matches.groups[0].value + + Write-Host "Uploading the" $PolicyId "policy..." + + $graphuri = 'https://graph.microsoft.com/beta/trustframework/policies/' + $PolicyId + '/$value' + $response = Invoke-RestMethod -Uri $graphuri -Method Put -Body $policycontent -Headers $headers + + Write-Host "Policy" $PolicyId "uploaded successfully." + } + } + else { + $warning = "File " + $filePath + " couldn't be not found." + Write-Warning -Message $warning + } + } +} +catch { + Write-Host "StatusCode:" $_.Exception.Response.StatusCode.value__ + + $_ + + $streamReader = [System.IO.StreamReader]::new($_.Exception.Response.GetResponseStream()) + $streamReader.BaseStream.Position = 0 + $streamReader.DiscardBufferedData() + $errResp = $streamReader.ReadToEnd() + $streamReader.Close() + + $ErrResp + + exit 1 +} + +exit 0 \ No newline at end of file diff --git a/.github/scripts/iac/Install-AzureCli.ps1 b/.github/scripts/iac/Install-AzureCli.ps1 new file mode 100644 index 0000000..039eb74 --- /dev/null +++ b/.github/scripts/iac/Install-AzureCli.ps1 @@ -0,0 +1 @@ +Invoke-WebRequest -Uri https://aka.ms/installazurecliwindows -OutFile .\AzureCLI.msi; Start-Process msiexec.exe -Wait -ArgumentList '/I AzureCLI.msi /quiet' \ No newline at end of file diff --git a/.github/scripts/iac/New-AzServicePrinciple.ps1 b/.github/scripts/iac/New-AzServicePrinciple.ps1 new file mode 100644 index 0000000..35b1ce5 --- /dev/null +++ b/.github/scripts/iac/New-AzServicePrinciple.ps1 @@ -0,0 +1,57 @@ +#----------------------------------------------------------------------- +# New-AzServicePrinciple [-Name []] [-TenantId []] [-SubscriptionId []] +# +# Example: .\New-AzServicePrinciple -Name -TenantId -SubscriptionId +# CLI: az ad sp create-for-rbac --name "myApp" --role contributor \ +# --scopes /subscriptions/{subscription-id}/resourceGroups/{resource-group} --sdk-auth +#----------------------------------------------------------------------- + +# *** +# *** Parameters +# *** +param +( + [string] $Name=$(throw '-Name is a required parameter. (myco-product-environment)'), + [string] $TenantId=$(throw '-TenantId is a required parameter. (00000000-0000-0000-0000-000000000000)'), + [string] $SubscriptionId=$(throw '-SubscriptionId is a required parameter. (00000000-0000-0000-0000-000000000000)') +) + +# *** +# *** Initialize +# *** +if ($IsWindows) { Set-ExecutionPolicy Unrestricted -Scope Process -Force } +$VerbosePreference = 'SilentlyContinue' #'Continue' +[String]$ThisScript = $MyInvocation.MyCommand.Path +[String]$ThisDir = Split-Path $ThisScript +[DateTime]$Now = Get-Date +Set-Location $ThisDir # Ensure our location is correct, so we can use relative paths +Write-Host "*****************************" +Write-Host "*** Starting: $ThisScript on $Now" +Write-Host "*****************************" +# Imports +Import-Module "../System.psm1" +Install-Module -Name Az.Accounts -AllowClobber -Scope CurrentUser +Install-Module -Name Az.Resources -AllowClobber -Scope CurrentUser + +# *** +# *** Auth +# *** +Write-Host "*** Auth ***" +Connect-AzAccount -Tenant $TenantId -Subscription $SubscriptionId + +$sp = New-AzADServicePrincipal -DisplayName $Name +$clientsec = [System.Net.NetworkCredential]::new("", $sp.passwordCredentials.secretText).Password +$jsonresp = + @{clientId=$sp.appId + clientSecret=$clientsec + subscriptionId=$SubscriptionId + tenantId=$TenantId + activeDirectoryEndpointUrl='https://login.microsoftonline.com' + resourceManagerEndpointUrl='https://management.azure.com/' + activeDirectoryGraphResourceId='https://graph.windows.net/' + sqlManagementEndpointUrl='https://management.core.windows.net:8443/' + galleryEndpointUrl='https://gallery.azure.com/' + managementEndpointUrl='https://management.core.windows.net/' + } +$jsonresp | ConvertTo-Json + diff --git a/.github/scripts/iac/New-SelfSignedCert.ps1 b/.github/scripts/iac/New-SelfSignedCert.ps1 new file mode 100644 index 0000000..9eed904 --- /dev/null +++ b/.github/scripts/iac/New-SelfSignedCert.ps1 @@ -0,0 +1,88 @@ +#################################################################################### +# To execute +# 1. Run Powershell as ADMINistrator +# 2. In powershell, set security polilcy for this script: +# Set-ExecutionPolicy Unrestricted -Scope Process -Force +# 3. Change directory to the script folder: +# CD C:\Scripts (wherever your script is) +# 4. In powershell, run script: +# .\New-SelfSignedCert.ps1 -Path Certs\ -File b2c-dev-SAML.pfx -Domain b2c-dev-Saml.MyCo.onmicrosoft.com +#################################################################################### + +param ( + [Parameter(Mandatory=$true,ValueFromPipelineByPropertyName=$true)] + [string]$Path = $(throw '-Path is a required parameter.'), #.\Certs + [Parameter(Mandatory=$true,ValueFromPipelineByPropertyName=$true)] + [string]$File = $(throw '-File is a required parameter.'), #MyCert.pfx + [Parameter(Mandatory=$true,ValueFromPipelineByPropertyName=$true)] + [string]$Domain = $(throw '-Domain is a required parameter.'), #dev.mydomain.com, yourappname.yourtenant.onmicrosoft.com + [string]$CertStoreLocation = 'Cert:\LocalMachine\My', + [int]$ExpirationMonths = 24, + [int]$KeyLength = 2048, + [SecureString]$Password = (ConvertTo-SecureString -String (New-Guid).ToString() -Force -AsPlainText) +) + +#################################################################################### +Set-ExecutionPolicy Unrestricted -Scope Process -Force +$VerbosePreference = 'SilentlyContinue' # 'SilentlyContinue' # 'Continue' +if ($MyInvocation.MyCommand.Path) { + [String]$ThisScript = $MyInvocation.MyCommand.Path +} else { + [String]$ThisScript = (Get-Location).Path +} +[String]$ThisDir = Split-Path $ThisScript +Set-Location $ThisDir # Ensure our location is correct, so we can use relative paths + +Write-Host "*****************************" +Write-Host "*** Starting: $ThisScript On: $(Get-Date)" +Write-Host "*****************************" +#################################################################################### +# Imports +Import-Module "../../System.psm1" +$crlf = "`r`n" + +$Path = Remove-Suffix -String $Path -Remove "\" +$File = Remove-Prefix -String $File -Remove "." +$File = Remove-Prefix -String $File -Remove "." +$File = Remove-Prefix -String $File -Remove "\" +[String] $FilePath = "$Path\$File" + +New-Path $Path + +# Remove existing cert +$foundCert = Get-ChildItem -Path $CertStoreLocation | Where-Object Subject -eq "CN=$Domain" | Select-Object * +if($foundCert.Thumbprint.Length -gt 0) +{ + ## Found, remove it now + $thumbprint=$foundCert.Thumbprint + Write-Verbose "$crlf[Verbose] Found thumbprint $thumbprint $crlf" + Get-ChildItem "Cert:\LocalMachine\My\$thumbprint" | Remove-Item + Write-Verbose "$crlf[Verbose] Removed thumbprint $thumbprint $crlf" + $thumbprint = "" +} + +$EKU = @("1.3.6.1.5.5.7.3.1") # Server Authentication +# $EKU = @("1.3.6.1.5.5.7.3.2") # Client Authentication instead of Server Authentication +# $EKU = @("1.3.6.1.5.5.7.3.1", "1.3.6.1.5.5.7.3.2") # Both Server and Client Authentication + +$Certificate = New-SelfSignedCertificate ` + -KeyExportPolicy Exportable ` + -Subject "CN=$Domain" ` + -KeyAlgorithm RSA ` + -KeyLength $KeyLength ` + -KeyUsage DigitalSignature ` + -NotAfter (Get-Date).AddMonths($ExpirationMonths) ` + -CertStoreLocation $CertStoreLocation ` + -TextExtension @("2.5.29.37={text}$($EKU -join ',')") +$thumbprint = $Certificate.Thumbprint +Write-Host "$crlf[Info] Created $CertStoreLocation with thumbprint $thumbprint $crlf" + +# Export Pfx + +# Export Pfx using the provided SecureString password +Export-PfxCertificate -Cert "$CertStoreLocation\$thumbprint" -FilePath $FilePath -Password $Password +Write-Host "$crlf[Info] Created $FilePath with provided password $crlf" + +# Export Cer +Export-Certificate -Cert "$CertStoreLocation\$thumbprint" -Filepath "$FilePath.cer" #-Type CERT -NoClobber +Write-Host "$crlf[Info] Created $FilePath.cer $crlf" diff --git a/.github/scripts/iac/Remove-LandingZone.ps1 b/.github/scripts/iac/Remove-LandingZone.ps1 new file mode 100644 index 0000000..21a2675 --- /dev/null +++ b/.github/scripts/iac/Remove-LandingZone.ps1 @@ -0,0 +1,48 @@ +#----------------------------------------------------------------------- +# Remove-LandingZone [-TenantId []] [-SubscriptionId []] +# +# Example: .\Remove-LandingZone -TenantId -SubscriptionId -ResourceGroup -KeyVault -StorageAccount -Workspace +#----------------------------------------------------------------------- + +# *** +# *** Parameters +# *** +param +( + [string] $TenantId=$(throw '-TenantId is a required parameter. (00000000-0000-0000-0000-000000000000)'), + [string] $SubscriptionId=$(throw '-TenantId is a required parameter. (00000000-0000-0000-0000-000000000000)'), + [string] $ResourceGroup=$(throw '-ResourceGroup is a required parameter. (rg-PRODUCT-ENVIRONMENT-001)'), + [string] $KeyVault=$(throw '-KeyVault is a required parameter. (kv-PRODUCT-ENVIRONMENT-001)'), + [string] $StorageAccount=$(throw '-StorageAccount is a required parameter. (stPRODUCTENVIRONMENT001)'), + [string] $Workspace=$(throw '-Workspace is a required parameter. (work-PRODUCT-ENVIRONMENT-001)') +) + +# *** +# *** Initialize +# *** +if ($IsWindows) { Set-ExecutionPolicy Unrestricted -Scope Process -Force } +$VerbosePreference = 'SilentlyContinue' #'Continue' +[String]$ThisScript = $MyInvocation.MyCommand.Path +[String]$ThisDir = Split-Path $ThisScript +[DateTime]$Now = Get-Date +Set-Location $ThisDir # Ensure our location is correct, so we can use relative paths +Write-Host "*****************************" +Write-Host "*** Starting: $ThisScript on $Now" +Write-Host "*****************************" +# Imports +Import-Module "../System.psm1" +Install-Module -Name Az.Accounts -AllowClobber -Scope CurrentUser +Install-Module -Name Az.Resources -AllowClobber -Scope CurrentUser + +# *** +# *** Auth +# *** +Write-Host "*** Auth ***" +Connect-AzAccount -Tenant $TenantId -Subscription $SubscriptionId + +# *** +# *** Execute +# *** +Remove-AzStorageAccount -ResourceGroupName $ResourceGroup -AccountName $StorageAccount +Remove-AzKeyVault -VaultName $KeyVault -PassThru +Remove-AzOperationalInsightsWorkspace -ResourceGroupName $ResourceGroup -Name $Workspace -ForceDelete \ No newline at end of file diff --git a/.github/scripts/iac/Set-AzKeyVaultPolicy.ps1 b/.github/scripts/iac/Set-AzKeyVaultPolicy.ps1 new file mode 100644 index 0000000..8a9fac3 --- /dev/null +++ b/.github/scripts/iac/Set-AzKeyVaultPolicy.ps1 @@ -0,0 +1,43 @@ +#----------------------------------------------------------------------- +# Set-AzKeyVaultPolicy [-Name []] [-ObjectId []] [-SecretPermissions []] +# +# Example: .\Set-AzKeyVaultPolicy -Name kv-PRODUCT-ENVIRONMENT-001 -ObjectId 00000000-0000-0000-0000-000000000000 -SecretPermissions list get +# CLI: +# az keyvault set-policy --name "" --spn --secret-permissions list get +# az keyvault update --name "" --resource-group "" --enabled-for-deployment "true" +#----------------------------------------------------------------------- + +# *** +# *** Parameters +# *** +param +( + [string] $Name=$(throw '-Name is a required parameter. (kv-PRODUCT-ENVIRONMENT-001)'), + [string] $ObjectId=$(throw '-ObjectId is a required parameter. (00000000-0000-0000-0000-000000000000)'), + [string] $SecretPermissions='list get' +) + +# *** +# *** Initialize +# *** +if ($IsWindows) { Set-ExecutionPolicy Unrestricted -Scope Process -Force } +$VerbosePreference = 'SilentlyContinue' #'Continue' +[String]$ThisScript = $MyInvocation.MyCommand.Path +[String]$ThisDir = Split-Path $ThisScript +[DateTime]$Now = Get-Date +Set-Location $ThisDir # Ensure our location is correct, so we can use relative paths +Write-Host "*****************************" +Write-Host "*** Starting: $ThisScript on $Now" +Write-Host "*****************************" +# Imports +Import-Module "../System.psm1" +Install-Module -Name Az.Accounts -AllowClobber -Scope CurrentUser +Install-Module -Name Az.Resources -AllowClobber -Scope CurrentUser + +# *** +# *** Auth +# *** +Write-Host "*** Auth ***" +Connect-AzAccount -Tenant $TenantId -Subscription $SubscriptionId + +Set-AzKeyVaultAccessPolicy -VaultName $Name -ObjectId $ObjectId -PermissionsToSecrets $SecretPermissions -EnabledForDeployment \ No newline at end of file diff --git a/.github/scripts/repo/New-GithubRepoBootstrap.ps1 b/.github/scripts/repo/New-GithubRepoBootstrap.ps1 new file mode 100644 index 0000000..e5c5a80 --- /dev/null +++ b/.github/scripts/repo/New-GithubRepoBootstrap.ps1 @@ -0,0 +1,232 @@ +# ================================ +# GitHub repo bootstrap (PowerShell) +# Creates repo, enables security, branch policy, environments +# Requires: GitHub CLI (gh) + authenticated session +# ================================ +# +# Pre-requisites (auto-executed): +# - Installs GitHub CLI if not present +# - Prompts for GitHub authentication if not already authenticated +# +# Example usage (copy/paste): +# +# .\New-GithubRepoBootstrap.ps1 -Owner goodtocode -Repo my-repo -Visibility private +# .\New-GithubRepoBootstrap.ps1 -Owner goodtocode -Repo my-oss-repo -Oss +# +param( + [Parameter(Mandatory=$true)][string]$Owner, + [Parameter(Mandatory=$true)][string]$Repo, + [ValidateSet('public','private')][string]$Visibility = 'private', + [switch]$Oss # if set, will use MIT license and public visibility +) + +# ---- 0) Create repository with README, .gitignore, license (if OSS) +$license = $Oss.IsPresent ? 'mit' : $null +$vis = $Oss.IsPresent ? 'public' : $Visibility + +if (-not (Get-Command gh -ErrorAction SilentlyContinue)) { + Write-Host "GitHub CLI not found. Installing via winget..." -ForegroundColor Red + winget install --id GitHub.cli -e --silent + Write-Host "GitHub CLI installed. Please restart your terminal or PowerShell session, then re-run this script." -ForegroundColor Red + exit +} + +# Check authentication +$ghAuth = gh auth status 2>$null +if (-not $ghAuth) { + Write-Host "GitHub CLI not authenticated. Please login." -ForegroundColor Red + gh auth login +} + +gh @createArgs | Out-Null + +Write-Host "Checking if repo exists..." +$repoExists = gh repo view "$Owner/$Repo" 2>&1 +Write-Host "Repo view output: $repoExists" + +# Check if repo exists before creating +$repoExists = gh repo view "$Owner/$Repo" 2>$null +if (-not $repoExists) { + $createArgs = @( + 'repo','create', "$Owner/$Repo", + '--' + $vis, + '--add-readme', + '--gitignore','VisualStudio' + ) + if ($license) { $createArgs += @('--license', $license) } + gh @createArgs | Out-Null + Write-Host "Created repo $Owner/$Repo" +} else { + Write-Host "Repo $Owner/$Repo already exists. Skipping creation." +} + +# ---- 1) Allow auto-merge (repo-level toggle) +# Enables future workflows to set --auto on PRs +if ($repoExists) { + $autoMergeStatus = gh api "repos/$Owner/$Repo" | ConvertFrom-Json | Select-Object -ExpandProperty allow_auto_merge + if (-not $autoMergeStatus) { + gh api -X PATCH "repos/$Owner/$Repo" -f allow_auto_merge=true | Out-Null + Write-Host "Enabled auto-merge." + } else { + Write-Host "Auto-merge already enabled." + } +} + +# ---- 2) Enable security & analysis: Secret Scanning + Push Protection +# (security_and_analysis object) +$secJson = @' +{ + "secret_scanning": { "status": "enabled" }, + "secret_scanning_push_protection": { "status": "enabled" } +} +'@ +if ($repoExists) { + $secStatus = gh api "repos/$Owner/$Repo" | ConvertFrom-Json | Select-Object -ExpandProperty security_and_analysis + if ($secStatus.secret_scanning.status -ne "enabled" -or $secStatus.secret_scanning_push_protection.status -ne "enabled") { + gh api -X PATCH "repos/$Owner/$Repo" -f "security_and_analysis=$secJson" | Out-Null + Write-Host "Enabled secret scanning and push protection." + } else { + Write-Host "Secret scanning and push protection already enabled." + } +} + +# ---- 3) Enable Dependabot alerts & security updates (and add version updates file) +# Alerts / Security updates are repository settings endpoints. +# (If your org enforces these by default, you can skip.) +# List/enable endpoints are under Repositories API group. +# Add dependabot.yml (version updates) if you want scheduled updates: +$dependabotYml = @" +version: 2 +updates: + - package-ecosystem: "nuget" + directory: "/" + schedule: + interval: "weekly" + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" +"@ + +$tmp = New-TemporaryFile +$dependabotYml | Set-Content -NoNewline -Path $tmp +if ($repoExists) { + $fileExists = gh api "/repos/$Owner/$Repo/contents/.github/dependabot.yml" 2>$null + if (-not $fileExists) { + gh api --method PUT "/repos/$Owner/$Repo/contents/.github/dependabot.yml" ` + -f message="chore: add dependabot version updates" ` + -f content="$( [Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes((Get-Content $tmp -Raw))) )" ` + -f branch="main" | Out-Null + Write-Host "Added dependabot.yml." + } else { + Write-Host "dependabot.yml already exists. Skipping." + } +} +Remove-Item $tmp -Force + +# ---- 4) (Option A) Add Advanced CodeQL workflow file for full automation +# Or Advanced setup is workflow-based & fully automatable. [8](https://graphite.com/guides/github-merge-queue) +$codeqlYml = @" +name: CodeQL +on: + push: + branches: [ main, dev ] + pull_request: + branches: [ main, dev ] + schedule: + - cron: '0 6 * * 1' +permissions: + contents: read + security-events: write +jobs: + analyze: + runs-on: ubuntu-latest + strategy: + matrix: + language: [ 'csharp' ] + steps: + - uses: actions/checkout@v4 + - uses: github/codeql-action/init@v4 + with: + languages: '`${{ matrix.language }}' + - uses: github/codeql-action/autobuild@v3 + - uses: github/codeql-action/analyze@v3 +"@ + +$tmp = New-TemporaryFile +$codeqlYml | Set-Content -NoNewline -Path $tmp +if ($repoExists) { + $fileExists = gh api "/repos/$Owner/$Repo/contents/.github/workflows/codeql-analysis.yml" 2>$null + if (-not $fileExists) { + gh api --method PUT "/repos/$Owner/$Repo/contents/.github/workflows/codeql-analysis.yml" ` + -f message="ci: add CodeQL advanced workflow" ` + -f content="$( [Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes((Get-Content $tmp -Raw))) )" ` + -f branch="main" | Out-Null + Write-Host "Added CodeQL workflow." + } else { + Write-Host "CodeQL workflow already exists. Skipping." + } +} +Remove-Item $tmp -Force + + +# ---- 5) Branch protection for main (require PRs, strict checks etc.) +# You can add named checks later once they appear (ci, CodeQL) to hard-enforce. +if ($repoExists) { + $protectionResult = gh api "repos/$Owner/$Repo/branches/main/protection" 2>&1 + if ($protectionResult -match 'Branch not protected' -or $protectionResult -match '404') { + $body = @{ + required_status_checks = $null + enforce_admins = $false + required_pull_request_reviews = @{ + required_approving_review_count = 1 + } + restrictions = $null + } | ConvertTo-Json -Compress + + $body | gh api -X PUT "repos/$Owner/$Repo/branches/main/protection" --input - -H "Accept: application/vnd.github+json" + Write-Host "Branch protection added." + } else { + Write-Host "Branch protection already exists. Skipping." + } +} +# Ref: Branch protection REST. [5](https://stackoverflow.com/questions/71623045/automatic-merge-after-tests-pass-using-actions) + +# ---- 6) Create Environments: development & production +# (You can set branch policy + required reviewers) +# Create/Update Environment (PUT) + optional deployment branch policy & protection rules +# Note: required_reviewers require usernames or team slugs (max 6). [7](https://docs.github.com/en/rest/deployments/environments)[12](https://docs.github.com/en/actions/reference/workflows-and-actions/deployments-and-environments) + +# development +if ($repoExists) { + Write-Host "Checking if development environment exists..." + $devEnvResponse = gh api "repos/$Owner/$Repo/environments/development" 2>&1 + if ($devEnvResponse -match '"Not Found"' -or $devEnvResponse -match '404') { + Write-Host "Development environment does not exist. Creating..." + gh api -X PUT "repos/$Owner/$Repo/environments/development" ` + -H "Accept: application/vnd.github+json" | Out-Null + Write-Host "Development environment created." + } else { + Write-Host "Development environment already exists. Skipping." + } +} +# Optionally add a custom branch policy pattern for dev (requires extra POST endpoint under env policies). +# See community examples for adding custom branch policies after creation. [13](https://stackoverflow.com/questions/70943164/create-environment-for-repository-using-gh) + +# NOTE: Replace reviewers with concrete users/teams via separate calls if needed. +if ($repoExists) { + Write-Host "Checking if production environment exists..." + $prodEnvResponse = gh api "repos/$Owner/$Repo/environments/production" 2>&1 + Write-Host "Prod env output: $prodEnvResponse" + if ($prodEnvResponse -match '"Not Found"' -or $prodEnvResponse -match '404') { + Write-Host "Production environment does not exist. Creating..." + gh api -X PUT "repos/$Owner/$Repo/environments/production" ` + -H "Accept: application/vnd.github+json" | Out-Null + Write-Host "Production environment created." + } else { + Write-Host "Production environment already exists. Skipping." + } +} +# Ref: Environments API supports branch policy and protection rules. [7](https://docs.github.com/en/rest/deployments/environments) + +Write-Host "✅ Bootstrap completed for $Owner/$Repo" diff --git a/.github/scripts/repo/New-GithubSecret.ps1 b/.github/scripts/repo/New-GithubSecret.ps1 new file mode 100644 index 0000000..fad416a --- /dev/null +++ b/.github/scripts/repo/New-GithubSecret.ps1 @@ -0,0 +1,69 @@ +# ================================ +# GitHub repo secrets (PowerShell) +# Creates secrets +# Requires: GitHub CLI (gh) + authenticated session +# ================================ +# +# Pre-requisites (auto-executed): +# - Installs GitHub CLI if not present +# - Prompts for GitHub authentication if not already authenticated +# +# Example usage (copy/paste): +# +# .\New-GithubSecret.ps1 -Owner goodtocode -Repo my-repo -Environment development -SecretName MY_SECRET -SecretValue "secret-value" +# +param( + [Parameter(Mandatory=$true)][string]$Owner, + [Parameter(Mandatory=$true)][string]$Repo, + [Parameter(Mandatory=$true)][string]$Environment, + [Parameter(Mandatory=$true)][string]$SecretName, + [Parameter(Mandatory=$true)][string]$SecretValue +) + +if (-not (Get-Command gh -ErrorAction SilentlyContinue)) { + Write-Host "GitHub CLI not found. Installing via winget..." -ForegroundColor Red + winget install --id GitHub.cli -e --silent + Write-Host "GitHub CLI installed. Please restart your terminal or PowerShell session, then re-run this script." -ForegroundColor Red + exit +} + +# Check authentication +$ghAuth = gh auth status 2>$null +if (-not $ghAuth) { + Write-Host "GitHub CLI not authenticated. Please login." -ForegroundColor Red + gh auth login +} + +gh @createArgs | Out-Null + +Write-Host "Checking if repo exists..." +$repoExists = gh repo view "$Owner/$Repo" 2>$null +if (-not $repoExists) { + Write-Host "Repo $Owner/$Repo does not exist. Cannot create secrets for a non-existent repository." + exit +} else { + Write-Host "Repo $Owner/$Repo already exists. Skipping creation." +} + +# Check Environment + if ($repoExists) { + Write-Host "Checking if $Environment environment exists..." + $envExist = gh api "repos/$Owner/$Repo/environments/$Environment" 2>$null + if (-not $envExist) { + Write-Host "$Environment environment does not exist. Cannot create secrets for a non-existent environment." + } +} + +# Add environment secrets (examples) +if ($repoExists -and $envExist) { + Write-Host "Checking if $SecretName secret exists in $Environment..." + $devSecretList = gh secret list --repo "$Owner/$Repo" --env "$Environment" 2>&1 + if ($devSecretList -match '"Not Found"' -or $devSecretList -match '404' -or -not ($devSecretList | Select-String "$SecretName")) { + Write-Host "$SecretName secret does not exist in $Environment. Creating..." + gh secret set "$SecretName" --body "$SecretValue" --repo "$Owner/$Repo" --env "$Environment" + Write-Host "$SecretName secret added to $Environment." + } else { + Write-Host "$SecretName already exists in $Environment. Skipping." + } +} +Write-Host "✅ Secrets completed for $Owner/$Repo in $Environment" diff --git a/.github/scripts/repo/az-create-for-rbac.cmd b/.github/scripts/repo/az-create-for-rbac.cmd new file mode 100644 index 0000000..e3ed6c0 --- /dev/null +++ b/.github/scripts/repo/az-create-for-rbac.cmd @@ -0,0 +1,19 @@ +REM +ECHO *** az-create-for-rbac.cmd *** +REM *** Usage: az-create-for-rbac.cmd + +REM *** Locals +SET FullPath=%1 +SET FullPath=%FullPath:"=% + + + +exit 0 + + + +az login + +az ad sp create-for-rbac --name "myApp" --role contributor \ + --scopes /subscriptions/{subscription-id}/resourceGroups/{resource-group} \ + --sdk-auth \ No newline at end of file diff --git a/.github/workflows/gtc-assertion-ci-cd-nuget.yml b/.github/workflows/gtc-assertion-ci-cd-nuget.yml new file mode 100644 index 0000000..29e0636 --- /dev/null +++ b/.github/workflows/gtc-assertion-ci-cd-nuget.yml @@ -0,0 +1,134 @@ +name: CI/CD Build, Test and Deploy + +on: + pull_request: + branches: + - main + paths: + - .github/workflows/gtc-assertion-nuget.yml + - src/** + push: + branches: + - main + paths: + - .github/workflows/gtc-assertion-nuget.yml + - src/** + workflow_dispatch: + inputs: + environment: + description: 'Environment to run' + required: true + default: 'development' + mode: + description: 'Running mode' + +permissions: + id-token: write + contents: read + security-events: write + +jobs: + ci: + name: 'CI Build, Test, Code QL, Publish' + runs-on: ubuntu-latest + if: github.event_name == 'pull_request' || github.event_name == 'workflow_dispatch' + environment: development + strategy: + matrix: + DOTNET_VERSION: ['9.x'] + + env: + RUNTIME_ENV: 'Development' + SRC_PATH: './src' + SRC_SLN: 'Goodtocode.Assertion.sln' + PROJECT_PATH: 'Goodtocode.Assertion' + PROJECT_FILE: 'Goodtocode.Assertion.csproj' + TEST_PATH: 'Goodtocode.Assertion.Tests' + TEST_PROJECT: 'Goodtocode.Assertion.Tests.csproj' + SCRIPTS_PATH: './.github/scripts' + + steps: + - name: checkout + uses: actions/checkout@v3 + + - name: dotnet version ${{ matrix.DOTNET_VERSION }} + uses: actions/setup-dotnet@v4 + with: + dotnet-version: ${{ matrix.DOTNET_VERSION }} + + - name: Set-Version.ps1 + run: | + $version = ${{ env.SCRIPTS_PATH }}/ci/Set-Version.ps1 -Path ${{ env.SRC_PATH }} -VersionToReplace 1.0.0 + echo $version + echo "VERSION=$version" >> $GITHUB_ENV + shell: pwsh + + - name: pipeline configuration secrets + run: | + echo "ASPNETCORE_ENVIRONMENT=${{ env.RUNTIME_ENV }}" >> $GITHUB_ENV + shell: pwsh + + - name: Initialize CodeQL + uses: github/codeql-action/init@v4 + with: + languages: csharp + + - name: Build + run: | + dotnet build ${{ env.SRC_PATH }}/${{ env.SRC_SLN }} --configuration Release + shell: pwsh + + - name: Test + run: | + mkdir -p TestResults-${{ matrix.DOTNET_VERSION }} + dotnet test ${{ env.SRC_PATH }}/${{ env.TEST_PATH }}/${{ env.TEST_PROJECT }} --logger "trx;LogFileName=test_results.trx" --results-directory TestResults-${{ matrix.DOTNET_VERSION }} --verbosity normal + shell: pwsh + + - name: Upload test results + uses: actions/upload-artifact@v4 + with: + name: dotnet-results-${{ matrix.DOTNET_VERSION }} + path: TestResults-${{ matrix.DOTNET_VERSION }} + if: ${{ always() }} + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v4 + + cd: + name: 'CD Pack and Publish to NuGet.org' + runs-on: ubuntu-latest + needs: ci + if: | + github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v') + environment: production + strategy: + matrix: + DOTNET_VERSION: ['9.x'] + env: + SRC_PATH: './src' + PROJECT_PATH: 'Goodtocode.Assertion' + PROJECT_FILE: 'Goodtocode.Assertion.csproj' + NUGET_OUTPUT: 'nupkg_output' + steps: + - name: checkout + uses: actions/checkout@v3 + + - name: Setup .NET ${{ matrix.DOTNET_VERSION }} + uses: actions/setup-dotnet@v4 + with: + dotnet-version: ${{ matrix.DOTNET_VERSION }} + + - name: Restore dependencies + run: dotnet restore ${{ env.SRC_PATH }}/${{ env.PROJECT_PATH }}/${{ env.PROJECT_FILE }} + + - name: Build + run: dotnet build ${{ env.SRC_PATH }}/${{ env.PROJECT_PATH }}/${{ env.PROJECT_FILE }} --configuration Release --no-restore + + - name: Test + run: dotnet test ${{ env.SRC_PATH }}/${{ env.PROJECT_PATH }}/${{ env.PROJECT_FILE }} --configuration Release --no-build --verbosity normal + + - name: Pack NuGet package + run: dotnet pack ${{ env.SRC_PATH }}/${{ env.PROJECT_PATH }}/${{ env.PROJECT_FILE }} --configuration Release --no-build --output ${{ env.NUGET_OUTPUT }} + + - name: Publish to NuGet.org + run: dotnet nuget push ${{ env.NUGET_OUTPUT }}/*.nupkg --api-key ${{ secrets.NUGET_API_KEY }} --source https://api.nuget.org/v3/index.json --skip-duplicate \ No newline at end of file diff --git a/src/Directory.Build.props b/src/Directory.Build.props new file mode 100644 index 0000000..9be778e --- /dev/null +++ b/src/Directory.Build.props @@ -0,0 +1,38 @@ + + + + true + true + Recommended + latest + false + 12 + enable + enable + + + + + false + + + + + disable + + + + True + + + + $([System.IO.Path]::GetDirectoryName($([MSBuild]::GetPathOfFileAbove('.gitignore', '$(MSBuildThisFileDirectory)')))) + + + + + + <_Parameter1>false + + + \ No newline at end of file diff --git a/src/Directory.Build.targets b/src/Directory.Build.targets new file mode 100644 index 0000000..f4fc12c --- /dev/null +++ b/src/Directory.Build.targets @@ -0,0 +1,13 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Get-CodeCoverage.ps1 b/src/Get-CodeCoverage.ps1 new file mode 100644 index 0000000..5df8c52 --- /dev/null +++ b/src/Get-CodeCoverage.ps1 @@ -0,0 +1,64 @@ +#################################################################################### +# To execute +# 1. In powershell, set security polilcy for this script: +# Set-ExecutionPolicy Unrestricted -Scope Process -Force +# 2. Change directory to the script folder: +# CD src (wherever your script is) +# 3. In powershell, run script: +# .\Get-CodeCoverage.ps1 -TestProjectFilter 'MyTests.*.csproj' -ProdPackagesOnly -ProductionAssemblies 'MyApp.Core','MyApp.Web' +# This script is for local use to analyze code coverage in more detail using HTML report. +#################################################################################### + +Param( + [string]$TestProjectFilter = 'Tests.*.csproj', + [switch]$ProdPackagesOnly = $false, + [string[]]$ProductionAssemblies = @( + "Cannery.Insights.Core.Application", + "Cannery.Insights.Presentation.WebApi", + "Cannery.Insights.Presentation.Blazor" + ) +) +#################################################################################### +if ($IsWindows) {Set-ExecutionPolicy Unrestricted -Scope Process -Force} +$VerbosePreference = 'SilentlyContinue' # 'Continue' +#################################################################################### + +& dotnet tool install -g coverlet.console +& dotnet tool install -g dotnet-reportgenerator-globaltool + +$timestamp = Get-Date -Format "yyyyMMdd-HHmmss" +$scriptPath = Get-Item -Path $PSScriptRoot +$coverageOutputPath = Join-Path $scriptPath "TestResults\Coverage\$timestamp" +$reportOutputPath = Join-Path $scriptPath "TestResults\Reports\$timestamp" + +New-Item -ItemType Directory -Force -Path $coverageOutputPath +New-Item -ItemType Directory -Force -Path $reportOutputPath + +# Find tests for projects with 'Tests.*.csproj' +$testProjects = Get-ChildItem $scriptPath -Filter $TestProjectFilter -Recurse +Write-Host "Found $($testProjects.Count) test projects." +foreach ($project in $testProjects) { + $testProjectPath = $project.FullName + Write-Host "Running tests for project: $($testProjectPath)" + + $buildOutput = Join-Path -Path $project.Directory.FullName -ChildPath "bin\Debug\net9.0\$($project.BaseName).dll" + $coverageFile = Join-Path $coverageOutputPath "coverage.cobertura.xml" + Write-Host "Analyzing code coverage for: $buildOutput" + coverlet $buildOutput --target "dotnet" --targetargs "test $($project.FullName) --no-build" --format cobertura --output $coverageFile + +} + +# Generate HTML report +if ($ProdPackagesOnly) { + $assemblyFilters = ($ProductionAssemblies | ForEach-Object { "+$_" }) -join ";" + $assemblyFilters = ($ProductionAssemblies | ForEach-Object { "+$_" }) -join ";" + & reportgenerator -reports:"$coverageOutputPath/**/coverage.cobertura.xml" -targetdir:$reportOutputPath -reporttypes:Html -assemblyfilters:$assemblyFilters +} +else { + & reportgenerator -reports:"$coverageOutputPath/**/coverage.cobertura.xml" -targetdir:$reportOutputPath -reporttypes:Html +} + +Write-Host "Code coverage report generated at: $reportOutputPath" + +$reportIndexHtml = Join-Path $reportOutputPath "index.html" +Invoke-Item -Path $reportIndexHtml \ No newline at end of file diff --git a/src/Goodtocode.Assertion.Tests/AssertionScopeTests.cs b/src/Goodtocode.Assertion.Tests/AssertionScopeTests.cs new file mode 100644 index 0000000..aa68e6d --- /dev/null +++ b/src/Goodtocode.Assertion.Tests/AssertionScopeTests.cs @@ -0,0 +1,80 @@ +using Goodtocode.Assertion; + +namespace Goodtocode.Assertion.Tests; + +[TestClass] +public sealed class AssertionScopeTests +{ + private enum CommandResponseType { Successful, Failed } + + [TestMethod] + public void AssertionScopeSuccessfulResponseNoException() + { + // Arrange + var responseType = CommandResponseType.Successful; + Exception? exception = null; + + // Act & Assert + using (new AssertionScope()) + { + responseType.ShouldBe(CommandResponseType.Successful); + exception.ShouldBeNull($"An exception was thrown: {exception?.Message}. Inner exception: {exception?.InnerException?.Message}"); + } + } + + [TestMethod] + public void AssertionScopeFailedResponseThrowsOnDispose() + { + // Arrange + var responseType = CommandResponseType.Failed; + Exception? exception = null; + + // Act & Assert + Assert.ThrowsException(() => + { + using (new AssertionScope()) + { + responseType.ShouldBe(CommandResponseType.Successful); + exception.ShouldBeNull($"An exception was thrown: {exception?.Message}. Inner exception: {exception?.InnerException?.Message}"); + } + }); + } + + [TestMethod] + public void AssertionScopeExceptionIsNotNullThrowsOnDispose() + { + // Arrange + var responseType = CommandResponseType.Successful; + Exception? exception = new InvalidOperationException("Test exception"); + + // Act & Assert + Assert.ThrowsException(() => + { + using (new AssertionScope()) + { + responseType.ShouldBe(CommandResponseType.Successful); + exception.ShouldBeNull($"An exception was thrown: {exception?.Message}. Inner exception: {exception?.InnerException?.Message}"); + } + }); + } + + [TestMethod] + public void AssertionScopeFailedResponseAndExceptionThrowsOnDisposeWithBothMessages() + { + // Arrange + var responseType = CommandResponseType.Failed; + Exception? exception = new InvalidOperationException("Test exception", new ArgumentException("Inner")); + + // Act & Assert + var ex = Assert.ThrowsException(() => + { + using (new AssertionScope()) + { + responseType.ShouldBe(CommandResponseType.Successful); + exception.ShouldBeNull($"An exception was thrown: {exception?.Message}. Inner exception: {exception?.InnerException?.Message}"); + } + }); + + Assert.IsTrue(ex.Message.Contains("should be", StringComparison.InvariantCultureIgnoreCase)); + } +} diff --git a/src/Goodtocode.Assertion.Tests/AssertionTests.cs b/src/Goodtocode.Assertion.Tests/AssertionTests.cs new file mode 100644 index 0000000..0c802db --- /dev/null +++ b/src/Goodtocode.Assertion.Tests/AssertionTests.cs @@ -0,0 +1,189 @@ +using Goodtocode.Assertion; + +namespace Goodtocode.Assertion.Tests; + +[TestClass] +public sealed class AssertionTests +{ + [TestMethod] + public void ShouldWithNonNullDoesNotThrow() + { + // Arrange + var value = "test"; + + // Act & Assert + value.Should(); + } + + [TestMethod] + public void ShouldWithNullThrows() + { + // Arrange + string? value = null; + + // Act & Assert + Assert.ThrowsException(() => value.Should()); + } + + [TestMethod] + public void ShouldNotWithNullDoesNotThrow() + { + // Arrange + string? value = null; + + // Act & Assert + value.ShouldNot(); + } + + [TestMethod] + public void ShouldNotWithNonNullThrows() + { + // Arrange + var value = "not null"; + + // Act & Assert + Assert.ThrowsException(() => value.ShouldNot()); + } + + [TestMethod] + public void ShouldBeWithEqualValuesDoesNotThrow() + { + // Arrange + int actual = 5; + int expected = 5; + + // Act & Assert + actual.ShouldBe(expected); + } + + [TestMethod] + public void ShouldBeWithNotEqualValuesThrows() + { + // Arrange + int actual = 5; + int expected = 6; + + // Act & Assert + Assert.ThrowsException(() => actual.ShouldBe(expected)); + } + + [TestMethod] + public void ShouldBeTrueWithTrueDoesNotThrow() + { + // Arrange + bool condition = true; + + // Act & Assert + condition.ShouldBeTrue(); + } + + [TestMethod] + public void ShouldBeTrueWithFalseThrows() + { + // Arrange + bool condition = false; + + // Act & Assert + Assert.ThrowsException(() => condition.ShouldBeTrue()); + } + + [TestMethod] + public void ShouldBeFalseWithFalseDoesNotThrow() + { + // Arrange + bool condition = false; + + // Act & Assert + condition.ShouldBeFalse(); + } + + [TestMethod] + public void ShouldBeFalseWithTrueThrows() + { + // Arrange + bool condition = true; + + // Act & Assert + Assert.ThrowsException(() => condition.ShouldBeFalse()); + } + + [TestMethod] + public void ShouldBeNullWithNullDoesNotThrow() + { + // Arrange + object? value = null; + + // Act & Assert + value.ShouldBeNull(); + } + + [TestMethod] + public void ShouldBeNullWithNonNullThrows() + { + // Arrange + object value = new(); + + // Act & Assert + Assert.ThrowsException(() => value.ShouldBeNull()); + } + + [TestMethod] + public void ShouldNotBeNullWithNonNullDoesNotThrow() + { + // Arrange + object value = new(); + + // Act & Assert + value.ShouldNotBeNull(); + } + + [TestMethod] + public void ShouldNotBeNullWithNullThrows() + { + // Arrange + object? value = null; + + // Act & Assert + Assert.ThrowsException(() => value.ShouldNotBeNull()); + } + + [TestMethod] + public void ShouldBeEmptyWithDefaultStructDoesNotThrow() + { + // Arrange + int value = default; + + // Act & Assert + value.ShouldBeEmpty(); + } + + [TestMethod] + public void ShouldBeEmptyWithNonDefaultStructThrows() + { + // Arrange + int value = 1; + + // Act & Assert + Assert.ThrowsException(() => value.ShouldBeEmpty()); + } + + [TestMethod] + public void ShouldNotBeEmptyWithNonDefaultStructDoesNotThrow() + { + // Arrange + int value = 1; + + // Act & Assert + value.ShouldNotBeEmpty(); + } + + [TestMethod] + public void ShouldNotBeEmptyWithDefaultStructThrows() + { + // Arrange + int value = default; + + // Act & Assert + Assert.ThrowsException(() => value.ShouldNotBeEmpty()); + } +} diff --git a/src/Goodtocode.Assertion.Tests/Goodtocode.Assertion.Tests.csproj b/src/Goodtocode.Assertion.Tests/Goodtocode.Assertion.Tests.csproj new file mode 100644 index 0000000..c69c2a7 --- /dev/null +++ b/src/Goodtocode.Assertion.Tests/Goodtocode.Assertion.Tests.csproj @@ -0,0 +1,29 @@ + + + + net9.0 + latest + enable + enable + true + Exe + true + true + + + + + + + + + + + + + + + + + + diff --git a/src/Goodtocode.Assertion.Tests/MSTestSettings.cs b/src/Goodtocode.Assertion.Tests/MSTestSettings.cs new file mode 100644 index 0000000..aaf278c --- /dev/null +++ b/src/Goodtocode.Assertion.Tests/MSTestSettings.cs @@ -0,0 +1 @@ +[assembly: Parallelize(Scope = ExecutionScope.MethodLevel)] diff --git a/src/Goodtocode.Assertion.sln b/src/Goodtocode.Assertion.sln new file mode 100644 index 0000000..dacaf55 --- /dev/null +++ b/src/Goodtocode.Assertion.sln @@ -0,0 +1,31 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.5.33516.290 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Goodtocode.Assertion", "Goodtocode.Assertion\Goodtocode.Assertion.csproj", "{79E44601-B5B4-8738-1338-8BEED365E0A1}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Goodtocode.Assertion.Tests", "Goodtocode.Assertion.Tests\Goodtocode.Assertion.Tests.csproj", "{9AA5EFC0-E6F4-8D68-52C0-46F61168027B}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {79E44601-B5B4-8738-1338-8BEED365E0A1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {79E44601-B5B4-8738-1338-8BEED365E0A1}.Debug|Any CPU.Build.0 = Debug|Any CPU + {79E44601-B5B4-8738-1338-8BEED365E0A1}.Release|Any CPU.ActiveCfg = Release|Any CPU + {79E44601-B5B4-8738-1338-8BEED365E0A1}.Release|Any CPU.Build.0 = Release|Any CPU + {9AA5EFC0-E6F4-8D68-52C0-46F61168027B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9AA5EFC0-E6F4-8D68-52C0-46F61168027B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9AA5EFC0-E6F4-8D68-52C0-46F61168027B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9AA5EFC0-E6F4-8D68-52C0-46F61168027B}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {394AC88B-6897-416E-B92F-1D370A044F06} + EndGlobalSection +EndGlobal diff --git a/src/Goodtocode.Assertion/AssertionFailedException.cs b/src/Goodtocode.Assertion/AssertionFailedException.cs new file mode 100644 index 0000000..094540a --- /dev/null +++ b/src/Goodtocode.Assertion/AssertionFailedException.cs @@ -0,0 +1,7 @@ +namespace Goodtocode.Assertion; + +public class AssertionFailedException(string message) : Exception(message) +{ +} + + diff --git a/src/Goodtocode.Assertion/AssertionRules.cs b/src/Goodtocode.Assertion/AssertionRules.cs new file mode 100644 index 0000000..de3472d --- /dev/null +++ b/src/Goodtocode.Assertion/AssertionRules.cs @@ -0,0 +1,61 @@ +namespace Goodtocode.Assertion; + +public static class AssertionRules +{ + public static void Should(this T? obj, string? message = null) + { + if (obj is null) + throw new AssertionFailedException(message ?? "Expected value should not be null."); + } + + public static void ShouldNot(this T? obj, string? message = null) + { + if (obj is not null) + throw new AssertionFailedException(message ?? "Expected value should be null."); + } + + public static void ShouldBe(this T actual, T expected, string? message = null) + { + if (!EqualityComparer.Default.Equals(actual, expected)) + { + throw new AssertionFailedException(message ?? $"Expected value should be '{expected}', but was '{actual}'."); + } + } + + public static void ShouldBeTrue(this bool condition, string? message = null) + { + if (!condition) + throw new AssertionFailedException(message ?? "Expected condition should be true."); + } + + public static void ShouldBeFalse(this bool condition, string? message = null) + { + if (condition) + throw new AssertionFailedException(message ?? "Expected condition should be false."); + } + + public static void ShouldBeNull(this T? obj, string? message = null) + { + if (obj is not null) + throw new AssertionFailedException(message ?? "Expected value should be null."); + } + + public static void ShouldNotBeNull(this T? obj, string? message = null) + { + if (obj is null) + throw new AssertionFailedException(message ?? "Expected value should not be null."); + } + + public static void ShouldBeEmpty(this T value, string? message = null) where T : struct + { + if (!EqualityComparer.Default.Equals(value, default)) + throw new AssertionFailedException(message ?? "Expected value should be empty."); + } + + public static void ShouldNotBeEmpty(this T value, string? message = null) where T : struct + { + if (EqualityComparer.Default.Equals(value, default)) + throw new AssertionFailedException(message ?? "Expected value should not be empty."); + } +} + diff --git a/src/Goodtocode.Assertion/AssertionScope.cs b/src/Goodtocode.Assertion/AssertionScope.cs new file mode 100644 index 0000000..a63a362 --- /dev/null +++ b/src/Goodtocode.Assertion/AssertionScope.cs @@ -0,0 +1,30 @@ +namespace Goodtocode.Assertion; + +public class AssertionScope : IDisposable +{ + private static readonly AsyncLocal>> _scopes = new(); + + public AssertionScope() + { + _scopes.Value ??= new Stack>(); + _scopes.Value.Push([]); + } + + public static void AddFailure(string message) + { + if (_scopes.Value != null && _scopes.Value.Count > 0) + _scopes.Value.Peek().Add(message); + } + + public void Dispose() + { + var failures = _scopes.Value?.Pop(); + if (failures != null && failures.Count > 0) + { + throw new AssertionFailedException(string.Join(Environment.NewLine, failures)); + } + GC.SuppressFinalize(this); + } + + public static bool IsActive => _scopes.Value != null && _scopes.Value.Count > 0; +} diff --git a/src/Goodtocode.Assertion/Goodtocode.Assertion.csproj b/src/Goodtocode.Assertion/Goodtocode.Assertion.csproj new file mode 100644 index 0000000..573d160 --- /dev/null +++ b/src/Goodtocode.Assertion/Goodtocode.Assertion.csproj @@ -0,0 +1,28 @@ + + + + Goodtocode.Assertion + Goodtocode.Assertion + 1.0.0 + netstandard2.0 + true + enable + enable + + Goodtocode.Assertion + Robert Good + Goodtocode + A simple, lightweight fluent assertion library for .NET. + assertion;fluent;testing;unit test + https://github.com/Goodtocode/aspect-assertion + https://github.com/Goodtocode/aspect-assertion + MIT + Copyright © 2026 Goodtocode + Goodtocode-icon.png + + + + + + + \ No newline at end of file diff --git a/src/Goodtocode.Assertion/goodtocode-icon.png b/src/Goodtocode.Assertion/goodtocode-icon.png new file mode 100644 index 0000000..69c8932 Binary files /dev/null and b/src/Goodtocode.Assertion/goodtocode-icon.png differ diff --git a/src/build.cmd b/src/build.cmd new file mode 100644 index 0000000..ae89052 --- /dev/null +++ b/src/build.cmd @@ -0,0 +1,5 @@ +@echo off +setlocal +cd "%~dp0" +dotnet build --configuration Release --interactive ^ + && dotnet test --configuration Release --no-build --no-restore --interactive \ No newline at end of file diff --git a/src/build.sh b/src/build.sh new file mode 100644 index 0000000..fa86913 --- /dev/null +++ b/src/build.sh @@ -0,0 +1,13 @@ +#!/usr/bin/env bash + +set -e + +SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) + +pushd "$SCRIPT_DIR" > /dev/null + +# Release config triggers also "dotnet format" +dotnet build --configuration Release --interactive +dotnet test --configuration Release --no-build --no-restore --interactive + +popd > /dev/null \ No newline at end of file diff --git a/src/nuget.config b/src/nuget.config new file mode 100644 index 0000000..24d22ee --- /dev/null +++ b/src/nuget.config @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + \ No newline at end of file