diff --git a/.github/prompts/save-branch-to-PR.prompt.md b/.github/prompts/save-branch-to-PR.prompt.md new file mode 100644 index 0000000..c71d589 --- /dev/null +++ b/.github/prompts/save-branch-to-PR.prompt.md @@ -0,0 +1,110 @@ +--- +agent: agent +--- + +# Save Branch to PR Workflow + +This prompt guides through the complete workflow of saving a feature branch by creating a PR, waiting for checks, merging, and cleaning up. + +## Prerequisites +- Git repository with GitHub remote +- GitHub CLI (`gh`) installed and authenticated +- Current branch should be a feature branch (not `main`) + +## Workflow Steps + +### Step 1: Verify Current Branch +Check that we are NOT on the `main` branch. If on `main`, stop and inform the user. + +```powershell +$currentBranch = git branch --show-current +if ($currentBranch -eq "main") { + Write-Error "Cannot run this workflow from the main branch. Please checkout a feature branch first." + return +} +Write-Host "Current branch: $currentBranch" +``` + +### Step 2: Push Branch and Create PR +Push the current branch to remote and create a Pull Request. + +```powershell +# Push branch to remote +git push -u origin $currentBranch + +# Create PR with auto-fill from commit messages +gh pr create --fill +``` + +If a PR already exists for this branch, continue with the existing PR. + +### Step 3: Wait for Workflow Checks +Wait for all GitHub Actions workflow checks to complete and verify they pass. + +```powershell +# Wait for checks to complete (will block until done) +gh pr checks --watch --required --interval 1 --fail-fast + +# Verify all checks passed +$checksResult = gh pr checks +if ($LASTEXITCODE -ne 0) { + Write-Error "Some checks failed. Please review and fix before merging." + return +} +Write-Host "All checks passed!" +``` + +### Step 4: Merge the PR +Merge the PR using a regular merge to preserve the branch history, and delete the remote branch. + +```powershell +# Merge PR and delete remote branch (regular merge to keep history) +gh pr merge --merge --delete-branch +``` + +### Step 5: Clean Up Local Branch +Switch to main and delete the local feature branch. + +```powershell +# Switch to main branch +git checkout main + +# Pull latest changes +git pull + +# Delete local feature branch +git branch -d $currentBranch +``` + +### Step 6: Check and Remove Codespaces +Check if there are any codespaces associated with this branch and remove them. + +```powershell +# List codespaces for this repository +$codespaces = gh codespace list --json name,gitStatus,repository --jq ".[] | select(.gitStatus.ref == `"$currentBranch`")" + +if ($codespaces) { + Write-Host "Found codespaces on branch $currentBranch. Removing..." + # Delete codespaces on this branch + gh codespace list --json name,gitStatus --jq ".[] | select(.gitStatus.ref == `"$currentBranch`") | .name" | ForEach-Object { + gh codespace delete --codespace $_ --force + Write-Host "Deleted codespace: $_" + } +} else { + Write-Host "No codespaces found on branch $currentBranch" +} +``` + +## Success Criteria +- [ ] Verified not on main branch +- [ ] PR created and pushed to remote +- [ ] All workflow checks passed +- [ ] PR merged successfully +- [ ] Local and remote feature branches deleted +- [ ] Any codespaces on the branch removed + +## Error Handling +- If on `main` branch: Stop immediately with clear message +- If checks fail: Stop and inform user to fix issues +- If merge conflicts: Stop and inform user to resolve conflicts +- If codespace deletion fails: Log warning but continue \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json index 7a73a41..572db51 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,2 +1,9 @@ { + "terminal.integrated.profiles.osx": { + "pwsh": { + "path": "pwsh", + "icon": "terminal-powershell", + "args": ["-NoExit", "-Command", "if (Test-Path './tools/Test_Helper') { Import-Module './tools/Test_Helper' -Force }"] + } + } } \ No newline at end of file diff --git a/Test/include/InvokeMockList.ps1 b/Test/include/InvokeMockList.ps1 index fcb3ecd..85ea7bd 100644 --- a/Test/include/InvokeMockList.ps1 +++ b/Test/include/InvokeMockList.ps1 @@ -1,54 +1,54 @@ -$MockCommandFile = $testRootPath | Join-Path -ChildPath "mockfiles.log" +# $MockCommandFile = $testRootPath | Join-Path -ChildPath "mockfiles.log" -function Trace-MockCommandFile{ - [CmdletBinding()] - param( - [string] $Command, - [string] $FileName - ) +# function Trace-MockCommandFile{ +# [CmdletBinding()] +# param( +# [string] $Command, +# [string] $FileName +# ) - # read content - $content = readMockCommandFile +# # read content +# $content = readMockCommandFile - # Check that the entry is already there - $result = $content | Where-Object{$_.command -eq $command} - if($null -ne $result) {return} +# # Check that the entry is already there +# $result = $content | Where-Object{$_.command -eq $command} +# if($null -ne $result) {return} - # add entry - $new = @{ - Command = $command - FileName = $fileName - } +# # add entry +# $new = @{ +# Command = $command +# FileName = $fileName +# } - $ret = @() - $ret += $content - $ret += $new +# $ret = @() +# $ret += $content +# $ret += $new - # Save list - writeMockCommandFile -Content $ret -} +# # Save list +# writeMockCommandFile -Content $ret +# } -function readMockCommandFile{ +# function readMockCommandFile{ - # Return empty list if the file does not exist - if(-not (Test-Path -Path $MockCommandFile)){ - return @() - } +# # Return empty list if the file does not exist +# if(-not (Test-Path -Path $MockCommandFile)){ +# return @() +# } - $ret = Get-Content -Path $MockCommandFile | ConvertFrom-Json +# $ret = Get-Content -Path $MockCommandFile | ConvertFrom-Json - # return an empty aray if content does not exists - $ret = $ret ?? @() +# # return an empty aray if content does not exists +# $ret = $ret ?? @() - return $ret -} +# return $ret +# } -function writeMockCommandFile($Content){ +# function writeMockCommandFile($Content){ - $list = $Content | ConvertTo-Json +# $list = $Content | ConvertTo-Json - $sorted = $list | Sort-Object fileName +# $sorted = $list | Sort-Object fileName - $sorted | Out-File -FilePath $MockCommandFile -} \ No newline at end of file +# $sorted | Out-File -FilePath $MockCommandFile +# } \ No newline at end of file diff --git a/Test/include/callPrivateContext.ps1 b/Test/include/callPrivateContext.ps1 index a7e7f83..d4aabd9 100644 --- a/Test/include/callPrivateContext.ps1 +++ b/Test/include/callPrivateContext.ps1 @@ -15,7 +15,8 @@ function Invoke-PrivateContext { param ( [Parameter(Mandatory, Position = 0)] [scriptblock]$ScriptBlock, - [string]$ModulePath + [string]$ModulePath, + [string[]]$Arguments ) if ([string]::IsNullOrEmpty($ModulePath)) { @@ -28,5 +29,11 @@ function Invoke-PrivateContext { throw "Failed to import the main module." } - & $module $ScriptBlock + if($Arguments){ + & $module $ScriptBlock -Arguments $Arguments + } + else { + & $module $ScriptBlock + } + } Export-ModuleMember -Function Invoke-PrivateContext diff --git a/Test/include/invokeCommand.mock.ps1 b/Test/include/invokeCommand.mock.ps1 index 5f3b13f..3bf6773 100644 --- a/Test/include/invokeCommand.mock.ps1 +++ b/Test/include/invokeCommand.mock.ps1 @@ -303,4 +303,63 @@ function Assert-MockFileNotfound{ Wait-Debugger throw "File not found or wrong case name. Expected[ $filename ] - Found[$( $file.name )]" } -} \ No newline at end of file +} + + +#region Mock Command File Trace + +$MockCommandFile = $testRootPath | Join-Path -ChildPath "mockfiles.log" + +function Trace-MockCommandFile{ + [CmdletBinding()] + param( + [string] $Command, + [string] $FileName + ) + + # read content + $content = readMockCommandFile + + # Check that the entry is already there + $result = $content | Where-Object{$_.command -eq $command} + if($null -ne $result) {return} + + # add entry + $new = @{ + Command = $command + FileName = $fileName + } + + $ret = @() + $ret += $content + $ret += $new + + # Save list + writeMockCommandFile -Content $ret +} + +function readMockCommandFile{ + + # Return empty list if the file does not exist + if(-not (Test-Path -Path $MockCommandFile)){ + return @() + } + + $ret = Get-Content -Path $MockCommandFile | ConvertFrom-Json + + # return an empty aray if content does not exists + $ret = $ret ?? @() + + return $ret +} + +function writeMockCommandFile($Content){ + + $list = $Content | ConvertTo-Json + + $sorted = $list | Sort-Object fileName + + $sorted | Out-File -FilePath $MockCommandFile +} + +# End region Mock Command File Trace \ No newline at end of file diff --git a/Test/include/run_BeforeAfter.ps1 b/Test/include/run_BeforeAfter.ps1 index 15439ee..180271a 100644 --- a/Test/include/run_BeforeAfter.ps1 +++ b/Test/include/run_BeforeAfter.ps1 @@ -5,21 +5,23 @@ # - After each test # - Before all tests # - After all tests +# +# Copy this file to Test Private to avoid being replaced by updates -function Run_BeforeAll{ - Write-Verbose "Run_BeforeAll" -} +# function Run_BeforeAll{ +# Write-Verbose "Run_BeforeAll" +# } -function Run_AfterAll{ - Write-Verbose "Run_AfterAll" -} +# function Run_AfterAll{ +# Write-Verbose "Run_AfterAll" +# } -function Run_BeforeEach{ - Write-Verbose "Run_BeforeEach" -} +# function Run_BeforeEach{ +# Write-Verbose "Run_BeforeEach" +# } -function Run_AfterEach{ - Write-Verbose "Run_AfterEach" -} +# function Run_AfterEach{ +# Write-Verbose "Run_AfterEach" +# } Export-ModuleMember -Function Run_* diff --git a/Test/public/dependencies.test.ps1 b/Test/public/dependencies.test.ps1 index 245c207..791cb9b 100644 --- a/Test/public/dependencies.test.ps1 +++ b/Test/public/dependencies.test.ps1 @@ -272,13 +272,19 @@ function Mock_GetMyModuleRootPath{ [Parameter(Mandatory,Position=2)][string]$Folder ) - # Mock GetMyModuleRootPath on SideBySide - # Check that happens before ImportFromModuleManager - # that we are testing here + # Mock "$($MODULE_NAME)RootPath" on SideBySide + + # Module path that pretends to be me $modulePath = "$Folder/$Name" + + # Ceate the fake me module New-ModuleV3 -Name $Name -Path $folder + + # Resolve the absolute path $path = Convert-Path -Path $modulePath - MockCallToString -Command 'GetMyModuleRootPath' -OutString "$path" + + # As we are testing this code in IncludeHelper module this is the command "Invoke-$($MODULE_NAME)RootPath" + MockCallToString -Command 'Invoke-IncludeHelperRootPath' -OutString "$path" } # Set-MyInvokeCommandAlias -Alias "TestGitHubRepo" -Command 'Invoke-WebRequest -Uri "{url}" -Method Head -ErrorAction SilentlyContinue | ForEach-Object { $_.StatusCode -eq 200 }' diff --git a/Test/public/openFilesUrls.test.ps1 b/Test/public/openFilesUrls.test.ps1 new file mode 100644 index 0000000..f786081 --- /dev/null +++ b/Test/public/openFilesUrls.test.ps1 @@ -0,0 +1,59 @@ +# Test for Open-Url function +# Open-Url is not exported, so we use Invoke-PrivateContext to call it. +# Open-Url uses Invoke-MyCommand with alias "OpenUrl" to dispatch URL opening. +# We mock the OpenUrl alias to echo back the URL for verification. + +function Test_OpenUrl_SingleUrl { + # Arrange + Reset-InvokeCommandMock + $url = "https://github.com" + $tag = New-Guid + + $command = 'Invoke-{modulename}OpenUrl -Url "{url}"' + $command = $command -replace "{modulename}", $MODULE_NAME + $command = $command -replace "{url}", $url + Set-InvokeCommandMock -Alias $command -Command "echo $tag" + + # Act + $result = Invoke-PrivateContext { + param($Arguments) + + Open-Url -Url $Arguments[0] + + } -Arguments $url + + # Assert + Assert-AreEqual -Expected $tag -Presented $result +} + +function Test_OpenUrl_MultipleUrls_Pipeline { + # Arrange + Reset-InvokeCommandMock + $url1 = "https://github.com" + $url2 = "https://google.com" + $url3 = "https://microsoft.com" + $tag1 = New-Guid + $tag2 = New-Guid + $tag3 = New-Guid + + foreach ($pair in @(@($url1, $tag1), @($url2, $tag2), @($url3, $tag3))) { + $command = 'Invoke-{modulename}OpenUrl -Url "{url}"' + $command = $command -replace "{modulename}", $MODULE_NAME + $command = $command -replace "{url}", $pair[0] + Set-InvokeCommandMock -Alias $command -Command "echo $($pair[1])" + } + + # Act + $result = Invoke-PrivateContext { + param($Arguments) + + $Arguments | ForEach-Object { Open-Url -Url $_ } + + } -Arguments @($url1, $url2, $url3) + + # Assert + Assert-Count -Expected 3 -Presented $result + Assert-AreEqual -Expected $tag1 -Presented $result[0] + Assert-AreEqual -Expected $tag2 -Presented $result[1] + Assert-AreEqual -Expected $tag3 -Presented $result[2] +} \ No newline at end of file diff --git a/include/dependencies.ps1 b/include/dependencies.ps1 index 7bc66d5..c53aff4 100644 --- a/include/dependencies.ps1 +++ b/include/dependencies.ps1 @@ -16,8 +16,10 @@ $MODULE_ROOT_PATH = $PSScriptRoot | split-path -Parent $MODULE_NAME = (Get-ChildItem -Path $MODULE_ROOT_PATH -Filter *.psd1 | Select-Object -First 1).BaseName +$rootPathCommandName = "$($MODULE_NAME)RootPath" + # SET MY INVOKE COMMAND ALIAS -Set-MyInvokeCommandAlias -Alias "GetMyModuleRootPath" -Command "Invoke-$($MODULE_NAME)RootPath" +Set-MyInvokeCommandAlias -Alias $rootPathCommandName -Command "Invoke-$($MODULE_NAME)RootPath" Set-MyInvokeCommandAlias -Alias "CloneRepo" -Command 'git clone {url} {folder}' Set-MyInvokeCommandAlias -Alias "TestGitHubRepo" -Command 'Invoke-WebRequest -Uri "{url}" -Method Head -ErrorAction SilentlyContinue | ForEach-Object { $_.StatusCode -eq 200 }' Set-MyInvokeCommandAlias -Alias "FindModule" -Command 'Find-Module -Name {name} -AllowPrerelease -ErrorAction SilentlyContinue' @@ -26,26 +28,6 @@ Set-MyInvokeCommandAlias -Alias "GetModule" -Command 'Get-Module -N Set-MyInvokeCommandAlias -Alias "GetModuleListAvailable" -Command 'Get-Module -Name {name} -ListAvailable' Set-MyInvokeCommandAlias -Alias "ImportModule" -Command 'Import-Module -Name {name} -Scope Global -Verbose:$false -PassThru' -# This function will be renamed to avoid collision with other modules -# function Invoke-MyModuleRootPath{ -# [CmdletBinding()] -# param() - -# # We will asume that this include file will be on a public,private or include folder. -# $root = $PSScriptRoot | split-path -Parent - -# # confirm that in root folder we have a psd1 file -# $psd1 = Get-ChildItem -Path $root -Filter *.psd1 -Recurse -ErrorAction SilentlyContinue - -# if(-Not $psd1){ -# throw "Wrong root folder. Not PSD1 file found in [$root]. Modify Invoke-GetMyModuleRootPath to adjust location" -# } - -# return $root -# } -# Copy-Item -path Function:Invoke-MyModuleRootPath -Destination Function:"Invoke-$($MODULE_NAME)RootPath" -# Export-ModuleMember -Function "Invoke-$($MODULE_NAME)RootPath" - function Import-Dependency{ [CmdletBinding(SupportsShouldProcess,ConfirmImpact = 'High')] param( @@ -67,7 +49,7 @@ function Import-Dependency{ } #2. import from side by side path - $meModuleRootPath = Invoke-MyCommand -Command 'GetMyModuleRootPath' + $meModuleRootPath = Get-ModuleNameRootPath $modulePath = $meModuleRootPath | split-path -Parent | Join-Path -ChildPath $Name if(Test-Path -Path $modulePath){ @@ -127,7 +109,7 @@ function Import-Dependency{ if ($testUrl -eq $true) { # Clone side by side this module - $local = Invoke-MyCommand -Command 'GetMyModuleRootPath' + $local = Invoke-MyCommand -Command $rootPathCommandName $modulePath = $local | split-path -Parent | Join-Path -ChildPath $Name if ($PSCmdlet.ShouldProcess("Cloning module $name", "Do you want to clone [$url] to [$modulePath]?")) { @@ -178,4 +160,22 @@ function Import-MyModule{ return $result } -} \ No newline at end of file +} + +# We need to allow to mock this calls to allow testig on IncludeHelper module +function Get-ModuleNameRootPath{ + [CmdletBinding()] + param() + + return Invoke-MyCommand -Command "$($MODULE_NAME)RootPath" +} + +# This function will be renamed to avoid collision with other modules +function Invoke-ModuleNameRootPath{ + [CmdletBinding()] + param() + + return $MODULE_ROOT_PATH +} +Copy-Item -path Function:Invoke-ModuleNameRootPath -Destination Function:"Invoke-$($MODULE_NAME)RootPath" +Export-ModuleMember -Function "Invoke-$($MODULE_NAME)RootPath" \ No newline at end of file diff --git a/include/getLongText.ps1 b/include/getLongText.ps1 new file mode 100644 index 0000000..725af1b --- /dev/null +++ b/include/getLongText.ps1 @@ -0,0 +1,28 @@ + +$EditFileAlias = $MODULE_NAME + "_EditFile" +Set-MyInvokeCommandAlias -Alias $EditFileAlias -Command "code -w {path}" + +function Get-LongText{ + [CmdletBinding()] + param( + [Parameter(Position = 0)][string]$Text + ) + + $tmpFilePath = "temp:" | Convert-Path | Join-Path -ChildPath "LongText_$([Guid]::NewGuid().ToString()).md" + + New-Item -Path $tmpFilePath -ItemType File -Force | Out-Null + + if( -not [string]::IsNullOrWhiteSpace($Text)){ + Set-Content -Path $tmpFilePath -Value $Text + } + + Invoke-MyCommand -Command $EditFileAlias -Parameters @{ path = $tmpFilePath } + + $content = Get-Content $tmpFilePath -Raw + + Remove-Item $tmpFilePath -Force + + Set-TextVariable $content + + return $content +} \ No newline at end of file diff --git a/include/openFilesUrls.ps1 b/include/openFilesUrls.ps1 index 7306bc7..033269d 100644 --- a/include/openFilesUrls.ps1 +++ b/include/openFilesUrls.ps1 @@ -3,7 +3,7 @@ # Provides controls to open files and URLs in the default system applications. # Use $MODULE_NAME variable to set up functions names -Set-MyInvokeCommandAlias -Alias OpenUrl -Command "Invoke-$($MODULE_NAME)OpenUrl -Url {url}" +Set-MyInvokeCommandAlias -Alias OpenUrl -Command $('Invoke-{modulename}OpenUrl -Url "{url}"' -replace "{modulename}", $MODULE_NAME) function Invoke-ModuleNameOpenUrl{ [CmdletBinding()] diff --git a/public/syncIncludeWithModule.ps1 b/public/syncIncludeWithModule.ps1 index bce3e58..f6a6882 100644 --- a/public/syncIncludeWithModule.ps1 +++ b/public/syncIncludeWithModule.ps1 @@ -1,6 +1,7 @@ function Sync-IncludeWithModule{ [CmdletBinding()] param( + [Parameter(Position=0)][ValidateSet("All","File","System")][string]$FileType ="All", [Parameter()][string]$DestinationModulePath, [Parameter()][switch]$DestinationIncludeHelper, [Parameter()][switch]$SourceLocal @@ -8,8 +9,11 @@ function Sync-IncludeWithModule{ $local = $SourceLocal.IsPresent - Get-IncludeFile -Local:$local | Add-IncludeToWorkspace -IfExists -DestinationModulePath $DestinationModulePath -SourceLocal:$SourceLocal -DestinationIncludeHelper:$DestinationIncludeHelper - - Get-IncludeSystemFiles -Local:$local | Add-IncludeToWorkspace -IfExists -DestinationModulePath $DestinationModulePath -SourceLocal:$SourceLocal -DestinationIncludeHelper:$DestinationIncludeHelper + if($FileType -eq "File" -or $FileType -eq "All"){ + Get-IncludeFile -Local:$local | Add-IncludeToWorkspace -IfExists -DestinationModulePath $DestinationModulePath -SourceLocal:$SourceLocal -DestinationIncludeHelper:$DestinationIncludeHelper + } + if($FileType -eq "System" -or $FileType -eq "All"){ + Get-IncludeSystemFiles -Local:$local | Add-IncludeToWorkspace -IfExists -DestinationModulePath $DestinationModulePath -SourceLocal:$SourceLocal -DestinationIncludeHelper:$DestinationIncludeHelper + } } Export-ModuleMember -Function Sync-IncludeWithModule \ No newline at end of file