diff --git a/GitHubBranches.ps1 b/GitHubBranches.ps1 index 502a785d..4491101f 100644 --- a/GitHubBranches.ps1 +++ b/GitHubBranches.ps1 @@ -3,9 +3,10 @@ @{ GitHubBranchTypeName = 'GitHub.Branch' - }.GetEnumerator() | ForEach-Object { - Set-Variable -Scope Script -Option ReadOnly -Name $_.Key -Value $_.Value - } + GitHubBranchProtectionRuleTypeName = 'GitHub.BranchProtectionRule' +}.GetEnumerator() | ForEach-Object { + Set-Variable -Scope Script -Option ReadOnly -Name $_.Key -Value $_.Value +} filter Get-GitHubRepositoryBranch { @@ -502,6 +503,622 @@ filter Remove-GitHubRepositoryBranch } } +filter Get-GitHubRepositoryBranchProtectionRule +{ + <# + .SYNOPSIS + Retrieve branch protection rules for a given GitHub repository. + + .DESCRIPTION + Retrieve branch protection rules for a given GitHub repository. + + The Git repo for this module can be found here: http://aka.ms/PowerShellForGitHub + + .PARAMETER OwnerName + Owner of the repository. + If not supplied here, the DefaultOwnerName configuration property value will be used. + + .PARAMETER RepositoryName + Name of the repository. + If not supplied here, the DefaultRepositoryName configuration property value will be used. + + .PARAMETER Uri + Uri for the repository. + The OwnerName and RepositoryName will be extracted from here instead of needing to provide + them individually. + + .PARAMETER BranchName + Name of the specific branch to be retrieved. If not supplied, all branches will be retrieved. + + .PARAMETER AccessToken + If provided, this will be used as the AccessToken for authentication with the + REST Api. Otherwise, will attempt to use the configured value or will run unauthenticated. + + .PARAMETER NoStatus + If this switch is specified, long-running commands will run on the main thread + with no commandline status update. When not specified, those commands run in + the background, enabling the command prompt to provide status information. + If not supplied here, the DefaultNoStatus configuration property value will be used. + + .INPUTS + GitHub.Branch + GitHub.Content + GitHub.Event + GitHub.Issue + GitHub.IssueComment + GitHub.Label + GitHub.Milestone + GitHub.PullRequest + GitHub.Project + GitHub.ProjectCard + GitHub.ProjectColumn + GitHub.Release + GitHub.Repository + + .OUTPUTS + GitHub.BranchProtectionRule + + .EXAMPLE + Get-GitHubRepositoryBranchProtectionRule -OwnerName microsoft -RepositoryName PowerShellForGitHub -BranchName master + + Retrieves branch protection rules for the master branch of the PowerShellForGithub repository. + + .EXAMPLE + Get-GitHubRepositoryBranchProtectionRule -Uri 'https://github.com/microsoft/PowerShellForGitHub' -BranchName master + + Retrieves branch protection rules for the master branch of the PowerShellForGithub repository. +#> + [CmdletBinding( + PositionalBinding = $false, + DefaultParameterSetName = 'Elements')] + [OutputType({ $script:GitHubBranchProtectionRuleTypeName })] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', '', + Justification = 'One or more parameters (like NoStatus) are only referenced by helper methods which get access to it from the stack via Get-Variable -Scope 1.')] + param( + [Parameter(ParameterSetName = 'Elements')] + [string] $OwnerName, + + [Parameter(ParameterSetName = 'Elements')] + [string] $RepositoryName, + + [Parameter( + Mandatory, + Position = 1, + ValueFromPipelineByPropertyName, + ParameterSetName = 'Uri')] + [Alias('RepositoryUrl')] + [string] $Uri, + + [Parameter( + Mandatory, + ValueFromPipelineByPropertyName, + Position = 2)] + [string] $BranchName, + + [string] $AccessToken, + + [switch] $NoStatus + ) + + Write-InvocationLog + + $elements = Resolve-RepositoryElements + $OwnerName = $elements.ownerName + $RepositoryName = $elements.repositoryName + + $telemetryProperties = @{ + 'OwnerName' = (Get-PiiSafeString -PlainText $OwnerName) + 'RepositoryName' = (Get-PiiSafeString -PlainText $RepositoryName) + } + + $params = @{ + UriFragment = "repos/$OwnerName/$RepositoryName/branches/$BranchName/protection" + Description = "Getting branch protection status for $RepositoryName" + Method = 'Get' + AcceptHeader = $script:lukeCageAcceptHeader + AccessToken = $AccessToken + TelemetryEventName = $MyInvocation.MyCommand.Name + TelemetryProperties = $telemetryProperties + NoStatus = (Resolve-ParameterWithDefaultConfigurationValue -Name NoStatus -ConfigValueName DefaultNoStatus) + } + + return (Invoke-GHRestMethod @params | Add-GitHubBranchProtectionRuleAdditionalProperties) +} + +filter New-GitHubRepositoryBranchProtectionRule +{ + <# + .SYNOPSIS + Creates a branch protection rule for a branch on a given GitHub repository. + + .DESCRIPTION + Creates a branch protection rules for a branch on a given GitHub repository. + + The Git repo for this module can be found here: http://aka.ms/PowerShellForGitHub + + .PARAMETER OwnerName + Owner of the repository. + If not supplied here, the DefaultOwnerName configuration property value will be used. + + .PARAMETER RepositoryName + Name of the repository. + If not supplied here, the DefaultRepositoryName configuration property value will be used. + + .PARAMETER Uri + Uri for the repository. + The OwnerName and RepositoryName will be extracted from here instead of needing to provide + them individually. + + .PARAMETER BranchName + Name of the specific branch to create the protection rule on. + + .PARAMETER StatusChecks + The list of status checks to require in order to merge into the branch. + + .PARAMETER RequireUpToDateBranches + Require branches to be up to date before merging. This setting will not take effect unless + at least one status check is defined. + + .PARAMETER EnforceAdmins + Enforce all configured restrictions for administrators. + + .PARAMETER DismissalUsers + Specify the user names of users who can dismiss pull request reviews. This can only be + specified for organization-owned repositories. + + .PARAMETER DismissalTeams + Specify which teams can dismiss pull request reviews. + + .PARAMETER DismissStaleReviews + If specified, approving reviews when someone pushes a new commit are automatically + dismissed. + + .PARAMETER RequireCodeOwnerReviews + Blocks merging pull requests until code owners review them. + + .PARAMETER RequiredApprovingReviewCount + Specify the number of reviewers required to approve pull requests. Use a number between 1 + and 6. + + .PARAMETER RestrictPushUsers + Specify which users have push access. + + .PARAMETER RestrictPushTeams + Specify which teams have push access. + + .PARAMETER RestrictPushApps + Specify which apps have push access. + + .PARAMETER RequireLinearHistory + Enforces a linear commit Git history, which prevents anyone from pushing merge commits to a + branch. Your repository must allow squash merging or rebase merging before you can enable a + linear commit history. + + .PARAMETER AllowForcePushes + Permits force pushes to the protected branch by anyone with write access to the repository. + + .PARAMETER AllowDeletions + Allows deletion of the protected branch by anyone with write access to the repository. + + .PARAMETER AccessToken + If provided, this will be used as the AccessToken for authentication with the + REST Api. Otherwise, will attempt to use the configured value or will run unauthenticated. + + .PARAMETER NoStatus + If this switch is specified, long-running commands will run on the main thread + with no commandline status update. When not specified, those commands run in + the background, enabling the command prompt to provide status information. + If not supplied here, the DefaultNoStatus configuration property value will be used. + + .INPUTS + GitHub.Repository + GitHub.Branch + + .OUTPUTS + GitHub.BranchRepositoryRule + + .NOTES + Protecting a branch requires admin or owner permissions to the repository. + + .EXAMPLE + New-GitHubRepositoryBranchProtectionRule -OwnerName microsoft -RepositoryName PowerShellForGitHub -BranchName master -EnforceAdmins + + Creates a branch protection rule for the master branch of the PowerShellForGithub repository + enforcing all configuration restrictions for administrators. + + .EXAMPLE + New-GitHubRepositoryBranchProtectionRule -Uri 'https://github.com/microsoft/PowerShellForGitHub' -BranchName master -RequiredApprovingReviewCount 1 + + Creates a branch protection rule for the master branch of the PowerShellForGithub repository + requiring one approving review. +#> + [CmdletBinding( + PositionalBinding = $false, + SupportsShouldProcess, + DefaultParameterSetName = 'Elements')] + [OutputType({$script:GitHubBranchProtectionRuleTypeName })] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', '', + Justification = 'One or more parameters (like NoStatus) are only referenced by helper methods which get access to it from the stack via Get-Variable -Scope 1.')] + param( + [Parameter(ParameterSetName = 'Elements')] + [string] $OwnerName, + + [Parameter(ParameterSetName = 'Elements')] + [string] $RepositoryName, + + [Parameter( + Mandatory, + ValueFromPipelineByPropertyName, + Position = 1, + ParameterSetName = 'Uri')] + [Alias('RepositoryUrl')] + [string] $Uri, + + [Parameter( + Mandatory, + ValueFromPipelineByPropertyName, + Position = 2)] + [string] $BranchName, + + [string[]] $StatusChecks, + + [switch] $RequireUpToDateBranches, + + [switch] $EnforceAdmins, + + [string[]] $DismissalUsers, + + [string[]] $DismissalTeams, + + [switch] $DismissStaleReviews, + + [switch] $RequireCodeOwnerReviews, + + [ValidateRange(1, 6)] + [int] $RequiredApprovingReviewCount, + + [string[]] $RestrictPushUsers, + + [string[]] $RestrictPushTeams, + + [string[]] $RestrictPushApps, + + [switch] $RequireLinearHistory, + + [switch] $AllowForcePushes, + + [switch] $AllowDeletions, + + [string] $AccessToken, + + [switch] $NoStatus + ) + + Write-InvocationLog + + $elements = Resolve-RepositoryElements + $OwnerName = $elements.ownerName + $RepositoryName = $elements.repositoryName + + $telemetryProperties = @{ + OwnerName = (Get-PiiSafeString -PlainText $OwnerName) + RepositoryName = (Get-PiiSafeString -PlainText $RepositoryName) + } + + $getGitHubRepositoryBranchProtectRuleParms = @{ + OwnerName = $OwnerName + RepositoryName = $RepositoryName + BranchName = $BranchName + } + + $ruleExists = $true + + try + { + Get-GitHubRepositoryBranchProtectionRule @getGitHubRepositoryBranchProtectRuleParms | + Out-Null + } + catch + { + # Temporary code to handle current differences in exception object between PS5 and PS7 + if ($PSVersionTable.PSedition -eq 'Core') + { + if ($_.Exception -is [Microsoft.PowerShell.Commands.HttpResponseException] -and + ($_.ErrorDetails.Message | ConvertFrom-Json).message -eq 'Branch not protected') + { + $ruleExists = $false + } + else + { + throw $_ + } + } + else + { + if ($_.Exception.Message -like '*Branch not protected*') + { + $ruleExists = $false + } + else + { + throw $_ + } + } + } + + if ($ruleExists) + { + $message = ("Branch protection rule for branch $BranchName already exists on Repository " + + $RepositoryName) + Write-Log -Message $message -Level Error + throw $message + } + + if ($PSBoundParameters.ContainsKey('DismissalTeams') -or + $PSBoundParameters.ContainsKey('RestrictPushTeams')) + { + $teams = Get-GitHubTeam -OwnerName $OwnerName -RepositoryName $RepositoryName + } + + $requiredStatusChecks = $null + if ($PSBoundParameters.ContainsKey('StatusChecks') -or + $PSBoundParameters.ContainsKey('RequireUpToDateBranches')) + { + if ($null -eq $StatusChecks) + { + $StatusChecks = @() + } + $requiredStatusChecks = @{ + strict = $RequireUpToDateBranches.ToBool() + contexts = $StatusChecks + } + } + + $dismissalRestrictions = @{} + + if ($PSBoundParameters.ContainsKey('DismissalUsers')) + { + $dismissalRestrictions['users'] = $DismissalUsers + } + if ($PSBoundParameters.ContainsKey('DismissalTeams')) + { + $dismissalTeamList = $teams | Where-Object -FilterScript { $DismissalTeams -contains $_.name } + $dismissalRestrictions['teams'] = @($dismissalTeamList.slug) + } + + $requiredPullRequestReviews = @{} + + if ($PSBoundParameters.ContainsKey('DismissStaleReviews')) + { + $requiredPullRequestReviews['dismiss_stale_reviews'] = $DismissStaleReviews.ToBool() + } + if ($PSBoundParameters.ContainsKey('RequireCodeOwnerReviews')) + { + $requiredPullRequestReviews['require_code_owner_reviews'] = $RequireCodeOwnerReviews.ToBool() + } + if ($dismissalRestrictions.count -gt 0) + { + $requiredPullRequestReviews['dismissal_restrictions'] = $dismissalRestrictions + } + if ($PSBoundParameters.ContainsKey('RequiredApprovingReviewCount')) + { + $requiredPullRequestReviews['required_approving_review_count'] = $RequiredApprovingReviewCount + } + + if ($requiredPullRequestReviews.count -eq 0) + { + $requiredPullRequestReviews = $null + } + + if ($PSBoundParameters.ContainsKey('RestrictPushUsers') -or + $PSBoundParameters.ContainsKey('RestrictPushTeams') -or + $PSBoundParameters.ContainsKey('RestrictPushApps')) + { + if ($null -eq $RestrictPushUsers) + { + $RestrictPushUsers = @() + } + + if ($null -eq $RestrictPushTeams) + { + $restrictPushTeamSlugs = @() + } + else + { + $restrictPushTeamList = $teams | Where-Object -FilterScript { + $RestrictPushTeams -contains $_.name } + $restrictPushTeamSlugs = @($restrictPushTeamList.slug) + } + + $restrictions = @{ + users = $RestrictPushUsers + teams = $restrictPushTeamSlugs + } + + if ($PSBoundParameters.ContainsKey('RestrictPushApps')) + { + $restrictions['apps'] = $RestrictPushApps + } + } + else + { + $restrictions = $null + } + + $hashBody = @{ + required_status_checks = $requiredStatusChecks + enforce_admins = $EnforceAdmins.ToBool() + required_pull_request_reviews = $requiredPullRequestReviews + restrictions = $restrictions + } + + if ($PSBoundParameters.ContainsKey('RequireLinearHistory')) + { + $hashBody['required_linear_history'] = $RequireLinearHistory.ToBool() + } + if ($PSBoundParameters.ContainsKey('AllowForcePushes')) + { + $hashBody['allow_force_pushes'] = $AllowForcePushes.ToBool() + } + if ($PSBoundParameters.ContainsKey('AllowDeletions')) + { + $hashBody['allow_deletions'] = $AllowDeletions.ToBool() + } + + if ($PSCmdlet.ShouldProcess( + "'$BranchName' branch of repository '$RepositoryName'", + 'Create GitHub Repository Branch Protection Rule')) + { + $jsonConversionDepth = 3 + + $params = @{ + UriFragment = "repos/$OwnerName/$RepositoryName/branches/$BranchName/protection" + Body = (ConvertTo-Json -InputObject $hashBody -Depth $jsonConversionDepth) + Description = "Setting $BranchName branch protection status for $RepositoryName" + Method = 'Put' + AcceptHeader = $script:lukeCageAcceptHeader + AccessToken = $AccessToken + TelemetryEventName = $MyInvocation.MyCommand.Name + TelemetryProperties = $telemetryProperties + NoStatus = (Resolve-ParameterWithDefaultConfigurationValue -Name NoStatus ` + -ConfigValueName DefaultNoStatus) + } + + return (Invoke-GHRestMethod @params | Add-GitHubBranchProtectionRuleAdditionalProperties) + } +} + +filter Remove-GitHubRepositoryBranchProtectionRule +{ + <# + .SYNOPSIS + Remove branch protection rules from a given GitHub repository. + + .DESCRIPTION + Remove branch protection rules from a given GitHub repository. + + The Git repo for this module can be found here: http://aka.ms/PowerShellForGitHub + + .PARAMETER OwnerName + Owner of the repository. + If not supplied here, the DefaultOwnerName configuration property value will be used. + + .PARAMETER RepositoryName + Name of the repository. + If not supplied here, the DefaultRepositoryName configuration property value will be used. + + .PARAMETER Uri + Uri for the repository. + The OwnerName and RepositoryName will be extracted from here instead of needing to provide + them individually. + + .PARAMETER BranchName + Name of the specific branch to remove the branch protection rule from. + + .PARAMETER AccessToken + If provided, this will be used as the AccessToken for authentication with the + REST Api. Otherwise, will attempt to use the configured value or will run unauthenticated. + + .PARAMETER NoStatus + If this switch is specified, long-running commands will run on the main thread + with no commandline status update. When not specified, those commands run in + the background, enabling the command prompt to provide status information. + If not supplied here, the DefaultNoStatus configuration property value will be used. + + .INPUTS + GitHub.Repository + GitHub.Branch + + .OUTPUTS + None + + .EXAMPLE + Remove-GitHubRepositoryBranchProtectionRule -OwnerName microsoft -RepositoryName PowerShellForGitHub -BranchName master + + Removes branch protection rules from the master branch of the PowerShellForGithub repository. + + .EXAMPLE + Removes-GitHubRepositoryBranchProtection -Uri 'https://github.com/microsoft/PowerShellForGitHub' -BranchName master + + Removes branch protection rules from the master branch of the PowerShellForGithub repository. + + .EXAMPLE + Removes-GitHubRepositoryBranchProtection -Uri 'https://github.com/master/PowerShellForGitHub' -BranchName master -Force + + Removes branch protection rules from the master branch of the PowerShellForGithub repository + without prompting for confirmation. +#> + [CmdletBinding( + PositionalBinding = $false, + SupportsShouldProcess, + DefaultParameterSetName = 'Elements', + ConfirmImpact = "High")] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', '', + Justification = 'One or more parameters (like NoStatus) are only referenced by helper methods which get access to it from the stack via Get-Variable -Scope 1.')] + [Alias('Delete-GitHubRepositoryBranchProtectionRule')] + param( + [Parameter(ParameterSetName = 'Elements')] + [string] $OwnerName, + + [Parameter(ParameterSetName = 'Elements')] + [string] $RepositoryName, + + [Parameter( + Mandatory, + Position = 1, + ValueFromPipelineByPropertyName, + ParameterSetName = 'Uri')] + [Alias('RepositoryUrl')] + [string] $Uri, + + [Parameter( + Mandatory, + ValueFromPipelineByPropertyName, + Position = 2)] + [string] $BranchName, + + [switch] $Force, + + [string] $AccessToken, + + [switch] $NoStatus + ) + + Write-InvocationLog + + $elements = Resolve-RepositoryElements + $OwnerName = $elements.ownerName + $RepositoryName = $elements.repositoryName + + $telemetryProperties = @{ + 'OwnerName' = (Get-PiiSafeString -PlainText $OwnerName) + 'RepositoryName' = (Get-PiiSafeString -PlainText $RepositoryName) + } + + if ($Force -and (-not $Confirm)) + { + $ConfirmPreference = 'None' + } + + if ($PSCmdlet.ShouldProcess("'$BranchName' branch of repository '$RepositoryName'", + 'Remove GitHub Repository Branch Protection Rule')) + { + $params = @{ + UriFragment = "repos/$OwnerName/$RepositoryName/branches/$BranchName/protection" + Description = "Removing $BranchName branch protection rule for $RepositoryName" + Method = 'Delete' + AcceptHeader = $script:lukeCageAcceptHeader + AccessToken = $AccessToken + TelemetryEventName = $MyInvocation.MyCommand.Name + TelemetryProperties = $telemetryProperties + NoStatus = (Resolve-ParameterWithDefaultConfigurationValue -Name NoStatus ` + -ConfigValueName DefaultNoStatus) + } + + return Invoke-GHRestMethod @params | Out-Null + } +} + filter Add-GitHubBranchAdditionalProperties { <# @@ -564,3 +1181,58 @@ filter Add-GitHubBranchAdditionalProperties Write-Output $item } } + +filter Add-GitHubBranchProtectionRuleAdditionalProperties +{ + <# + .SYNOPSIS + Adds type name and additional properties to ease pipelining to GitHub Branch Protection Rule objects. + + .PARAMETER InputObject + The GitHub object to add additional properties to. + + .PARAMETER TypeName + The type that should be assigned to the object. + + .INPUTS + PSCustomObject + + .OUTPUTS + GitHub.Branch +#> + [CmdletBinding()] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseSingularNouns', '', + Justification = 'Internal helper that is definitely adding more than one property.')] + param( + [Parameter( + Mandatory, + ValueFromPipeline)] + [AllowNull()] + [AllowEmptyCollection()] + [PSCustomObject[]] $InputObject, + + [ValidateNotNullOrEmpty()] + [string] $TypeName = $script:GitHubBranchProtectionRuleTypeName + ) + + foreach ($item in $InputObject) + { + $item.PSObject.TypeNames.Insert(0, $TypeName) + + if (-not (Get-GitHubConfiguration -Name DisablePipelineSupport)) + { + $elements = Split-GitHubUri -Uri $item.url + $repositoryUrl = Join-GitHubUri @elements + Add-Member -InputObject $item -Name 'RepositoryUrl' -Value $repositoryUrl -MemberType NoteProperty -Force + + $hostName = $(Get-GitHubConfiguration -Name 'ApiHostName') + + if ($item.url -match "^https?://(?:www\.|api\.|)$hostName/repos/(?:[^/]+)/(?:[^/]+)/branches/([^/]+)/.*$") + { + Add-Member -InputObject $item -Name 'BranchName' -Value $Matches[1] -MemberType NoteProperty -Force + } + } + + Write-Output $item + } +} diff --git a/GitHubCore.ps1 b/GitHubCore.ps1 index 4ea6fabd..e20bb8a5 100644 --- a/GitHubCore.ps1 +++ b/GitHubCore.ps1 @@ -10,6 +10,7 @@ hellcatAcceptHeader = 'application/vnd.github.hellcat-preview+json' inertiaAcceptHeader = 'application/vnd.github.inertia-preview+json' londonAcceptHeader = 'application/vnd.github.london-preview+json' + lukeCageAcceptHeader = 'application/vnd.github.luke-cage-preview+json' machineManAcceptHeader = 'application/vnd.github.machine-man-preview' mercyAcceptHeader = 'application/vnd.github.mercy-preview+json' mockingbirdAcceptHeader = 'application/vnd.github.mockingbird-preview' diff --git a/PowerShellForGitHub.psd1 b/PowerShellForGitHub.psd1 index 5cbc5e48..21dd8820 100644 --- a/PowerShellForGitHub.psd1 +++ b/PowerShellForGitHub.psd1 @@ -101,6 +101,7 @@ 'Get-GitHubReleaseAsset', 'Get-GitHubRepository', 'Get-GitHubRepositoryBranch', + 'Get-GitHubRepositoryBranchProtectionRule', 'Get-GitHubRepositoryCollaborator', 'Get-GitHubRepositoryContributor', 'Get-GitHubRepositoryFork', @@ -138,6 +139,7 @@ 'New-GitHubRepository', 'New-GitHubRepositoryFromTemplate', 'New-GitHubRepositoryBranch', + 'New-GitHubRepositoryBranchProtectionRule', 'New-GitHubRepositoryFork', 'Remove-GitHubAssignee', 'Remove-GitHubComment', @@ -157,6 +159,7 @@ 'Remove-GitHubReleaseAsset', 'Remove-GitHubRepository', 'Remove-GitHubRepositoryBranch' + 'Remove-GitHubRepositoryBranchProtectionRule', 'Rename-GitHubGistFile', 'Rename-GitHubRepository', 'Reset-GitHubConfiguration', @@ -210,6 +213,7 @@ 'Delete-GitHubReleaseAsset', 'Delete-GitHubRepository', 'Delete-GitHubRepositoryBranch', + 'Delete-GitHubRepositoryBranchProtectionRule', 'Fork-GitHubGist', 'Get-GitHubAsset', 'Get-GitHubBranch', diff --git a/Tests/GitHubBranches.tests.ps1 b/Tests/GitHubBranches.tests.ps1 index 2d52a19b..5e770b99 100644 --- a/Tests/GitHubBranches.tests.ps1 +++ b/Tests/GitHubBranches.tests.ps1 @@ -15,6 +15,8 @@ param() $moduleRootPath = Split-Path -Path $PSScriptRoot -Parent . (Join-Path -Path $moduleRootPath -ChildPath 'Tests\Common.ps1') +Set-StrictMode -Version 1.0 + try { Describe 'Getting branches for repository' { @@ -330,6 +332,384 @@ try } } } + Describe 'GitHubBranches\Get-GitHubRepositoryBranchProtectionRule' { + Context 'When getting GitHub repository branch protection' { + BeforeAll { + $repoName = [Guid]::NewGuid().Guid + $branchName = 'master' + $protectionUrl = ("https://api.github.com/repos/$script:ownerName/" + + "$repoName/branches/$branchName/protection") + $repo = New-GitHubRepository -RepositoryName $repoName -AutoInit + New-GitHubRepositoryBranchProtectionRule -Uri $repo.svn_url -BranchName $branchName | Out-Null + $rule = Get-GitHubRepositoryBranchProtectionRule -Uri $repo.svn_url -BranchName $branchName + } + + It 'Should have the expected type and addititional properties' { + $rule.PSObject.TypeNames[0] | Should -Be 'GitHub.BranchProtectionRule' + $rule.url | Should -Be $protectionUrl + $rule.enforce_admins.enabled | Should -BeFalse + $rule.required_linear_history.enabled | Should -BeFalse + $rule.allow_force_pushes.enabled | Should -BeFalse + $rule.allow_deletions.enabled | Should -BeFalse + $rule.RepositoryUrl | Should -Be $repo.RepositoryUrl + } + + Context 'When specifying the "Uri" parameter through the pipeline' { + BeforeAll { + $rule = $repo | Get-GitHubRepositoryBranchProtectionRule -BranchName $branchName + } + + It 'Should have the expected type and addititional properties' { + $rule.PSObject.TypeNames[0] | Should -Be 'GitHub.BranchProtectionRule' + $rule.url | Should -Be $protectionUrl + $rule.enforce_admins.enabled | Should -BeFalse + $rule.required_linear_history.enabled | Should -BeFalse + $rule.allow_force_pushes.enabled | Should -BeFalse + $rule.allow_deletions.enabled | Should -BeFalse + $rule.RepositoryUrl | Should -Be $repo.RepositoryUrl + } + } + + Context 'When specifying the "BranchName" and "Uri" parameters through the pipeline' { + BeforeAll { + $branch = Get-GitHubRepositoryBranch -Uri $repo.svn_url -BranchName $branchName + $rule = $branch | Get-GitHubRepositoryBranchProtectionRule + } + + It 'Should have the expected type and addititional properties' { + $rule.PSObject.TypeNames[0] | Should -Be 'GitHub.BranchProtectionRule' + $rule.url | Should -Be $protectionUrl + $rule.enforce_admins.enabled | Should -BeFalse + $rule.required_linear_history.enabled | Should -BeFalse + $rule.allow_force_pushes.enabled | Should -BeFalse + $rule.allow_deletions.enabled | Should -BeFalse + $rule.RepositoryUrl | Should -Be $repo.RepositoryUrl + } + } + + AfterAll -ScriptBlock { + if ($repo) + { + Remove-GitHubRepository -Uri $repo.svn_url -Confirm:$false + } + } + } + } + + Describe 'GitHubBranches\New-GitHubRepositoryBranchProtectionRule' { + BeforeAll { + $repoName = [Guid]::NewGuid().Guid + $branchName = 'master' + $newGitHubRepositoryParms = @{ + OrganizationName = $script:organizationName + RepositoryName = $repoName + AutoInit = $true + } + + $repo = New-GitHubRepository @newGitHubRepositoryParms + } + + Context 'When setting base protection options' { + BeforeAll { + $targetBranchName = [Guid]::NewGuid().Guid + + $protectionUrl = ("https://api.github.com/repos/$script:organizationName/" + + "$repoName/branches/$targetBranchName/protection") + + $newGitHubRepositoryBranchParms = @{ + OwnerName = $script:organizationName + RepositoryName = $repoName + BranchName = $branchName + TargetBranchName = $targetBranchName + } + + $branch = New-GitHubRepositoryBranch @newGitHubRepositoryBranchParms + + $newGitHubRepositoryBranchProtectionParms = @{ + Uri = $repo.svn_url + BranchName = $targetBranchName + EnforceAdmins = $true + RequireLinearHistory = $true + AllowForcePushes = $true + AllowDeletions = $true + } + + $rule = New-GitHubRepositoryBranchProtectionRule @newGitHubRepositoryBranchProtectionParms + } + + It 'Should have the expected type and addititional properties' { + $rule.PSObject.TypeNames[0] | Should -Be 'GitHub.BranchProtectionRule' + $rule.url | Should -Be $protectionUrl + $rule.enforce_admins.enabled | Should -BeTrue + $rule.required_linear_history.enabled | Should -BeTrue + $rule.allow_force_pushes.enabled | Should -BeTrue + $rule.allow_deletions.enabled | Should -BeTrue + $rule.RepositoryUrl | Should -Be $repo.RepositoryUrl + } + } + + Context 'When setting required status checks' { + BeforeAll { + $targetBranchName = [Guid]::NewGuid().Guid + + $protectionUrl = ("https://api.github.com/repos/$script:organizationName/" + + "$repoName/branches/$targetBranchName/protection") + + $newGitHubRepositoryBranchParms = @{ + OwnerName = $script:organizationName + RepositoryName = $repoName + BranchName = $branchName + TargetBranchName = $targetBranchName + } + + $branch = New-GitHubRepositoryBranch @newGitHubRepositoryBranchParms + + $statusChecks = 'test' + + $newGitHubRepositoryBranchProtectionParms = @{ + Uri = $repo.svn_url + BranchName = $targetBranchName + RequireUpToDateBranches = $true + StatusChecks = $statusChecks + } + + $rule = New-GitHubRepositoryBranchProtectionRule @newGitHubRepositoryBranchProtectionParms + } + + It 'Should have the expected type and addititional properties' { + $rule.PSObject.TypeNames[0] | Should -Be 'GitHub.BranchProtectionRule' + $rule.url | Should -Be $protectionUrl + $rule.required_status_checks.strict | Should -BeTrue + $rule.required_status_checks.contexts | Should -Be $statusChecks + } + } + + Context 'When setting required pull request reviews' { + BeforeAll { + $targetBranchName = [Guid]::NewGuid().Guid + + $protectionUrl = ("https://api.github.com/repos/$script:organizationName/" + + "$repoName/branches/$targetBranchName/protection") + + $newGitHubRepositoryBranchParms = @{ + OwnerName = $script:organizationName + RepositoryName = $repoName + BranchName = $branchName + TargetBranchName = $targetBranchName + } + + $branch = New-GitHubRepositoryBranch @newGitHubRepositoryBranchParms + + $newGitHubRepositoryBranchProtectionParms = @{ + Uri = $repo.svn_url + BranchName = $targetBranchName + DismissalUsers = $script:ownerName + DismissStaleReviews = $true + RequireCodeOwnerReviews = $true + RequiredApprovingReviewCount = 1 + } + + $rule = New-GitHubRepositoryBranchProtectionRule @newGitHubRepositoryBranchProtectionParms + } + + It 'Should have the expected type and addititional properties' { + $rule.PSObject.TypeNames[0] | Should -Be 'GitHub.BranchProtectionRule' + $rule.url | Should -Be $protectionUrl + $rule.required_pull_request_reviews.dismissal_restrictions.users.login | + Should -Contain $script:OwnerName + } + } + + Context 'When setting push restrictions' { + BeforeAll { + $targetBranchName = [Guid]::NewGuid().Guid + + $protectionUrl = ("https://api.github.com/repos/$script:organizationName/" + + "$repoName/branches/$targetBranchName/protection") + + $newGitHubRepositoryBranchParms = @{ + OwnerName = $script:organizationName + RepositoryName = $repoName + BranchName = $branchName + TargetBranchName = $targetBranchName + } + + $branch = New-GitHubRepositoryBranch @newGitHubRepositoryBranchParms + + $newGitHubRepositoryBranchProtectionParms = @{ + Uri = $repo.svn_url + BranchName = $targetBranchName + RestrictPushUsers = $script:OwnerName + } + + $rule = New-GitHubRepositoryBranchProtectionRule @newGitHubRepositoryBranchProtectionParms + } + + It 'Should have the expected type and addititional properties' { + $rule.PSObject.TypeNames[0] | Should -Be 'GitHub.BranchProtectionRule' + $rule.url | Should -Be $protectionUrl + $rule.restrictions.users.login | Should -Contain $script:OwnerName + } + } + + Context 'When the branch rule already exists' { + BeforeAll { + $targetBranchName = [Guid]::NewGuid().Guid + + $newGitHubRepositoryBranchParms = @{ + OwnerName = $script:organizationName + RepositoryName = $repoName + BranchName = $branchName + TargetBranchName = $targetBranchName + } + + $branch = New-GitHubRepositoryBranch @newGitHubRepositoryBranchParms + + $newGitHubRepositoryBranchProtectionParms = @{ + Uri = $repo.svn_url + BranchName = $targetBranchName + } + + $rule = New-GitHubRepositoryBranchProtectionRule @newGitHubRepositoryBranchProtectionParms + } + + It 'Should throw the correct exception' { + $errorMessage = "Branch protection rule for branch $targetBranchName already exists on Repository $repoName" + { New-GitHubRepositoryBranchProtectionRule @newGitHubRepositoryBranchProtectionParms } | + Should -Throw $errorMessage + } + } + + Context 'When specifying the "Uri" parameter through the pipeline' { + BeforeAll { + $targetBranchName = [Guid]::NewGuid().Guid + + $newGitHubRepositoryBranchParms = @{ + OwnerName = $script:organizationName + RepositoryName = $repoName + BranchName = $branchName + TargetBranchName = $targetBranchName + } + + $branch = New-GitHubRepositoryBranch @newGitHubRepositoryBranchParms + + $protectionUrl = ("https://api.github.com/repos/$script:organizationName/" + + "$repoName/branches/$targetBranchName/protection") + + $rule = $repo | New-GitHubRepositoryBranchProtectionRule -BranchName $targetBranchName + } + + It 'Should have the expected type and addititional properties' { + $rule.PSObject.TypeNames[0] | Should -Be 'GitHub.BranchProtectionRule' + $rule.url | Should -Be $protectionUrl + $rule.enforce_admins.enabled | Should -BeFalse + $rule.required_linear_history.enabled | Should -BeFalse + $rule.allow_force_pushes.enabled | Should -BeFalse + $rule.allow_deletions.enabled | Should -BeFalse + $rule.RepositoryUrl | Should -Be $repo.RepositoryUrl + } + } + + Context 'When specifying the "BranchName" and "Uri" parameters through the pipeline' { + BeforeAll { + $targetBranchName = [Guid]::NewGuid().Guid + + $newGitHubRepositoryBranchParms = @{ + OwnerName = $script:organizationName + RepositoryName = $repoName + BranchName = $branchName + TargetBranchName = $targetBranchName + } + + $branch = New-GitHubRepositoryBranch @newGitHubRepositoryBranchParms + + $protectionUrl = ("https://api.github.com/repos/$script:organizationName/" + + "$repoName/branches/$targetBranchName/protection") + + $rule = $branch | New-GitHubRepositoryBranchProtectionRule + } + + It 'Should have the expected type and addititional properties' { + $rule.PSObject.TypeNames[0] | Should -Be 'GitHub.BranchProtectionRule' + $rule.url | Should -Be $protectionUrl + $rule.enforce_admins.enabled | Should -BeFalse + $rule.required_linear_history.enabled | Should -BeFalse + $rule.allow_force_pushes.enabled | Should -BeFalse + $rule.allow_deletions.enabled | Should -BeFalse + $rule.RepositoryUrl | Should -Be $repo.RepositoryUrl + } + } + + AfterAll -ScriptBlock { + if ($repo) + { + Remove-GitHubRepository -Uri $repo.svn_url -Confirm:$false + } + } + } + + Describe 'GitHubBranches\Remove-GitHubRepositoryBranchProtectionRule' { + Context 'When removing GitHub repository branch protection' { + BeforeAll { + $repoName = [Guid]::NewGuid().Guid + $branchName = 'master' + $repo = New-GitHubRepository -RepositoryName $repoName -AutoInit + + New-GitHubRepositoryBranchProtectionRule -Uri $repo.svn_url -BranchName $branchName | + Out-Null + } + + It 'Should not throw' { + { Remove-GitHubRepositoryBranchProtectionRule -Uri $repo.svn_url -BranchName $branchName -Force } | + Should -Not -Throw + } + + It 'Should have removed the protection rule' { + { Get-GitHubRepositoryBranchProtectionRule -Uri $repo.svn_url -BranchName $branchName } | + Should -Throw + } + + Context 'When specifying the "Uri" parameter through the pipeline' { + BeforeAll { + $rule = New-GitHubRepositoryBranchProtectionRule -Uri $repo.svn_url -BranchName $branchName + } + + It 'Should not throw' { + { $repo | Remove-GitHubRepositoryBranchProtectionRule -BranchName $branchName -Force} | + Should -Not -Throw + } + + It 'Should have removed the protection rule' { + { Get-GitHubRepositoryBranchProtectionRule -Uri $repo.svn_url -BranchName $branchName } | + Should -Throw + } + } + + Context 'When specifying the "Uri" and "BranchName" parameters through the pipeline' { + BeforeAll { + $rule = New-GitHubRepositoryBranchProtectionRule -Uri $repo.svn_url -BranchName $branchName + } + + It 'Should not throw' { + { $rule | Remove-GitHubRepositoryBranchProtectionRule -Force } | + Should -Not -Throw + } + + It 'Should have removed the protection rule' { + { Get-GitHubRepositoryBranchProtectionRule -Uri $repo.svn_url -BranchName $branchName } | + Should -Throw + } + } + + AfterAll -ScriptBlock { + if ($repo) + { + Remove-GitHubRepository -Uri $repo.svn_url -Confirm:$false + } + } + } + } + } finally { diff --git a/USAGE.md b/USAGE.md index 456c0186..71eaf035 100644 --- a/USAGE.md +++ b/USAGE.md @@ -42,6 +42,9 @@ * [Branches](#branches) * [Adding a new Branch to a Repository](#adding-a-new-branch-to-a-repository) * [Removing a Branch from a Repository](#removing-a-branch-from-a-repository) + * [Getting a repository branch protection rule](#getting-a-repository-branch-protection-rule) + * [Creating a repository branch protection rule](#creating-a-repository-branch-protection-rule) + * [Removing a repository branch protection rule](#removing-a-repository-branch-protection-rule) * [Forks](#forks) * [Get all the forks for a repository](#get-all-the-forks-for-a-repository) * [Create a new fork](#create-a-new-fork) @@ -534,6 +537,28 @@ Disable-GitHubRepositorySecurityFix -OwnerName microsoft -RepositoryName PowerSh ---------- +### Branches + +#### Getting a repository branch protection rule + +```powershell +Get-GitHubRepositoryBranchProtectionRule -OwnerName microsoft -RepositoryName PowerShellForGitHub -BranchName master +``` + +#### Creating a repository branch protection rule + +```powershell +New-GitHubRepositoryBranchProtectionRule -OwnerName microsoft -RepositoryName PowerShellForGitHub -BranchName master -RequiredApprovingReviewCount 1 +``` + +#### Removing a repository branch protection rule + +```powershell +Remove-GitHubRepositoryBranchProtectionRule -OwnerName microsoft -RepositoryName PowerShellForGitHub -BranchName master +``` + +---------- + ### Forks #### Get all the forks for a repository