Arcane.DevOps.psm1
<# .SYNOPSIS Gets the root directory of the current Git repository. .DESCRIPTION The Get-RepoRoot function retrieves the root directory of the current Git repository. This is useful for scripts that need to access files or directories within the repository. .EXAMPLE Get-RepoRoot This example retrieves the root directory of the current Git repository. #> function Get-RepoRoot { $root = git rev-parse --show-toplevel return $root } <# .SYNOPSIS Loads environment variables from a .env file into the current PowerShell session. .DESCRIPTION The Get-EnvironmentVariables function reads key-value pairs from a .env file and sets them as environment variables in the current PowerShell session. This is useful for managing configuration settings and secrets in a centralized file. .PARAMETER FilePath The path to the .env file that contains the environment variables to load. If not specified, the function will look for a .env file in the current directory. .EXAMPLE Get-EnvironmentVariables -FilePath "/path/to/.env" This example loads environment variables from the specified .env file. .EXAMPLE Get-EnvironmentVariables This example loads environment variables from a .env file in the current directory. .NOTES Make sure the .env file is formatted with each environment variable on a new line in the format KEY=VALUE. #> function Get-EnvironmentVariables { $envFilePath = Join-Path (Get-RepoRoot) ".env" if (Test-Path $envFilePath) { Get-Content $envFilePath | ForEach-Object { if ($_ -match "^\s*([^#][^=]+?)\s*=\s*(.+?)\s*$") { [System.Environment]::SetEnvironmentVariable($matches[1], $matches[2]) } } } } <# .SYNOPSIS Installs and updates a specific PowerShell module. .DESCRIPTION Ensures that specified PowerShell module is installed and up-to-date. .INPUTS None. .OUTPUTS None. .EXAMPLE Get-CurrentModule "Az" #> function Get-CurrentModule { Param( [Parameter(Mandatory = $true, ValueFromPipeline = $true)] $Module ) # Get current version if any $version = (Get-Module -ListAvailable $Module) | ` Sort-Object Version -Descending | ` Select-Object Version -First 1 | ` Select-Object ModuleVersion -ExpandProperty Version # Get available version $galleryVersion = Find-Module -Name $Module | ` Sort-Object Version -Descending | ` Select-Object Version -First 1 | ` Select-Object OnlineVersion -ExpandProperty Version if ($null -eq $version) { Write-Host "Installing module $Module..." -NoNewline Install-Module $Module -Force | Out-Null -WarningAction Ignore Write-Host "Done!" -ForegroundColor Green } else { if ([version]$galleryVersion -gt [version]$version) { Write-Host "Updating module $Module..." -NoNewline Update-Module $Module -Force | Out-Null -WarningAction Ignore Write-Host "Done!" -ForegroundColor Green } } } <# .SYNOPSIS Displays the help information for the script. .DESCRIPTION The Show-PullRequestHelp function provides detailed information on how to use the script, including available parameters and examples. .EXAMPLE Show-PullRequestHelp This command displays the help information for the script. #> function Show-PullRequestHelp { Write-Output "Usage: .\New-ChangeSet.ps1 [--all] [--filter <regex>] [--help]" Write-Output "" Write-Output "Options:" Write-Output " -all Include 'Chores' and 'Other' sections in the output." Write-Output " -filter <regex> Filter out commits matching the given regular expression." Write-Output " -output [filename] Creates/updates the pull request to GitHub." Write-Output " -help Show this help message and exit." } <# .SYNOPSIS Checks and sets Jira credentials in the environment variables. .DESCRIPTION This function checks if the Jira email and token are set in the environment variables. If not, it prompts the user to enter the Jira email and optionally create a new Jira API token. The entered credentials are saved in a .env file and set as environment variables. .PARAMETER None This function does not take any parameters. .NOTES The function will prompt the user for input if the required environment variables are not set. It will also open a browser window to the Jira API token creation page if needed. .EXAMPLE Confirm-JiraCredentials # This will check for Jira credentials and prompt the user to enter them if they are not already set. #> function Confirm-JiraCredentials { $envFilePath = ".env" $reloadEnv = $false if (-not $env:JIRA_EMAIL) { # Try to get the email from git config $gitEmail = git config user.email if ($gitEmail) { $jiraEmail = Read-Host "Enter your Jira email (Press Enter to use '$gitEmail')" if ($jiraEmail -eq "") { $jiraEmail = $gitEmail } } else { $jiraEmail = Read-Host "Enter your Jira email" } Add-Content -Path $envFilePath -Value "JIRA_EMAIL=$jiraEmail" [System.Environment]::SetEnvironmentVariable("JIRA_EMAIL", $jiraEmail) $reloadEnv = $true } if (-not $env:JIRA_TOKEN) { $createToken = Read-Host "Do you want to create a new Jira API token? (yes/no)" if ($createToken -eq "" -or $createToken.Substring(0, 1).ToLower() -eq "y") { Start-Process "https://id.atlassian.com/manage-profile/security/api-tokens" } # Mask the input for the Jira API token $secureJiraToken = Read-Host "Enter your Jira API token" -AsSecureString $jiraToken = ConvertFrom-SecureString $secureJiraToken -AsPlainText Add-Content -Path $envFilePath -Value "JIRA_TOKEN=$jiraToken" [System.Environment]::SetEnvironmentVariable("JIRA_TOKEN", $jiraToken) $reloadEnv = $true } if ($reloadEnv) { Get-EnvironmentVariables } } <# .SYNOPSIS Retrieves a pull request template. .DESCRIPTION The Get-Template function retrieves a pull request template. .EXAMPLE PS C:\> Get-Template This example retrieves a template using the Get-Template function. #> function Get-Template { # Find the first pull_request_template.md file in the repository $template_file = Get-ChildItem -Recurse -Filter "pull_request_template.md" -Force | Select-Object -First 1 # Check if the template file was found if (-not $template_file) { $template_content = "## What was changed?`n`n<!-- Add requirements -->`n`n `n`n## What was changed?`n<!-- Add changelog -->`n" } else { # Read the pull request template $template_content = Get-Content -Path $template_file.FullName -Raw } return $template_content } <# .SYNOPSIS Retrieves the issue identifier from the current branch name. .DESCRIPTION The Get-IssueIdentifier function extracts the issue identifier from the current branch name. .EXAMPLE PS C:\> Get-IssueIdentifier This example retrieves the issue identifier from the current branch name. #> function Get-IssueIdentifier { if ($current_branch -match '(patch|feature)/([a-z0-9]+-[0-9]+)') { $issue_identifier = $matches[2].ToUpper() return $issue_identifier } return "" } <# .SYNOPSIS Retrieves the change set for a given repository. .DESCRIPTION The Get-ChangeSet function retrieves the change set for a specified repository. It can be used to gather information about the changes made in the repository. .PARAMETER Template The pull request template, and existing content, to use. .EXAMPLE PS> Get-ChangeSet $template #> function Get-ChangeSet { Param ( [string]$template ) # Fetch the commit messages from the current branch that are not in the base branch and reverse the order $saves = git log "$base_branch..HEAD" --pretty=format:"%s" | ForEach-Object { $_ } | Sort-Object { $_ } -Descending # Initialize the changeset $changeset = "" # Initialize variables for each conventional commit type $docs_commits = "" $feat_commits = "" $fix_commits = "" $test_commits = "" $chore_commits = "" $ci_commits = "" $other_commits = "" # Filter and format the commits foreach ($save in $saves) { # Apply filter if specified if ($filter -ne "" -and $save -match $filter) { continue } # Extract the conventional commit type and the message if ($save -match '^(Merge.*|.*\(#\d+\))$') { continue } if ($save -match '^(feat|fix|docs|test|chore|ci)(\([^\)]+\))?:\s*(.*)$') { $save_type = $matches[1] $save_message = $matches[3] switch ($save_type) { "docs" { $docs_commits += "- $save_message`n" } "feat" { $feat_commits += "- $save_message`n" } "test" { $test_commits += "- $save_message`n" } "fix" { $fix_commits += "- $save_message`n" } "chore" { $chore_commits += "- $save_message`n" } default { $other_commits += "- $save_message`n" } } } else { if ($all) { $other_commits += "- $save`n" } } } # Format the changeset if ($feat_commits -ne "") { $changeset += "### Features:`n`n$feat_commits`n" } if ($fix_commits -ne "") { $changeset += "### Fixes:`n`n$fix_commits`n" } if ($test_commits -ne "") { $changeset += "### Tests:`n`n$test_commits`n" } if ($docs_commits -ne "") { $changeset += "### Documentation:`n`n$docs_commits`n" } if ($ci_commits -ne "") { $changeset += "### Continuous Integration:`n`n$ci_commits`n" } if ($all) { if ($chore_commits -ne "") { $changeset += "### Chores:`n`n$chore_commits`n" } if ($other_commits -ne "") { $changeset += "### Other:`n`n$other_commits`n" } } # Append updated content to end of the template if ($template -ne $null -and $template.IndexOf("<!-- Add changelog -->") -gt 0) { return $template.Replace("<!-- Add changelog -->", "$changeset") } return $template + $changeset } <# .SYNOPSIS Retrieves and formats the summary for a pull request. .DESCRIPTION The Get-Summary function retrieves the Jira issue details and formats the summary for a pull request. It uses the Jira issue identifier from the current branch name to fetch the issue details from Jira. The function then formats the description and summary based on the issue details and the provided template. .PARAMETER template The template string to be used for formatting the summary. .RETURNS The formatted summary string. .EXAMPLE $template = Get-Template $summary = Get-Summary -template $template Write-Output $summary .NOTES This function requires the JiraPS module and Jira credentials to be set in the environment variables. #> function Get-Summary { Param ( [string]$template ) # Tell the module what is the server's address Set-JiraConfigServer -Server "https://evinova.atlassian.net" # Configure authentication $password = ConvertTo-SecureString $env:JIRA_TOKEN -AsPlainText -Force $creds = New-Object System.Management.Automation.PSCredential ($env:JIRA_EMAIL, $password) New-JiraSession -Credential $creds -ErrorAction Stop | Out-Null # Get the remote issue $issue_identifier = Get-IssueIdentifier $Global:Title = "[$issue_identifier]" $issue = Get-JiraIssue -Issue $issue_identifier -ErrorAction SilentlyContinue if ($issue -eq $null) { Write-Warning "Issue $issue_identifier not found in Jira." return $template } # Format the description $summary = $($issue.Description) $summary = $summary -split "`n", 2 | Select-Object -First 1 # Format title $Global:Title += " $($issue.Summary)" if ($Global:Title.Length -gt 50) { $Global:Title = $Global:Title.Substring(0, 47) + "..." } # Format summary $has_summary = $template.IndexOf("<!-- Add requirements -->") -gt 0 $summary = ($has_summary) ? $template.Replace("<!-- Add requirements -->", "$summary") : $template + "`n" + $summary return $summary } <# .SYNOPSIS Sets the content of a PR in GitHub. .DESCRIPTION The Set-PullRequestRemoteContent function is used to set or update content on the remote GitHub server. .EXAMPLE Set-PullRequestRemoteContent #> function Set-PullRequestRemoteContent { if ($null -ne $output -and $output -ne "") { return } # Check if a PR already exists and get its URL $pr_url = "" try { $pr_url = gh pr view --json url --jq '.url' } catch { $pr_url = "" } # Create or update the PR if ($pr_url.Length -gt 0) { gh pr edit $pr_url --body "$content" } else { gh pr create --base "$base_branch" --head "$current_branch" --title "$Global:Title" --body "$content" --draft } # Open the PR in the browser gh pr view --web } <# .SYNOPSIS Generates a pull request. .DESCRIPTION Generates a pull request using the template with details of the Jira ticket reference and the changeset for the current branch. .PARAMETER all Include 'Chores' and 'Other' sections in the output. .PARAMETER filter Filter out commits matching the given regular expression. .PARAMETER help Show this help message and exit. .PARAMETER output The output file to save the pull request content. .EXAMPLE .pr.ps1 -all --filter "feat" #> function New-PullRequest { Param ( [switch]$all, [string]$filter, [switch]$help, [string]$output ) $base_branch = git remote show origin | Select-String 'HEAD branch' | ForEach-Object { $_.ToString().Split()[-1] } $current_branch = git rev-parse --abbrev-ref HEAD # Load environment variables from .env file Get-EnvironmentVariables # Check if Jira credentials are set Confirm-JiraCredentials # Install Jira module Get-CurrentModule "JiraPS" if ($help) { Show-PullRequestHelp exit 0 } # Load template and merge with summary and changeset $content = Get-Template $content = Get-Summary $content $content = Get-ChangeSet $content # Publish to GitHub Set-PullRequestRemoteContent # Output the content if ($output -ne $null -and $output -ne "") { $content | Out-File -FilePath $output } } <# .SYNOPSIS Gets the comment-based help and converts to GitHub Flavored Markdown. .PARAMETER Name A command name or module name to get comment-based help. For modules, include '.psm1' to generate help for all functions in module. .EXAMPLE Get-HelpByMarkdown -Name New-PullRequest > .\New-PullRequest.md .EXAMPLE Get-HelpByMarkdown -Name Arcane.DevOps.psm1 > .\Arcane.DevOps.md .INPUTS System.String .OUTPUTS System.String #> function Get-HelpByMarkdown { param ( [Parameter(Mandatory = $True)] $Name ) function EncodePartOfHtml { param ( [string] $Value ) ($Value -replace '<', '<') -replace '>', '>' } function GetCode { param ( $Example ) $codeAndRemarks = (($Example | Out-String) -replace ($Example.title), '').Trim() -split "`r`n" $code = New-Object "System.Collections.Generic.List[string]" for ($i = 0; $i -lt $codeAndRemarks.Length; $i++) { if ($codeAndRemarks[$i] -eq 'DESCRIPTION' -and $codeAndRemarks[$i + 1] -eq '-----------') { break } if (1 -le $i -and $i -le 2) { continue } $code.Add($codeAndRemarks[$i]) } $code -join "`r`n" } function GetRemark { param ( $Example ) $codeAndRemarks = (($Example | Out-String) -replace ($Example.title), '').Trim() -split "`r`n" $isSkipped = $false $remark = New-Object "System.Collections.Generic.List[string]" for ($i = 0; $i -lt $codeAndRemarks.Length; $i++) { if (!$isSkipped -and $codeAndRemarks[$i - 2] -ne 'DESCRIPTION' -and $codeAndRemarks[$i - 1] -ne '-----------') { continue } $isSkipped = $true $remark.Add($codeAndRemarks[$i]) } $remark -join "`r`n" } function Print-HelpLink { param ( $Function ) @" - [$($Function.Name)](#$($Function.Name.ToLower())) "@ } function Print-Help { param ( $Function ) @" ## $($Function.Name) ### SYNOPSIS $($Function.Synopsis) ### SYNTAX ``````powershell $((($Function.syntax | Out-String) -replace "`r`n", "`r`n`r`n").Trim()) `````` ### DESCRIPTION $(($Function.description | Out-String).Trim()) ### PARAMETERS "@ + $(foreach ($parameter in $Function.parameters.parameter) { @" #### -$($parameter.name) <$($parameter.type.name)> $(($parameter.description | Out-String).Trim()) `````` $(((($parameter | Out-String).Trim() -split "`r`n")[-5..-1] | % { $_.Trim() }) -join "`r`n") `````` "@ }) + @" ### INPUTS $($Function.inputTypes.inputType.type.name) ### OUTPUTS $($Function.returnValues.returnValue[0].type.name) ### NOTES $(($Function.alertSet.alert | Out-String).Trim()) ### EXAMPLES "@ + $(foreach ($example in $Function.examples.example) { @" #### $(($example.title -replace '-*', '').Trim()) ``````powershell $(GetCode $example) `````` $(GetRemark $example) "@ }) + @" "@ } try { if ($Host.UI.RawUI) { $rawUI = $Host.UI.RawUI $oldSize = $rawUI.BufferSize $typeName = $oldSize.GetType().FullName if ($IsWindows) { $newSize = New-Object $typeName (500, $oldSize.Height) $rawUI.BufferSize = $newSize } } if ($Name.ToLower().EndsWith('.psm1')) { # Print header @" # $($Name.Substring(0, $Name.Length - 5)) The following functions are available in this module: "@ $modulePath = Join-Path -Path (Get-Location).Path -ChildPath $Name $tokens = $errors = $null $ast = [System.Management.Automation.Language.Parser]::ParseFile( $modulePath, [ref]$tokens, [ref]$errors) $functionDefinitions = $ast.FindAll({ param([System.Management.Automation.Language.Ast] $Ast) $Ast -is [System.Management.Automation.Language.FunctionDefinitionAst] -and # Class methods have a FunctionDefinitionAst under them as well, but we don't want them. ($PSVersionTable.PSVersion.Major -lt 5 -or $Ast.Parent -isnot [System.Management.Automation.Language.FunctionMemberAst]) }, $true) $functionDefinitions | Sort-Object -Property Name | ForEach-Object { # Only top level functions try { $topLevel = $functionDefinitions[0].Parent.ToString().Length -eq $_.Parent.ToString().Length if ($topLevel ) { $Function = Get-Help $_.Name -Full Print-HelpLink $Function } } catch { } } $functionDefinitions | Sort-Object -Property Name | ForEach-Object { # Only top level functions try { $topLevel = $functionDefinitions[0].Parent.ToString().Length -eq $_.Parent.ToString().Length if ($topLevel ) { $Function = Get-Help $_.Name -Full Print-Help $Function } } catch { } } } else { $Function = Get-Help $Name -Full Print-Help $Function } } finally { if ($Host.UI.RawUI -and $IsWindows) { $rawUI = $Host.UI.RawUI $rawUI.BufferSize = $oldSize } } } <# .SYNOPSIS Updates the manifest file. .DESCRIPTION Updates the manifest file to expose all latest function versions. .PARAMETER Path The path to the module manifest file. By default, ./Rpic.PowerShell.psm1. .PARAMETER IncrementMajor Increments the major version number. .PARAMETER IncrementMinor Increments the minor version number. .PARAMETER IncrementBuild Increments the build version number. .INPUTS None. .OUTPUTS None. .EXAMPLE PS> Update-RpicManifest -IncrementBuild #> function Update-PackageManifest { Param( [Parameter(Mandatory = $false)] $Path = "./Arcane.DevOps.psm1", [Switch]$IncrementMajor, [Switch]$IncrementMinor, [Switch]$IncrementBuild ) Write-Host "Updating manifest..." -NoNewLine New-RpicModuleCheck "PSScriptAnalyzer" $module = Get-ChildItem *.psm1 | Select-Object -First 1 Import-Module $module.Name -Force -ea SilentlyContinue $moduleName = $module.Name.Replace(".psm1", "") $functions = @() $tokens = $errors = $null $ast = [System.Management.Automation.Language.Parser]::ParseFile( $module.FullName, [ref]$tokens, [ref]$errors) $functionDefinitions = $ast.FindAll({ param([System.Management.Automation.Language.Ast] $Ast) $Ast -is [System.Management.Automation.Language.FunctionDefinitionAst] -and ($PSVersionTable.PSVersion.Major -lt 5 -or $Ast.Parent -isnot [System.Management.Automation.Language.FunctionMemberAst]) }, $true) $lines = 0; $functionDefinitions | Sort-Object -Property Name | ForEach-Object { # Only top level functions try { $topLevel = $functionDefinitions[0].Parent.ToString().Length -eq $_.Parent.ToString().Length if ($topLevel ) { $functions += $_.Name $lines ++ } } catch { } } try { Update-ModuleManifest -Path "./$($moduleName).psd1" -RootModule "$($moduleName).psd1" -ea SilentlyContinue | Out-Null } catch { # do nothing } try { Update-ModuleManifest -Path "./$($moduleName).psd1" -FunctionsToExport $functions -ea SilentlyContinue | Out-Null } catch { # do nothing } try { Update-ModuleManifest -Path "./$($moduleName).psd1" -CmdletsToExport $functions -ea SilentlyContinue | Out-Null } catch { # do nothing } Write-Host "Done!" -ForegroundColor Green Write-Host "Updated the module manifest $($moduleName).psd1 with $($lines) functions." if ($IncrementMajor -or $IncrementMinor -or $IncrementBuild) { $current = Test-ModuleManifest -Path "./$($moduleName).psd1" $major = $current.Version.Major $minor = $current.Version.Minor $build = $current.Version.Build if ($IncrementMajor) { $major = $major + 1 $minor = 0 $build = 0 } if ($IncrementMinor) { $minor = $minor + 1 $build = 0 } if ($IncrementBuild) { $build = $build + 1 } $version = $major, $minor, $build -join "." Write-Host "Incrementing version to $version" Update-ModuleManifest -Path "./$($moduleName).psd1" -ModuleVersion $version } } |